import os
from hashlib import sha256, md5
from pathlib import Path

import aiofiles
from rich import filesize

from bulk_restore_tool._log_util import LogUtil
from bulk_restore_tool.models import RestorableFile, JobDefinition, NAME_COLLISION


class HashCheckError(ValueError):
    def __init__(
        self,
        msg: str,
        hash_type: str,
        expected_hash: str,
        actual_hash: str,
        bytes_read: int,
    ):
        super().__init__(msg)

        self.hash_type = hash_type
        self.expected_hash = expected_hash
        self.actual_hash = actual_hash
        self.bytes_read = bytes_read

    def __str__(self):
        return f"{self.args[0]} (Expected: {self.expected_hash} Actual: {self.actual_hash}  Bytes_read: {self.bytes_read})"


async def validate_checksum(file: Path, sha256_expected, md5_expected):
    chunk_read_size = 10 * 1024 * 1024  # 10MB reads
    if sha256_expected is not None:
        file_hasher = sha256()
        expected_checksum = sha256_expected
        checksum_type = "SHA256"
    else:
        file_hasher = md5()
        expected_checksum = md5_expected
        checksum_type = "MD5"

    bytes_read = 0
    async with aiofiles.open(file, "rb") as written_file:
        while True:
            file_bytes = await written_file.read(chunk_read_size)
            if not file_bytes:
                break
            file_hasher.update(file_bytes)
            bytes_read += len(file_bytes)

    actual_checksum = file_hasher.hexdigest()

    if actual_checksum != expected_checksum:
        raise HashCheckError(
            "checksum failure",
            checksum_type,
            expected_checksum,
            actual_checksum,
            bytes_read,
        )


def search_for_filepath_collisions_and_update_restorable_file(
    file: RestorableFile, definition: JobDefinition, log_util: LogUtil
):
    """
    Look for an attempt to resolve any filename collisions between the file we are about to restore
    and the files already on disk.

    The discovered actual restore location is stored within the RestorableFile object and nothing is returned
    from this routine.
    """
    target_path: Path = definition.folder / file.get_restore_path(
        definition.forceWinPathSanitization
    )

    path_parts = target_path.parts
    search_path = None
    read_next_path_component = True
    i = 0
    loop_exit = len(path_parts) - 1
    while i < loop_exit:
        if search_path is None:
            search_path = Path(path_parts[0])
        elif read_next_path_component:
            search_path = search_path / path_parts[i]
        else:
            search_path = search_path.with_stem(search_path.name + NAME_COLLISION)

        if not search_path.exists():
            # we are done. the remainder of the path has no collisions on it.
            return

        elif search_path.is_dir():
            # we are good to move on to the next level.
            i += 1
            read_next_path_component = True

        elif search_path.is_file() or search_path.is_symlink():
            # we have a collision, resolve it
            file.resolve_path_name_collision(
                search_path.absolute().as_posix(),
                (definition.folder / file.deviceId).absolute().as_posix(),
                log_util,
            )
            # do not increment "i" -> we need to check if the resolved path collides with anything
            read_next_path_component = False

    # Add the final path component, the filename, to the search path
    search_path = search_path / path_parts[-1]

    # loop until we find a name that is not on disk
    while search_path.exists():
        # we have a collision
        file.resolve_path_name_collision(
            search_path.absolute().as_posix(),
            (definition.folder / file.deviceId).absolute().as_posix(),
            log_util,
        )
        # Update the search path to reflect the now renamed filename
        search_path = search_path.with_stem(search_path.name + NAME_COLLISION)


def list_db_files_in_folder_with_sizes(directory: str):
    """
    List all DB files in the given directory along with their sizes.
    :param directory: Path to the directory
    """
    try:
        listing = f"Database files in folder: {directory}\n"

        file_names = os.listdir(directory)
        for file_name in file_names:
            # only do reporting on .db files (sqlite)
            if not file_name.endswith(".db"):
                continue

            file_path = os.path.join(directory, file_name)
            # Check if it's a file (not a directory)
            if os.path.isfile(file_path):
                # Get the file size in bytes
                size_in_bytes = os.path.getsize(file_path)
                readable_size = filesize.decimal(size_in_bytes)

                listing += f"\t{file_name}    {readable_size}\n"

        return listing

    except FileNotFoundError:
        return f"Error: The directory '{directory}' does not exist."
    except PermissionError:
        return f"Error: Permission denied to access '{directory}'."
    except Exception as e:
        return f"An unexpected error occurred: {e}"
