from __future__ import annotations

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

from liverylab_core import install_relocation


GRID_PRESETS_DIRNAME = "GridPresets"
GRID_PRESETS_FILENAME = "grid_presets.json"
GRID_PRESETS_SCHEMA = 1
GRID_PREVIEWS_DIRNAME = "previews"


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


def _clean_text(value: Any, limit: int = 240) -> str:
    text = str(value or "").strip()
    if len(text) > limit:
        return text[:limit].rstrip()
    return text


def _slug(value: Any, fallback: str = "grid") -> str:
    text = _clean_text(value, 80).lower()
    text = re.sub(r"[^a-z0-9]+", "_", text).strip("_")
    return text or fallback


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)):
        return [_json_ready(item) for item in value]
    return value


def presets_root(library_root: str | Path) -> Path:
    root = Path(library_root) / GRID_PRESETS_DIRNAME
    root.mkdir(parents=True, exist_ok=True)
    return root


def registry_path(library_root: str | Path) -> Path:
    return presets_root(library_root) / GRID_PRESETS_FILENAME


def previews_root(library_root: str | Path) -> Path:
    root = presets_root(library_root) / GRID_PREVIEWS_DIRNAME
    root.mkdir(parents=True, exist_ok=True)
    return root


def _preview_filename(preset_id: str) -> str:
    return f"{_slug(preset_id, 'grid')}.png"


def _preview_path(library_root: str | Path, preset_id: str, preview_png: str = "") -> Path:
    preview_name = Path(str(preview_png or "")).name.strip() or _preview_filename(preset_id)
    return previews_root(library_root) / preview_name


def _validate_square_png(source: str | Path) -> Path:
    path = Path(source).expanduser()
    if path.suffix.lower() != ".png":
        raise ValueError("Grid preview image must be a PNG file")
    if not path.is_file():
        raise FileNotFoundError(f"Grid preview PNG not found: {path}")
    from PIL import Image

    with Image.open(path) as image:
        width, height = image.size
    if width != height:
        raise ValueError(f"Grid preview PNG must be square. Selected image is {width}x{height}.")
    return path


def _read_registry(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {"schema": GRID_PRESETS_SCHEMA, "items": []}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {"schema": GRID_PRESETS_SCHEMA, "items": []}
    items = data.get("items", []) if isinstance(data, dict) else []
    return {"schema": GRID_PRESETS_SCHEMA, "items": [_normalize_preset(item) for item in items if isinstance(item, dict)]}


def _backup_existing(path: Path) -> str:
    if not path.exists():
        return ""
    backup_dir = path.parent / "_backups"
    backup_dir.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup = backup_dir / f"{path.name}.{stamp}.bak"
    shutil.copy2(path, backup)
    return str(backup)


def _write_registry(path: Path, items: list[dict[str, Any]]) -> dict[str, Any]:
    path.parent.mkdir(parents=True, exist_ok=True)
    backup = _backup_existing(path)
    payload = {
        "schema": GRID_PRESETS_SCHEMA,
        "updatedAt": _now_iso(),
        "items": _json_ready([_normalize_preset(item) for item in items]),
    }
    path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
    return {"path": str(path), "backup": backup}


def _entry_label(entry: dict[str, Any]) -> str:
    return (
        _clean_text(entry.get("displayName") or entry.get("display_name") or entry.get("skinId") or entry.get("skin_id") or "Entry", 120)
    )


def _normalize_ai_vehicle_spec(value: Any) -> str:
    text = str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
    if text in {"endurance", "endu", "endurance_spec", "ll_endurance"}:
        return "endurance"
    return "standard"


def _normalize_entry(entry: dict[str, Any], index: int = 0) -> dict[str, Any]:
    skin_id = _clean_text(entry.get("skinId") or entry.get("skin_id"), 120)
    display_name = _clean_text(entry.get("displayName") or entry.get("display_name") or skin_id or "Skin", 160)
    driver_name = _clean_text(entry.get("driverName") or entry.get("driver_name") or display_name, 160)
    return {
        "id": _clean_text(entry.get("id"), 80) or f"entry_{index + 1:02d}_{uuid.uuid4().hex[:8]}",
        "skinKey": _clean_text(entry.get("skinKey") or entry.get("skin_key"), 240),
        "carModel": _clean_text(entry.get("carModel") or entry.get("car_model"), 120),
        "carContentPath": _clean_text(entry.get("carContentPath") or entry.get("car_content_path"), 240),
        "skinId": skin_id,
        "displayName": display_name,
        "driverName": driver_name,
        "team": _clean_text(entry.get("team"), 160),
        "carNumber": _clean_text(entry.get("carNumber") or entry.get("car_number"), 12),
        "aiVehicleSpec": _normalize_ai_vehicle_spec(entry.get("aiVehicleSpec") or entry.get("ai_vehicle_spec")),
        "thumbnailPngDiskPath": _clean_text(entry.get("thumbnailPngDiskPath") or entry.get("thumbnail_png_disk_path"), 400),
        "notes": _clean_text(entry.get("notes"), 400),
    }


def _normalize_preset(preset: dict[str, Any]) -> dict[str, Any]:
    created = _clean_text(preset.get("createdAt") or preset.get("created_at"), 80) or _now_iso()
    updated = _clean_text(preset.get("updatedAt") or preset.get("updated_at"), 80) or created
    entries = preset.get("entries", [])
    if not isinstance(entries, list):
        entries = []
    name = _clean_text(preset.get("name"), 120) or "New Grid"
    preview_png = Path(str(preset.get("previewPng") or preset.get("preview_png") or "")).name.strip()
    return {
        "id": _clean_text(preset.get("id"), 80) or f"{_slug(name)}_{uuid.uuid4().hex[:8]}",
        "name": name,
        "description": _clean_text(preset.get("description"), 600),
        "previewPng": preview_png,
        "entries": [_normalize_entry(entry, index) for index, entry in enumerate(entries) if isinstance(entry, dict)],
        "createdAt": created,
        "updatedAt": updated,
    }


def _unique_id(items: list[dict[str, Any]], preferred: str) -> str:
    existing = {str(item.get("id") or "").lower() for item in items}
    base = _slug(preferred)
    candidate = base
    counter = 2
    while candidate.lower() in existing:
        candidate = f"{base}_{counter}"
        counter += 1
    return candidate


def _attach_preview_info(library_root: str | Path, preset: dict[str, Any]) -> dict[str, Any]:
    item = dict(preset)
    storage_root = Path(library_root).expanduser().resolve().parent
    entries: list[dict[str, Any]] = []
    for raw_entry in item.get("entries") or []:
        if not isinstance(raw_entry, dict):
            continue
        entry = dict(raw_entry)
        thumbnail = str(entry.get("thumbnailPngDiskPath") or "").strip()
        if thumbnail:
            entry["thumbnailPngDiskPath"] = install_relocation.rebase_managed_path(
                thumbnail,
                storage_root,
                expected_type="file",
            )
        entries.append(entry)
    item["entries"] = entries
    preset_id = str(item.get("id") or "").strip()
    if not preset_id:
        return item
    preview_png = Path(str(item.get("previewPng") or "")).name.strip()
    candidates = []
    if preview_png:
        candidates.append(_preview_path(library_root, preset_id, preview_png))
    candidates.append(_preview_path(library_root, preset_id))
    preview_path = next((path for path in candidates if path.is_file()), None)
    if preview_path:
        item["previewPng"] = preview_path.name
        item["previewPngPath"] = str(preview_path)
    else:
        item["previewPngPath"] = ""
    return item


def list_grid_presets(library_root: str | Path) -> dict[str, Any]:
    path = registry_path(library_root)
    data = _read_registry(path)
    return {
        "schema": GRID_PRESETS_SCHEMA,
        "items": [_attach_preview_info(library_root, item) for item in data["items"]],
        "path": str(path),
        "root": str(path.parent),
    }


def create_grid_preset(library_root: str | Path, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    payload = payload or {}
    path = registry_path(library_root)
    items = _read_registry(path)["items"]
    name = _clean_text(payload.get("name"), 120) or "New Grid"
    now = _now_iso()
    preset = _normalize_preset({
        "id": _unique_id(items, name),
        "name": name,
        "description": payload.get("description", ""),
        "entries": payload.get("entries", []),
        "createdAt": now,
        "updatedAt": now,
    })
    items.append(preset)
    write = _write_registry(path, items)
    return {"item": _attach_preview_info(library_root, preset), "items": [_attach_preview_info(library_root, item) for item in items], **write}


def save_grid_preset(library_root: str | Path, preset: dict[str, Any]) -> dict[str, Any]:
    if not isinstance(preset, dict):
        raise ValueError("Grid preset payload is required")
    path = registry_path(library_root)
    items = _read_registry(path)["items"]
    normalized = _normalize_preset({**preset, "updatedAt": _now_iso()})
    preset_id = str(normalized.get("id") or "").strip()
    if not preset_id:
        raise ValueError("Grid preset id is required")
    found = False
    next_items: list[dict[str, Any]] = []
    for item in items:
        if str(item.get("id") or "").lower() == preset_id.lower():
            normalized["createdAt"] = item.get("createdAt") or normalized["createdAt"]
            next_items.append(normalized)
            found = True
        else:
            next_items.append(item)
    if not found:
        next_items.append(normalized)
    write = _write_registry(path, next_items)
    return {"item": _attach_preview_info(library_root, normalized), "items": [_attach_preview_info(library_root, item) for item in next_items], **write}


def duplicate_grid_preset(library_root: str | Path, preset_id: str) -> dict[str, Any]:
    clean_id = _clean_text(preset_id, 80)
    if not clean_id:
        raise ValueError("Grid preset id is required")
    path = registry_path(library_root)
    items = _read_registry(path)["items"]
    source = next((item for item in items if str(item.get("id") or "").lower() == clean_id.lower()), None)
    if source is None:
        raise FileNotFoundError(f"Grid preset not found: {clean_id}")
    now = _now_iso()
    name = f"{source.get('name') or 'Grid'} Copy"
    duplicate = _normalize_preset({
        **source,
        "id": _unique_id(items, name),
        "name": name,
        "createdAt": now,
        "updatedAt": now,
    })
    source_preview = _preview_path(library_root, source.get("id") or "", source.get("previewPng") or "")
    if source_preview.is_file():
        target_preview = _preview_path(library_root, duplicate["id"])
        target_preview.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source_preview, target_preview)
        duplicate["previewPng"] = target_preview.name
    items.append(duplicate)
    write = _write_registry(path, items)
    return {"item": _attach_preview_info(library_root, duplicate), "items": [_attach_preview_info(library_root, item) for item in items], **write}


def delete_grid_preset(library_root: str | Path, preset_id: str) -> dict[str, Any]:
    clean_id = _clean_text(preset_id, 80)
    if not clean_id:
        raise ValueError("Grid preset id is required")
    path = registry_path(library_root)
    items = _read_registry(path)["items"]
    deleted = next((item for item in items if str(item.get("id") or "").lower() == clean_id.lower()), None)
    kept = [item for item in items if str(item.get("id") or "").lower() != clean_id.lower()]
    if len(kept) == len(items):
        raise FileNotFoundError(f"Grid preset not found: {clean_id}")
    removed_preview = ""
    for candidate in {
        _preview_path(library_root, clean_id, (deleted or {}).get("previewPng") or ""),
        _preview_path(library_root, clean_id),
    }:
        if candidate.is_file():
            candidate.unlink()
            removed_preview = str(candidate)
    write = _write_registry(path, kept)
    return {"deletedId": clean_id, "removedPreview": removed_preview, "items": [_attach_preview_info(library_root, item) for item in kept], **write}


def update_grid_preset_preview_image(library_root: str | Path, preset_id: str, preview_png: str | Path) -> dict[str, Any]:
    clean_id = _clean_text(preset_id, 80)
    if not clean_id:
        raise ValueError("Grid preset id is required")
    source = _validate_square_png(preview_png)
    path = registry_path(library_root)
    items = _read_registry(path)["items"]
    found = False
    next_items: list[dict[str, Any]] = []
    updated: dict[str, Any] | None = None
    for item in items:
        if str(item.get("id") or "").lower() == clean_id.lower():
            target = _preview_path(library_root, item.get("id") or clean_id)
            target.parent.mkdir(parents=True, exist_ok=True)
            if source.resolve() != target.resolve():
                shutil.copy2(source, target)
            updated = _normalize_preset({**item, "previewPng": target.name, "updatedAt": _now_iso()})
            next_items.append(updated)
            found = True
        else:
            next_items.append(item)
    if not found:
        raise FileNotFoundError(f"Grid preset not found: {clean_id}")
    write = _write_registry(path, next_items)
    return {
        "item": _attach_preview_info(library_root, updated or {}),
        "items": [_attach_preview_info(library_root, item) for item in next_items],
        **write,
    }
