"""Service layer for adding maps to the UMC.

Covers two related workflows:
  - installing brand new .bsp files uploaded by the user (physical install + UMC entry)
  - re-using maps that are already installed on the server (UMC entry only)
"""

import json
import re
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from typing import Any

from filelock import FileLock

from core import constants
from core.domain.map_queries import map_file_exists
from core.domain.umc_operations import (
    add_category,
    add_map_to_category,
    category_exists,
    find_map_categories,
    map_exists,
    map_exists_raise_error,
)
from core.exceptions import CoreError, InstallServiceError
from core.io.map_installer import install_map, is_map_installed
from core.io.umc_parser import parse_umc_file
from core.io.umc_writer import write
from core.logger_config import logger

TEMP_FOLDER: Path = constants.TEMP_FOLDER
UMC_MASTER_FILE = constants.UMC_MASTER_FILE
UMC_SERVER_FILE = constants.UMC_FILE

# ==========================================================================
# 1. VALIDATION
# ==========================================================================


def validate_category_name(category_name: str) -> None:
    """Verify that the category name is appropriate and compliant."""
    if not re.match(r"^[A-Za-z0-9_\-$& ]+$", category_name):
        msg = f"Invalid or dangerous category name : {category_name}"
        raise InstallServiceError(msg)


def validate_workflow_inputs(flask_files: list[Any], category_name: str | None) -> None:
    """Validates that the required arguments are present at the workflow input."""
    if not flask_files or all(f.filename == "" for f in flask_files):
        logger.warning("Installation rejected: No BSP received")
        msg = "No BSP files were received."
        raise ValueError(msg)

    if not category_name:
        logger.warning("Installation Failed: No category selected")
        msg = "No category selected"
        raise ValueError(msg)


def validate_existing_maps_request(category_name: str, maps: dict[str, dict[str, Any]]) -> None:
    """Validates the request before modifying the UMC with already-installed maps."""
    if not category_name.strip():
        msg = "Category name cannot be empty."
        raise ValueError(msg)
    if not maps:
        msg = "No maps have been provided."
        raise ValueError(msg)
    for map_name in maps:
        if not map_file_exists(map_name):
            msg = f"The BSP file '{map_name}.bsp' does not exist."
            raise InstallServiceError(msg)


# ==========================================================================
# 2. GESTION DU SYSTÈME DE FICHIERS (Responsabilité : Cycle de vie I/O temporaire)
# ==========================================================================


@contextmanager
def temporary_upload_handler(flask_files: list[Any]) -> Generator[list[Path]]:
    """Context manager responsible for storing and cleaning up uploads."""
    TEMP_FOLDER.mkdir(parents=True, exist_ok=True)
    saved_files: list[Path] = []

    try:
        for file in flask_files:
            filename = Path(file.filename).name
            destination = TEMP_FOLDER / filename

            logger.info(f"Temporary backup : {filename}")
            file.save(destination)
            saved_files.append(destination)

        logger.info(f"{len(saved_files)} file(s) saved in temp")
        yield saved_files

    finally:
        logger.info("Cleaning the Temporary Folder")
        for file in TEMP_FOLDER.glob("*"):
            try:
                file.unlink()
                logger.info(f"Time removed : {file.name}")
            except OSError as e:
                logger.warning(f"Cannot delete {file.name}: {e}")


# ==========================================================================
# 3. LOGIQUE MÉTIER UNITAIRE (Responsabilité : Traitement d'une carte unique)
# ==========================================================================


def _process_single_map_installation(
    bsp_file: Path,
    category_name: str,
    umc_data: dict[str, Any],
    maps_options: dict[str, Any],
    delete_source: bool,
) -> dict[str, Any]:
    """Supports the physical and logical installation of a single new BSP card."""
    map_name = bsp_file.stem
    logger.info(f"Installing the map : {map_name}")

    map_exists_raise_error(umc_data, category_name, map_name)

    if not is_map_installed(bsp_file):
        install_result = install_map(bsp_file, delete_source=delete_source)
    else:
        logger.info("Map already is installed")
        install_result = {"status": "skipped", "message": f"Map '{map_name}' is already installed."}

    map_opts = maps_options.get(map_name, {})
    logger.info(f"Options UMC : {map_opts}")
    add_map_to_category(umc_data, category_name, map_name, map_opts)
    logger.info(f"Map Installed : {map_name}")

    return {"map": map_name, "options": map_opts, "install_result": install_result}


def _add_single_existing_map(
    umc_data: dict[str, Any], category_name: str, map_name: str, options: dict[str, Any]
) -> None:
    """Adds an already-installed map to a category (no physical copy/compression)."""
    if map_exists(umc_data, category_name, map_name):
        msg = f"The map '{map_name}' is already present in '{category_name}'."
        raise InstallServiceError(msg)
    add_map_to_category(umc_data, category_name, map_name, options)


# ==========================================================================
# 4. WORKFLOWS PUBLICS (Responsabilité : Transactions et Rapports)
# ==========================================================================


def install_maps_to_umc(
    bsp_files: list[str | Path],
    category_name: str,
    category_options: dict[str, Any] | None = None,
    maps_options: dict[str, Any] | None = None,
    fail_fast: bool = False,
    delete_source: bool = False,
    umc_file: Path | None = None,
) -> dict[str, Any]:
    """Securely manages the entire UMC write transaction (FileLock) for new BSP files."""
    category_options = category_options or {}
    maps_options = maps_options or {}
    target_umc = Path(umc_file) if umc_file else constants.UMC_MASTER_FILE
    lock_file = target_umc.with_suffix(".lock")

    validate_category_name(category_name)

    logger.info(f"Start of installation in the category '{category_name}'")
    report: dict[str, list[Any]] = {"success": [], "errors": []}

    logger.info(f"Acquiring the lock on the UMC file : {lock_file}")

    with FileLock(lock_file, timeout=10):
        logger.info(f"Reading the UMC file : {target_umc}")
        data = parse_umc_file(target_umc)

        if not category_exists(data, category_name):
            logger.info(f"Create category : {category_name}")
            add_category(data, category_name, category_options)
        else:
            logger.info(f"Existing category : {category_name}")

        for bsp_path in map(Path, bsp_files):
            try:
                success_data = _process_single_map_installation(
                    bsp_path, category_name, data, maps_options, delete_source
                )
                report["success"].append(success_data)
            except (CoreError, OSError) as e:
                logger.error(f"{bsp_path.stem} : {e}")
                if fail_fast:
                    msg = f"Installation failed for {bsp_path.stem} : {e}"
                    raise InstallServiceError(msg) from e
                report["errors"].append({"map": bsp_path.stem, "error": str(e)})

        logger.info("Saving the UMC File")
        write(target_umc, data)

    report["summary"] = {"installed": len(report["success"]), "errors": len(report["errors"])}
    logger.info(
        f"Installation Complete - {report['summary']['installed']} map(s) installed, {report['summary']['errors']} error(s)"
    )
    return report


def handle_maps_installation_workflow(
    flask_files: list[Any],
    category_name: str | None,
    category_options_raw: str | None,
    maps_options_raw: str | None,
) -> dict[str, Any]:
    """Coordinates the API entry point by connecting validation, temporary storage, and business logic."""
    logger.info("New installation request received")

    validate_workflow_inputs(flask_files, category_name)

    try:
        category_options = json.loads(category_options_raw or "{}")
        maps_options = json.loads(maps_options_raw or "{}")

        logger.info(f"Target category : {category_name}")
        logger.info(f"Number of BSPs received : {len(flask_files)}")

        with temporary_upload_handler(flask_files) as saved_files:
            logger.info("Launch of the Installation Service")
            result = install_maps_to_umc(
                bsp_files=saved_files,
                category_name=category_name,
                category_options=category_options,
                maps_options=maps_options,
            )

        logger.info(
            f"Installation Complete : {result['summary']['installed']} success, {result['summary']['errors']} error(s)"
        )

    except (CoreError, OSError, ValueError) as e:
        logger.error(f"Installation Error : {e}")
        raise
    else:
        return {"success": True, "result": result}


def add_existing_maps_to_category(
    category_name: str,
    category_options: dict[str, Any],
    maps: dict[str, dict[str, Any]],
    umc_file: Path | None = None,
) -> dict[str, Any]:
    """Adds already-installed maps to a category inside the UMC (no physical copy)."""
    validate_existing_maps_request(category_name, maps)
    target_umc = Path(umc_file) if umc_file else UMC_MASTER_FILE
    lock_file = target_umc.with_suffix(".lock")
    report: dict[str, list[Any]] = {"success": [], "errors": []}
    logger.info(f"Adding existing maps to '{category_name}'")
    with FileLock(lock_file, timeout=10):
        umc_data = parse_umc_file(target_umc)
        if not category_exists(umc_data, category_name):
            logger.info(f"Creating category '{category_name}'")
            add_category(umc_data, category_name, category_options)
        for map_name, options in maps.items():
            try:
                _add_single_existing_map(umc_data, category_name, map_name, options)
                report["success"].append(map_name)
                logger.info(f"Added '{map_name}' to '{category_name}'")
            except (CoreError, OSError) as exc:
                logger.error(exc)
                report["errors"].append({"map": map_name, "error": str(exc)})
        logger.info("Writing UMC file")
        write(target_umc, umc_data)
    report["summary"] = {"added": len(report["success"]), "errors": len(report["errors"])}
    return report


def get_available_categories(selected_maps: list[str]) -> list[str]:
    """Returns the categories in which none of the selected maps are already present."""
    umc_data = parse_umc_file(constants.UMC_MASTER_FILE)
    available_categories = set(umc_data.keys())
    for map_name in selected_maps:
        used_categories = find_map_categories(umc_data, map_name)
        available_categories.difference_update(used_categories)
    return sorted(available_categories)


def get_sorted_categories_workflow() -> list[str]:
    """Retrieves and sorts the list of all existing UMC categories."""
    data = parse_umc_file(UMC_MASTER_FILE)
    return sorted(data.keys())
