import asyncio
import base64
import json
import os
import zipfile
from datetime import datetime
from typing import List, Tuple, Union

import aiofiles.os
from aiofiles.os import listdir
from fastapi import status, HTTPException  # noqa: F401

from server.data.definitions import WEBUI_WORK_FILES_DIR, Paths
from server.models.error import Error
from server.models.file import File

PATH_WEBUI_WORK_DIR = f"{os.environ.get(WEBUI_WORK_FILES_DIR, Paths.DEFAULT_WEBUI_WORK_DIR.value)}"
PATH_WEBUI_ZIP_DIR = f"{PATH_WEBUI_WORK_DIR}/{Paths.WEBUI_WORK_SUBDIR.value}"


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:
        print(f"File not found: {filePath}")
    except zipfile.LargeZipFile:
        print(f"File too large to compress: {filePath}")
    except Exception as e:
        print(f"Unexpected error while adding file: {filePath} - {e}")


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, ".."))
            zipFile(zipWriter, fullPath, zipName)


async def buildArchive(
    serial: str,
    ecpVersion: str,
    fileName: str,
    filePaths: List[str],
    files: List[str],
    outputDir: str = PATH_WEBUI_ZIP_DIR,
) -> Tuple[str, str]:
    """
    Builds a zip archive of the given files.

    :param serial: The serial number of the EVSE (e.g., 00000126_10000.0A)
    :param ecpVersion: The ECP version of the EVSE (e.g., 4.0.0-alpha-.rc.1)
    :param fileName: The filename of the zip file
    :param filePaths: The paths of the files to archive
    :param files: The files to archive
    :param outputDir: The directory to store the zip archive (default: PATH_WEBUI_ZIP_DIR)
    :return: A tuple containing the path where the archive is stored and the name of the zip file
    """

    timestamp = datetime.now().strftime("%y%m%d_%H%M")
    zipBaseName = f"{timestamp}___{fileName}_{serial}_{ecpVersion}"
    zipPath = os.path.join(outputDir, f"{zipBaseName}.zip")

    loop = asyncio.get_running_loop()

    with zipfile.ZipFile(zipPath, "w", zipfile.ZIP_DEFLATED) as zipWriter:
        for basePath in filePaths:
            for targetFile in files:
                fullPath = os.path.join(basePath, targetFile)
                if not os.path.exists(fullPath):
                    print(f"Skipping missing file: {fullPath}")
                    continue

                if os.path.isdir(fullPath):
                    await loop.run_in_executor(None, zipDir, zipWriter, fullPath)
                else:
                    # For files, strip out the parent directory in the zip
                    zipName = targetFile  # Use only the file name (no parent directory)
                    await loop.run_in_executor(None, zipFile, zipWriter, fullPath, zipName)

    return zipPath, zipBaseName


async def getJsonFile(path: str, getFileName: str) -> Union[File, bool]:
    """
    Retrieves a specific config files from the given path and converts into fields from Type File.

    :param path: The Path where the files are located for transforming into base64 strings.
    :param getFileName: The filename of the config file to retrieve.
    :return: A File instances.
    """
    for dirPath, _, fileNames in os.walk(path):
        for fileName in fileNames:
            if fileName == getFileName:
                filePath = os.path.join(dirPath, fileName)

                try:
                    async with aiofiles.open(filePath, "rb") as file:
                        # Load JSON content
                        jsonString = await file.read()
                        # Load JSON content
                    jsonContent = json.loads(jsonString)
                    # Convert JSON content to string
                    jsonString = json.dumps(jsonContent)

                    # Encode the string as base64
                    content = base64.b64encode(jsonString.encode("utf-8")).decode("utf-8")

                    # Split the filename by "."
                    parts = fileName.rsplit(".", 1)
                    # If there is a dot in the filename, return the part after the dot
                    # Otherwise, return an empty string
                    extension = parts[-1] if len(parts) > 1 else ""

                    return File(fileName=fileName, content=content, datatype=extension)

                except (FileNotFoundError, PermissionError, UnicodeDecodeError) as error:
                    errorResponse = Error(
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR, type="ExceptionError", message=error
                    )
                    raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errorResponse.dict())

    return False


async def getJsonFilesFromDir(path: str, onlyList: bool = False) -> List[File]:
    """
    Retrieves all the files from the given path and converts into fields from Type File.

    :param path: The Path where the files are located for transforming into base64 strings.
    :param onlyList: The boolean flag to indicate the raw data should be returned.
    :return: List of File instances.
    """
    files = []
    for dirPath, _, fileNames in os.walk(path):
        for fileName in fileNames:
            filePath = os.path.join(dirPath, fileName)

            try:
                content = ""

                if not onlyList:
                    async with aiofiles.open(filePath, "rb") as file:
                        # Load JSON content
                        jsonString = await file.read()

                        jsonContent = json.loads(jsonString)
                        # Convert JSON content to string
                        jsonString = json.dumps(jsonContent)

                        # Encode the string as base64
                        content = base64.b64encode(jsonString.encode("utf-8")).decode("utf-8")

                # Split the filename by "."
                parts = fileName.rsplit(".", 1)
                # If there is a dot in the filename, return the part after the dot
                # Otherwise, return an empty string
                extension = parts[-1] if len(parts) > 1 else ""

                files.append(File(fileName=fileName, content=content, datatype=extension))
            except (FileNotFoundError, PermissionError, UnicodeDecodeError) as error:
                errorResponse = Error(
                    status=status.HTTP_500_INTERNAL_SERVER_ERROR, type="ExceptionError", message=error
                )
                raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errorResponse.dict())

    return files


async def getFile(path: str, getFileName: str) -> Union[File, bool]:
    """
    Retrieves a specific files from the given path and converts into fields from Type File.

    :param path: The Path where the files are located for transforming into base64 strings.
    :param getFileName: The filename to retrieve.
    :return: A File instances.
    """
    for dirPath, _, fileNames in os.walk(path):
        for fileName in fileNames:
            if fileName == getFileName:
                filePath = os.path.join(dirPath, fileName)

                try:
                    async with aiofiles.open(filePath, "rb") as file:
                        # Read the file
                        fileContent = await file.read()

                    # Encode the content as base64
                    content = base64.b64encode(fileContent).decode("utf-8")

                    # Split the filename by "."
                    parts = fileName.rsplit(".", 1)
                    # If there is a dot in the filename, return the part after the dot
                    # Otherwise, return an empty string
                    extension = parts[-1] if len(parts) > 1 else ""

                    return File(fileName=fileName, content=content, datatype=extension)

                except (FileNotFoundError, PermissionError, UnicodeDecodeError) as error:
                    errorResponse = Error(
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR, type="ExceptionError", message=error
                    )
                    raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errorResponse.dict())

    return False


async def getFilesFromDir(path: str, onlyList: bool = False) -> List[File]:
    """
    Retrieves all the files from the given path and converts into fields from Type File.

    :param path: The Path where the files are located for transforming into base64 strings.
    :param onlyList: The boolean flag to indicate the raw data should be returned.
    :return: List of File instances.
    """
    files = []
    for dirPath, _, fileNames in os.walk(path):
        for fileName in fileNames:
            filePath = os.path.join(dirPath, fileName)

            try:
                content = ""

                if not onlyList:
                    async with aiofiles.open(filePath, "rb") as file:
                        # Read the file
                        fileContent = await file.read()

                        # Encode the content as base64
                        content = base64.b64encode(fileContent).decode("utf-8")

                # Split the filename by "."
                parts = fileName.rsplit(".", 1)
                # If there is a dot in the filename, return the part after the dot
                # Otherwise, return an empty string
                extension = parts[-1] if len(parts) > 1 else ""

                files.append(File(fileName=fileName, content=content, datatype=extension))
            except (FileNotFoundError, PermissionError, UnicodeDecodeError) as error:
                errorResponse = Error(
                    status=status.HTTP_500_INTERNAL_SERVER_ERROR, type="ExceptionError", message=error
                )
                raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errorResponse.dict())

    return files


async def saveImage(file: File, path: str) -> Tuple[bool, Union[None, str]]:
    """
    This method does save the image in the given path.

    :param file: The file for saving on the persistent path.
    :return: Returns True if the save action succeeds else False.
    """
    try:
        decodedContent = base64.b64decode(file.content)

        # Specify the file path where you want to save the decoded content
        filePath = f"{path}/{file.file_name}"

        # Write the decoded content to a file
        async with aiofiles.open(filePath, "wb") as file:
            await file.write(decodedContent)

        return True, None
    except (NotADirectoryError, UnicodeDecodeError, PermissionError) as error:
        return False, error


async def saveConfig(file: File, path: str) -> Tuple[bool, Union[None, str]]:
    """
    This method does save the config in the given path.

    :param file: The file for saving on the persistent path.
    :return: Returns True if the save action succeeds else False.
    """
    try:
        # Decode the base64-encoded string back into bytes
        decodedBytes = base64.b64decode(file.content.encode("utf-8"))

        # Decode the bytes back into a JSON string
        jsonString = decodedBytes.decode("utf-8")

        # Parse the JSON string into a Python dictionary
        jsonContent = json.loads(jsonString)

        # Specify the file path where you want to save the decoded content
        filePath = f"{path}/{file.file_name}"

        # Write the dictionary content into a JSON file
        async with aiofiles.open(filePath, "w") as f:
            await f.write(json.dumps(jsonContent, indent=4))

        return True, None
    except (NotADirectoryError, UnicodeDecodeError, PermissionError) as error:
        return False, error


async def deleteFile(path: str, deleteFileName: str) -> Union[File, bool]:
    """
    Deletes a specific files from the given path.

    :param path: The Path where the files are located for lookup.
    :param deleteFileName: The filename to delete.
    :return: True if the file was deleted successfully or False otherwise.
    """
    for dirPath, _, fileNames in os.walk(path):
        for fileName in fileNames:
            if fileName == deleteFileName:
                filePath = os.path.join(dirPath, fileName)

                try:
                    # Split the filename by "."
                    parts = fileName.rsplit(".", 1)
                    # If there is a dot in the filename, return the part after the dot
                    # Otherwise, return an empty string
                    extension = parts[-1] if len(parts) > 1 else ""

                    # Delete the file
                    await aiofiles.os.remove(filePath)

                    return File(fileName=fileName, content="", datatype=extension)
                except (OSError, PermissionError, IsADirectoryError, FileNotFoundError) as error:
                    errorResponse = Error(
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR, type="ExceptionError", message=error
                    )
                    raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=errorResponse.dict())

    return False
