from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any


NUMBERPLATE_NONE_SOURCE_TYPES = {"none", "hidden", "off", "no_numberplate", "no-numberplate"}
NUMBERPLATE_STOCK_SOURCE_TYPES = {"acevo", "stock", "default", "game"}

MY_SKIN_DROP_KEYS = {
    "loc_files",
    "numberplate_bake",
    "numberplateBake",
    "numberplate_generated_font",
}
MY_SKIN_PORTABLE_PATH_DROP_KEYS = {
    "game_root",
    "local_root",
}
MY_SKIN_PORTABLE_SNAPSHOT_PATH_DROP_KEYS = {
    "saved_car_path",
    "thumbnail_texture_path",
}
NUMBERPLATE_VARIANT_DROP_KEYS = {
    "template_id",
    "baked_template",
    "published_template_font_path",
    "game_text_fields_by_surface",
}
NUMBERPLATE_GENERATED_PATH_KEYS = {
    "font_path",
    "background_path",
    "hood_background_path",
    "left_background_path",
    "right_background_path",
}
GRID_ENTRY_DROP_KEYS = {
    "thumbnailPngDiskPath",
    "thumbnail_png_disk_path",
}
DRIVER_PROJECT_DROP_KEYS = {
    "assetRoot",
    "profileAssetRoot",
    "profileManifestPath",
    "projectAssetRoot",
    "projectPath",
    "projectRoot",
    "sourcePngPath",
    "sourcePath",
    "texturePath",
    "texturemipsPath",
    "uvTemplatePngPath",
    "previewPngPath",
}
DRIVER_PROFILE_DROP_KEYS = DRIVER_PROJECT_DROP_KEYS | {
    "manifestPath",
    "projectAssetRoot",
    "sourcePngOriginalPath",
    "sourceBlurPngOriginalPath",
    "markPngOriginalPath",
    "diskPath",
    "disk_path",
    "thumbnail_png_disk_path",
    "effective_thumbnail_disk_path",
}
PORTABLE_MANAGED_ROOT_NAMES = {
    "userdata": "UserData",
    "liverylibrary": "LiveryLibrary",
    "liveryprojects": "LiveryProjects",
    "officialliverylibrary": "OfficialLiveryLibrary",
    "officialliveryprojects": "OfficialLiveryProjects",
}
WINDOWS_ABSOLUTE_PATH_RE = re.compile(r"^[A-Za-z]:[\\/]")


def json_ready(value: Any) -> Any:
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, dict):
        return {str(key): json_ready(item) for key, item in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [json_ready(item) for item in value]
    return value


def portable_managed_path(value: str) -> str:
    text = str(value or "")
    normalized = text.replace("\\", "/")
    is_absolute = bool(
        WINDOWS_ABSOLUTE_PATH_RE.match(text)
        or normalized.startswith("/")
        or normalized.startswith("//")
    )
    if not is_absolute:
        return text
    parts = [part for part in normalized.split("/") if part]
    for index, part in enumerate(parts):
        canonical = PORTABLE_MANAGED_ROOT_NAMES.get(part.lower())
        if canonical:
            return "/".join([canonical, *parts[index + 1 :]])
    return text


def clean_portable_managed_paths(value: Any) -> Any:
    if isinstance(value, dict):
        return {str(key): clean_portable_managed_paths(item) for key, item in value.items()}
    if isinstance(value, list):
        return [clean_portable_managed_paths(item) for item in value]
    if isinstance(value, tuple):
        return [clean_portable_managed_paths(item) for item in value]
    if isinstance(value, str):
        return portable_managed_path(value)
    return value


def is_legacy_numberplate_bake_path(path: Path | str) -> bool:
    parts = Path(str(path)).parts
    return any(str(part).lower() == "liverylab_baked" for part in parts)


def _path_is_liverylab_generated_numberplate(value: Any) -> bool:
    text = str(value or "").replace("/", "\\").lower()
    return "\\numberplates\\liverylab_baked\\" in f"\\{text}"


def clean_disabled_numberplate_variant_metadata(value: Any, start_number: Any = "") -> dict[str, Any]:
    if not isinstance(value, dict) or not value:
        return {}
    cleaned = dict(value)
    for key in NUMBERPLATE_VARIANT_DROP_KEYS:
        cleaned.pop(key, None)
    for key in NUMBERPLATE_GENERATED_PATH_KEYS:
        if _path_is_liverylab_generated_numberplate(cleaned.get(key)):
            cleaned.pop(key, None)

    source = str(cleaned.get("source_type") or cleaned.get("numberplate_source") or "").strip().lower()
    if source in NUMBERPLATE_NONE_SOURCE_TYPES or cleaned.get("numberplate_hidden") is True:
        cleaned["source_type"] = "none"
        cleaned["numberplate_source"] = "none"
        cleaned["numberplate_hidden"] = True
        return json_ready(cleaned)
    if source in NUMBERPLATE_STOCK_SOURCE_TYPES:
        cleaned["source_type"] = "acevo"
        cleaned["numberplate_source"] = "acevo"
        cleaned["numberplate_hidden"] = False
        cleaned["owns_actor_numberplate"] = False
        return json_ready(cleaned)

    fallback_start_number = str(cleaned.get("start_number") or start_number or "").strip()
    return {"start_number": fallback_start_number} if fallback_start_number else {}


def _clean_my_skin_files(files: Any) -> Any:
    if not isinstance(files, dict):
        return files
    cleaned = dict(files)
    for key in list(cleaned):
        key_lower = str(key).lower()
        value_lower = str(cleaned.get(key) or "").replace("/", "\\").lower()
        if (
            key_lower == "window_overlay"
            or key_lower.startswith("numberplate_bake")
            or "liverylab_window_overlay" in value_lower
            or "\\numberplates\\liverylab_baked\\" in f"\\{value_lower}"
        ):
            cleaned.pop(key, None)
    return cleaned


def clean_my_skin_entry(entry: dict[str, Any], *, portable_paths: bool = False) -> dict[str, Any]:
    data = dict(entry)
    for key in MY_SKIN_DROP_KEYS:
        data.pop(key, None)
    if portable_paths:
        for key in MY_SKIN_PORTABLE_PATH_DROP_KEYS:
            data.pop(key, None)
        snapshot = data.get("ace_snapshot_preview")
        if isinstance(snapshot, dict):
            cleaned_snapshot = dict(snapshot)
            for key in MY_SKIN_PORTABLE_SNAPSHOT_PATH_DROP_KEYS:
                cleaned_snapshot.pop(key, None)
            if cleaned_snapshot:
                data["ace_snapshot_preview"] = cleaned_snapshot
            else:
                data.pop("ace_snapshot_preview", None)
    if "files" in data:
        data["files"] = _clean_my_skin_files(data.get("files"))

    variant = clean_disabled_numberplate_variant_metadata(data.get("numberplate_variant"), data.get("start_number"))
    if variant:
        data["numberplate_variant"] = variant
    else:
        data.pop("numberplate_variant", None)
    if portable_paths:
        data = clean_portable_managed_paths(data)
    return json_ready(data)


def clean_my_skins_registry_payload(data: Any, *, portable_paths: bool = False) -> Any:
    if isinstance(data, dict):
        cleaned = dict(data)
        rows = cleaned.get("my_skins")
        if isinstance(rows, list):
            cleaned["my_skins"] = [
                clean_my_skin_entry(row, portable_paths=portable_paths)
                for row in rows
                if isinstance(row, dict)
            ]
        return json_ready(cleaned)
    if isinstance(data, list):
        return [
            clean_my_skin_entry(row, portable_paths=portable_paths)
            for row in data
            if isinstance(row, dict)
        ]
    return data


def clean_my_skin_metadata_payload(data: Any, *, portable_paths: bool = False) -> Any:
    if not isinstance(data, dict):
        return data
    cleaned = dict(data)
    skin = cleaned.get("skin")
    if isinstance(skin, dict):
        cleaned["skin"] = clean_my_skin_entry(skin, portable_paths=portable_paths)
        return json_ready(cleaned)
    return clean_my_skin_entry(cleaned, portable_paths=portable_paths)


def clean_grid_presets_payload(data: Any) -> Any:
    if not isinstance(data, dict):
        return data
    cleaned = dict(data)
    items = cleaned.get("items")
    if not isinstance(items, list):
        return json_ready(cleaned)
    next_items: list[dict[str, Any]] = []
    for item in items:
        if not isinstance(item, dict):
            continue
        preset = dict(item)
        entries = preset.get("entries")
        if isinstance(entries, list):
            preset["entries"] = [
                {str(key): value for key, value in entry.items() if str(key) not in GRID_ENTRY_DROP_KEYS}
                for entry in entries
                if isinstance(entry, dict)
            ]
        next_items.append(preset)
    cleaned["items"] = next_items
    return json_ready(cleaned)


def clean_driver_project_payload(data: Any) -> Any:
    if isinstance(data, dict):
        return {
            str(key): clean_driver_project_payload(value)
            for key, value in data.items()
            if str(key) not in DRIVER_PROJECT_DROP_KEYS
        }
    if isinstance(data, list):
        return [clean_driver_project_payload(item) for item in data]
    return json_ready(data)


def clean_driver_profile_payload(data: Any) -> Any:
    if isinstance(data, dict):
        return {
            str(key): clean_driver_profile_payload(value)
            for key, value in data.items()
            if str(key) not in DRIVER_PROFILE_DROP_KEYS
        }
    if isinstance(data, list):
        return [clean_driver_profile_payload(item) for item in data]
    return json_ready(data)


def clean_json_payload_for_archive(archive_name: str, data: Any, *, portable_paths: bool = False) -> Any:
    name = str(archive_name).replace("/", "\\").lower()
    if name.endswith("\\my_skins.json") or name == "my_skins.json":
        cleaned = clean_my_skins_registry_payload(data, portable_paths=portable_paths)
    elif name.endswith("\\liverylab_skin.json") or name == "liverylab_skin.json":
        cleaned = clean_my_skin_metadata_payload(data, portable_paths=portable_paths)
    elif name.endswith("\\grid_presets.json") or name == "grid_presets.json":
        cleaned = clean_grid_presets_payload(data)
    elif name.endswith("\\driver_project.json") or name == "driver_project.json":
        cleaned = clean_driver_project_payload(data)
    elif "\\drivers\\" in f"\\{name}" and name.endswith("\\manifest.json"):
        cleaned = clean_driver_profile_payload(data)
    else:
        cleaned = data
    return clean_portable_managed_paths(cleaned) if portable_paths else cleaned


def rewrite_json_file_for_archive(path: Path, *, portable_paths: bool = False) -> bool:
    if not path.exists() or path.suffix.lower() != ".json":
        return False
    try:
        original = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return False
    cleaned = clean_json_payload_for_archive(str(path), original, portable_paths=portable_paths)
    if cleaned == original:
        return False
    path.write_text(json.dumps(json_ready(cleaned), indent=2, ensure_ascii=False), encoding="utf-8")
    return True
