"""Workflow for deploying active mods to the live server and cleaning up disabled ones."""

import json
import os
import shutil
from pathlib import Path
from typing import Any

from core import constants
from core.io.cfg_format import generate_overrides
from core.io.json_store import load_config, load_deployed_manifest, save_deployed_manifest
from core.logger_config import logger
from core.utils import format_size

STATIC_ASSET_FOLDERS = ["/materials/", "/models/", "/sound/", "/extensions/", "/gamedata/"]


def clean_server(active_mods: list[str]) -> int:
    """Remove mods files from the server.

    Removes ONLY the files for mods that have been disabled from the server.
    Does not affect mods that are still active.
    """
    deployed_manifest = load_deployed_manifest()
    if not deployed_manifest:
        return 0

    logger.info("[Cleanup] Check for disabled mods to clean up...")
    mods_to_remove = [mod_id for mod_id in deployed_manifest if mod_id not in active_mods]

    total_deleted = 0
    for mod_id in mods_to_remove:
        total_deleted += _purge_mod_files(mod_id, deployed_manifest)

    if total_deleted > 0:
        save_deployed_manifest(deployed_manifest)

    return total_deleted


def deploy_mods(
    specific_mod_id: str | None = None, *, force_full_deploy: bool = False
) -> dict[str, Any]:
    """Differentially deploys configurations and injects/updates active mods."""
    config = load_config()
    load_order: list[str] = config.get("load_order", [])

    if specific_mod_id is None:
        clean_server(load_order)
        deployed_manifest = load_deployed_manifest()
        mods_to_process = load_order
    else:
        deployed_manifest = load_deployed_manifest()
        if specific_mod_id not in load_order:
            logger.info(
                f"[Deployment Solo] Mod {specific_mod_id} is disabled. Cleaning its files from server..."
            )
            files_deleted = _purge_mod_files(specific_mod_id, deployed_manifest)
            save_deployed_manifest(deployed_manifest)

            return {
                "files_copied": 0,
                "files_deleted": files_deleted,
                "bytes_copied": 0,
                "readable_size": "0 B",
                "action": "cleaned",
            }
        mods_to_process = [specific_mod_id]

    logger.info(
        f"[Deployment] Beginning deployment of mods (Mode Force Full : {force_full_deploy})"
    )

    total_files = 0
    total_bytes = 0

    mods_to_process = [specific_mod_id] if specific_mod_id else load_order

    for mod_id in mods_to_process:
        mod_path = constants.REPOSITORY / mod_id
        manifest_path = mod_path / "manifest.json"
        mod_cstrike_path = mod_path / "cstrike"

        if not manifest_path.exists() or not mod_cstrike_path.exists():
            continue

        with manifest_path.open(encoding="utf-8") as f:
            manifest = json.load(f)

        generate_overrides(mod_id, manifest)
        logger.info(f"[Success] Injecting mod : {manifest['name']}")

        old_mod_files = deployed_manifest.get(mod_id, [])

        current_mod_files, files_copied, bytes_copied = _process_mod_files(
            mod_cstrike_path, force_full_deploy
        )
        total_files += files_copied
        total_bytes += bytes_copied

        for file_path in old_mod_files:
            if file_path not in current_mod_files and _remove_server_file(file_path):
                logger.info(f"[Cleanup Delta] Orphaned file removed from server: {file_path}")

        deployed_manifest[mod_id] = current_mod_files

    save_deployed_manifest(deployed_manifest)
    logger.info("[Deployment] End of deployment with success")

    return {
        "files_copied": total_files,
        "files_deleted": 0,
        "bytes_copied": total_bytes,
        "readable_size": format_size(total_bytes),
        "action": "deployed",
    }


def _purge_mod_files(mod_id: str, deployed_manifest: dict[str, list[str]]) -> int:
    """Removes all tracked files of a specific mod from the server and cleans the manifest entry."""
    deleted_count = 0
    if mod_id in deployed_manifest:
        logger.info(f"[Cleanup] Removing files for disabled mod : {mod_id}")
        for rel_path_str in deployed_manifest[mod_id]:
            if _remove_server_file(rel_path_str):
                deleted_count += 1
        del deployed_manifest[mod_id]
    return deleted_count


def _clean_empty_parent_directories(file_path: Path) -> None:
    """Traverse the directory tree and delete parent folders if they are empty."""
    parent = file_path.parent
    while parent != constants.SERVER_CSTRIKE:
        try:
            if not any(parent.iterdir()):
                parent.rmdir()
                parent = parent.parent
            else:
                break
        except OSError:
            break


def _remove_server_file(rel_path_str: str) -> bool:
    """Deletes a single file from the server and cleans up its parent directories if they become empty."""
    server_file = constants.SERVER_ROOT / rel_path_str
    if server_file.exists() and server_file.is_file():
        try:
            server_file.unlink()
            _clean_empty_parent_directories(server_file)
        except OSError as e:
            logger.error(f"Cannot delete {rel_path_str} : {e}")
        else:
            return True

    return False


def _should_copy_file(dest_path: Path, force_full: bool, rel_path_str: str) -> bool:
    """Determines whether a file should be overwritten or ignored if it is a large asset."""
    if not dest_path.exists() or force_full:
        return True
    is_static_asset = any(folder in rel_path_str for folder in STATIC_ASSET_FOLDERS)
    return not is_static_asset


def _process_mod_files(mod_cstrike_path: Path, force_full: bool) -> tuple[list[str], int, int]:
    """Browse the source directory of a mod, copy the eligible files, and return the statistics."""
    current_files = []
    copied_files_count = 0
    copied_bytes_count = 0

    for root, _, files in os.walk(mod_cstrike_path):
        for file in files:
            if file.endswith(".cfg.base"):
                continue

            src_file = Path(root) / file
            rel_path = Path("cstrike") / src_file.relative_to(mod_cstrike_path)
            dest_file = constants.SERVER_ROOT / rel_path
            rel_path_str = rel_path.as_posix()

            current_files.append(rel_path_str)

            if _should_copy_file(dest_file, force_full, rel_path_str):
                dest_file.parent.mkdir(parents=True, exist_ok=True)
                copied_bytes_count += src_file.stat().st_size
                shutil.copy2(src_file, dest_file)
                copied_files_count += 1

    return current_files, copied_files_count, copied_bytes_count
