from __future__ import unicode_literals

import datetime
from errno import ENOENT
import logging
from stat import S_IFDIR, S_IFREG
from time import time

from fuse import FUSE, FuseOSError, Operations

from django.core import management
from django.core.cache import caches
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count

from document_indexing.models import Index, IndexInstanceNode
from documents.models import Document

from ...literals import (
    MAX_FILE_DESCRIPTOR, MIN_FILE_DESCRIPTOR, FILE_MODE, DIRECTORY_MODE
)
from ...settings import (
    setting_document_lookup_cache_timeout, setting_node_lookup_cache_timeout
)

logger = logging.getLogger(__name__)


class IndexFS(Operations):
    def _get_next_file_descriptor(self):
        while(True):
            self.file_descriptor_count += 1
            if self.file_descriptor_count > MAX_FILE_DESCRIPTOR:
                self.file_descriptor_count = MIN_FILE_DESCRIPTOR

            try:
                if not self.file_descriptors[self.file_descriptor_count]:
                    return self.file_descriptor_count
            except KeyError:
                return self.file_descriptor_count

    def _path_to_node(self, path, access_only=False, directory_only=True):
        logger.debug('path: %s', path)
        logger.debug('directory_only: %s', directory_only)

        parts = path.split('/')

        logger.debug('parts: %s', parts)

        node = self.index.instance_root

        if len(parts) > 1 and parts[1] != '':
            obj = self.cache.get(path)

            if obj:
                node_pk = obj.get('node_pk')
                if node_pk:
                    if access_only:
                        return True
                    else:
                        return IndexInstanceNode.objects.get(pk=node_pk)

                document_pk = obj.get('document_pk')
                if document_pk:
                    if access_only:
                        return True
                    else:
                        return Document.objects.get(pk=document_pk)

            for count, part in enumerate(parts[1:]):
                try:
                    node = node.children.get(value=part)
                except IndexInstanceNode.DoesNotExist:
                    logger.debug('%s does not exists', part)

                    if directory_only:
                        return None
                    else:
                        try:
                            if node.index_template_node.link_documents:
                                result = node.documents.get(label=part)
                                logger.debug(
                                    'path %s is a valid file path', path
                                )
                                self.cache.set(
                                    path, {'document_pk': result.pk},
                                    setting_document_lookup_cache_timeout.value
                                )

                                return result
                            else:
                                return None
                        except Document.DoesNotExist:
                            logger.debug(
                                'path %s is a file, but is not found', path
                            )
                            return None
                        except MultipleObjectsReturned:
                            return None
                except MultipleObjectsReturned:
                    return None

            self.cache.set(
                path, {'node_pk': node.pk},
                setting_node_lookup_cache_timeout.value
            )

        logger.debug('node: %s', node)
        logger.debug('node is root: %s', node.is_root_node())

        return node

    def __init__(self, index_slug):
        self.file_descriptor_count = MIN_FILE_DESCRIPTOR
        self.file_descriptors = {}
        self.cache = caches['default']

        try:
            self.index = Index.objects.get(slug=index_slug)
        except Index.DoesNotExist:
            print 'Unknown index slug: {}.'.format(index_slug)
            exit(1)

    def access(self, path, fh=None):
        result = self._path_to_node(
            path=path, access_only=True, directory_only=False
        )

        if not result:
            raise FuseOSError(ENOENT)

    def getattr(self, path, fh=None):
        logger.debug('path: %s, fh: %s', path, fh)

        now = time()
        result = self._path_to_node(path=path, directory_only=False)

        if not result:
            raise FuseOSError(ENOENT)

        if isinstance(result, IndexInstanceNode):
            return {
                'st_mode': (S_IFDIR | DIRECTORY_MODE), 'st_ctime': now,
                'st_mtime': now, 'st_atime': now, 'st_nlink': 2
            }
        else:
            return {
                'st_mode': (S_IFREG | FILE_MODE),
                'st_ctime': (
                    result.date_added.replace(tzinfo=None) - result.date_added.utcoffset() - datetime.datetime(1970, 1, 1)
                ).total_seconds(),
                'st_mtime': (
                    result.latest_version.timestamp.replace(tzinfo=None) - result.latest_version.timestamp.utcoffset() - datetime.datetime(1970, 1, 1)
                ).total_seconds(),
                'st_atime': now,
                'st_size': result.size
            }

    def open(self, path, flags):
        result = self._path_to_node(path=path, directory_only=False)

        if isinstance(result, Document):
            next_file_descriptor = self._get_next_file_descriptor()
            self.file_descriptors[next_file_descriptor] = result.open()
            return next_file_descriptor
        else:
            raise FuseOSError(ENOENT)

    def release(self, path, fh):
        self.file_descriptors[fh] = None
        del(self.file_descriptors[fh])

    def read(self, path, size, offset, fh):
        self.file_descriptors[fh].seek(offset)
        return self.file_descriptors[fh].read(size)

    def readdir(self, path, fh):
        logger.debug('path: %s', path)

        node = self._path_to_node(path=path, directory_only=True)

        if not node:
            raise FuseOSError(ENOENT)

        yield '.'
        yield '..'

        # Nodes
        queryset = node.get_children().values('value').exclude(
            value__contains='/'
        )

        for duplicate in queryset.order_by().annotate(count_id=Count('id')).filter(count_id__gt=1):
            queryset = queryset.exclude(label=duplicate['label'])

        for child_node in queryset.values_list('value', flat=True):
            yield child_node

        # Documents
        if node.index_template_node.link_documents:
            queryset = node.documents.values('label').exclude(
                label__contains='/'
            )

            for duplicate in queryset.order_by().annotate(count_id=Count('id')).filter(count_id__gt=1):
                queryset = queryset.exclude(label=duplicate['label'])

            for document_label in queryset.values_list('label', flat=True):
                yield document_label


class Command(management.BaseCommand):
    help = 'Mount an index as a FUSE filesystem.'

    def add_arguments(self, parser):
        parser.add_argument('slug', nargs='?', help='Index slug')

        parser.add_argument('mount_point', nargs='?', help='Mount point')

    def handle(self, *args, **options):
        if not options.get('slug') or not options.get('mount_point'):
            self.stderr.write(self.style.ERROR('Incorrect number of arguments'))
            exit(1)

        FUSE(
            operations=IndexFS(index_slug=options['slug']),
            mountpoint=options['mount_point'], nothreads=True, foreground=True
        )
