import hashlib
import os
from collections.abc import Buffer
from typing import AnyStr
from uuid import uuid4

from core.logger import Logger
from data.schemas.system.file import File
from data.schemas.system.file_dao import fileDao

logger = Logger(__name__)


class FileService:
    _file_path = "files/"

    @classmethod
    def save_file(cls, file: Buffer, path: str) -> str:
        """
        Saves a file to the file system
        :param Buffer file:
        :param str path: custom/path/to/
        :return:
        """
        # ik function with side effects... not good practice, but it works
        file_name = str(uuid4())
        file_path = os.path.join(cls._file_path, path).replace("\\", "/")

        os.makedirs(file_path, exist_ok=True)

        with open(f"{file_path}/{file_name}", "wb") as f:
            f.write(file)

        return f"{path}/{file_name}"

    @classmethod
    def get_file(cls, name: str) -> AnyStr:
        """
        Retrieves a file from the file system
        :param str name: custom/path/to/fileName
        :return: file
        """
        file_path = os.path.join(cls._file_path, name).replace("\\", "/")
        with open(file_path, "rb") as f:
            return f.read()

    @classmethod
    def delete_file(cls, name: str) -> None:
        """
        Deletes a file from the file system
        :param str name: custom/path/to/fileName
        :return:
        """
        file_path = os.path.join(cls._file_path, name).replace("\\", "/")
        os.remove(file_path)

    @classmethod
    async def create_and_save_file(cls, content: Buffer, path: str) -> File:
        """
        Saves a file to the file system
        :param Buffer content:
        :param str path: custom/path/to/
        :return: File
        :rtype: File
        """
        # ik function with side effects... not good practice, but it works

        file_id = str(uuid4())
        file_path = os.path.join(cls._file_path, path).replace("\\", "/")
        file = f"{file_path}/{file_id}"

        try:
            os.makedirs(file_path, exist_ok=True)

            with open(file, "wb") as f:
                f.write(content)

            fid = await fileDao.create(
                File(0, file_id, path, cls.get_hash(cls.get_file(f"{path}/{file_id}")))
            )
            return await fileDao.get_by_id(fid)
        except Exception as e:
            if os.path.exists(file):
                os.remove(file)
            logger.error(f"Error saving file", e)
            raise e

    @classmethod
    async def clean_files(cls) -> None:
        local_files = []
        for root, dirs, files in os.walk(cls._file_path):
            for file in files:
                local_files.append(
                    os.path.relpath(os.path.join(root, file), cls._file_path).replace(
                        "\\", "/"
                    )
                )

        logger.debug(f"Cleaning files", local_files)

        for file in await fileDao.get_all():
            if not any(
                str(os.path.join(file.path, file.file_id).replace("\\", "/")) in f
                for f in local_files
            ):
                logger.warning(f"Referenced to non-existing file {file.file_id}")
                await fileDao.delete(file, hard_delete=True)
                logger.debug(f"Deleted file {file.file_id}")
                continue
            logger.debug(f"File {file.file_id} exists")

        for file in local_files:
            if not any(
                file in os.path.join(f.path, f.file_id).replace("\\", "/")
                for f in await fileDao.get_all()
            ):
                logger.warning(f"Non-referenced file {file}")
                os.remove(f"{cls._file_path}/{file}")
                logger.debug(f"Deleted file {file}")
                continue
            logger.debug(f"File {file} exists")

    @staticmethod
    def get_hash(file_stream: AnyStr) -> str:
        to_hash = file_stream
        if isinstance(file_stream, str):
            to_hash = file_stream.encode()

        return hashlib.sha256(to_hash).hexdigest()
