# -*- coding: utf-8 -*-
import dateutil
import os

from django.conf import settings
from django.core.files import base, storage
from django.utils.deconstruct import deconstructible
from django_mds.client import MDSClient, APIError
from io import StringIO, BytesIO

from events.avatars_extra.utils import generate_code
from events.common_app.tvm2_client import get_service_ticket


__all__ = ['APIError']


class StorageError(Exception):
    pass


class ReadError(StorageError):
    pass


class SaveError(StorageError):
    pass


class ModeError(StorageError):
    pass


class MdsClient(MDSClient):
    def __init__(self, namespace=None):
        namespace = namespace or settings.MDS_NAMESPACE
        use_tvm2 = namespace != settings.MDS_OLD_NAMESPACE
        super().__init__(
            settings.MDS_PUBLIC_URL,
            settings.MDS_PRIVATE_URL,
            namespace,
            settings.MDS_TOKEN,
            max_retries=settings.MDS_MAX_RETRIES,
            timeout=settings.MDS_TIMEOUT,
            use_tvm2=use_tvm2,
        )

    def _get_tvm2_ticket(self):
        return get_service_ticket(settings.MDS_TVM2_CLIENT)

    def validate_tvm2_params(self):
        pass


@deconstructible
class MdsStorage(storage.Storage):
    def __init__(self, namespace=None, expire=None):
        namespace = namespace or settings.MDS_NAMESPACE
        self.client = MdsClient(namespace)
        self.expire = expire
        super().__init__()

    def url(self, name):
        return self.client.read_url(name)

    def _open(self, name, mode):
        return MdsFile(name, self, mode)

    def _save(self, name, content, append=False):
        try:
            return self.client.upload(content, name, expire=self.expire)
        except APIError as e:
            raise SaveError from e

    def _fetch(self, name):
        try:
            return self.client.get(name)
        except APIError as e:
            raise ReadError from e

    def size(self, name):
        try:
            return int(self.client.size(name) or 0)
        except APIError as e:
            raise ReadError from e

    def delete(self, name):
        try:
            return self.client.delete(name)
        except APIError as e:
            raise SaveError from e

    def exists(self, name):
        try:
            return self.client.exists(name)
        except APIError as e:
            raise ReadError from e

    def get_expiration_date(self, name):
        # 'x-mds-expiration-date': 'Thu, 10 Jun 2021 08:17:16 GMT'
        try:
            response = self.client._head(name)
            return dateutil.parser.parse(response.headers.get('x-mds-expiration-date', ''))
        except APIError as e:
            raise ReadError from e
        except ValueError:
            return None

    def get_available_name(self, name, max_length=255):
        return '{0}_{1}'.format(generate_code(), self._crop_filename(name))

    def _crop_filename(self, name):
        filename, ext = os.path.splitext(name)
        filename = filename[:32]
        ext = ext.strip('.')[-7:]
        if ext:
            filename = '{0}.{1}'.format(filename, ext)
        return filename


class MdsFile(base.File):
    def __init__(self, name, storage, mode):
        self.name = name
        self._storage = storage
        self._stream = None

        if 'r' in mode:
            self._mode = 'r'
        elif 'w' in mode:
            self._mode = 'w'
        elif 'a' in mode:
            self._mode = 'a'
        else:
            raise ValueError('mode must contain at least one of "r", "w" or "a"')

        if '+' in mode:
            raise ValueError('mixed mode access not supported yet.')

    def open(self, mode=None):
        pass

    def read(self, num_bytes=None):
        if self._mode != 'r':
            raise ModeError('reading from a file opened for writing.')

        if self._stream is None:
            content = self._storage._fetch(self.name)
            self._stream = BytesIO(content)

        if num_bytes is None:
            return self._stream.read()

        return self._stream.read(num_bytes)

    def write(self, content):
        if self._mode not in ('w', 'a'):
            raise ModeError('writing to a file opened for reading.')

        if self._stream is None:
            self._stream = StringIO()

        return self._stream.write(content)

    def close(self):
        if self._stream is None:
            return

        if self._mode in ('w', 'a'):
            self._storage._save(
                self.name, self._stream.getvalue(), append=(self._mode == 'a')
            )

    @property
    def size(self):
        if not hasattr(self, '_size'):
            self._size = self._storage.size(self.name)
        return self._size

    @property
    def closed(self):
        return self._stream is None

    def seek(self, offset, mode=0):
        """
        @param mode: this value is passed as is into StringIO.seek
        """
