import asyncio
import hashlib
import logging
import os
from datetime import datetime
from pathlib import Path
import zipfile

from contextlib import contextmanager
from typing import Any, Generator, IO

from gateway_core.application.ports.FileSystemPort import FileSystemPort

logger = logging.getLogger(__name__)

class LinuxFileSystemAdapter(FileSystemPort):
    """Adapter for access on Linux file system"""

    def __init__(self) -> None:
        pass

    async def gatherFileList(self, path: Path, dataTypes: None | list[str] = None, recursive: bool = False) -> list[str]:
        """
        Gather and return a lists of all files in the given path, with the given data endings.

        :param path: The absolute path to the directory to search
        :param dataTypes: List of data types to include, if None all files are included
        :param recursive:  Includes files form subfolders
        :return: A list of the files
        """
        parsedFiles = []
        for f in os.listdir(path):
            if recursive and os.path.isdir(os.path.join(path, f)):
                listofDir = await self.gatherFileList(os.path.join(path, f), dataTypes)
                parsedFiles.extend([f"{f}/{file}" for file in listofDir])

            if not os.path.isfile(os.path.join(path, f)):
                continue

            if dataTypes:
                for dataType in dataTypes:
                    if f.endswith(dataType):
                        parsedFiles.append(f)
            else:
                parsedFiles.append(f)

        return parsedFiles

    @contextmanager
    def getFile(self, path: Path, write=False) -> Generator[IO[Any], Any, None]:
        """returns a file from the given path"""
        if not os.path.exists(path):
            if write:
                os.makedirs(path.parent, exist_ok=True)
            else:
                logger.warning(f"Path {path} does not exist")

        mode = "wb" if write else "rb"
        file = open(path, mode)

        try:
            yield file
        finally:
            file.close()


    async def getFileSize(self, path: Path) -> int:
        """returns files size of a file in the given path"""
        fileSize = path.stat().st_size

        return fileSize

    async def createZip(self, path: Path) -> Path:
        """
        Creates a zip file from the given path.

        :param path: The absolute path to the file to create
        :return: The path to the created zip file
        """
        timestamp = datetime.now().strftime("%y%m%d_%H%M")
        zipBaseName = f"{timestamp}_zipFile"
        zipPath = os.path.join(path.parent, f"{zipBaseName}.zip")

        if not os.path.exists(path) or not os.path.isdir(path):
            logger.warning(f"Path {path} is not a directory.")
            return None, None

        loop = asyncio.get_running_loop()

        with zipfile.ZipFile(zipPath, "w", zipfile.ZIP_DEFLATED) as zipWriter:
            await loop.run_in_executor(None, LinuxFileSystemAdapter.zipDir, zipWriter, str(path))

        return Path(zipPath)

    def deleteFile(self, path: Path):
        """removes a file from the given path"""
        os.remove(path)

    def getChecksum(self, path: Path, checksumType: str) -> str:
        checksum = ""

        if checksumType.lower() == "md5":
            with open(path, "rb") as file:
                checksum = hashlib.md5(file.read()).hexdigest()
        else:
            logger.error(f"Unexpected checksumType: {checksumType}")

        return checksum

    @staticmethod
    def zipFile(zipWriter: zipfile.ZipFile, filePath: str, zipName: str):
        """
        Adds a single file to an open zip archive.

        :param zipWriter: An instance of zipfile.ZipFile opened in write or append mode
        :param filePath: The absolute path to the file to add
        :param zipName: The name to use for the file inside the archive (relative path)
        """
        try:
            zipWriter.write(filePath, zipName)
        except FileNotFoundError:
            logger.error(f"File not found: {filePath}")
        except zipfile.LargeZipFile:
            logger.error(f"File too large to compress: {filePath}")
        except Exception as e:
            logger.error(f"Unexpected error while adding file: {filePath} - {e}")

    @staticmethod
    def zipDir(zipWriter: zipfile.ZipFile, dirPath: str):
        """
        Recursively adds a directory and all its contents to an open zip archive.
        Includes empty directories.

        :param zipWriter: An instance of zipfile.ZipFile opened in write or append mode
        :param dirPath: The absolute path to the directory to add
        """
        for root, dirs, filesInDir in os.walk(dirPath):
            # Include empty directories
            if not filesInDir and not dirs:
                zipName = os.path.relpath(root, os.path.join(dirPath, "..")) + "/"
                zipInfo = zipfile.ZipInfo(zipName)
                zipWriter.writestr(zipInfo, "")
            for file in filesInDir:
                fullPath = os.path.join(root, file)
                zipName = os.path.relpath(fullPath, os.path.join(dirPath, ".."))
                LinuxFileSystemAdapter.zipFile(zipWriter, fullPath, zipName)
