from __future__ import annotations

import base64
import json
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any

from . import install_relocation


NUMBERPLATES_DIRNAME = "Numberplates"
TEMPLATES_DIRNAME = "templates"
TEMPLATE_MANIFEST_FILENAME = "manifest.json"
TEMPLATE_PREVIEW_FILENAME = "preview.png"
DEFAULT_TEXT_LAYER = {"x": 0.0, "y": 0.0, "scale": 0.3, "spacing": 0.02}
DEFAULT_TEXT_COLOR = [1.0, 1.0, 1.0, 1.0]


def _norm_slashes(value: str | Path) -> str:
    return str(value or "").replace("/", "\\")


def _clean_template_id(value: str) -> str:
    cleaned = re.sub(r"[^a-zA-Z0-9_.-]+", "_", str(value or "").strip()).strip("._-").lower()
    if not cleaned:
        raise ValueError("Template name is required")
    return cleaned[:80]


def _template_root(library_root: str | Path) -> Path:
    root = Path(library_root).expanduser().resolve()
    return root / NUMBERPLATES_DIRNAME / TEMPLATES_DIRNAME


def _resolve_template_asset_source(template_dir: Path, value: Any, asset_type: str) -> Any:
    if not isinstance(value, dict):
        return value
    result = dict(value)
    numberplate_root = template_dir.parent.parent
    asset_id = str(result.get("assetId") or result.get("id") or "").strip()
    if asset_id and Path(asset_id).name == asset_id:
        bucket = "fonts" if asset_type == "font" else "backgrounds"
        managed_source = numberplate_root / bucket / asset_id / "source.png"
        if managed_source.is_file():
            result["sourcePngPath"] = str(managed_source)
            result.pop("pendingPngPath", None)
            return result
    source_path = str(
        result.get("sourcePngPath")
        or result.get("pendingPngPath")
        or result.get("sourcePng")
        or ""
    ).strip()
    if source_path:
        storage_root = numberplate_root.parent.parent
        rebased = install_relocation.rebase_managed_path(
            source_path,
            storage_root,
            expected_type="file",
        )
        if rebased != source_path:
            result["sourcePngPath"] = rebased
            if result.get("pendingPngPath"):
                result["pendingPngPath"] = rebased
    return result


def _resolve_template_asset_sources(template_dir: Path, manifest: dict[str, Any]) -> None:
    backgrounds = manifest.get("backgrounds")
    if isinstance(backgrounds, dict):
        manifest["backgrounds"] = {
            str(key): _resolve_template_asset_source(template_dir, value, "background")
            for key, value in backgrounds.items()
        }
    if isinstance(manifest.get("font"), dict):
        manifest["font"] = _resolve_template_asset_source(template_dir, manifest["font"], "font")


def _read_template_manifest(template_dir: Path) -> dict[str, Any] | None:
    manifest_path = template_dir / TEMPLATE_MANIFEST_FILENAME
    if not manifest_path.exists():
        return None
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except Exception as exc:
        return {
            "formatVersion": 1,
            "type": "template",
            "id": template_dir.name,
            "name": template_dir.name,
            "status": "invalid",
            "error": str(exc),
            "assetDir": str(template_dir),
            "manifestPath": str(manifest_path),
        }
    if not isinstance(manifest, dict):
        return None
    manifest.setdefault("formatVersion", 1)
    manifest.setdefault("type", "template")
    manifest.setdefault("id", template_dir.name)
    manifest.setdefault("name", manifest.get("id") or template_dir.name)
    manifest.setdefault("status", "local")
    _resolve_template_asset_sources(template_dir, manifest)
    manifest["assetDir"] = str(template_dir)
    manifest["manifestPath"] = str(manifest_path)
    preview_name = str(manifest.get("previewPng") or "").strip()
    preview_path = template_dir / preview_name if preview_name else template_dir / TEMPLATE_PREVIEW_FILENAME
    if preview_path.exists():
        manifest.setdefault("previewPng", preview_path.name)
        manifest["previewPngPath"] = str(preview_path)
    return manifest


def _write_template_manifest(template_dir: Path, manifest: dict[str, Any]) -> Path:
    template_dir.mkdir(parents=True, exist_ok=True)
    manifest_path = template_dir / TEMPLATE_MANIFEST_FILENAME
    manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
    return manifest_path


def _write_template_preview(template_dir: Path, data_url: str) -> Path | None:
    value = str(data_url or "").strip()
    if not value:
        return None
    match = re.match(r"^data:image/png;base64,(?P<data>[A-Za-z0-9+/=\s]+)$", value)
    if not match:
        raise ValueError("Numberplate template preview must be a PNG data URL")
    raw = base64.b64decode(re.sub(r"\s+", "", match.group("data")), validate=True)
    if not raw.startswith(b"\x89PNG\r\n\x1a\n"):
        raise ValueError("Numberplate template preview data is not a PNG")
    template_dir.mkdir(parents=True, exist_ok=True)
    preview_path = template_dir / TEMPLATE_PREVIEW_FILENAME
    preview_path.write_bytes(raw)
    return preview_path


def _coerce_asset_ref(value: Any) -> dict[str, str]:
    if isinstance(value, dict):
        asset_id = str(value.get("assetId") or value.get("id") or "").strip()
        rel = str(value.get("rel") or value.get("assetRel") or value.get("textureRel") or value.get("path") or "").strip()
        source_png = str(value.get("sourcePngPath") or value.get("pendingPngPath") or value.get("sourcePng") or "").strip()
        label = str(value.get("label") or value.get("name") or asset_id or rel or Path(source_png).name).strip()
        asset_type = str(value.get("type") or "").strip()
        source = str(value.get("source") or ("png" if source_png and not asset_id else "")).strip()
    else:
        asset_id = ""
        rel = str(value or "").strip()
        label = rel
        asset_type = ""
        source_png = ""
        source = ""
    result = {
        "assetId": asset_id,
        "rel": _norm_slashes(rel),
        "label": label,
        "type": asset_type,
    }
    if source_png:
        result["sourcePngPath"] = source_png
    if source:
        result["source"] = source
    return result


def _coerce_text_layer(value: Any, fallback: dict[str, float] | None = None) -> dict[str, float]:
    source = value if isinstance(value, dict) else {}
    base = fallback or DEFAULT_TEXT_LAYER

    def numeric(key: str, lo: float, hi: float) -> float:
        raw = source.get(key, base.get(key, DEFAULT_TEXT_LAYER[key]))
        try:
            number = float(raw)
        except Exception:
            number = float(DEFAULT_TEXT_LAYER[key])
        return max(lo, min(hi, number))

    return {
        "x": numeric("x", -1.0, 1.0),
        "y": numeric("y", -1.0, 1.0),
        "scale": numeric("scale", 0.05, 1.0),
        "spacing": numeric("spacing", -0.1, 0.5),
    }


def _coerce_text_color(value: Any, fallback: list[float] | None = None) -> list[float]:
    source = value if isinstance(value, (list, tuple)) else fallback or DEFAULT_TEXT_COLOR
    values: list[float] = []
    for index in range(4):
        try:
            number = float(source[index])
        except Exception:
            number = float(DEFAULT_TEXT_COLOR[index])
        values.append(max(0.0, min(1.0, number)))
    return values


def _coerce_template_payload(payload: dict[str, Any], existing: dict[str, Any] | None = None) -> dict[str, Any]:
    existing = existing if isinstance(existing, dict) else {}
    name = str(payload.get("name") or payload.get("id") or existing.get("name") or "").strip()
    template_id = _clean_template_id(str(payload.get("id") or name))
    now = datetime.now().isoformat(timespec="seconds")
    backgrounds = payload.get("backgrounds") if isinstance(payload.get("backgrounds"), dict) else {}
    surface_layers = payload.get("surfaceTextLayers") if isinstance(payload.get("surfaceTextLayers"), dict) else {}
    text_layer = _coerce_text_layer(payload.get("textLayer") or existing.get("textLayer"))
    text_color = _coerce_text_color(payload.get("textColor") or payload.get("text_color"), existing.get("textColor") or existing.get("text_color"))

    result = {
        "formatVersion": 1,
        "type": "template",
        "id": template_id,
        "name": name or template_id,
        "status": "local",
        "createdAt": existing.get("createdAt") or now,
        "updatedAt": now,
        "backgrounds": {
            "leftDoor": _coerce_asset_ref(backgrounds.get("leftDoor") or backgrounds.get("left") or existing.get("backgrounds", {}).get("leftDoor")),
            "rightDoor": _coerce_asset_ref(backgrounds.get("rightDoor") or backgrounds.get("right") or existing.get("backgrounds", {}).get("rightDoor")),
            "hood": _coerce_asset_ref(backgrounds.get("hood") or existing.get("backgrounds", {}).get("hood")),
        },
        "font": _coerce_asset_ref(payload.get("font") or existing.get("font")),
        "fontSource": str(payload.get("fontSource") or existing.get("fontSource") or "").strip(),
        "evoFontRel": _norm_slashes(str(payload.get("evoFontRel") or existing.get("evoFontRel") or "").strip()),
        "textLayer": text_layer,
        "textColor": text_color,
        "text_color": text_color,
        "surfaceTextLayers": {
            key: _coerce_text_layer(layer, text_layer)
            for key, layer in surface_layers.items()
            if key in {"leftDoor", "rightDoor", "hood"} and isinstance(layer, dict)
        },
        "testNumber": re.sub(r"\D+", "", str(payload.get("testNumber") or existing.get("testNumber") or "250"))[:3] or "250",
    }
    for key in ("publishedFont", "gameFont", "publishedAt"):
        source = payload.get(key) if key in payload else existing.get(key)
        if source:
            result[key] = source
    preview_png = str(payload.get("previewPng") or existing.get("previewPng") or "").strip()
    if preview_png:
        result["previewPng"] = Path(preview_png).name
    return result


def list_numberplate_studio_templates(library_root: str | Path) -> dict[str, Any]:
    root = Path(library_root).expanduser().resolve()
    template_root = _template_root(root)
    template_root.mkdir(parents=True, exist_ok=True)
    templates: list[dict[str, Any]] = []
    for template_dir in sorted((path for path in template_root.iterdir() if path.is_dir()), key=lambda path: path.name.lower()):
        manifest = _read_template_manifest(template_dir)
        if manifest:
            templates.append(manifest)
    templates.sort(key=lambda row: str(row.get("name") or row.get("id") or "").lower())
    return {
        "libraryRoot": str(root),
        "numberplateRoot": str(root / NUMBERPLATES_DIRNAME),
        "templateRoot": str(template_root),
        "templates": templates,
    }


def get_numberplate_studio_template(library_root: str | Path, template_id: str) -> dict[str, Any] | None:
    root = Path(library_root).expanduser().resolve()
    template_root = _template_root(root)
    clean_id = _clean_template_id(template_id)
    template_dir = (template_root / clean_id).resolve()
    try:
        template_dir.relative_to(template_root.resolve())
    except ValueError as exc:
        raise ValueError("Invalid numberplate template id") from exc
    if not template_dir.exists():
        return None
    return _read_template_manifest(template_dir)


def save_numberplate_studio_template(library_root: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
    root = Path(library_root).expanduser().resolve()
    template_root = _template_root(root)
    initial_id = str(payload.get("id") or payload.get("name") or "").strip()
    template_id = _clean_template_id(initial_id)
    template_dir = (template_root / template_id).resolve()
    try:
        template_dir.relative_to(template_root.resolve())
    except ValueError as exc:
        raise ValueError("Invalid numberplate template id") from exc
    existing = _read_template_manifest(template_dir) or {}
    manifest = _coerce_template_payload(payload, existing)
    preview_path = _write_template_preview(template_dir, str(payload.get("previewDataUrl") or ""))
    if preview_path:
        manifest["previewPng"] = preview_path.name
    manifest_path = _write_template_manifest(template_dir, manifest)
    if preview_path:
        manifest["previewPngPath"] = str(preview_path)
    return {
        **manifest,
        "assetDir": str(template_dir),
        "manifestPath": str(manifest_path),
    }


def delete_numberplate_studio_template(library_root: str | Path, template_id: str) -> dict[str, Any]:
    root = Path(library_root).expanduser().resolve()
    template_root = _template_root(root)
    clean_id = _clean_template_id(template_id)
    template_dir = (template_root / clean_id).resolve()
    try:
        template_dir.relative_to(template_root.resolve())
    except ValueError as exc:
        raise ValueError("Invalid numberplate template id") from exc
    if not template_dir.exists():
        raise FileNotFoundError(f"Numberplate template not found: {clean_id}")
    trash_root = template_root / "_trash"
    trash_root.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
    trash_dir = trash_root / f"{clean_id}.{stamp}"
    shutil.move(str(template_dir), str(trash_dir))
    return {
        "id": clean_id,
        "deleted": True,
        "trashDir": str(trash_dir),
    }
