from __future__ import annotations

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

from liverylab_core.game_file_writer import GameFileWriter
from liverylab_core.proto import ProtoField, build_fields, is_printable_utf8, parse_fields, patch_strings_recursive


AI_SPEC_STANDARD = "standard"
AI_SPEC_ENDURANCE = "endurance"
AI_SPEC_VALUES = {AI_SPEC_STANDARD, AI_SPEC_ENDURANCE}
MANIFEST_FILENAME = "ai_vehicle_specs.json"
CLONE_SUFFIX = "_liverylab_endurance"
CLONE_NAME = "LiveryLab Endurance"
CLONE_SHORT_NAME = "LL Endurance"


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


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


def split_content_path(path: str) -> list[str]:
    return [part for part in norm_slashes(path).split("\\") if part]


def normalize_game_rel_path(value: str) -> str:
    return norm_slashes(str(value or "").strip())


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


def _slug(value: Any, fallback: str = "item") -> str:
    text = _clean_text(value, 120).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, set)):
        return [_json_ready(item) for item in value]
    return value


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 AI_SPEC_ENDURANCE
    return AI_SPEC_STANDARD


def manifest_path(library_root: str | Path) -> Path:
    root = Path(library_root) / "GridPresets"
    root.mkdir(parents=True, exist_ok=True)
    return root / MANIFEST_FILENAME


def _read_manifest(library_root: str | Path) -> dict[str, Any]:
    path = manifest_path(library_root)
    if not path.exists():
        return {"schema": 1, "resources": {}, "activeGridId": "", "activeGridName": ""}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {"schema": 1, "resources": {}, "activeGridId": "", "activeGridName": ""}
    if not isinstance(data, dict):
        return {"schema": 1, "resources": {}, "activeGridId": "", "activeGridName": ""}
    resources = data.get("resources") if isinstance(data.get("resources"), dict) else {}
    data["resources"] = {str(key): value for key, value in resources.items() if isinstance(value, dict)}
    data["schema"] = 1
    return data


def _write_manifest(library_root: str | Path, manifest: dict[str, Any]) -> Path:
    path = manifest_path(library_root)
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = dict(manifest)
    payload["schema"] = 1
    payload["updatedAt"] = _now_iso()
    path.write_text(json.dumps(_json_ready(payload), indent=2, ensure_ascii=False), encoding="utf-8")
    return path


def content_to_disk(root: str | Path, rel: str) -> Path:
    root_path = Path(root)
    parts = split_content_path(rel)
    if parts and parts[0].lower() == "content" and root_path.name.lower() == "content":
        return root_path / Path(*parts[1:])
    return root_path / Path(*parts)


def resolve_content_path(root: str | Path, rel: str) -> Path:
    root_path = Path(root)
    parts = split_content_path(rel)
    candidates = [root_path / Path(*parts)]
    if parts and parts[0].lower() == "content":
        candidates.append(root_path / Path(*parts[1:]))
        candidates.append(root_path.parent / Path(*parts))
    for candidate in candidates:
        if candidate.exists():
            return candidate
    return candidates[0]


def game_rel_from_disk(root: str | Path, path: Path | None) -> str:
    if path is None:
        return ""
    root_path = Path(root)
    try:
        return norm_slashes(str(path.resolve().relative_to(root_path.resolve())))
    except Exception:
        return ""


def _collect_proto_strings(raw: bytes, prefix: str = "", depth: int = 0, max_depth: int = 8) -> list[tuple[str, str]]:
    if depth > max_depth:
        return []
    found: list[tuple[str, str]] = []
    try:
        fields = parse_fields(raw)
    except Exception:
        return found
    for field in fields:
        path = f"{prefix}.{field.number}" if prefix else str(field.number)
        if field.wire != 2:
            continue
        text = is_printable_utf8(field.value)
        if text is not None:
            found.append((path, text))
            continue
        found.extend(_collect_proto_strings(bytes(field.value), path, depth + 1, max_depth))
    return found


def _actor_list_resources(actor_data: bytes, field_number: int, suffix: str | None = None) -> list[str]:
    out: list[str] = []
    for field in parse_fields(actor_data):
        if field.number != 103 or field.wire != 2:
            continue
        for payload_field in parse_fields(field.value):
            if payload_field.number != 13 or payload_field.wire != 2:
                continue
            for resource_field in parse_fields(payload_field.value):
                if resource_field.number == field_number and resource_field.wire == 2:
                    text = is_printable_utf8(resource_field.value)
                    if text and (suffix is None or text.lower().endswith(suffix.lower())):
                        out.append(norm_slashes(text))
    return out


def _actor_add_resource_data(actor_data: bytes, new_rel: str, field_number: int) -> tuple[bytes, bool]:
    normalized = normalize_game_rel_path(new_rel)
    actor_fields = parse_fields(actor_data)
    changed = False
    new_actor_fields: list[ProtoField] = []
    for field in actor_fields:
        if field.number != 103 or field.wire != 2:
            new_actor_fields.append(field)
            continue
        payload_fields = parse_fields(field.value)
        new_payload_fields: list[ProtoField] = []
        patched = False
        for payload_field in payload_fields:
            if payload_field.number != 13 or payload_field.wire != 2:
                new_payload_fields.append(payload_field)
                continue
            resource_fields = parse_fields(payload_field.value)
            existing = [
                norm_slashes(text)
                for text in (is_printable_utf8(resource.value) for resource in resource_fields if resource.number == field_number and resource.wire == 2)
                if text
            ]
            if normalized in existing:
                new_payload_fields.append(payload_field)
                patched = True
                continue
            insert_at = None
            for index, resource in enumerate(resource_fields):
                if resource.number == field_number and resource.wire == 2:
                    insert_at = index + 1
            new_field = ProtoField(field_number, 2, normalized.encode("utf-8"))
            if insert_at is None:
                resource_fields.append(new_field)
            else:
                resource_fields.insert(insert_at, new_field)
            new_payload_fields.append(ProtoField(payload_field.number, payload_field.wire, build_fields(resource_fields)))
            changed = True
            patched = True
        new_actor_fields.append(ProtoField(field.number, field.wire, build_fields(new_payload_fields)) if patched else field)
    return (build_fields(new_actor_fields), True) if changed else (actor_data, False)


def _actor_remove_resource_data(actor_data: bytes, rel_to_remove: str, field_number: int) -> tuple[bytes, bool]:
    target = normalize_game_rel_path(rel_to_remove).lower()
    actor_fields = parse_fields(actor_data)
    changed = False
    new_actor_fields: list[ProtoField] = []
    for field in actor_fields:
        if field.number != 103 or field.wire != 2:
            new_actor_fields.append(field)
            continue
        payload_fields = parse_fields(field.value)
        new_payload_fields: list[ProtoField] = []
        patched = False
        for payload_field in payload_fields:
            if payload_field.number != 13 or payload_field.wire != 2:
                new_payload_fields.append(payload_field)
                continue
            kept: list[ProtoField] = []
            removed = False
            for resource in parse_fields(payload_field.value):
                if resource.number == field_number and resource.wire == 2:
                    text = is_printable_utf8(resource.value)
                    if text is not None and normalize_game_rel_path(text).lower() == target:
                        changed = True
                        removed = True
                        continue
                kept.append(resource)
            if removed:
                new_payload_fields.append(ProtoField(payload_field.number, payload_field.wire, build_fields(kept)))
                patched = True
            else:
                new_payload_fields.append(payload_field)
        new_actor_fields.append(ProtoField(field.number, field.wire, build_fields(new_payload_fields)) if patched else field)
    return (build_fields(new_actor_fields), True) if changed else (actor_data, False)


def _extract_mechanical_info(data: bytes) -> dict[str, Any]:
    info: dict[str, Any] = {"manufacturer": "", "name": "", "shortName": "", "visuals": [], "field9": [], "field10": []}
    for field in parse_fields(data):
        if field.wire != 2:
            continue
        text = is_printable_utf8(field.value)
        if text is None:
            continue
        if field.number == 1:
            info["manufacturer"] = text
        elif field.number == 2:
            info["name"] = text
        elif field.number == 3:
            info["shortName"] = text
        elif field.number == 8:
            info["visuals"].append(norm_slashes(text))
        elif field.number == 9:
            info["field9"].append(norm_slashes(text))
        elif field.number == 10:
            info["field10"].append(norm_slashes(text))
    return info


def _insert_after_last(fields: list[ProtoField], field_number: int, new_fields: list[ProtoField]) -> list[ProtoField]:
    if not new_fields:
        return fields
    insert_at = None
    for index, field in enumerate(fields):
        if field.number == field_number:
            insert_at = index + 1
    if insert_at is None:
        fields.extend(new_fields)
    else:
        for offset, field in enumerate(new_fields):
            fields.insert(insert_at + offset, field)
    return fields


def _clone_mechanical_data(base_data: bytes, visual_rels: list[str], lumirank_rel: str, bumper_rel: str) -> bytes:
    visual_values = [normalize_game_rel_path(value) for value in visual_rels if value]
    visual_seen: set[str] = set()
    visual_fields = []
    for visual in visual_values:
        key = visual.lower()
        if key in visual_seen:
            continue
        visual_seen.add(key)
        visual_fields.append(ProtoField(8, 2, visual.encode("utf-8")))

    fields = parse_fields(base_data)
    next_fields: list[ProtoField] = []
    visual_insert_at = None
    skipped_visuals = False
    for field in fields:
        if field.number == 8 and field.wire == 2:
            if visual_insert_at is None:
                visual_insert_at = len(next_fields)
            skipped_visuals = True
            continue
        if field.number == 2 and field.wire == 2:
            base_name = is_printable_utf8(field.value) or ""
            name = f"{base_name} {CLONE_NAME}".strip() if CLONE_NAME.lower() not in base_name.lower() else base_name
            next_fields.append(ProtoField(field.number, field.wire, name.encode("utf-8")))
            continue
        if field.number == 3 and field.wire == 2:
            next_fields.append(ProtoField(field.number, field.wire, CLONE_SHORT_NAME.encode("utf-8")))
            continue
        next_fields.append(field)

    if visual_insert_at is None:
        visual_insert_at = next((index + 1 for index, field in enumerate(next_fields) if field.number == 5), len(next_fields))
    if skipped_visuals or visual_fields:
        for offset, field in enumerate(visual_fields):
            next_fields.insert(visual_insert_at + offset, field)

    existing9 = {norm_slashes(text).lower() for text in (is_printable_utf8(field.value) for field in next_fields if field.number == 9 and field.wire == 2) if text}
    existing10 = {norm_slashes(text).lower() for text in (is_printable_utf8(field.value) for field in next_fields if field.number == 10 and field.wire == 2) if text}
    if lumirank_rel and normalize_game_rel_path(lumirank_rel).lower() not in existing9:
        next_fields = _insert_after_last(next_fields, 9, [ProtoField(9, 2, normalize_game_rel_path(lumirank_rel).encode("utf-8"))])
    if bumper_rel and normalize_game_rel_path(bumper_rel).lower() not in existing10:
        next_fields = _insert_after_last(next_fields, 10, [ProtoField(10, 2, normalize_game_rel_path(bumper_rel).encode("utf-8"))])
    return build_fields(next_fields)


def _edit_string_fields_by_number(raw: bytes, replacements: dict[int, str | None]) -> bytes:
    changed = False
    next_fields: list[ProtoField] = []
    for field in parse_fields(raw):
        replacement = replacements.get(field.number)
        if field.wire == 2 and replacement is not None:
            current = is_printable_utf8(field.value)
            if current is not None and current != replacement:
                next_fields.append(ProtoField(field.number, field.wire, replacement.encode("utf-8")))
                changed = True
                continue
        next_fields.append(field)
    return build_fields(next_fields) if changed else raw


def _edit_carfinalstate_data(data: bytes, mechanical_preset: str, visual_preset: str) -> bytes:
    normalized_mech = normalize_game_rel_path(mechanical_preset)
    normalized_visual = normalize_game_rel_path(visual_preset)
    changed = False
    next_fields: list[ProtoField] = []
    for field in parse_fields(data):
        if field.number == 10 and field.wire == 2:
            nested = _edit_string_fields_by_number(bytes(field.value), {1: normalized_mech, 2: normalized_visual})
            if nested != bytes(field.value):
                next_fields.append(ProtoField(field.number, field.wire, nested))
                changed = True
            else:
                next_fields.append(field)
            continue
        if field.wire == 2:
            patched = patch_strings_recursive(bytes(field.value), {})
            next_fields.append(ProtoField(field.number, field.wire, patched) if patched != bytes(field.value) else field)
            continue
        next_fields.append(field)
    return build_fields(next_fields) if changed else data


def _extract_finalstate_info(data: bytes) -> dict[str, str]:
    info = {"mechanical": "", "visual": ""}
    for field in parse_fields(data):
        if field.number != 10 or field.wire != 2:
            continue
        for subfield in parse_fields(field.value):
            if subfield.wire != 2:
                continue
            text = is_printable_utf8(subfield.value)
            if not text:
                continue
            if subfield.number == 1:
                info["mechanical"] = norm_slashes(text)
            elif subfield.number == 2:
                info["visual"] = norm_slashes(text)
    return info


def _part_entry_rel(raw: bytes) -> str:
    try:
        for field in parse_fields(raw):
            if field.number == 1 and field.wire == 2:
                return norm_slashes(is_printable_utf8(field.value) or "")
    except Exception:
        return ""
    return ""


def _uuid101(value: uuid.UUID) -> bytes:
    high = (value.int >> 64) & ((1 << 64) - 1)
    low = value.int & ((1 << 64) - 1)
    return build_fields([ProtoField(1, 0, high), ProtoField(2, 0, low)])


def _make_finalstate_part_field(part_rel: str, slot: int, unique_seed: str) -> ProtoField:
    normalized = normalize_game_rel_path(part_rel)
    generated_uuid = uuid.uuid5(uuid.NAMESPACE_URL, f"liverylab-ai-spec-v1|{unique_seed}|{normalized}")
    payload = build_fields(
        [
            ProtoField(1, 2, normalized.encode("utf-8")),
            ProtoField(100, 2, str(generated_uuid).encode("utf-8")),
            ProtoField(101, 2, _uuid101(generated_uuid)),
            ProtoField(102, 0, int(slot)),
        ]
    )
    return ProtoField(30, 2, payload)


def _ensure_finalstate_parts(data: bytes, parts: list[dict[str, Any]], unique_seed: str) -> bytes:
    targets = {normalize_game_rel_path(str(part.get("partRel") or "")).lower() for part in parts if part.get("partRel")}
    existing = set()
    next_fields: list[ProtoField] = []
    for field in parse_fields(data):
        if field.number == 30 and field.wire == 2:
            part_rel = _part_entry_rel(bytes(field.value))
            if part_rel and part_rel.lower() in targets:
                continue
            if part_rel:
                existing.add(part_rel.lower())
        next_fields.append(field)
    for part in parts:
        part_rel = normalize_game_rel_path(str(part.get("partRel") or ""))
        if not part_rel or part_rel.lower() in existing:
            continue
        next_fields.append(_make_finalstate_part_field(part_rel, int(part.get("slot") or 0), unique_seed))
    return build_fields(next_fields)


def _carpart_rel_from_compatible(root: Path, part_rel: str) -> str:
    path = resolve_content_path(root, part_rel)
    if not path.exists():
        raise FileNotFoundError(f"Compatible part not found: {part_rel}")
    for field in parse_fields(path.read_bytes()):
        if field.number == 1 and field.wire == 2:
            text = is_printable_utf8(field.value)
            if text:
                return norm_slashes(text)
    raise RuntimeError(f"Compatible part has no carpart reference: {part_rel}")


def _carpart_slot(root: Path, carpart_rel: str) -> int:
    path = resolve_content_path(root, carpart_rel)
    if not path.exists():
        raise FileNotFoundError(f"Car part not found: {carpart_rel}")
    for field in parse_fields(path.read_bytes()):
        if field.number == 3 and field.wire == 0:
            return int(field.value)
    raise RuntimeError(f"Car part has no slot id: {carpart_rel}")


def _compatible_part_info(root: Path, part_path: Path) -> dict[str, Any] | None:
    rel = game_rel_from_disk(root, part_path)
    if not rel:
        return None
    try:
        carpart_rel = _carpart_rel_from_compatible(root, rel)
        slot = _carpart_slot(root, carpart_rel)
        strings = [value for _path, value in _collect_proto_strings(part_path.read_bytes())]
        carpart_path = resolve_content_path(root, carpart_rel)
        if carpart_path.exists():
            strings.extend(value for _path, value in _collect_proto_strings(carpart_path.read_bytes()))
    except Exception:
        return None
    return {
        "partRel": norm_slashes(rel),
        "carpartRel": norm_slashes(carpart_rel),
        "slot": slot,
        "searchText": " ".join([rel, carpart_rel, *strings]).lower(),
    }


def _scan_compatible_parts(root: Path, car_content_path: str) -> list[dict[str, Any]]:
    car_path = resolve_content_path(root, car_content_path)
    if not car_path.exists():
        return []
    parts: list[dict[str, Any]] = []
    for path in sorted(car_path.rglob("*.compatiblepart")):
        info = _compatible_part_info(root, path)
        if info:
            parts.append(info)
    return parts


def _find_endurance_parts(root: Path, car_content_path: str) -> tuple[list[dict[str, Any]], list[str]]:
    warnings: list[str] = []
    compatible_parts = _scan_compatible_parts(root, car_content_path)
    lumirank = next((part for part in compatible_parts if "lumirank" in part["searchText"] and int(part["slot"]) == 42), None)
    if lumirank is None:
        lumirank = next((part for part in compatible_parts if "lumirank" in part["searchText"]), None)

    bumper_candidates: list[tuple[int, dict[str, Any]]] = []
    for part in compatible_parts:
        text = str(part.get("searchText") or "")
        if "aux" in text or "lightbar" in text:
            continue
        if "bumper" not in text and "front_bumper" not in text and "body" not in text:
            continue
        score = 0
        if int(part.get("slot") or 0) == 2:
            score += 6
        if "endurance" in text:
            score += 5
        if "endu" in text:
            score += 4
        if "front_bumper" in text or "front bumper" in text:
            score += 2
        if score >= 6:
            bumper_candidates.append((score, part))
    bumper = sorted(bumper_candidates, key=lambda row: (-row[0], row[1]["partRel"]))[0][1] if bumper_candidates else None

    parts: list[dict[str, Any]] = []
    if lumirank:
        parts.append({"kind": "lumirank", **lumirank})
    else:
        warnings.append("Lumirank part not found")
    if bumper:
        parts.append({"kind": "bumper", **bumper})
    else:
        warnings.append("Endurance bumper part not found; Lumirank-only spec will be used if possible")
    return parts, warnings


def endurance_support(game_root: str | Path, car_content_path: str) -> dict[str, Any]:
    normalized = normalize_game_rel_path(car_content_path)
    if not normalized:
        return {"supported": False, "carContentPath": "", "warnings": ["Missing car content path"], "reason": "Missing car content path"}
    root = Path(game_root)
    parts, warnings = _find_endurance_parts(root, normalized)
    usable_parts = [part for part in parts if part.get("partRel") and int(part.get("slot") or 0) > 0]
    supported = bool(usable_parts)
    reason = "" if supported else "No usable Endurance parts found"
    return {
        "supported": supported,
        "carContentPath": normalized,
        "parts": _json_ready(usable_parts),
        "warnings": warnings,
        "reason": reason,
    }


def _find_actor_file(root: Path, car_content_path: str) -> Path | None:
    parts = split_content_path(car_content_path)
    if not parts:
        return None
    car_folder = parts[-1]
    candidate = resolve_content_path(root, f"{car_content_path}\\{car_folder}.actor")
    if candidate.exists():
        return candidate
    cars_root = root / "content" / "cars"
    if not cars_root.exists():
        return None
    try:
        for path in cars_root.rglob(f"{car_folder}.actor"):
            if path.is_file():
                return path
    except Exception:
        pass
    return None


def _find_mechanical_for_visual(root: Path, actor_path: Path | None, visual_rel: str) -> str:
    candidates: list[str] = []
    if actor_path and actor_path.exists():
        try:
            candidates.extend(_actor_list_resources(actor_path.read_bytes(), 10, ".mechanicalcarpreset"))
        except Exception:
            pass
    cars_root = root / "content" / "cars"
    if cars_root.exists():
        try:
            candidates.extend(game_rel_from_disk(root, path) for path in cars_root.rglob("*.mechanicalcarpreset") if path.is_file())
        except Exception:
            pass
    target = normalize_game_rel_path(visual_rel)
    seen: set[str] = set()
    for rel in candidates:
        normalized = normalize_game_rel_path(rel)
        key = normalized.lower()
        if not normalized or key in seen:
            continue
        seen.add(key)
        path = resolve_content_path(root, normalized)
        if not path.exists():
            continue
        try:
            info = _extract_mechanical_info(path.read_bytes())
        except Exception:
            continue
        if target in info.get("visuals", []):
            return normalized
    return ""


def _find_carfinalstate_for_visual(root: Path, visual_rel: str, mechanical_rel: str) -> str:
    savedcars = root / "system" / "savedcars"
    if not savedcars.exists():
        return ""
    visual_target = normalize_game_rel_path(visual_rel)
    mechanical_target = normalize_game_rel_path(mechanical_rel)
    try:
        for path in savedcars.rglob("*.carfinalstate"):
            if not path.is_file():
                continue
            try:
                info = _extract_finalstate_info(path.read_bytes())
            except Exception:
                continue
            if info.get("visual") == visual_target and info.get("mechanical") == mechanical_target:
                return game_rel_from_disk(root, path)
    except Exception:
        return ""
    return ""


def _make_carfinalstate_filename(car_content_path: str, mechanical_rel: str, visual_rel: str) -> str:
    car_name = split_content_path(car_content_path)[-1]
    mech_name = Path(norm_slashes(mechanical_rel).replace("\\", "/")).stem
    visual_name = Path(norm_slashes(visual_rel).replace("\\", "/")).stem
    return f"{car_name}_{mech_name}_{visual_name}.carfinalstate"


def _clone_mechanical_rel(base_mechanical_rel: str) -> str:
    normalized = normalize_game_rel_path(base_mechanical_rel)
    path = Path(normalized.replace("\\", "/"))
    stem = path.stem
    if stem.endswith(CLONE_SUFFIX):
        return normalized
    return norm_slashes(str(path.with_name(f"{stem}{CLONE_SUFFIX}{path.suffix}")))


def _skin_key(entry: dict[str, Any]) -> str:
    return str(entry.get("skinKey") or entry.get("skin_key") or "").strip().lower()


def _model_skin_key(entry: dict[str, Any]) -> str:
    return ":".join(
        [
            str(entry.get("carModel") or entry.get("car_model") or "").strip().lower(),
            str(entry.get("skinId") or entry.get("skin_id") or "").strip().lower(),
        ]
    )


def _find_skin(entry: dict[str, Any], skins: list[dict[str, Any]]) -> dict[str, Any] | None:
    key = _skin_key(entry)
    if key:
        found = next((skin for skin in skins if str(skin.get("key") or "").strip().lower() == key), None)
        if found:
            return found
    model_key = _model_skin_key(entry)
    return next(
        (
            skin
            for skin in skins
            if ":".join([str(skin.get("car_model") or skin.get("carModel") or "").strip().lower(), str(skin.get("skin_id") or skin.get("skinId") or "").strip().lower()])
            == model_key
        ),
        None,
    )


def _entry_context(root: Path, entry: dict[str, Any], skins: list[dict[str, Any]]) -> tuple[dict[str, Any] | None, list[str]]:
    warnings: list[str] = []
    skin = _find_skin(entry, skins)
    label = _clean_text(entry.get("displayName") or entry.get("skinId") or entry.get("skinKey") or "Grid entry", 120)
    if not skin:
        return None, [f"{label}: livery not found in My Liveries"]
    files = skin.get("files") if isinstance(skin.get("files"), dict) else {}
    car_model = _clean_text(skin.get("car_model") or skin.get("carModel") or entry.get("carModel") or entry.get("car_model"), 120)
    car_content_path = normalize_game_rel_path(skin.get("car_content_path") or skin.get("carContentPath") or entry.get("carContentPath") or entry.get("car_content_path") or (f"content\\cars\\{car_model}" if car_model else ""))
    actor_rel = normalize_game_rel_path(files.get("actor") or "")
    actor_path = resolve_content_path(root, actor_rel) if actor_rel else _find_actor_file(root, car_content_path)
    if actor_path and actor_path.exists():
        actor_rel = game_rel_from_disk(root, actor_path)
    else:
        return None, [f"{label}: actor file missing"]

    visual_rel = normalize_game_rel_path(files.get("visual_preset") or "")
    if not visual_rel:
        return None, [f"{label}: visual preset missing"]
    base_mechanical_rel = normalize_game_rel_path(files.get("mechanical_preset") or "")
    if not base_mechanical_rel:
        base_mechanical_rel = _find_mechanical_for_visual(root, actor_path, visual_rel)
    if not base_mechanical_rel:
        return None, [f"{label}: base mechanical preset missing"]
    base_mechanical_path = resolve_content_path(root, base_mechanical_rel)
    if not base_mechanical_path.exists():
        return None, [f"{label}: base mechanical preset file missing"]

    base_finalstate_rel = ""
    carfinalstates = files.get("carfinalstates") if isinstance(files.get("carfinalstates"), list) else []
    for value in [*carfinalstates, files.get("carfinalstate")]:
        rel = normalize_game_rel_path(value or "")
        if not rel:
            continue
        path = resolve_content_path(root, rel)
        if not path.exists():
            continue
        try:
            info = _extract_finalstate_info(path.read_bytes())
        except Exception:
            continue
        if info.get("visual") == visual_rel and info.get("mechanical") == base_mechanical_rel:
            base_finalstate_rel = rel
            break
    if not base_finalstate_rel:
        base_finalstate_rel = _find_carfinalstate_for_visual(root, visual_rel, base_mechanical_rel)
    if not base_finalstate_rel:
        return None, [f"{label}: base car final state missing"]

    parts, part_warnings = _find_endurance_parts(root, car_content_path)
    warnings.extend(f"{label}: {warning}" for warning in part_warnings)
    usable_parts = [part for part in parts if part.get("partRel") and int(part.get("slot") or 0) > 0]
    if not usable_parts:
        return None, [f"{label}: no usable Endurance parts found"]

    clone_mechanical_rel = _clone_mechanical_rel(base_mechanical_rel)
    target_finalstate_rel = norm_slashes(f"system\\savedcars\\{_make_carfinalstate_filename(car_content_path, clone_mechanical_rel, visual_rel)}")
    return (
        {
            "label": label,
            "skin": skin,
            "carModel": car_model,
            "carContentPath": car_content_path,
            "actorRel": actor_rel,
            "baseMechanicalRel": base_mechanical_rel,
            "cloneMechanicalRel": clone_mechanical_rel,
            "visualRel": visual_rel,
            "baseFinalstateRel": base_finalstate_rel,
            "targetFinalstateRel": target_finalstate_rel,
            "parts": usable_parts,
        },
        warnings,
    )


def _spec_key(context: dict[str, Any]) -> str:
    return "|".join(
        [
            normalize_game_rel_path(context["carContentPath"]).lower(),
            normalize_game_rel_path(context["baseMechanicalRel"]).lower(),
        ]
    )


def _merge_spec_context(target: dict[str, Any], context: dict[str, Any], entry_index: int) -> None:
    target.setdefault("visualRels", [])
    target.setdefault("finalstates", [])
    if context["visualRel"] not in target["visualRels"]:
        target["visualRels"].append(context["visualRel"])
    if not any(row.get("targetFinalstateRel") == context["targetFinalstateRel"] for row in target["finalstates"]):
        target["finalstates"].append(
            {
                "visualRel": context["visualRel"],
                "baseFinalstateRel": context["baseFinalstateRel"],
                "targetFinalstateRel": context["targetFinalstateRel"],
            }
        )
    target.setdefault("entryIndexes", []).append(entry_index)


def _write_if_changed(writer: GameFileWriter, path: Path, data: bytes, label: str, backup: bool = True) -> bool:
    if path.exists():
        try:
            if path.read_bytes() == data:
                return False
        except OSError:
            pass
    writer.write_bytes(path, data, label=label, backup=backup)
    return True


def _ensure_actor_registration(writer: GameFileWriter, actor_path: Path, mechanical_rel: str) -> bool:
    old_data = actor_path.read_bytes()
    new_data, changed = _actor_add_resource_data(old_data, mechanical_rel, 10)
    if changed:
        writer.write_bytes(actor_path, new_data, label="actor AI vehicle spec mechanical registration", backup=True)
    return changed


def _remove_actor_registration(writer: GameFileWriter, actor_path: Path, mechanical_rel: str) -> bool:
    if not actor_path.exists():
        return False
    old_data = actor_path.read_bytes()
    new_data, changed = _actor_remove_resource_data(old_data, mechanical_rel, 10)
    if changed:
        writer.write_bytes(actor_path, new_data, label="actor AI vehicle spec mechanical cleanup", backup=True)
    return changed


def _ensure_spec_resource(root: Path, writer: GameFileWriter, spec: dict[str, Any]) -> dict[str, Any]:
    base_mechanical_path = resolve_content_path(root, spec["baseMechanicalRel"])
    clone_mechanical_path = resolve_content_path(root, spec["cloneMechanicalRel"])
    actor_path = resolve_content_path(root, spec["actorRel"])
    parts = spec.get("parts") if isinstance(spec.get("parts"), list) else []
    lumirank_rel = next((str(part.get("partRel") or "") for part in parts if part.get("kind") == "lumirank"), "")
    bumper_rel = next((str(part.get("partRel") or "") for part in parts if part.get("kind") == "bumper"), "")

    clone_data = _clone_mechanical_data(base_mechanical_path.read_bytes(), spec.get("visualRels", []), lumirank_rel, bumper_rel)
    mechanical_written = _write_if_changed(writer, clone_mechanical_path, clone_data, "LiveryLab AI Endurance mechanical preset", backup=True)
    actor_changed = _ensure_actor_registration(writer, actor_path, spec["cloneMechanicalRel"])

    finalstate_rows: list[dict[str, Any]] = []
    finalstate_writes = 0
    for row in spec.get("finalstates", []):
        base_path = resolve_content_path(root, str(row.get("baseFinalstateRel") or ""))
        target_path = resolve_content_path(root, str(row.get("targetFinalstateRel") or ""))
        final_data = _edit_carfinalstate_data(base_path.read_bytes(), spec["cloneMechanicalRel"], str(row.get("visualRel") or ""))
        final_data = _ensure_finalstate_parts(final_data, parts, str(row.get("targetFinalstateRel") or ""))
        if _write_if_changed(writer, target_path, final_data, "LiveryLab AI Endurance car final state", backup=True):
            finalstate_writes += 1
        finalstate_rows.append(
            {
                "visualRel": row.get("visualRel") or "",
                "baseFinalstateRel": row.get("baseFinalstateRel") or "",
                "targetFinalstateRel": row.get("targetFinalstateRel") or "",
            }
        )

    resource = {
        "kind": AI_SPEC_ENDURANCE,
        "carModel": spec.get("carModel") or "",
        "carContentPath": spec.get("carContentPath") or "",
        "actorRel": spec.get("actorRel") or "",
        "baseMechanicalRel": spec.get("baseMechanicalRel") or "",
        "cloneMechanicalRel": spec.get("cloneMechanicalRel") or "",
        "visualRels": spec.get("visualRels") or [],
        "finalstates": finalstate_rows,
        "parts": parts,
        "updatedAt": _now_iso(),
    }
    return {
        "resource": resource,
        "mechanicalWritten": mechanical_written,
        "actorChanged": actor_changed,
        "finalstateWrites": finalstate_writes,
        "cloneMechanicalRel": spec["cloneMechanicalRel"],
        "cloneMechanicalName": Path(spec["cloneMechanicalRel"].replace("\\", "/")).stem,
    }


def _cleanup_resource(
    root: Path,
    writer: GameFileWriter,
    resource: dict[str, Any],
    keep_finalstates: set[str] | None = None,
    *,
    backup: bool = True,
) -> dict[str, Any]:
    keep_finalstates = {normalize_game_rel_path(value).lower() for value in (keep_finalstates or set())}
    removed_finalstates = 0
    actor_changed = False
    mechanical_deleted = False

    actor_rel = normalize_game_rel_path(resource.get("actorRel") or "")
    clone_mechanical_rel = normalize_game_rel_path(resource.get("cloneMechanicalRel") or "")
    if actor_rel and clone_mechanical_rel:
        actor_path = resolve_content_path(root, actor_rel)
        old_data = actor_path.read_bytes() if actor_path.exists() else b""
        new_data, changed = _actor_remove_resource_data(old_data, clone_mechanical_rel, 10) if old_data else (old_data, False)
        if changed:
            writer.write_bytes(actor_path, new_data, label="actor AI vehicle spec mechanical cleanup", backup=backup)
        actor_changed = changed

    for row in resource.get("finalstates") if isinstance(resource.get("finalstates"), list) else []:
        rel = normalize_game_rel_path(row.get("targetFinalstateRel") or "")
        if not rel or rel.lower() in keep_finalstates:
            continue
        path = resolve_content_path(root, rel)
        if path.exists():
            writer.delete_file(path, label="LiveryLab AI Endurance car final state cleanup", backup=backup)
            removed_finalstates += 1

    if clone_mechanical_rel:
        path = resolve_content_path(root, clone_mechanical_rel)
        if path.exists():
            writer.delete_file(path, label="LiveryLab AI Endurance mechanical preset cleanup", backup=backup)
            mechanical_deleted = True

    return {
        "actorChanged": actor_changed,
        "removedFinalstates": removed_finalstates,
        "mechanicalDeleted": mechanical_deleted,
    }


def _find_orphan_resources(root: Path, keep_clone_rels: set[str] | None = None) -> list[dict[str, Any]]:
    keep = {normalize_game_rel_path(value).lower() for value in (keep_clone_rels or set())}
    cars_root = root / "content" / "cars"
    if not cars_root.exists():
        return []
    savedcars = root / "system" / "savedcars"
    resources: list[dict[str, Any]] = []
    try:
        mechanical_paths = sorted(cars_root.rglob(f"*{CLONE_SUFFIX}.mechanicalcarpreset"))
    except Exception:
        mechanical_paths = []
    for mechanical_path in mechanical_paths:
        if not mechanical_path.is_file():
            continue
        clone_rel = game_rel_from_disk(root, mechanical_path)
        if not clone_rel or clone_rel.lower() in keep:
            continue
        parts = split_content_path(clone_rel)
        car_name = ""
        if len(parts) >= 3 and parts[0].lower() == "content" and parts[1].lower() == "cars":
            car_name = parts[2]
        if not car_name:
            car_name = mechanical_path.parent.parent.name
        car_content_path = f"content\\cars\\{car_name}" if car_name else ""
        actor_path = _find_actor_file(root, car_content_path) if car_content_path else None
        actor_rel = game_rel_from_disk(root, actor_path) if actor_path and actor_path.exists() else ""
        finalstates: list[dict[str, str]] = []
        if savedcars.exists():
            try:
                for finalstate_path in savedcars.rglob("*.carfinalstate"):
                    if not finalstate_path.is_file():
                        continue
                    try:
                        info = _extract_finalstate_info(finalstate_path.read_bytes())
                    except Exception:
                        continue
                    if normalize_game_rel_path(info.get("mechanical") or "").lower() != clone_rel.lower():
                        continue
                    finalstates.append(
                        {
                            "visualRel": info.get("visual") or "",
                            "baseFinalstateRel": "",
                            "targetFinalstateRel": game_rel_from_disk(root, finalstate_path),
                        }
                    )
            except Exception:
                pass
        resources.append(
            {
                "kind": AI_SPEC_ENDURANCE,
                "carModel": car_name,
                "carContentPath": car_content_path,
                "actorRel": actor_rel,
                "baseMechanicalRel": "",
                "cloneMechanicalRel": clone_rel,
                "visualRels": [row.get("visualRel") or "" for row in finalstates if row.get("visualRel")],
                "finalstates": finalstates,
                "parts": [],
                "orphan": True,
            }
        )
    return resources


def prepare_grid_for_export(
    game_root: str | Path,
    library_root: str | Path,
    grid: dict[str, Any],
    skins: list[dict[str, Any]],
    backup_root: str | Path | None = None,
) -> dict[str, Any]:
    root = Path(game_root)
    manifest = _read_manifest(library_root)
    source_grid = grid if isinstance(grid, dict) else {}
    prepared_grid = copy.deepcopy(source_grid)
    entries = prepared_grid.get("entries") if isinstance(prepared_grid.get("entries"), list) else []
    prepared_grid["entries"] = entries
    warnings: list[str] = []
    required_specs: dict[str, dict[str, Any]] = {}
    entry_contexts: dict[int, dict[str, Any]] = {}

    for index, entry in enumerate(entries):
        if not isinstance(entry, dict):
            continue
        spec_value = normalize_ai_vehicle_spec(entry.get("aiVehicleSpec") or entry.get("ai_vehicle_spec"))
        entry["aiVehicleSpec"] = spec_value
        if spec_value != AI_SPEC_ENDURANCE:
            continue
        context, context_warnings = _entry_context(root, entry, skins)
        warnings.extend(context_warnings)
        if not context:
            entry["aiVehicleSpecResolved"] = AI_SPEC_STANDARD
            continue
        key = _spec_key(context)
        if key not in required_specs:
            required_specs[key] = {
                "key": key,
                "carModel": context["carModel"],
                "carContentPath": context["carContentPath"],
                "actorRel": context["actorRel"],
                "baseMechanicalRel": context["baseMechanicalRel"],
                "cloneMechanicalRel": context["cloneMechanicalRel"],
                "parts": context["parts"],
                "visualRels": [],
                "finalstates": [],
                "entryIndexes": [],
            }
        _merge_spec_context(required_specs[key], context, index)
        entry_contexts[index] = context

    existing_resources = manifest.get("resources") if isinstance(manifest.get("resources"), dict) else {}
    required_clone_rels = {
        normalize_game_rel_path(spec.get("cloneMechanicalRel") or "").lower()
        for spec in required_specs.values()
        if spec.get("cloneMechanicalRel")
    }
    orphan_resources = _find_orphan_resources(root, required_clone_rels)
    writer: GameFileWriter | None = None
    if required_specs or existing_resources or orphan_resources:
        writer = GameFileWriter(root, "prepare LiveryLab AI vehicle specs", [root / "content" / "cars", root / "system" / "savedcars"], backup_root=backup_root)

    next_resources: dict[str, Any] = {}
    prepared_count = 0
    finalstate_count = 0
    cleanup_count = 0
    ensured: dict[str, dict[str, Any]] = {}
    if writer is not None:
        for key, spec in required_specs.items():
            result = _ensure_spec_resource(root, writer, spec)
            ensured[key] = result
            next_resources[key] = result["resource"]
            prepared_count += 1
            finalstate_count += len(result["resource"].get("finalstates") or [])

        for key, resource in existing_resources.items():
            if key not in required_specs:
                cleanup = _cleanup_resource(root, writer, resource)
                cleanup_count += int(bool(cleanup.get("actorChanged") or cleanup.get("mechanicalDeleted") or cleanup.get("removedFinalstates")))
                continue
            old_finalstates = {
                normalize_game_rel_path(row.get("targetFinalstateRel") or "").lower()
                for row in (resource.get("finalstates") if isinstance(resource.get("finalstates"), list) else [])
            }
            new_finalstates = {
                normalize_game_rel_path(row.get("targetFinalstateRel") or "").lower()
                for row in (next_resources[key].get("finalstates") if isinstance(next_resources[key].get("finalstates"), list) else [])
            }
            stale = old_finalstates - new_finalstates
            if stale:
                cleanup = _cleanup_resource(root, writer, {**resource, "cloneMechanicalRel": ""}, keep_finalstates=new_finalstates)
                cleanup_count += int(cleanup.get("removedFinalstates") or 0)
        for resource in orphan_resources:
            cleanup = _cleanup_resource(root, writer, resource)
            cleanup_count += int(bool(cleanup.get("actorChanged") or cleanup.get("mechanicalDeleted") or cleanup.get("removedFinalstates")))

    for index, context in entry_contexts.items():
        key = _spec_key(context)
        result = ensured.get(key)
        if not result:
            entries[index]["aiVehicleSpecResolved"] = AI_SPEC_STANDARD
            continue
        entries[index]["aiVehicleSpecResolved"] = AI_SPEC_ENDURANCE
        entries[index]["mechanicalPreset"] = result["cloneMechanicalName"]
        entries[index]["mechanicalPresetName"] = CLONE_NAME
        entries[index]["mechanicalPresetShortName"] = CLONE_SHORT_NAME
        entries[index]["mechanicalPresetPath"] = result["cloneMechanicalRel"]
        entries[index]["visualPresetPath"] = context["visualRel"]
        entries[index]["carFinalStatePath"] = context["targetFinalstateRel"]

    for index, entry in enumerate(entries):
        if not isinstance(entry, dict):
            continue
        if normalize_ai_vehicle_spec(entry.get("aiVehicleSpec")) != AI_SPEC_ENDURANCE:
            entry["aiVehicleSpecResolved"] = AI_SPEC_STANDARD

    manifest.update(
        {
            "activeGridId": _clean_text(prepared_grid.get("id"), 80),
            "activeGridName": _clean_text(prepared_grid.get("name"), 120),
            "resources": next_resources,
        }
    )
    manifest_file = _write_manifest(library_root, manifest)
    return {
        "grid": prepared_grid,
        "manifestPath": str(manifest_file),
        "preparedCount": prepared_count,
        "finalstateCount": finalstate_count,
        "cleanupCount": cleanup_count,
        "warnings": warnings,
        "writer": writer.to_dict() if writer else {},
        "resources": list(next_resources.values()),
    }


def cleanup_all(
    game_root: str | Path,
    library_root: str | Path,
    backup_root: str | Path | None = None,
    *,
    backup: bool = True,
    require_content_cars: bool = True,
) -> dict[str, Any]:
    root = Path(game_root)
    manifest = _read_manifest(library_root)
    resources = manifest.get("resources") if isinstance(manifest.get("resources"), dict) else {}
    manifest_clone_rels = {
        normalize_game_rel_path(resource.get("cloneMechanicalRel") or "").lower()
        for resource in resources.values()
        if isinstance(resource, dict) and resource.get("cloneMechanicalRel")
    }
    orphan_resources = _find_orphan_resources(root, manifest_clone_rels)
    if not resources and not orphan_resources:
        manifest.update({"activeGridId": "", "activeGridName": "", "resources": {}})
        manifest_file = _write_manifest(library_root, manifest)
        return {"manifestPath": str(manifest_file), "cleanupCount": 0, "writer": {}, "resources": []}
    writer = GameFileWriter(
        root,
        "cleanup LiveryLab AI vehicle specs",
        [root / "content" / "cars", root / "system" / "savedcars"],
        backup_root=backup_root,
        require_content_cars=require_content_cars,
    )
    cleanup_count = 0
    for resource in resources.values():
        cleanup = _cleanup_resource(root, writer, resource, backup=backup)
        cleanup_count += int(bool(cleanup.get("actorChanged") or cleanup.get("mechanicalDeleted") or cleanup.get("removedFinalstates")))
    for resource in orphan_resources:
        cleanup = _cleanup_resource(root, writer, resource, backup=backup)
        cleanup_count += int(bool(cleanup.get("actorChanged") or cleanup.get("mechanicalDeleted") or cleanup.get("removedFinalstates")))
    manifest.update({"activeGridId": "", "activeGridName": "", "resources": {}})
    manifest_file = _write_manifest(library_root, manifest)
    return {"manifestPath": str(manifest_file), "cleanupCount": cleanup_count, "writer": writer.to_dict(), "resources": []}


def status(library_root: str | Path) -> dict[str, Any]:
    manifest = _read_manifest(library_root)
    resources = manifest.get("resources") if isinstance(manifest.get("resources"), dict) else {}
    return {
        "manifestPath": str(manifest_path(library_root)),
        "activeGridId": manifest.get("activeGridId") or "",
        "activeGridName": manifest.get("activeGridName") or "",
        "resourceCount": len(resources),
        "resources": list(resources.values()),
    }
