import shutil

import grpc
import base64
import io
import logging
import json
import requests
from requests.exceptions import RequestException
from typing import Optional

import boto3
from boto3.exceptions import Boto3Error
from botocore.exceptions import ClientError

from .config import ENV_CONFIG, ROOT_CERT_FILE
from load.projects.cloud.cloud_helper.metadata_compute import SaToken
from .utils import get_creds
from .grpc_options import COMMON_CHANNEL_OPTIONS
from .kms import decrypt as kms_decrypt
from .metadata_compute import get_instance_attributes
from yandex.cloud.priv.storage.v1 import bucket_service_pb2, bucket_service_pb2_grpc, \
    operation_service_pb2_grpc, operation_service_pb2

ACCESS_KEY_ATTR = 'encrypted_access_key'
SECRET_KEY_ATTR = 'encrypted_secret_key'
DEFAULT_ACCESS_KEY = b'\x00\x00\x00\x01\x00\x00\x00\x14e1036fkplthqau96vife\x00' \
                     b'\x00\x00\x10%Z\x9d\xaa1\xa9\xd8\x96\xc2\xe3\x17V\xba\xfe' \
                     b'\xe3\xb7\x00\x00\x00\x0cb\xc4yK&k\xa9\xf0\xb8\xbc\xef5\x85r' \
                     b'\x1e\xc7\x0cl\x0c\xcc\x15\xac\xb7\x0c\x9f\xc8M,c\x04:^*\xba' \
                     b'\xc5\xa3\xeb<~}\xb6\x0b\xfaR\x8e\xcb\xfa\xed'
DEFAULT_SECRET_KEY = b'\x00\x00\x00\x01\x00\x00\x00\x14e1036fkplthqau96vife\x00' \
                     b'\x00\x00\x10\xc2\xc5\xdd\xe3\x1f/\xed\x03\x8b\xea\xb2\x9d#Oy' \
                     b'\xee\x00\x00\x00\x0c\xa7\x84Eg\xa8\x16\xec\x15\x1d2\xf3M\xb1B' \
                     b'\xaa\\}\n\xa67p?\x10\xb9\xadj\x88\xd9/\xcb\xb3\xa5E*\xb3\xce6' \
                     b'\x13\x0e\xca\xc2e/|\x0c\xcbaW&\xf6:\xb4\x13[\x1dS\x9d\x15\xdf' \
                     b'\xcbxa\xff\x9cr\x14jQ'
PRESIGN_URL_EXPIRED_TIME = 600
TIMEOUT = 100.0
LOGGER = logging.getLogger(__name__)


def decrypt_secret(attr_name, default_key):
    attrs = get_instance_attributes() or {}
    if encrypted_key := attrs.get(attr_name):
        encrypted_key = base64.b64decode(encrypted_key)
    else:
        encrypted_key = default_key
    return kms_decrypt(encrypted_key, SaToken.get(), ENV_CONFIG.KMS_AAD, ENV_CONFIG.KMS_KEY_ID)


def get_client():
    session = boto3.session.Session()
    return session.client(
        service_name='s3',
        region_name='ru-central1',
        endpoint_url=ENV_CONFIG.OBJECT_STORAGE_URL,
        aws_access_key_id=decrypt_secret(ACCESS_KEY_ATTR, DEFAULT_ACCESS_KEY),
        aws_secret_access_key=decrypt_secret(SECRET_KEY_ATTR, DEFAULT_SECRET_KEY)
    )


def upload_fileobj(file_obj, s3_object, bucket) -> bool:
    """Upload a file-like (readable) to an S3 bucket

    :param file_obj: File-like object to upload
    :param bucket: Bucket to upload to
    :param s3_object: S3 object name.
    :return: True if file was uploaded, else False
    """
    client = get_client()
    try:
        client.upload_fileobj(file_obj, bucket, s3_object)
        return True
    except (ClientError, Boto3Error):
        LOGGER.exception(f'Failed to upload {s3_object=}.')
        return False


def download_file_to_buffer(s3_name, bucket) -> Optional[io.BytesIO]:
    """Download a file from S3 bucket to io buffer

    :param bucket: Bucket to download from
    :param s3_name: S3 object name. If not specified then file_name is used
    :return: io.BytesIO
    """
    client = get_client()
    buffer = io.BytesIO()

    try:
        client.download_fileobj(bucket, s3_name, buffer)
        return buffer
    except (ClientError, Boto3Error):
        LOGGER.exception(f'Failed to download s3 object: {s3_name}.')
        return None


def get_presign_url(bucket_name, file_name, storage_host, storage_port, token, request_id, method="GET"):
    presign_request_object = {
        "expires": PRESIGN_URL_EXPIRED_TIME,
        "name": file_name,
        "method": method,
        "headers": {
            "X-Request-Id": request_id
        }
    }
    body = {
        "bucket_name": bucket_name,
        "presign_host": storage_host,
        "objects": [
            presign_request_object
        ]
    }
    response = requests.post(f"{storage_host}:{storage_port}/management/presign_urls",
                             data=json.dumps(body), headers={"X-YaCloud-SubjectToken": token})
    presign_url = response.json()[0]
    return presign_url


def download_by_presign_url(bucket, file_name, token, request_id):
    try:
        presign_url = get_presign_url(bucket, file_name, ENV_CONFIG.OBJECT_STORAGE_HTTP_HOST,
                                      ENV_CONFIG.OBJECT_STORAGE_HTTP_PORT, token, request_id, method="GET")
        response = requests.get(presign_url, headers={"X-YaCloud-SubjectToken": token})
        return response.content
    except RequestException as error:
        LOGGER.error(f"Couldn't download ammo {file_name} from bucket {bucket}: {error}")
        return


def upload_by_presign_url(bucket, file_name, data, token, request_id):
    try:
        presign_url = get_presign_url(bucket, file_name, ENV_CONFIG.OBJECT_STORAGE_HTTP_HOST,
                                      ENV_CONFIG.OBJECT_STORAGE_HTTP_PORT, token, request_id, method="PUT")
        response = requests.put(presign_url, data=data, headers={"X-YaCloud-SubjectToken": token})
        return response.status_code == 200
    except (KeyError, RequestException) as error:
        LOGGER.error(f"Couldn't upload ammo {file_name} to bucket {bucket}: {error}")
        return False


def get_file(bucket, file_name, token, object_storage_host, path_to_download):
    object_storage_host = object_storage_host or ENV_CONFIG.OBJECT_STORAGE_URL
    try:
        with requests.get(f"{object_storage_host}/{bucket}/{file_name}",
                          headers={"X-YaCloud-SubjectToken": token},
                          stream=True) as r:
            r.raise_for_status()
            with open(path_to_download, 'wb') as file:
                shutil.copyfileobj(r.raw, file)
    except RequestException as error:
        err_message = f"Couldn't download ammo {file_name} from bucket {bucket}: {error}"
        LOGGER.error(err_message)
        raise RequestException(err_message) from error


def check_access_to_file(bucket, file_name, token):
    url = f"{ENV_CONFIG.OBJECT_STORAGE_URL}/{bucket}/{file_name}"
    resp = requests.head(url,
                         headers={"X-YaCloud-SubjectToken": token})
    resp.raise_for_status()


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


def create_bucket(folder_id, bucket_name, token, request_id, timeout=TIMEOUT):
    # TODO , default_storage_class=None, max_size=None, anonymous_access_flags=None, acl=None):
    channel = _get_channel(token)
    stub = bucket_service_pb2_grpc.BucketServiceStub(channel)
    request = bucket_service_pb2.CreateBucketRequest(
        folder_id=folder_id,
        name=bucket_name
    )
    response = stub.Create(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Create bucket: {response}")
    return response


def delete_bucket(bucket_name, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = bucket_service_pb2_grpc.BucketServiceStub(channel)
    request = bucket_service_pb2.DeleteBucketRequest(
        name=bucket_name
    )
    response = stub.Delete(request, timeout, metadata=[('x-request-id', request_id)])
    LOGGER.info(f"Delete bucket: {response}")
    return response


def get_bucket_stats(bucket_name, token, request_id, timeout=TIMEOUT):
    channel = _get_channel(token)
    stub = bucket_service_pb2_grpc.BucketServiceStub(channel)
    request = bucket_service_pb2.GetBucketStatsRequest(
        name=bucket_name
    )
    response = stub.GetStats(request, timeout, metadata=[('x-request-id', request_id)])
    #                    ('authorization', f'Bearer {token}'),])
    LOGGER.info(f"Get bucket stats: {response}")
    return response


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 storage operation: {response}")
    return response
