import logging
from functools import lru_cache

from tenacity import Retrying, stop_after_attempt, retry_if_exception

import grpc
from yandex.cloud.priv.compute.v1 import instance_service_pb2, instance_service_pb2_grpc, image_service_pb2, \
    image_service_pb2_grpc, operation_service_pb2_grpc, operation_service_pb2
from google.protobuf.field_mask_pb2 import FieldMask
from .config import ENV_CONFIG, ROOT_CERT_FILE
from .utils import get_creds
from .grpc_options import COMMON_CHANNEL_OPTIONS

LOGGER = logging.getLogger(__name__)

TIMEOUT = 100.0


def _get_channel(token):
    return grpc.secure_channel(
        f"{ENV_CONFIG.COMPUTE_HOST}:{ENV_CONFIG.COMPUTE_PORT}",
        get_creds(ENV_CONFIG.COMPUTE_HOST, ENV_CONFIG.COMPUTE_PORT, token, root_cert=ROOT_CERT_FILE),
        options=COMMON_CHANNEL_OPTIONS,
    )


@lru_cache()
def get_instance_service_stub(token):
    channel = _get_channel(token)
    return instance_service_pb2_grpc.InstanceServiceStub(channel)


def get_image_id(image_family, token, request_id, folder_id='standard-images', timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = image_service_pb2_grpc.ImageServiceStub(channel)
    request = image_service_pb2.GetImageLatestByFamilyRequest(folder_id=folder_id, family=image_family)
    image = stub.GetLatestByFamily(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.debug(f"Get image for {image_family}: {image}")
    return image.id


def get_instance(instance_id, token, request_id, logger, full=False, timeout=TIMEOUT):
    stub = get_instance_service_stub(token)
    view = instance_service_pb2.InstanceView.FULL if full else instance_service_pb2.InstanceView.BASIC
    request = instance_service_pb2.GetInstanceRequest(instance_id=instance_id, view=view)

    for attempt in Retrying(stop=stop_after_attempt(2),
                            retry=retry_if_exception(
                                lambda e: isinstance(e, grpc.RpcError) and e.code() == grpc.StatusCode.UNAVAILABLE),
                            reraise=True):
        with attempt:
            response = stub.Get(request, timeout, metadata=[('x-request-id', request_id)])
            logger.debug("Get instance %s ", response)
            return response


def get_instance_list(folder_id, token, request_id, timeout=TIMEOUT):
    # TODO page_size and next
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.ListInstancesRequest(folder_id=folder_id)
    response = stub.List(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.debug(f"Get instance list for folder {folder_id} status: {response}")
    return response


def set_ipv6_to_network_interface(network_interface_specs):
    new_network_interface_specs = []
    for interface in network_interface_specs:
        interface.MergeFrom(instance_service_pb2.NetworkInterfaceSpec(
            primary_v6_address_spec=instance_service_pb2.PrimaryAddressSpec()))
        new_network_interface_specs.append(interface)
    return new_network_interface_specs


def create_instance(folder_id, vm_name, description, labels, image_id, platform_id, service_account_id, zone_id,
                    metadata, network_interface_specs, memory, cores, disk_size, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.CreateInstanceRequest(
        folder_id=folder_id,
        name=vm_name,
        description=description,
        labels=labels,
        zone_id=zone_id,
        platform_id=platform_id,
        service_account_id=service_account_id,
        resources_spec=instance_service_pb2.ResourcesSpec(memory=memory, cores=cores),
        metadata=metadata,
        boot_disk_spec=instance_service_pb2.AttachedDiskSpec(
            disk_spec=instance_service_pb2.AttachedDiskSpec.DiskSpec(size=disk_size, image_id=image_id),
            auto_delete=True),
        network_interface_specs=network_interface_specs,
    )
    response = stub.Create(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Create tank: {response}")
    metadata = instance_service_pb2.CreateInstanceMetadata()
    response.metadata.Unpack(metadata)
    return response, metadata


def delete_instance(instance_id, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.DeleteInstanceRequest(instance_id=instance_id)
    response = stub.Delete(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Delete instance {instance_id}: {response}")
    return response


def stop_instance(instance_id, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.StopInstanceRequest(instance_id=instance_id)
    response = stub.Stop(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Stop instance {instance_id}: {response}")
    return response


def start_instance(instance_id, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.StartInstanceRequest(instance_id=instance_id)
    response = stub.Start(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Start instance {instance_id}: {response}")
    return response


def restart_instance(instance_id, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    request = instance_service_pb2.RestartInstanceRequest(instance_id=instance_id)
    response = stub.Restart(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Restart instance {instance_id}: {response}")
    return response


def update_instance(request, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = instance_service_pb2_grpc.InstanceServiceStub(channel)
    response = stub.Update(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Update instance {request.instance_id} with {request}: {response}")
    return response


def update_image_id(instance_id, size, image_id, token, request_id, timeout=TIMEOUT):
    mask = FieldMask(paths=["boot_disk_spec"])
    request = instance_service_pb2.UpdateInstanceRequest(
        instance_id=instance_id,
        boot_disk_spec=instance_service_pb2.AttachedDiskSpec(disk_spec=instance_service_pb2.AttachedDiskSpec.DiskSpec(size=size, image_id=image_id)),
        update_mask=mask
    )
    return update_instance(request, token, request_id, timeout)


def update_metadata(instance_id, metadata, token, request_id, timeout=TIMEOUT):
    mask = FieldMask(paths=["metadata"])
    request = instance_service_pb2.UpdateInstanceRequest(
        instance_id=instance_id,
        metadata=metadata,
        update_mask=mask
    )
    return update_instance(request, token, request_id, timeout)


def get_operation(operation_id, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = operation_service_pb2_grpc.OperationServiceStub(channel)
    request = operation_service_pb2.GetOperationRequest(operation_id=operation_id)
    response = stub.Get(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.debug(f"Get compute operation: {response}")
    return response
