import logging
import mimetypes
import os
from datetime import datetime, timezone
from mimetypes import guess_type
from os import remove
from pathlib import Path
from shutil import rmtree

import gevent
import orjson
import requests
import yaml
from . import config_load as config
from .version import FileVersions


class RFilePerms:
    def __init__(self, owner=None, rules=None):
        self.owner = owner
        self.rules = rules or ["all allow read,write"]

    def can(self, who, what):
        if who == self.owner and who is not None:
            return True
        for rule in reversed(self.rules):
            rwho, rallow, rwhat = rule.split()
            if who in rwho.split(",") or rwho == "all":
                if what in rwhat.split(","):
                    return rallow == "allow"
        return False

    @classmethod
    def from_str(cls, s):
        o = yaml.load(s)
        if not o:
            return None
        try:
            res = cls(**o)
        except TypeError:
            return None
        return res

    def to_str(self):
        return yaml.dump(self.__dict__)

    @classmethod
    def from_fp(cls, fp):
        return cls.from_str(fp.read())

    def to_fp(self, fp):
        return fp.write(self.to_str())


class RFile:
    @staticmethod
    def _id_validation(id):
        if not id:
            raise ValueError('Путь не может быть пустым')
        parts = id.split('/')
        if '.' in parts or '..' in parts:
            raise ValueError('Недопустимый путь', id)

    def __init__(self, id: str, **kwargs):
        self._id_validation(id)
        self.id = id
        self._rdisk = None
        self._token = None
        self._token_unpack = None

        for k, v in kwargs.items():
            setattr(self, k, v)

    def history(self):
        if self.is_dir():
            return None

    def save(self):
        pass

    @property
    def _root(self):
        return self._rdisk.root

    @property
    def _root_id(self):
        return self._root.id

    @property
    def _root_path(self):
        return self._root._path

    @property
    def _base_path(self):
        return self._rdisk.base_path

    @property
    def _path(self):
        return Path(self._rdisk.base_path) / Path(self.id.lstrip("/"))

    @property
    def _is_root(self):
        return self.id == "/"

    @property
    def is_root(self):
        return self.is_root

    @property
    def _exists(self):
        return self._path.exists()

    @property
    def exists(self):
        return self._exists

    @property
    def meta_tuuid(self):
        tuuid_path = self._meta_tuuid_path
        if tuuid_path.exists():
            return tuuid_path.read_text()

    @meta_tuuid.setter
    def meta_tuuid(self, value):
        self._meta_tuuid_path.write_text(value)

    def meta_get_flag(self, flag):
        return (self.meta_path / f'flag-{flag}').exists()

    def meta_set_flag(self, flag, value):
        flag_path = self.meta_path / f'flag-{flag}'
        if value:
            flag_path.touch(exist_ok=True)
        else:
            try:
                flag_path.unlink()
            except FileNotFoundError:
                pass


    @property
    def meta_flags(self):
        self.meta_path.mkdir(exist_ok=True, parents=True)
        return [
            file_path.name[len('flag-'):]
            for file_path in self.meta_path.iterdir()
            if file_path.name.startswith('flag-')]

    @property
    def flags(self):
        """Для json поля"""
        return orjson.dumps(self.meta_flags)

    @property
    def meta_path(self):
        return Path(str(self._path) + ".meta")

    @property
    def _meta_tuuid_path(self):
        return self.meta_path / "tuuid"

    @property
    def _meta_history_path(self):
        return self.meta_path / "history"

    @property
    def _meta_history_log_path(self):
        return self.meta_path / "history.log"

    @property
    def _meta_perms_path(self):
        return self.meta_path / "perms.yml"

    @property
    def _meta_attach_path(self):
        return self.meta_path / "attach"

    @property
    def _permissions(self):
        if not self._meta_perms_path.exists():
            self._create_meta()
        with self._meta_perms_path.open() as fp:
            return RFilePerms.from_fp(fp)

    @property
    def perm_str(self):
        perms = self._permissions
        if not perms:
            return None
        return perms.to_str()

    @perm_str.setter
    def perm_str(self, s):
        permlist = RFilePerms.from_str(s)
        with self._meta_perms_path.open("w") as fp:
            permlist.to_fp(fp)

    @property
    def name(self):
        if self._is_root:
            return "Общая папка"
        if 'document_is_internal' in self.meta_flags:
            return self._path.stem  # without extension
        return self._path.name

    @property
    def is_dir(self):
        return self._path.is_dir()

    @property
    def abspath(self):
        return str(self._path.absolute())

    @property
    def parent_id(self):
        if self._is_root:
            return None
        r = "/".join(self.id.split("/")[:-1])
        if r == "":
            r = "/"
        return r

    @property
    def parent(self):
        if not self.parent_id:
            return None
        return RFile(self.parent_id, _rdisk=self._rdisk)

    @property
    def _stat(self):
        if not hasattr(self, "__stat"):
            self.__stat = self._path.stat()
        return self.__stat

    @property
    def st_mtime(self):
        return datetime.utcfromtimestamp(self._stat.st_mtime).replace(tzinfo=timezone.utc)

    @property
    def st_ctime(self):
        return datetime.utcfromtimestamp(self._stat.st_ctime).replace(tzinfo=timezone.utc)

    @property
    def mimetype(self):
        mimetype, encoding = guess_type(str(self._path))
        return mimetype

    @property
    def st_size(self):
        return self._stat.st_size

    @property
    def parents(self):
        parent = self.parent

        if not parent:
            return []

        return parent.parents + [parent]

    def can(self, what):
        if (self.id == '/obj' or self.id.startswith('/obj/')) and not self._is_obj_api:
            # Доступ разрешен только по полному пути
            # ??? Возможно это лишняя проверка и будет стрелять при рекурсивной проверке прав
            # startswith защищает нас от отсутствия рекурсивной проверки прав
            return False

        if self._is_admin:
            # флаг - TODO, пока если нужно админ руками залезет
            return True

        # При восстановлении из корзины требуется доступ
        if self.id == '/Trash':# or self.id.startswith('/Trash/'):
            # Доступ только админу
            return False

        if self.id.startswith('/Users/'):
            # TODO1: убрать этот хак, явно в perms каждого юзера прописать доступ
            user_dir = f'/Users/{self._login}'
            if self.id == user_dir or self.id.startswith(user_dir + '/'):
                return True
            return False

        perms = self._permissions

        if not perms:
            return True

        if perms.owner == self._login and self._login is not None:
            return True

        for rule_line in perms.rules:
            r_who, r_allow, r_what = rule_line.split(" ")
            r_who = r_who.split(",")
            r_what = r_what.split(",")
            r_allow = r_allow == "allow"

            if what not in r_what:
                continue

            if "all" in r_who:
                return r_allow

            if self._login in r_who or any(s in r_who for s in self._scope):
                return r_allow

        return False

    @property
    def children(self):
        if not self.is_dir:
            return None
        # Убираем из листинга /obj безусловно, /Users частично, треш - только для админа
        # Аттачи селектят напрямую, без иерархии
        # Ограничения прямого доступа - на подсистеме выше

        # Наличие /obj в children не требуется для их корректной работы (там разбросаны прямые mkdir итп)
        # if self.id.startswith('obj/') or self.id.startswith('/obj'):
        #     return []

        files = []
        for file in self._path.iterdir():
            if file.suffix == ".meta":
                continue
            file = RFile(self.id.rstrip("/") + "/" + file.name, _rdisk=self._rdisk)
            if not file.can("read"):
                continue
            files.append(file)
        return files

    @property
    def _login(self):
        if not self._rdisk.token:
            return self._rdisk.login
        return self._rdisk.token["login"]

    @property
    def _is_admin(self):
        return self._rdisk.is_admin

    @property
    def _is_obj_api(self):
        # API CmfAttachments, каталог /obj
        return self._rdisk.is_obj_api

    @property
    def _scope(self):
        # TODO: FIXME:
        if self._login == "sergey.osintsev@carbonsoft.ru":
            return ["topmanager"]
        # scope в токене не полный + токена может не быть
        # TODO1: сохранять g.current_user_rg_member_of, если получится понять как это получить в webdav
        if not self._rdisk.token:
            return []
        return self._rdisk.token["scope"] or []

    def _create_meta(self):
        self.meta_path.mkdir(exist_ok=True, parents=True)
        self._meta_attach_path.mkdir(exist_ok=True, parents=True)
        self._meta_history_path.mkdir(exist_ok=True, parents=True)

        try:
            self._meta_perms_path.touch()
            # если упадем - файл есть, не записываем
            with self._meta_perms_path.open("w") as fp:
                RFilePerms(self._login).to_fp(fp)
        except FileExistsError:
            pass

        self._meta_history_log_path.touch(exist_ok=True)

    def create(self, is_dir, exist_ok=False, parents=False):
        if parents and self.parent:
            self.parent.create(is_dir=True, exist_ok=True, parents=True)

        self._create_meta()
        if self._exists:
            if exist_ok:
                return
            else:
                raise FileExistsError(self)

        if is_dir:
            self._path.mkdir()
        else:
            self._path.touch()

    def add_child(self, name, is_dir):
        if self._is_root:
            child_name = "/" + name
        else:
            child_name = self.id + "/" + name
        file = RFile(child_name, _rdisk=self._rdisk)
        file.create(is_dir)
        return file

    def find_child_by_id(self, id, mkdir=False, *args, **kwargs):
        # mkdir - создает папку, если её еще нет
        rf = RFile(id, _rdisk=self._rdisk)
        if not rf._exists:
            if mkdir:
                rf.create(is_dir=True)
                return rf
            return None
        return rf

    def delete(self):
        # не у всех файлов есть мета
        if os.path.exists(self.meta_path):
            rmtree(self.meta_path)
        if self.is_dir:
            rmtree(self._path)
        else:
            remove(self._path)

    def rename(self, new_id: str):
        # todo check permissions
        if 'document_is_internal' in self.meta_flags:
            if not new_id.endswith('.html'):
                new_id += '.html'
        target = self._rdisk.get_rfile(new_id)
        if target._exists:
            raise FileExistsError(f'Файл c именем "{target.name}" уже существует')
        if self._path == target._path:
            return
        if self._path in target._path.parents:
            raise FileExistsError(f'Не могу переместить папку "{self.name}" в "{target.name}"')
        self._path.rename(target._path)
        self.meta_path.rename(target.meta_path)
        return target

    def read(self):
        pass

    def file_versions(self) -> FileVersions:
        fvs = FileVersions.for_file_path(self._path)
        return fvs

    def backup_smart(self):
        fvs = self.file_versions()
        fvs.backup_smart(author=self._login)

    def backup(self):
        md5_path = self.meta_path.joinpath('md5')
        if not md5_path.exists():
            self.write_md5()
        fvs = self.file_versions()
        fvs.backup(author=self._login)

    @staticmethod
    def pdf_bytes_to_img(path_img: str, pdf_bytes: bytes) -> None:
        """
        Конвертирует PDF -> PNG
        """
        # Ленивая загрузка Task:DEV-1624970955
        import fitz
        pdf = fitz.open("pdf", pdf_bytes)
        page = pdf.loadPage(0)
        images = page.get_pixmap()
        images.save(path_img)

    @staticmethod
    def pdf_to_img(path_pdf: str, path_img: str) -> None:
        """
        Конвертирует PDF -> PNG
        """
        if os.path.exists(path_pdf):
            with open(path_pdf, "rb") as file_pdf:
                RFile.pdf_bytes_to_img(path_img=path_img, pdf_bytes=file_pdf.read())

    @staticmethod
    def obj_to_format(data: bytes, suffix: str, to_format: str, mimetype: str = '') -> bytes:
        """
        Конвертация данных в HTML через libreoffice
        :param data: Данные файла в виде байт
        :param suffix: Расширение файла
        :param to_format: Формат файла который хотим получить
        """
        res = requests.post(config.CONVERTER_URL, files={'file': data}, data={'suffix': suffix, 'format': to_format,
                                                                              'mimetype': mimetype})
        if res.status_code != 200:
            raise Exception(f'Не удалось конвертировать в {to_format}: {res.content}')
        else:
            return res.content

    @staticmethod
    def obj_to_pdf(data: bytes, output_path: str, suffix: str) -> None:
        """
        Конвертация данных в PDF через libreoffice
        :param data: Данные файла в виде байт
        :param output_path: Путь сохранения итогового pdf файла
        :param suffix: Расширение файла
        """
        with open(output_path, 'wb+') as f:
            f.write(RFile.obj_to_format(data, suffix, 'pdf'))

    @staticmethod
    def excel_to_pdf_sheets(data: bytes, meta_dir: str) -> None:
        """
        Конвертация в PDF через libreoffice, каждый лист отдельно
        """
        import subprocess

        tar_path = f"{meta_dir}/fullview.tar"

        res = requests.post(config.CONVERTER_URL, files={'file': data}, data={'format': 'pdf_sheets'})
        if res.status_code != 200:
            raise Exception(f'Не удалось конвертировать в pdf: {res.content}')

        with open(tar_path, 'wb+') as f:
            f.write(res.content)

        proc = subprocess.run(
            ['tar', '-xf', 'fullview.tar'],
            cwd=meta_dir,
            timeout=60,
            check=True)
        os.remove(tar_path)

    @staticmethod
    def get_content_msg(msg_file: Path) -> bytes:
        """
        Метод извлекает данные из файла MSG и возвращает байтовую строку
        :param msg_file: Путь до файла MSG
        :return content: Байтовая строка
        """
        import extract_msg
        from tabulate import tabulate

        table = list()
        msg = extract_msg.openMsg(msg_file)

        if msg.sender:
            table.append(['ОТ:', msg.sender])
        if msg.to:
            table.append(['КОМУ:', msg.to])
        if msg.cc:
            table.append(['КОПИЯ:', msg.cc])
        if msg.bcc:
            table.append(['СКРЫТАЯ КОПИЯ:', msg.bcc])
        if msg.subject:
            table.append(['ТЕМА:', msg.subject])
        if msg.attachments:
            attachments = [f'<{attach.name} ({len(attach.data)} байт)>' for attach in msg.attachments]
            table.append(['ВЛОЖЕНИЯ:', ', '.join(attachments)])

        content = tabulate(table, maxcolwidths=[16, 64]) + '\n\n'
        content += f'{msg.body}'

        return bytes(content, 'utf-8')

    @staticmethod
    def excel_extensions():
        return ['.xls', '.xlsx', '.ods']

    @staticmethod
    def word_extensions():
        return ['.doc', '.docx', '.rtf', '.odt']

    def make_fullview(self):
        """ Для макроса документов office делаем fullview - превью в pdf"""
        import shutil
        file_path = f"{self.meta_path}/fullview{self._path.suffix}"
        gevent.sleep(1)
        shutil.copy(self.abspath, file_path)

        with open(file_path, "rb") as file_fullview:
            content = file_fullview.read()

            if self._path.suffix in self.excel_extensions():
                self.excel_to_pdf_sheets(content, self.meta_path)
            if self._path.suffix in self.word_extensions():
                url_fullview_path = self.meta_path / "fullview.pdf"
                self.obj_to_pdf(content, url_fullview_path, self._path.suffix)

    def get_fullview(self, create=True):
        res = []

        meta_path = self.meta_path

        if self._path.suffix in self.excel_extensions():
            for file in os.listdir(meta_path):
                if os.path.isfile(os.path.join(meta_path, file)) and file.startswith('fullview_page'):
                    int_dir = Path('/files', self.id.lstrip("/") + ".meta") #
                    res.append(str(Path(int_dir, file)))
        elif self._path.suffix in self.word_extensions():
            fullview_name = os.path.join(meta_path, 'fullview.pdf')
            if os.path.exists(fullview_name):
                int_filename = Path('/files', self.id.lstrip("/") + ".meta/fullview.pdf")
                res.append(str(int_filename))

        # Если фуллвью не найдено попробуем его сделать
        if create and not res:
            self.make_fullview()
            res = self.get_fullview(create=False)

        return res

    def make_preview(self):
        logging.info('call make_preview')
        self._create_meta()
        import shutil
        url_preview_path = self.meta_path / "preview.pdf"
        url_preview_img_path = self.meta_path / "preview_img.png"

        # TODO: move this to the global scope to avoid running it every time the function is called since it's a static code and the types aren't changing in runtime
        mimetypes.add_type('application/vnd.ms-outlook', '.msg')
        mimetypes.add_type('text/plain', '.log')
        mimetype, _ = mimetypes.guess_type(str(self._path))

        if mimetype is None:
            # setting default mime type
            # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
            mimetype = 'application/octet-stream'

        try:
            if mimetype.startswith('image'):
                # миниатюра сам файл
                from PIL import Image
                img = Image.open(self._path)
                img.save(url_preview_img_path)
            elif mimetype == 'application/pdf':
                shutil.copy(self.abspath, url_preview_path)
                # Просто делаем миниатюру
                self.pdf_to_img(self._path, url_preview_img_path)
            elif mimetype.startswith('video'):
                from moviepy.editor import VideoFileClip
                clip = VideoFileClip(str(self._path))
                clip.save_frame(url_preview_img_path, t=0.00)
            elif mimetype == 'application/vnd.ms-outlook':
                content = self.get_content_msg(self._path)
                self.obj_to_pdf(content, url_preview_path, self._path.suffix)
                self.pdf_to_img(url_preview_path, url_preview_img_path)
            else:
                # пытаемся получить pdf для быстрого просмотра и из него сделать миниатюру
                file_path = f"{self.meta_path}/preview{self._path.suffix}"
                gevent.sleep(1)
                shutil.copy(self.abspath, file_path)
                with open(file_path, "rb") as file_preview:
                    content = file_preview.read()
                    self.obj_to_pdf(content, url_preview_path, self._path.suffix)
                    self.pdf_to_img(url_preview_path, url_preview_img_path)
            # эти превью также надо скопировать в текущую версию
            fvs = self.file_versions()
            current_version = fvs.versions[-1]
            if url_preview_path.exists():
                shutil.copy(url_preview_path, fvs.versions_dir_path.joinpath(f'{current_version.filename }.{url_preview_path.name}'))
            if url_preview_img_path.exists():
                shutil.copy(url_preview_img_path, fvs.versions_dir_path.joinpath(f'{current_version.filename }.{url_preview_img_path.name}'))

            self.make_fullview()

        except Exception:
            logging.exception(f'Не удалось создать preview: {self.abspath}')
            return self
        
    @staticmethod
    def md5(file_path):
        import hashlib
        hash_md5 = hashlib.md5()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(1048576), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()
    
    def write_md5(self):
        md5sum = self.md5(self._path)
        md5_path = self.meta_path.joinpath('md5')
        if not md5_path.parent.exists():
            self._create_meta()
        md5_path.write_text(md5sum)
        
    def get_md5(self):
        md5_path = self.meta_path.joinpath('md5')
        if not md5_path.exists():
            self.write_md5()
        return md5_path.read_text()
    
    def calc_dirty(self):
        md5sum_new = self.md5(self._path)
        md5sum_old = self.get_md5()
        if self.parent.name.startswith('CmfDocument') and md5sum_new != md5sum_old:
            self.parent.meta_path.joinpath('dirty').touch(exist_ok=True)

    def write(self, data, backup=True, smart_backup=True):
        if self.is_dir:
            raise ValueError("Не могу записывать в директории")
        # TODO: атомарность
        # TODO: история
        # Текущий файл уже лежит в версиях, можно сразу перезаписывать
        self._path.write_bytes(data)
        self.calc_dirty()
        self.write_md5()
        if backup:
            if smart_backup:
                self.backup_smart()
            else:
                self.backup()
        gevent.spawn(self.make_preview)
        
    def stream_write(self, stream, *args, backup=True, smart_backup=True, make_preview=True, **kwargs):
        import shutil
        import tempfile
        if self.is_dir:
            raise ValueError("Не могу записывать в директории")
        # Проверяем место на диске
        total, used, free = shutil.disk_usage(self._root_path)  # Память в байтах
        chunk_size = 8192
        file_size = 0
        try:
            temp_filename = Path(tempfile.mktemp())
            with open(temp_filename, 'wb+') as f:
                chunk = stream.read(chunk_size)
                while chunk:
                    file_size += len(chunk)
                    if file_size > config.MAX_SIZE_MEGABYTE * 1_000_000:  # 1 мегабайт = 1_000_000 байт
                        raise Exception(f"Файл больше, чем {config.MAX_SIZE_MEGABYTE} МБ. "
                                        f"Для загрузки, пожалуйста уменьшите размер файла")
                    if (free - file_size) < 2_000_000_000:
                        raise Exception(f"На диске недостаточно места {free - file_size} < 2_000_000_000 байт")
                    # чтобы хватило на бекап
                    if (free - file_size) < file_size * 2:
                        raise Exception(f"На диске недостаточно места {free - file_size} < {file_size * 2} байт")
                    f.write(chunk)
                    chunk = stream.read(chunk_size)
            shutil.move(temp_filename, self._path)
        except:
            if temp_filename.exists():
                temp_filename.unlink()
            raise
        self.calc_dirty()
        self.write_md5()
        if backup:
            if smart_backup:
                self.backup_smart()
            else:
                self.backup()
        if make_preview:
            gevent.spawn(self.make_preview)

    def write_text(self, text: str, **kwargs):
        return self.write(text.encode(), **kwargs)

    def __str__(self):
        return str(self.id)

    def __repr__(self):
        return f"RFile('{self.id}')"
