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 tempfile
from pathlib import Path
from PIL import Image
from moviepy.editor import VideoFileClip
import fitz

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:
    MAX_WIDTH = 1024  # Параметризуемая ширина
    JPEG_QUALITY = 20  # Качество JPEG для миниатюры
    MAX_THUMBNAIL_SIZE = 130 * 1024  # Максимальный размер 130Кб

    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 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, suffix:str, mimetype:str) -> 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', 'suffix': suffix, 'mimetype': mimetype})
        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, mimetype=None):
        """ Для макроса документов office делаем fullview - превью в pdf"""
        if mimetype is None:
            mimetype = self._rdisk.io.io_mimetype(self.path) or 'application/octet-stream'
        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, suffix=suffix, mimetype=mimetype)
            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):
        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_thumbnail_path = os.path.join(self.meta_path, "thumbnail.jpg")

        # Получение MIME-типа один раз для оптимизации
        mimetype = self._rdisk.io.io_mimetype(self.path) or 'application/octet-stream'

        try:
            if mimetype.startswith('image'):
                self._process_image(self.tmp_readonly_path, url_thumbnail_path)
            elif mimetype == 'application/pdf':
                temp_img_path = self.pdf_bytes_to_img(self.tmp_readonly_path)
                self._process_image(temp_img_path, url_thumbnail_path)
                temp_img_path.unlink(missing_ok=True)
            elif mimetype.startswith('video'):
                self._process_video(url_thumbnail_path)
            elif mimetype == 'application/vnd.ms-outlook':
                self._process_outlook(url_preview_path, url_thumbnail_path, suffix)
            else:
                self._process_other(url_preview_path, url_thumbnail_path, suffix)
            
            self._copy_preview_to_version(url_preview_path, url_thumbnail_path)
            self.make_fullview(mimetype)
        
        except Exception:
            logging.exception(f'Не удалось создать preview: {self.abspath}')
            return self

    def _process_image(self, input_path, output_path):
        try:
            with Image.open(input_path) as img:
                if img.mode in ("RGBA", "P"):
                    img = img.convert("RGB")
                
                # уменьшим размер если требуется
                if img.width > self.MAX_WIDTH:
                    new_height = int((self.MAX_WIDTH / img.width) * img.height)
                    img = img.resize((self.MAX_WIDTH, new_height), Image.Resampling.LANCZOS)
                
                temp_path = Path(tempfile.mktemp(suffix=".jpg"))
                img.save(temp_path, format="JPEG", quality=100, optimize=True)
                # Если больше максимального размера, то сжимаем
                if temp_path.stat().st_size > self.MAX_THUMBNAIL_SIZE:
                    img.save(temp_path, format="JPEG", quality=self.JPEG_QUALITY, optimize=True)
                
                self._rdisk.io.io_write_bytes(output_path, temp_path.read_bytes())
                temp_path.unlink(missing_ok=True)
        except IOError:
            logging.error("Ошибка обработки изображения")

    @staticmethod
    def pdf_bytes_to_img(fp) -> None:
        """
        Конвертирует PDF -> PNG и передает его в _process_image
        """
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
            temp_path = Path(temp_file.name)
            pdf = fitz.open(fp)
            page = pdf.load_page(0)
            images = page.get_pixmap()
            images.save(temp_path)
        return temp_path

    def pdf_to_img(self, src_path: str, dst_path: str) -> None:
        """
        Конвертирует PDF -> PNG и передает в _process_image
        """
        if self._rdisk.io.io_exists(src_path):
            temp_img_path = self.pdf_bytes_to_img(src_path)
            self._process_image(temp_img_path, dst_path)
            temp_img_path.unlink(missing_ok=True)

    def _process_video(self, output_path):
        try:
            temp_path = Path(tempfile.mktemp(suffix=".jpg"))
            clip = VideoFileClip(self.tmp_readonly_path)
            # Кадр со 2 секунды
            clip.save_frame(str(temp_path), t=0.02)
            self._process_image(temp_path, output_path)
            temp_path.unlink(missing_ok=True)
        except Exception as e:
            logging.error(f"Ошибка обработки видео: {e}")

    def _process_outlook(self, pdf_path, img_path, suffix):
        data = self._rdisk.io.io_read_bytes(self.path)
        parsed_data = self.get_content_msg(data)
        self.obj_to_pdf(parsed_data, pdf_path, suffix)
        temp_img_path = self.pdf_bytes_to_img(pdf_path)
        self._process_image(temp_img_path, img_path)
        temp_img_path.unlink(missing_ok=True)

    def _process_other(self, pdf_path, img_path, suffix):
        content = self._rdisk.io.io_read_bytes(self.path)
        self.obj_to_pdf(content, pdf_path, suffix)
        temp_img_path = self.pdf_bytes_to_img(pdf_path)
        self._process_image(temp_img_path, img_path)
        temp_img_path.unlink(missing_ok=True)

    def _copy_preview_to_version(self, preview_path, thumbnail_path):
        fvs = self.file_versions()
        current_version = fvs.versions[-1]

        for src_path in [preview_path, thumbnail_path]:
            if self._rdisk.io.io_exists(src_path):
                filename = self._rdisk.io.io_name(src_path)
                dst = os.path.join(fvs.versions_dir_path, f'{current_version.filename}.{filename}')
                self._rdisk.io.io_copy(src_path, dst)

    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
        #fixme этоже ограничение задается в глобальных настройках, надо зарефакторить?
        max_file_size = config.MAX_SIZE_MEGABYTE * 1_000_000
        min_disc_space_avl = 2_000_000_000
        file_versions = self.file_versions()

        try:
            # Создание временного файла безопасным способом
            with tempfile.NamedTemporaryFile(delete=False) as temp_file:
                temp_filename = Path(temp_file.name)
            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_copy(temp_filename, self.path)
        finally:
            if temp_filename.exists():
                temp_filename.unlink()

        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}')"
