from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from liverylab_core import package_state, workflow_scopes
from liverylab_core.game_file_writer import GameFileWriter


SAFETY_VERSION = 1
SAFETY_DIR = "liverylab_mp_safety"
SAFETY_JS_NAME = "liverylab_mp_safety.js"
UNSAFE_SKINS_JS_NAME = "liverylab_mp_unsafe_skins.js"
SMOKE_JS_NAME = "liverylab_mp_safety_smoke.js"
SAFETY_SCRIPT_TAG = f"    <script defer src='js/{SAFETY_JS_NAME}'></script>"
UNSAFE_SKINS_SCRIPT_TAG = f"    <script defer src='js/{UNSAFE_SKINS_JS_NAME}'></script>"
SAFETY_SCRIPT_RE = re.compile(r"^[ \t]*<script\b[^>]*liverylab_mp_safety\.js[^>]*></script>[ \t]*\r?\n?", re.IGNORECASE | re.MULTILINE)
UNSAFE_SKINS_SCRIPT_RE = re.compile(r"^[ \t]*<script\b[^>]*liverylab_mp_unsafe_skins\.js[^>]*></script>[ \t]*\r?\n?", re.IGNORECASE | re.MULTILINE)
SMOKE_SCRIPT_RE = re.compile(r"^[ \t]*<script\b[^>]*liverylab_mp_safety_smoke\.js[^>]*></script>[ \t]*\r?\n?", re.IGNORECASE | re.MULTILINE)
TITLE_LOC_KEY = "lblLiveryLabMpJoinUnavailable"
MESSAGE_LOC_KEY = "msgLiveryLabMpJoinUnavailable"
LOCALIZATION_TEXT: dict[str, dict[str, str]] = {
    "en": {
        TITLE_LOC_KEY: "LiveryLab Notice — Multiplayer Join Not Available",
        MESSAGE_LOC_KEY: "This selected livery cannot be used for multiplayer. Select an official AC EVO livery and try again.",
    },
    "de": {
        TITLE_LOC_KEY: "LiveryLab Notice — Multiplayer-Beitritt nicht verfügbar",
        MESSAGE_LOC_KEY: "Diese ausgewählte Lackierung kann im Multiplayer nicht verwendet werden. Wähle eine offizielle AC EVO-Lackierung und versuche es erneut.",
    },
    "es": {
        TITLE_LOC_KEY: "LiveryLab Notice — Unirse al multijugador no disponible",
        MESSAGE_LOC_KEY: "Esta decoración seleccionada no se puede usar en multijugador. Selecciona una decoración oficial de AC EVO e inténtalo de nuevo.",
    },
    "fr": {
        TITLE_LOC_KEY: "LiveryLab Notice — Connexion multijoueur indisponible",
        MESSAGE_LOC_KEY: "Cette livrée sélectionnée ne peut pas être utilisée en multijoueur. Sélectionnez une livrée officielle d'AC EVO et réessayez.",
    },
    "it": {
        TITLE_LOC_KEY: "LiveryLab Notice — Accesso al multiplayer non disponibile",
        MESSAGE_LOC_KEY: "Questa livrea selezionata non può essere usata in multiplayer. Seleziona una livrea ufficiale di AC EVO e riprova.",
    },
    "cn": {
        TITLE_LOC_KEY: "LiveryLab Notice — 无法加入多人游戏",
        MESSAGE_LOC_KEY: "当前选择的涂装无法用于多人游戏。请选择官方 AC EVO 涂装后重试。",
    },
}


SAFETY_JS = r'''(function () {
    "use strict";

    const VERSION = 1;
    const OVERLAY_ID = "liverylab-mp-safety-warning";
    const TITLE_LOC_KEY = "lblLiveryLabMpJoinUnavailable";
    const MESSAGE_LOC_KEY = "msgLiveryLabMpJoinUnavailable";
    const STATE = {
        patchedConnect: false,
        patchedBindControls: false
    };

    function unsafeSkins() {
        return Array.isArray(window.LiveryLabMpUnsafeSkins) ? window.LiveryLabMpUnsafeSkins : [];
    }

    function normalize(value) {
        return String(value || "").trim().toLowerCase();
    }

    function carModel(car) {
        return normalize(car && (car.name || car.carModel || car.model));
    }

    function skinId(car) {
        const preset = car && car.preset;
        if (preset && typeof preset === "object") return normalize(preset.name || preset.id);
        return normalize(car && (car.skinId || car.visual || car.presetName));
    }

    function selectedSummary(car) {
        return {
            carModel: carModel(car),
            skinId: skinId(car),
            displayName: String((car && (car.display_name || car.displayName)) || ""),
            visual: String((car && car.translated && car.translated.visual) || "")
        };
    }

    function findUnsafe(car) {
        const model = carModel(car);
        const skin = skinId(car);
        if (!model || !skin) return null;
        return unsafeSkins().find(function (row) {
            return normalize(row && row.carModel) === model && normalize(row && row.skinId) === skin;
        }) || null;
    }

    function modalText() {
        return {
            titleKey: TITLE_LOC_KEY,
            messageKey: MESSAGE_LOC_KEY,
            title: "LiveryLab Notice — Multiplayer Join Not Available",
            message: "This selected livery cannot be used for multiplayer. Select an official AC EVO livery and try again."
        };
    }

    function showNativeModal(page) {
        const modal = (page && page.pageModal) || document.querySelector("ks-modaldialog");
        if (!modal || typeof modal.Show !== "function") return false;
        const text = modalText();
        try {
            modal.Show({
                type: "warning",
                buttons: 0x100,
                title: text.titleKey,
                message: text.messageKey,
                confirm: "btnReturn",
                autoHide: false
            });
            modal.onConfirm = function () {
                if (typeof modal.Hide === "function") modal.Hide();
            };
            return true;
        } catch (error) {
            console.warn("LiveryLab Safety native modal failed", error);
            return false;
        }
    }

    function ensureOverlay() {
        let overlay = document.getElementById(OVERLAY_ID);
        if (overlay) return overlay;
        const text = modalText();
        overlay = document.createElement("div");
        overlay.id = OVERLAY_ID;
        overlay.style.cssText = [
            "position:fixed",
            "inset:0",
            "z-index:2147483647",
            "display:none",
            "align-items:center",
            "justify-content:center",
            "background:rgba(0,0,0,.72)",
            "font-family:Arial,sans-serif",
            "color:#f4f4f4"
        ].join(";");
        overlay.innerHTML = [
            "<div style='width:min(760px,calc(100vw - 96px));background:#17191f;border:2px solid #d54747;padding:28px;box-shadow:0 18px 60px rgba(0,0,0,.55)'>",
            "<h1 style='margin:0 0 14px 0;font-size:30px;font-weight:700'>" + text.title + "</h1>",
            "<p style='margin:0 0 24px 0;font-size:20px;line-height:1.35'>" + text.message + "</p>",
            "<button id='liverylab-mp-safety-close' type='button' style='min-width:190px;padding:13px 18px;background:#f4f4f4;color:#111;border:0;font-size:18px;font-weight:700;cursor:pointer'>Return</button>",
            "</div>"
        ].join("");
        document.body.appendChild(overlay);
        overlay.querySelector("#liverylab-mp-safety-close").addEventListener("click", function () {
            overlay.style.display = "none";
        });
        return overlay;
    }

    function showBlockedModal(car, context, page) {
        if (showNativeModal(page)) return;
        const overlay = ensureOverlay();
        overlay.style.display = "flex";
    }

    function shouldBlock(car) {
        return !!findUnsafe(car);
    }

    function patchServerList(ServerList) {
        if (!ServerList || !ServerList.prototype) return;
        if (!STATE.patchedConnect && typeof ServerList.prototype.connectToServer === "function") {
            const originalConnect = ServerList.prototype.connectToServer;
            ServerList.prototype.connectToServer = function (isSpectator) {
                const car = this.selectedCar || (window.CurrentCar && window.CurrentCar.model);
                if (shouldBlock(car)) {
                    showBlockedModal(car, isSpectator ? "spectate" : "join", this);
                    return;
                }
                return originalConnect.call(this, isSpectator);
            };
            STATE.patchedConnect = true;
        }
        if (!STATE.patchedBindControls && typeof ServerList.prototype.bindControls === "function") {
            const originalBindControls = ServerList.prototype.bindControls;
            ServerList.prototype.bindControls = function () {
                const result = originalBindControls.apply(this, arguments);
                if (this.btnClipboard && typeof this.btnClipboard.onPressed === "function" && !this.btnClipboard.__liverylabMpSafetyWrapped) {
                    const originalClipboard = this.btnClipboard.onPressed;
                    const page = this;
                    this.btnClipboard.onPressed = function () {
                        const car = page.selectedCar || (window.CurrentCar && window.CurrentCar.model);
                        if (shouldBlock(car)) {
                            showBlockedModal(car, "clipboard", page);
                            return;
                        }
                        return originalClipboard.apply(this, arguments);
                    };
                    this.btnClipboard.__liverylabMpSafetyWrapped = true;
                }
                return result;
            };
            STATE.patchedBindControls = true;
        }
    }

    window.LiveryLabMpSafety = {
        version: VERSION,
        shouldBlock: shouldBlock,
        showBlockedModal: showBlockedModal,
        selectedSummary: selectedSummary
    };

    customElements.whenDefined("ks-page-serverlist").then(function () {
        patchServerList(customElements.get("ks-page-serverlist"));
    }).catch(function (error) {
        console.warn("LiveryLab Safety could not patch server list", error);
    });
})();'''


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def paths(game_root: str | Path) -> dict[str, Path]:
    root = Path(game_root)
    ui_root = root / "uiresources"
    return {
        "root": root,
        "ui": ui_root,
        "multiplayer": ui_root / "multiplayer.html",
        "safety_js": ui_root / "js" / SAFETY_JS_NAME,
        "unsafe_skins_js": ui_root / "js" / UNSAFE_SKINS_JS_NAME,
        "smoke_js": ui_root / "js" / SMOKE_JS_NAME,
        "safety_dir": ui_root / SAFETY_DIR,
        "manifest": ui_root / SAFETY_DIR / "manifest.json",
        "localization": ui_root / "localization",
    }


def unsafe_skins_from_registry(items: list[dict[str, Any]]) -> list[dict[str, str]]:
    seen: set[tuple[str, str]] = set()
    rows: list[dict[str, str]] = []
    for item in items:
        if not isinstance(item, dict):
            continue
        if workflow_scopes.scope_for_entry(item) != workflow_scopes.SCOPE_ADVANCED:
            continue
        car_model = str(item.get("car_model") or "").strip()
        skin_id = str(item.get("skin_id") or "").strip()
        if not car_model or not skin_id:
            continue
        key = (car_model.lower(), skin_id.lower())
        if key in seen:
            continue
        seen.add(key)
        rows.append(
            {
                "carModel": car_model,
                "skinId": skin_id,
                "displayName": str(item.get("display_name") or skin_id).strip(),
                "skinKey": str(item.get("key") or "").strip(),
            }
        )
    rows.sort(key=lambda row: (row["carModel"].lower(), row["skinId"].lower()))
    return rows


def unsafe_skins_script(items: list[dict[str, Any]]) -> str:
    payload = unsafe_skins_from_registry(items)
    return "window.LiveryLabMpUnsafeSkins = " + json.dumps(payload, indent=2, ensure_ascii=False) + ";\n"


def _manifest_payload(p: dict[str, Path]) -> dict[str, Any]:
    if not p["manifest"].exists():
        return {}
    try:
        data = json.loads(p["manifest"].read_text(encoding="utf-8-sig"))
    except (OSError, json.JSONDecodeError):
        return {}
    return data if isinstance(data, dict) else {}


def _script_current(path: Path) -> bool:
    if not path.exists():
        return False
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return f"const VERSION = {SAFETY_VERSION}" in text or f"version: {SAFETY_VERSION}" in text


def _patch_multiplayer_html(html: str) -> str:
    patched = SMOKE_SCRIPT_RE.sub("", html)
    patched = SAFETY_SCRIPT_RE.sub("", patched)
    patched = UNSAFE_SKINS_SCRIPT_RE.sub("", patched)
    tags = UNSAFE_SKINS_SCRIPT_TAG + "\n" + SAFETY_SCRIPT_TAG
    marker = "    <script type=\"module\" src='js/components.js'></script>"
    if marker in patched:
        return patched.replace(marker, marker + "\n" + tags, 1)
    marker_alt = "    <script type=\"module\" src=\"js/components.js\"></script>"
    if marker_alt in patched:
        return patched.replace(marker_alt, marker_alt + "\n" + tags, 1)
    return patched.replace("</head>", f"{tags}\n</head>", 1)


def _loc_path(p: dict[str, Path], locale: str) -> Path:
    return p["localization"] / f"{locale}.loc"


def _loc_has_entry(text: str, key: str, value: str) -> bool:
    expected = f"{key}={value}"
    return any(line.strip() == expected for line in text.splitlines())


def _loc_text_with_entries(text: str, entries: dict[str, str]) -> tuple[str, bool]:
    newline = "\r\n" if "\r\n" in text else "\n"
    lines = text.splitlines()
    changed = False
    for key, value in entries.items():
        prefix = f"{key}="
        new_line = f"{key}={value}"
        for index, line in enumerate(lines):
            if line.startswith(prefix):
                if line != new_line:
                    lines[index] = new_line
                    changed = True
                break
        else:
            lines.append(new_line)
            changed = True
    if not changed:
        return text, False
    return newline.join(lines) + newline, True


def _localization_status(p: dict[str, Path]) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for locale, entries in LOCALIZATION_TEXT.items():
        path = _loc_path(p, locale)
        text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
        rows.append(
            {
                "locale": locale,
                "path": str(path),
                "exists": path.exists(),
                "title": _loc_has_entry(text, TITLE_LOC_KEY, entries[TITLE_LOC_KEY]),
                "message": _loc_has_entry(text, MESSAGE_LOC_KEY, entries[MESSAGE_LOC_KEY]),
            }
        )
    return rows


def _sync_localization_entries(p: dict[str, Path], writer: GameFileWriter) -> list[dict[str, Any]]:
    updates: list[dict[str, Any]] = []
    for locale, entries in LOCALIZATION_TEXT.items():
        path = _loc_path(p, locale)
        text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
        out_text, changed = _loc_text_with_entries(text, entries)
        result = None
        if changed:
            result = writer.write_bytes(
                path,
                out_text.encode("utf-8"),
                label=f"LiveryLab Safety localization ({locale})",
                backup=True,
            )
        updates.append(
            {
                "locale": locale,
                "path": str(path),
                "changed": changed,
                "backup": result.backup if result else "",
            }
        )
    return updates


def status(game_root: str | Path, backup_root: str | Path | None = None) -> dict[str, Any]:
    p = paths(game_root)
    package = package_state.content_package_status(p["root"])
    html = p["multiplayer"].read_text(encoding="utf-8", errors="ignore") if p["multiplayer"].exists() else ""
    manifest = _manifest_payload(p)
    installed_version = manifest.get("version") if isinstance(manifest.get("version"), int) else None
    safety_tag = SAFETY_JS_NAME in html
    unsafe_tag = UNSAFE_SKINS_JS_NAME in html
    smoke_tag = SMOKE_JS_NAME in html
    safety_js_exists = p["safety_js"].exists()
    unsafe_js_exists = p["unsafe_skins_js"].exists()
    manifest_exists = p["manifest"].exists()
    version_current = bool(installed_version == SAFETY_VERSION or (installed_version is None and _script_current(p["safety_js"])))
    localization = _localization_status(p)
    localization_current = all(row["exists"] and row["title"] and row["message"] for row in localization)
    installed = bool(safety_tag and unsafe_tag and safety_js_exists and unsafe_js_exists and manifest_exists and version_current and localization_current)
    requires_cleanup = bool(smoke_tag or p["smoke_js"].exists())
    needs_repair = bool(not installed or requires_cleanup)
    blocked = bool(package.get("activeExists"))
    if blocked:
        label = "Blocked: content.kspkg active"
    elif installed and not requires_cleanup:
        label = "Installed"
    elif safety_tag or unsafe_tag or safety_js_exists or unsafe_js_exists or manifest_exists:
        label = "Repair required"
    elif requires_cleanup:
        label = "Smoke cleanup required"
    else:
        label = "Missing"
    return {
        "schema": 1,
        "version": SAFETY_VERSION,
        "root": str(p["root"]),
        "installed": installed,
        "label": label,
        "needsRepair": needs_repair,
        "blocked": blocked,
        "blockReason": "content.kspkg is active" if blocked else "",
        "contentPackage": package,
        "multiplayerHtmlExists": p["multiplayer"].exists(),
        "multiplayerHasSafetyTag": safety_tag,
        "multiplayerHasUnsafeSkinsTag": unsafe_tag,
        "multiplayerHasSmokeTag": smoke_tag,
        "safetyScriptExists": safety_js_exists,
        "unsafeSkinsScriptExists": unsafe_js_exists,
        "smokeScriptExists": p["smoke_js"].exists(),
        "manifestExists": manifest_exists,
        "installedVersion": installed_version,
        "versionCurrent": version_current,
        "localizationCurrent": localization_current,
        "localization": localization,
        "unsafeSkinCount": int(manifest.get("unsafeSkinCount") or 0),
        "manifestPath": str(p["manifest"]),
        "backupRoot": str(backup_root or ""),
    }


def install_or_repair(game_root: str | Path, unsafe_skin_items: list[dict[str, Any]], backup_root: str | Path | None = None) -> dict[str, Any]:
    p = paths(game_root)
    if not p["multiplayer"].exists():
        raise FileNotFoundError(f"multiplayer.html not found: {p['multiplayer']}")
    writer = GameFileWriter(p["root"], "install LiveryLab Safety", [p["ui"]], backup_root=backup_root)
    html = p["multiplayer"].read_text(encoding="utf-8", errors="ignore")
    patched = _patch_multiplayer_html(html)
    if patched != html:
        writer.write_bytes(p["multiplayer"], patched.encode("utf-8"), label="LiveryLab Safety hook", backup=True)
    writer.write_bytes(p["safety_js"], SAFETY_JS.encode("utf-8"), label="LiveryLab Safety script", backup=True)
    unsafe_rows = unsafe_skins_from_registry(unsafe_skin_items)
    writer.write_bytes(
        p["unsafe_skins_js"],
        ("window.LiveryLabMpUnsafeSkins = " + json.dumps(unsafe_rows, indent=2, ensure_ascii=False) + ";\n").encode("utf-8"),
        label="LiveryLab Safety skin manifest script",
        backup=True,
    )
    loc_updates = _sync_localization_entries(p, writer)
    writer.delete_file(p["smoke_js"], label="old LiveryLab Safety smoke script", backup=True)
    writer.write_bytes(
        p["manifest"],
        json.dumps(
            {
                "schema": 1,
                "version": SAFETY_VERSION,
                "installedAt": _now_iso(),
                "unsafeSkinCount": len(unsafe_rows),
                "localizationLocales": sorted(LOCALIZATION_TEXT),
            },
            indent=2,
        ).encode("utf-8"),
        label="LiveryLab Safety manifest",
        backup=True,
    )
    return {"status": status(game_root, backup_root), "writer": writer.to_dict(), "unsafeSkins": unsafe_rows, "localization": loc_updates}


def refresh_unsafe_skins(game_root: str | Path, unsafe_skin_items: list[dict[str, Any]], backup_root: str | Path | None = None) -> dict[str, Any]:
    p = paths(game_root)
    writer = GameFileWriter(p["root"], "refresh LiveryLab Safety skin manifest", [p["ui"]], backup_root=backup_root)
    unsafe_rows = unsafe_skins_from_registry(unsafe_skin_items)
    writer.write_bytes(
        p["unsafe_skins_js"],
        ("window.LiveryLabMpUnsafeSkins = " + json.dumps(unsafe_rows, indent=2, ensure_ascii=False) + ";\n").encode("utf-8"),
        label="LiveryLab Safety skin manifest script",
        backup=True,
    )
    manifest = _manifest_payload(p)
    if manifest:
        manifest["unsafeSkinCount"] = len(unsafe_rows)
        manifest["refreshedAt"] = _now_iso()
        writer.write_bytes(
            p["manifest"],
            json.dumps(manifest, indent=2).encode("utf-8"),
            label="LiveryLab Safety manifest",
            backup=True,
        )
    return {"status": status(game_root, backup_root), "writer": writer.to_dict(), "unsafeSkins": unsafe_rows}
