# coding: utf-8

import io
import os
import tempfile
import six
import datetime
import dateutil.parser
import dateutil.tz
import threading
import logging

import webdav3.client as wc
import webdav3.exceptions as we
import webdav3.urn as wu

from fs import errors
from fs.base import FS
from fs.enums import ResourceType, Seek
from fs.info import Info
from fs.iotools import line_iterator
from fs.mode import Mode
from fs.path import dirname
from cachetools import TTLCache


log = logging.getLogger(__name__)

basics = frozenset(['name'])
details = frozenset(('type', 'accessed', 'modified', 'created',
                     'metadata_changed', 'size'))
access = frozenset(('permissions', 'user', 'uid', 'group', 'gid'))


utc = dateutil.tz.tzutc()
epoch = datetime.datetime(1970, 1, 1, tzinfo=utc)


class WebDAVFile(io.RawIOBase):

    def __init__(self, wdfs, path, mode, size):
        super(WebDAVFile, self).__init__()

        self.fs = wdfs
        self.path = path
        self.res = self.fs.get_resource(self.path)
        self.mode = mode
        self.size = size
        self._lock = threading.RLock()
        self.data = self._get_file_data()

        self.pos = 0

        if self.mode.appending:
            self.pos = self.__length_hint__()
        if self.mode.writing:
            self.write(b'')
        if self.mode.reading:
            self.read(0)

    def _init_file_data(self):
        if self.fs.use_temp_files:
            if self.fs.temp_path and not os.path.exists(self.fs.temp_path):
                raise ValueError(f"Specified temp_path '{self.fs.temp_path}' does not exist.")
            return tempfile.NamedTemporaryFile(dir=self.fs.temp_path)
        return io.BytesIO()

    def _get_file_data(self):
        with self._lock:
            data = self._init_file_data()
            try:
                self.res.write_to(data)
                if not self.mode.appending:
                    data.seek(io.SEEK_SET)
            except we.RemoteResourceNotFound:
                data.write(b'')

            return data

    def __length_hint__(self):
        return self.size

    def __repr__(self):
        _repr = "WebDAVFile({!r}, {!r}, {!r})"
        return _repr.format(self.fs, self.path, self.mode.to_platform_bin())

    def close(self):
        if not self.closed:
            log.debug("closing")
            self.flush()
            super(WebDAVFile, self).close()
            self.data.close()

    def flush(self):
        if self.mode.writing:
            log.debug("flush")
            self.data.seek(io.SEEK_SET)
            self.res.read_from(self.data)

    def readline(self, size=-1):
        return next(line_iterator(self, None if size == -1 else size))

    def readable(self):
        return self.mode.reading

    def read(self, size=-1):
        if not self.mode.reading:
            raise IOError("File is not in read mode")
        self.pos = self.pos + size if size != -1 else self.__length_hint__()
        return self.data.read(size)

    def seekable(self):
        return True

    def seek(self, pos, whence=Seek.set):
        if whence == Seek.set:
            if pos < 0:
                raise ValueError('Negative seek position {}'.format(pos))
            self.pos = pos
        elif whence == Seek.current:
            self.pos = max(0, self.pos + pos)
        elif whence == Seek.end:
            if pos > 0:
                raise ValueError('Positive seek position {}'.format(pos))
            self.pos = max(0, self.__length_hint__() + pos)
        else:
            raise ValueError('invalid value for whence')

        self.data.seek(self.pos)
        return self.pos

    def tell(self):
        return self.pos

    def truncate(self, size=None):
        self.data.truncate(size)
        data_size = self.__length_hint__()
        if size and data_size < size:
            self.data.write(b'\0' * (size - data_size))
        return size or data_size

    def writable(self):
        return self.mode.writing

    def write(self, data):
        if not self.mode.writing:
            raise IOError("File is not in write mode")
        bytes_written = self.data.write(data)
        self.seek(bytes_written, Seek.current)
        return bytes_written


class WebDAVFS(FS):

    _meta = {
        'case_insensitive': False,
        'invalid_path_chars': '\0',
        'network': True,
        'read_only': False,
        'thread_safe': True,
        'unicode_paths': True,
        'virtual': False,
    }

    def __init__(self, url, login=None, password=None, root=None, timeout=None,
                 cache_maxsize=10000, cache_ttl=60,
                 use_temp_files=False, temp_path=None):
        self.url = url
        self.root = root
        self.use_temp_files = use_temp_files
        self.temp_path = temp_path
        super(WebDAVFS, self).__init__()

        options = {
            'webdav_hostname': self.url,
            'webdav_login': login,
            'webdav_password': password,
            'webdav_timeout': timeout,
            'root': self.root
        }
        self.info_cache = TTLCache(maxsize=cache_maxsize,
                                   ttl=cache_ttl)
        self.client = wc.Client(options)

    def _create_resource(self, path):
        urn = wu.Urn(path)
        res = wc.Resource(self.client, urn)
        return res

    def get_resource(self, path):
        return self._create_resource(path.encode('utf-8'))

    @staticmethod
    def _create_info_dict(info):
        info_dict = {
            'basic': {"is_dir": False},
            'details': {'type': int(ResourceType.file)},
            'access': {},
            'other': {}
        }

        if six.PY2:
            def decode(s):
                return s.decode('utf-8') if isinstance(s, bytes) else s
        else:
            def decode(s):
                return s

        def decode_datestring(s):
            dt = dateutil.parser.parse(s)
            return (dt - epoch).total_seconds()

        for key, val in six.iteritems(info):
            if key in basics:
                info_dict['basic'][key] = decode(val)
            elif key in details:
                if key == 'size' and val:
                    val = int(val)
                elif val:
                    if key in ('modified', 'created', 'accessed'):
                        val = decode_datestring(val)
                if key in ('modified', 'created', 'accessed'):
                    info_dict['details'][key] = decode(val) or None
                else:
                    info_dict['details'][key] = decode(val)
            elif key in access:
                info_dict['access'][key] = decode(val)
            else:
                info_dict['other'][key] = decode(val)

        return info_dict

    def create(self, path, wipe=False):
        with self._lock:
            if not wipe and self.exists(path):
                return False
            with self.openbin(path, 'wb') as new_file:
                new_file.truncate(0)
            return True

    def exists(self, path):
        _path = self.validatepath(path)
        return self.client.check(_path.encode('utf-8'))

    def getinfo(self, path, namespaces=None):
        _path = self.validatepath(path)

        if _path in '/':
            self.info_cache.clear()
            info_dict = {
                "basic": {
                    "name": "",
                    "is_dir": True
                },
                "details": {
                    "type": ResourceType.directory
                },
                'access': {},
                "other": {}
            }

        try:
            namespaces = namespaces or ()
            urn = wu.Urn(_path.encode('utf-8'))
            path = self.client.get_full_path(urn)
            if path in self.info_cache:
                info = self.info_cache[path]
            else:
                response = self.client.execute_request(action='info',
                                                       path=urn.quote())
                info = wc.WebDavXmlUtils.parse_info_response(content=response.content, path=path, hostname=self.client.webdav.hostname)
                if info['name'] is None:
                    info['name'] = _path.split("/")[-1]
                if wc.WebDavXmlUtils.parse_is_dir_response(content=response.content, path=path, hostname=self.client.webdav.hostname):
                    info['isdir'] = True
                    info['files'] = []
                    for i in wc.WebDavXmlUtils.parse_get_list_info_response(response.content):
                        if i['path'].rstrip('/') != path.rstrip('/'):
                            if i['name'] is None:
                                i['name'] = i['path'].split("/")[-1]
                            self.info_cache[i['path']] = i
                            filename = wu.Urn(i['path'], i['isdir']).filename()
                            if six.PY2:
                                filename = filename.decode('utf-8')
                            filename = filename.rstrip('/')
                            info['files'].append(filename)
                self.info_cache[path] = info
            info_dict = self._create_info_dict(info)
            if info.get('isdir', False):
                info_dict['basic']['is_dir'] = True
                info_dict['details']['type'] = ResourceType.directory
        except we.RemoteResourceNotFound as exc:
            raise errors.ResourceNotFound(path, exc=exc)

        return Info(info_dict)

    def listdir(self, path):
        info = self.getinfo(path)
        if not info.is_dir:
            raise errors.DirectoryExpected(path)

        for i in info.raw['other']['files']:
            yield i

        return

    def makedir(self, path, permissions=None, recreate=False):
        _path = self.validatepath(path)

        if _path in '/':
            if not recreate:
                raise errors.DirectoryExists(path)

        elif not (recreate and self.isdir(path)):
            if self.exists(_path):
                raise errors.DirectoryExists(path)
            try:
                self.client.mkdir(_path.encode('utf-8'))
            except we.RemoteParentNotFound as exc:
                raise errors.ResourceNotFound(path, exc=exc)

        return self.opendir(path)

    def openbin(self, path, mode='r', buffering=-1, **options):
        _mode = Mode(mode)
        _mode.validate_bin()
        _path = self.validatepath(path)

        log.debug("openbin: %s, %s", path, mode)
        info = None
        with self._lock:
            try:
                info = self.getinfo(_path)
                log.debug("Info: %s", info)
            except errors.ResourceNotFound:
                if not _mode.create:
                    raise errors.ResourceNotFound(path)
                # Check the parent is an existing directory
                if self.gettype(dirname(_path)) is not ResourceType.directory:
                    raise errors.DirectoryExpected(dirname(path))
            else:
                if info.is_dir:
                    raise errors.FileExpected(path)
                if _mode.exclusive:
                    raise errors.FileExists(path)
        size = info.size if info else 0
        return WebDAVFile(self, _path, _mode, size)

    def remove(self, path):
        _path = self.validatepath(path)
        if self.getinfo(path).is_dir:
            raise errors.FileExpected(path)
        self.client.clean(_path.encode('utf-8'))

    def removedir(self, path):
        _path = self.validatepath(path)
        if path in '/':
            raise errors.RemoveRootError()
        if not self.getinfo(path).is_dir:
            raise errors.DirectoryExpected(path)
        if not self.isempty(_path):
            raise errors.DirectoryNotEmpty(path)
        self.client.clean(_path.encode('utf-8'))

    def setbytes(self, path, contents):
        if not isinstance(contents, bytes):
            raise TypeError('contents must be bytes')
        _path = self.validatepath(path)
        bin_file = io.BytesIO(contents)
        with self._lock:
            resource = self._create_resource(_path.encode('utf-8'))
            resource.read_from(bin_file)

    def setinfo(self, path, info):
        _path = self.validatepath(path)
        if not self.exists(_path):
            raise errors.ResourceNotFound(path)

    def copy(self, src_path, dst_path, overwrite=False):
        _src_path = self.validatepath(src_path)
        _dst_path = self.validatepath(dst_path)

        with self._lock:
            if not self.getinfo(_src_path).is_file:
                raise errors.FileExpected(src_path)
            if not overwrite and self.exists(_dst_path):
                raise errors.DestinationExists(dst_path)
            try:
                self.client.copy(_src_path.encode('utf-8'), _dst_path.encode('utf-8'))
            except we.RemoteResourceNotFound as exc:
                raise errors.ResourceNotFound(src_path, exc=exc)
            except we.RemoteParentNotFound as exc:
                raise errors.ResourceNotFound(dst_path, exc=exc)

    def move(self, src_path, dst_path, overwrite=False):
        _src_path = self.validatepath(src_path)
        _dst_path = self.validatepath(dst_path)

        if not self.getinfo(_src_path).is_file:
            raise errors.FileExpected(src_path)
        if not overwrite and self.exists(_dst_path):
            raise errors.DestinationExists(dst_path)
        with self._lock:
            try:
                self.client.move(_src_path.encode('utf-8'), _dst_path.encode('utf-8'), overwrite=overwrite)
            except we.RemoteResourceNotFound as exc:
                raise errors.ResourceNotFound(src_path, exc=exc)
            except we.RemoteParentNotFound as exc:
                raise errors.ResourceNotFound(dst_path, exc=exc)
