# -*- coding: utf8 -*-
import logging
import requests
from retrying import retry


class GCSServiceOperationError(Exception):
    pass


__gcs_service = None
__gcs_credentials = None


def gcs_credentials():
    import google.auth.exceptions

    global __gcs_credentials

    if __gcs_credentials is None:
        try:
            __gcs_credentials, _ = google.auth.default()
        except google.auth.exceptions.DefaultCredentialsError:
            pass

    return __gcs_credentials


def gcs_service(credentials=None):
    from google.cloud import storage

    global __gcs_service

    if __gcs_service is None:
        __gcs_service = storage.Client(credentials=credentials)

    return __gcs_service


class GCSService(object):
    RETRYABLE_ERRORS = (IOError, )
    DEFAULT_MIMETYPE = 'application/octet-stream'
    NUM_RETRIES = 5

    def __init__(self, signed_url_service):
        self.signed_url_service = signed_url_service


class GCSDownload(GCSService):
    def __init__(self, auth_method, signed_url_service):
        super(GCSDownload, self).__init__(signed_url_service)
        self._auth_method = auth_method

    def download(self, object_name):
        auth_session = get_auth_session(self._auth_method)

        signed_urls = self.signed_url_service.get_signed_urls(['GET'], [object_name])
        url = signed_urls['GET'][0]

        r = auth_session.get(url)
        r.raise_for_status()
        data = r.content

        logging.debug('downloaded  %s(%s)', object_name, len(data))

        return data


class GCSDownloadDirectDownload(GCSService):
    def __init__(self):
        super(GCSDownloadDirectDownload, self).__init__(None)

    @classmethod
    def download(cls, bucket_name, object_name):
        gcs = gcs_service()

        blob = gcs.bucket(bucket_name).blob(object_name)
        data = blob.download_as_string()

        return data


class S3DownloadDirectDownload(GCSService):
    def __init__(self):
        super(S3DownloadDirectDownload, self).__init__(None)

    @classmethod
    def download(cls, bucket_name, object_name):
        import boto3

        s3 = boto3.resource('s3')

        obj = s3.Object(bucket_name, object_name)
        try:
            return obj.get()['Body'].read()
        except Exception as ex:
            return None


class GCSUploadDirect(GCSService):
    def __init__(self):
        super(GCSUploadDirect, self).__init__(None)

    @classmethod
    def upload(cls, bucket_name, object_name, file_obj, headers, credentials=None):
        gcs = gcs_service(credentials=credentials)

        blob = gcs.bucket(bucket_name).blob(object_name)

        content_type = headers.get('Content-Type')

        blob.upload_from_file(file_obj, content_type)


class S3UploadDirect(GCSService):
    def __init__(self):
        super(S3UploadDirect, self).__init__(None)

    @classmethod
    def upload(cls, bucket_name, object_name, file_obj, headers, credentials=None):
        import boto3

        s3 = boto3.client('s3')

        s3.upload_fileobj(file_obj, bucket_name, object_name)


class FileWithCallback(object):
    def __init__(self, file_obj, callback):
        file_obj.seek(0, 2)
        self._total = file_obj.tell()
        file_obj.seek(0)

        self._callback = callback
        self._file_obj = file_obj

    def __len__(self):
        return self._total

    def read(self, size):
        data = self._file_obj.read(size)

        if self._callback is not None:
            self._callback(len(data))

        return data


class GCSUpload(GCSService):
    def __init__(self, auth_method, head_url, put_url):
        super(GCSUpload, self).__init__(None)
        self._head_url = head_url
        self._put_url = put_url
        self._auth_method = auth_method

    def upload(self, file_obj, headers, callback=None):
        auth_session = get_auth_session(self._auth_method)

        resp = None

        file_obj_with_callback = FileWithCallback(file_obj, callback)

        if self._head_url:
            resp = auth_session.head(self._head_url)

            if resp.status_code in (204, 404):
                logging.debug('file not found, uploading')
                resp = None

        if resp is None:
            resp = auth_session.put(self._put_url, data=file_obj_with_callback, headers=headers)

        if resp.status_code in (401, 403):
            raise NonRetryException()

        resp.raise_for_status()


class GCSDeleteAll(GCSService):
    @classmethod
    def delete_all(cls, bucket_name, volume_id, max_files=None):
        import google.cloud.exceptions

        logging.info('delete all at %s/%s', bucket_name, volume_id)
        gcs = gcs_service()

        try:
            list_iter = gcs.bucket(bucket_name).list_blobs(prefix=volume_id)
        except google.cloud.exceptions.NotFound:
            logging.warning('bucket %s was not found', bucket_name)
            return

        total_deleted = 0
        for blob in list_iter:
            try:
                gcs.bucket(bucket_name).delete_blob(blob.name)
            except google.cloud.exceptions.NotFound:
                pass

            total_deleted += 1

            if max_files is not None and max_files == total_deleted:
                break

        logging .info('total deleted %s', total_deleted)

        return total_deleted


def get_auth_session(auth_method):
    if auth_method == 'gcloud':
        import google.auth.transport.requests

        credentials = gcs_credentials()

        return google.auth.transport.requests.AuthorizedSession(credentials)

    return requests


s3_moniker = 's3://'
gcs_moniker = 'gcs://'


def remove_moniker(name):
    try:
        index = name.index('://')
        return name[index + 3:]
    except ValueError:
        return name


class NonRetryException(Exception):
    pass


def retry_if_retry_possible_error(exception):
    return not isinstance(exception, NonRetryException)


@retry(retry_on_exception=retry_if_retry_possible_error, wait_exponential_multiplier=50, wait_exponential_max=5000)
def do_upload(
        auth, bucket_name, object_name, full_path_to_data, headers, head_url, put_url, credentials=None, callback=None):
    global __gcs_credentials

    if credentials is not None:
        __gcs_credentials = credentials

    if hasattr(full_path_to_data, 'read'):
        file_obj = full_path_to_data
        should_close = False
    else:
        file_obj = open(full_path_to_data, 'rb')
        should_close = True

    try:
        if put_url:
            GCSUpload(auth, head_url, put_url).upload(file_obj, headers, callback)
            return

        if bucket_name.startswith(s3_moniker):
            bucket_name = remove_moniker(bucket_name)
            S3UploadDirect().upload(bucket_name, object_name, file_obj, headers)
            return

        if bucket_name.startswith(gcs_moniker):
            bucket_name = remove_moniker(gcs_moniker)

        GCSUploadDirect().upload(bucket_name, object_name, file_obj, headers)
    except Exception:
        if should_close:
            file_obj.close()

        raise


def do_download(auth, bucket_name, object_name, signed_url_service=None):
    if signed_url_service:
        return GCSDownload(auth, signed_url_service).download(object_name)

    if bucket_name.startswith(s3_moniker):
        bucket_name = remove_moniker(bucket_name)
        return S3DownloadDirectDownload().download(bucket_name, object_name)

    if bucket_name.startswith(gcs_moniker):
        bucket_name = remove_moniker(bucket_name)

    return GCSDownloadDirectDownload().download(bucket_name, object_name)


def do_delete_all(bucket_name, volume_id, max_files):
    return GCSDeleteAll(signed_url_service=None).delete_all(bucket_name, volume_id, max_files)
