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
try:
    from yaml import CSafeLoader as SafeLoader, CDumper as Dumper
except ImportError:
    from yaml import SafeLoader, Dumper
from . import config_load as config
from .version import FileVersions

OFFICE_EXTENSIONS = [
    '.doc', '.docm', '.docx', '.dot', '.dotm', '.dotx', '.fodt', '.html', '.odg', 
    '.odp', '.ods', '.odt', '.ott', '.pps', '.ppsm', '.ppsx', '.ppt', '.pptm', 
    '.pptx', '.potm', '.potx', '.rtf', '.txt', '.uot', '.vsd', '.vsdm', '.vsdx', 
    '.vstm', '.vstx', '.xls', '.xlsm', '.xlsx', '.xlm', '.xlt', '.xltm', '.xltx', 
    '.xml', '.csv'
]

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, Loader=SafeLoader)
        if not o:
            return None
        try:
            res = cls(**o)
        except TypeError:
            return None
        return res

    def to_str(self):
        return yaml.dump(self.__dict__, Dumper=Dumper)

    @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:
    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)

    @staticmethod
    def _id_validation(id):
        if not id:
            raise ValueError('Путь не может быть пустым')
        
        parts = id.split('/')
        if '.' in parts or '..' in parts:
            raise ValueError('Недопустимый путь', id)
        
    @property
    def path(self):
        return os.path.join(self._rdisk.base_path, *self.id.split("/"))
    
    @property
    def meta_path(self):
        return f"{self.path.rstrip('/')}.meta"

    @property
    def meta_tuuid_path(self):
        return os.path.join(self.meta_path, "tuuid") 
    
    @property
    def meta_tuuid(self):
        if self._rdisk.io.io_exists(self.meta_tuuid_path):
            return self._rdisk.io.io_read_text(self.meta_tuuid_path)

    @meta_tuuid.setter
    def meta_tuuid(self, value):
        self._rdisk.io.io_write_text(self.meta_tuuid_path, value)
    
    @property
    def meta_history_path(self):
        return os.path.join(self.meta_path, "history")

    @property
    def meta_history_log_path(self):
        return os.path.join(self.meta_path, "history.log")

    @property
    def meta_perms_path(self):
        return os.path.join(self.meta_path, "perms.yml")

    @property
    def meta_attach_path(self):
        return os.path.join(self.meta_path, "attach")
    
    @property
    def is_root(self):
        return self.id == "/"

    @property
    def exists(self):
        return self._rdisk.io.io_exists(self.path)

    def meta_get_flag(self, flag):
        flag_path = os.path.join(self.meta_path, f"flag-{flag}")
        return self._rdisk.io.io_exists(flag_path)

    def meta_set_flag(self, flag, value):
        flag_path = os.path.join(self.meta_path, f"flag-{flag}")
        if value:
            self._rdisk.io.io_touch(flag_path, exist_ok=True)
        else:
            self._rdisk.io.io_unlink(flag_path, missing_ok=True)

    @property
    def meta_flags(self):
        self._rdisk.io.io_mkdir(self.meta_path, exist_ok=True, parents=True)

        res = []
        for path in self._rdisk.io.io_iterdir(self.meta_path):
            name = self._rdisk.io.io_name(path)
            if name.startswith('flag-'):
                res.append(name[len('flag-'):])
        return res

    @property
    def flags(self):
        return orjson.dumps(self.meta_flags)

    @property
    def name(self):
        if self.is_root:
            return "Общая папка"
        
        if 'document_is_internal' in self.meta_flags:
            return self._rdisk.io.io_stem(self.path)
        return self._rdisk.io.io_name(self.path)

    @property
    def is_dir(self):
        return self._rdisk.io.io_is_dir(self.path)

    @property
    def abspath(self):
        return str(self._rdisk.io.io_absolute(self.path))

    @property
    def parent_id(self):
        if self.is_root:
            return None
        
        parent = "/".join(self.id.split("/")[:-1])
        return parent if len(parent) else "/"

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

    @property
    def stat(self):
        if not hasattr(self, "_stat"):
            self._stat = self._rdisk.io.io_stat(self.path)
        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 st_size(self):
        return self.stat.st_size

    @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 []

        children = []
        for file in self._rdisk.io.io_iterdir(self.path):
            if self._rdisk.io.io_suffix(file) == ".meta":
                continue

            filename = self._rdisk.io.io_name(file)
            file = RFile(self.id.rstrip("/") + "/" + filename, _rdisk=self._rdisk)
            if not file.can("read"):
                continue
            
            children.append(file)
        return children

    @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):
        for path in (self.meta_path, self.meta_attach_path, self.meta_history_path):
            self._rdisk.io.io_mkdir(path, exist_ok=True, parents=True)

        try:
            self._rdisk.io.io_touch(self.meta_perms_path)
            with self._rdisk.io.io_open(self.meta_perms_path, "w") as fp:
                RFilePerms(self._login).to_fp(fp)
        except FileExistsError:
            pass

        self._rdisk.io.io_touch(self.meta_history_log_path, 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._rdisk.io.io_mkdir(self.path)
        else:
            self._rdisk.io.io_touch(self.path)

    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):
        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 self._rdisk.io.io_exists(self.meta_path):
            self._rdisk.io.io_rmtree(self.meta_path)

        if self.is_dir:
            self._rdisk.io.io_rmtree(self.path)
        else:
            self._rdisk.io.io_unlink(self.path)

    def rename(self, new_id: str):
        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 self._rdisk.io.io_parents(target.path):
            raise FileExistsError(f'Не могу переместить папку "{self.name}" в "{target.name}"')
        
        self._rdisk.io.io_rename(self.path, target.path)
        self._rdisk.io.io_rename(self.meta_path, target.meta_path)
        return target

    @property
    def permissions(self):
        if not self._rdisk.io.io_exists(self.meta_perms_path):
            self._create_meta()

        with self._rdisk.io.io_open(self.meta_perms_path) 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._rdisk.io.io_open(self.meta_perms_path, "w") as fp:
            permlist.to_fp(fp)

    @property
    def mimetype(self):
        return self._rdisk.io.io_mimetype(self.path)

    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/') or self.id == "/Users":
            # TODO1: убрать этот хак, явно в perms каждого юзера прописать доступ
            user_dir = os.path.join("Users", *self._login.split("/"))
            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

    def read(self):
        pass

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

    def pdf_to_img(self, src_path: str, dst_path: str) -> None:
        """
        Конвертирует PDF -> PNG
        """
        if self._rdisk.io.io_exists(src_path):
            with self._rdisk.io.io_open(src_path, "rb") as src_file:
                with self._rdisk.io.io_open(dst_path, "wb") as dst_file:
                    RFile.pdf_bytes_to_img(dst_file, src_file.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

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

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

        temp_dir = tempfile.mkdtemp()
        tar_path = tempfile.mktemp(suffix=".tar", dir=temp_dir)

        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)

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

        for src_file in os.scandir(temp_dir):
            with open(src_file, "rb+") as f:
                dst_path = os.path.join(str(self.meta_path), src_file.name)
                self._rdisk.io.io_write_bytes(dst_path, f.read())
            os.remove(src_file.path)
        os.rmdir(temp_dir)

    @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')

    def make_fullview(self):
        """ Для макроса документов office делаем fullview - превью в pdf"""
        suffix = self._rdisk.io.io_suffix(self.path)
        file_path = os.path.join(self.meta_path, f"fullview{suffix}")

        gevent.sleep(1)

        self._rdisk.io.io_copy(self.abspath, file_path)

        with self._rdisk.io.io_open(file_path, "rb") as file_fullview:
            content = file_fullview.read()

            if suffix in ['.xls', '.xlsx', '.ods']:
                self.excel_to_pdf_sheets(content)
            elif suffix in ['.doc', '.docx', '.rtf', '.odt']:
                url_fullview_path = os.path.join(self.meta_path, "fullview.pdf")
                self.obj_to_pdf(content, url_fullview_path, suffix)

    def get_fullview(self, create=True):
        res = []
        suffix = self._rdisk.io.io_suffix(self.path)

        if suffix in ['.xls', '.xlsx', '.ods']:
            for obj in self._rdisk.io.io_iterdir(self.meta_path):
                if self._rdisk.io.io_is_file(obj) and self._rdisk.io.io_name(obj).startswith('fullview_page'):
                    int_dir = Path('/files', self.id.lstrip("/") + ".meta") #
                    res.append(str(Path(int_dir, self._rdisk.io.io_name(obj))))
        elif suffix in ['.doc', '.docx', '.rtf', '.odt']:
            fullview_name = os.path.join(str(self.meta_path), 'fullview.pdf')
            if self._rdisk.io.io_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):
        import tempfile
        logging.info('call make_preview')
        
        self._create_meta()

        suffix = self._rdisk.io.io_suffix(self.path)
        url_preview_path = os.path.join(self.meta_path, "preview.pdf")
        url_preview_img_path = os.path.join(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

        # setting default mime type
        # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
        mimetype = self._rdisk.io.io_mimetype(self.path) or 'application/octet-stream'

        try:
            if mimetype.startswith('image'):
                # миниатюра сам файл
                from PIL import Image
                temp_path = Path(tempfile.mktemp(suffix=suffix))
                temp_path.touch()

                img = Image.open(self.tmp_readonly_path)
                img.save(str(temp_path))

                self._rdisk.io.io_write_bytes(url_preview_img_path, temp_path.read_bytes())
                temp_path.unlink()
            elif mimetype == 'application/pdf':
                self._rdisk.io.io_copy(self.path, url_preview_path)
                self.pdf_to_img(self.path, url_preview_img_path)
            elif mimetype.startswith('video'):
                from moviepy.editor import VideoFileClip
                
                temp_path = Path(tempfile.mktemp(suffix=".png"))
                temp_path.touch()

                clip = VideoFileClip(self.tmp_readonly_path)
                clip.save_frame(str(temp_path), t=0.00)

                self._rdisk.io.io_write_bytes(url_preview_img_path, temp_path.read_bytes())
                temp_path.unlink()
            elif mimetype == 'application/vnd.ms-outlook':
                data = self._rdisk.io.io_read_bytes(self.path)
                parsed_data = self.get_content_msg(data)
                self.obj_to_pdf(parsed_data, url_preview_path, suffix)
                self.pdf_to_img(url_preview_path, url_preview_img_path)
            else:
                content = self._rdisk.io.io_read_bytes(self.path)
                self.obj_to_pdf(content, url_preview_path, suffix)
                self.pdf_to_img(url_preview_path, url_preview_img_path)

            # эти превью также надо скопировать в текущую версию
            fvs = self.file_versions()
            current_version = fvs.versions[-1]

            if self._rdisk.io.io_exists(url_preview_path):
                filename = self._rdisk.io.io_name(url_preview_path)
                dst = os.path.join(fvs.versions_dir_path, f'{current_version.filename}.{filename}')
                self._rdisk.io.io_copy(url_preview_path, dst)

            if self._rdisk.io.io_exists(url_preview_img_path):
                filename = self._rdisk.io.io_name(url_preview_img_path)
                dst = os.path.join(fvs.versions_dir_path, f'{current_version.filename}.{filename}')
                self._rdisk.io.io_copy(url_preview_img_path, dst)

            self.make_fullview()

        except Exception:
            logging.exception(f'Не удалось создать preview: {self.abspath}')
            return self

    def file_versions(self) -> FileVersions:
        fvs = FileVersions.for_file_path(self.path, self._rdisk)
        return fvs

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

    def backup(self):
        md5_path = os.path.join(self.meta_path, "md5")
        if not self._rdisk.io.io_exists(md5_path):
            self.write_md5()
        fvs = self.file_versions()
        fvs.backup(author=self._login)

    def md5(self, file_path):
        import hashlib
        hash_md5 = hashlib.md5()
        with self._rdisk.io.io_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 = os.path.join(self.meta_path, "md5")
        md5_path_parent = self._rdisk.io.io_parent(md5_path)

        if not self._rdisk.io.io_exists(md5_path_parent):
            self._create_meta()
            
        self._rdisk.io.io_write_text(md5_path, md5sum)
        
    def get_md5(self):
        md5_path = os.path.join(self.meta_path, "md5")
        if not self._rdisk.io.io_exists(md5_path):
            self.write_md5()
        return self._rdisk.io.io_read_text(md5_path)
    
    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:
            path = os.path.join(self.parent.meta_path, 'dirty')
            self._rdisk.io.io_touch(path, exist_ok=True)

    def write(self, data, backup=True, smart_backup=True, make_preview=True):
        if self.is_dir:
            raise ValueError("Не могу записывать в директории")
        # TODO: атомарность
        # TODO: история
        # Текущий файл уже лежит в версиях, можно сразу перезаписывать
        self._rdisk.io.io_write_bytes(self.path, data)

        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 stream_write(self, stream, *args, backup=True, smart_backup=True, make_preview=True, **kwargs):
        import tempfile

        if self.is_dir:
            raise ValueError("Не могу записывать в директории")

        total, used, free = self._rdisk.io.io_disk_usage(self._rdisk.root.path)  # Память в байтах
        chunk_size = 8192
        file_size = 0
        max_file_size = config.MAX_SIZE_MEGABYTE * 1_000_000
        min_disc_space_avl = 2_000_000_000
        file_versions = self.file_versions()

        try:
            temp_filename = Path(tempfile.mktemp())
            with open(temp_filename, 'wb+') as f:
                while True:
                    chunk = stream.read(chunk_size)
                    if not chunk:
                        break

                    file_size += len(chunk)

                    if file_size > max_file_size:
                        raise Exception(
                            f"Файл больше, чем {config.MAX_SIZE_MEGABYTE} МБ. "
                            f"Для загрузки, пожалуйста уменьшите размер файла"
                            )
                    
                    if (free - file_size) < min_disc_space_avl:
                        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)

            # Офисные файлы бекапяться при первой загрузке и текущая версия уже лежит в папке versions
            if self.st_size > 0 and Path(self.path).suffix not in OFFICE_EXTENSIONS and backup and not file_versions.versions:
                if smart_backup:
                    self.backup_smart()
                else:
                    self.backup()
            self._rdisk.io.io_write_bytes(self.path, temp_filename.read_bytes())
            temp_filename.unlink()
        except:
            if temp_filename.exists():
                temp_filename.unlink()
            raise

        self.calc_dirty()
        self.write_md5()
        file_versions = self.file_versions()
        # Для офисных файлов всегда создаем версии, начиная с 0
        if backup and (Path(self.path).suffix in OFFICE_EXTENSIONS or file_versions.versions):
            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)
    
    @property
    def tmp_readonly_path(self):
        return self._rdisk.io.io_tmp_readonly_path(self.path)
    
    def __str__(self):
        return str(self.id)

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