from __future__ import annotations

import re
import tempfile
import uuid
from pathlib import Path
from typing import Any, Callable

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

PROJECT_PLATE_PNG = "plate_r.png"
PLATE_LABEL = "PLATE_R"
PLATE_SLOT_ID = 27
ACTOR_COMPATIBLE_PART_FIELD = 21
ACTOR_ROAD_PLATE_FIELD = 12


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


def split_content_path(value: str | None) -> list[str]:
    return [part for part in re.split(r"[\\/]+", str(value or "").strip()) if part]


def normalize_game_rel_path(value: str | None) -> str:
    return norm_slashes(value).strip().lstrip("\\/")


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


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


def content_to_disk(root: Path, rel: str) -> Path:
    return root / Path(*split_content_path(rel))


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


def _is_plate_mesh_rel(value: str) -> bool:
    lower = norm_slashes(value).lower()
    return lower.endswith(".mesh") and "\\parts\\plates\\" in lower


def _is_texture_rel(value: str) -> bool:
    return norm_slashes(value).lower().endswith(".texture")


def _is_material_rel(value: str) -> bool:
    return norm_slashes(value).lower().endswith(".material")


def _actor_resource_payloads(actor_data: bytes, field_number: int) -> list[bytes]:
    payloads: list[bytes] = []
    for top in parse_fields(actor_data):
        if top.number != 103 or top.wire != 2 or not isinstance(top.value, (bytes, bytearray)):
            continue
        for payload in parse_fields(bytes(top.value)):
            if payload.number == field_number and payload.wire == 2 and isinstance(payload.value, (bytes, bytearray)):
                payloads.append(bytes(payload.value))
            if payload.number != 13 or payload.wire != 2 or not isinstance(payload.value, (bytes, bytearray)):
                continue
            for resource in parse_fields(bytes(payload.value)):
                if resource.number == field_number and resource.wire == 2 and isinstance(resource.value, (bytes, bytearray)):
                    payloads.append(bytes(resource.value))
    return payloads


def actor_list_resources(actor_data: bytes, field_number: int) -> list[str]:
    resources: list[str] = []
    for payload in _actor_resource_payloads(actor_data, field_number):
        text = is_printable_utf8(payload)
        if text is not None:
            resources.append(text)
    return resources


def is_liverylab_road_plate_compatible_rel(rel: str, car_content_path: str) -> bool:
    normalized = normalize_game_rel_path(rel).lower()
    car_prefix = normalize_game_rel_path(car_content_path).lower().rstrip("\\/")
    if not car_prefix:
        return False
    expected_prefix = f"{car_prefix}\\presets\\parts\\liverylab_"
    return normalized.startswith(expected_prefix) and normalized.endswith("_plate_r.compatiblepart")


def actor_liverylab_road_plate_refs(actor_data: bytes, car_content_path: str) -> list[str]:
    refs: list[str] = []
    seen: set[str] = set()
    for rel in actor_list_resources(actor_data, ACTOR_COMPATIBLE_PART_FIELD):
        normalized = normalize_game_rel_path(rel)
        key = normalized.lower()
        if key in seen or not is_liverylab_road_plate_compatible_rel(normalized, car_content_path):
            continue
        seen.add(key)
        refs.append(normalized)
    return refs


def prune_actor_liverylab_road_plate_refs(
    actor_data: bytes,
    car_content_path: str,
    protected_refs: list[str] | None = None,
    remove_refs: list[str] | None = None,
) -> tuple[bytes, list[str]]:
    protected = {normalize_game_rel_path(ref).lower() for ref in protected_refs or [] if str(ref or "").strip()}
    forced_remove = {normalize_game_rel_path(ref).lower() for ref in remove_refs or [] if str(ref or "").strip()}
    changed_data = actor_data
    removed: list[str] = []
    for rel in actor_liverylab_road_plate_refs(actor_data, car_content_path):
        key = rel.lower()
        if key not in forced_remove and key in protected:
            continue
        changed_data, did_change = remove_actor_resource_data(changed_data, rel, ACTOR_COMPATIBLE_PART_FIELD)
        if did_change:
            removed.append(rel)
    return changed_data, removed


def inspect_actor_road_plate(game_root: str | Path, actor_path: str | Path | None) -> dict[str, Any]:
    root = Path(game_root)
    actor = Path(actor_path) if actor_path else None
    if actor is None or not actor.exists():
        return {"supported": False, "reason": "actor missing"}

    try:
        actor_data = actor.read_bytes()
    except OSError as exc:
        return {"supported": False, "reason": f"actor read failed: {exc}"}

    for payload in _actor_resource_payloads(actor_data, ACTOR_ROAD_PLATE_FIELD):
        strings = [text for _path, text in collect_proto_strings(payload)]
        direct = is_printable_utf8(payload)
        if direct:
            strings.append(direct)
        if PLATE_LABEL not in strings:
            continue
        mesh_rel = next((norm_slashes(value) for value in strings if _is_plate_mesh_rel(value)), "")
        if not mesh_rel:
            continue
        mesh_path = resolve_content_path(root, mesh_rel)
        if not mesh_path.exists():
            return {"supported": False, "reason": "plate mesh missing", "meshRel": mesh_rel}
        material_rel = _plate_mesh_material_rel(mesh_path)
        if not material_rel:
            return {"supported": False, "reason": "plate material reference missing", "meshRel": mesh_rel}
        material_path = resolve_content_path(root, material_rel)
        if not material_path.exists():
            return {
                "supported": False,
                "reason": "plate material missing",
                "meshRel": mesh_rel,
                "materialRel": material_rel,
            }
        plate_family = _plate_family_from_rel(mesh_rel)
        plate_side = _plate_side_from_rel(mesh_rel)
        texture_rel = _plate_material_texture_rel(root, mesh_rel, material_path)
        if not texture_rel:
            return {
                "supported": False,
                "reason": "plate texture reference missing",
                "meshRel": mesh_rel,
                "materialRel": material_rel,
            }
        texture_path = resolve_content_path(root, texture_rel)
        mips_path = texture_path.with_suffix(".texturemips")
        if not texture_path.exists() or not mips_path.exists():
            return {
                "supported": False,
                "reason": "plate texture files missing",
                "meshRel": mesh_rel,
                "materialRel": material_rel,
                "textureRel": texture_rel,
            }
        texture_desc = _texture_descriptor_summary(texture_path)
        return {
            "supported": True,
            "actorPath": str(actor),
            "actorRel": game_rel_from_disk(root, actor),
            "label": PLATE_LABEL,
            "meshRel": mesh_rel,
            "meshPath": str(mesh_path),
            "materialRel": norm_slashes(material_rel),
            "materialPath": str(material_path),
            "textureRel": norm_slashes(texture_rel),
            "texturePath": str(texture_path),
            "mipsPath": str(mips_path),
            "plateFamily": plate_family,
            "plateSide": plate_side,
            "textureWidth": texture_desc.get("width", 0),
            "textureHeight": texture_desc.get("height", 0),
            "textureMips": texture_desc.get("mips", 0),
        }
    return {"supported": False, "reason": "PLATE_R resource missing"}


def _plate_mesh_material_rel(mesh_path: Path) -> str:
    strings = [norm_slashes(text) for _path, text in collect_proto_strings(mesh_path.read_bytes())]
    preferred = [
        value
        for value in strings
        if _is_material_rel(value) and "\\parts\\plates\\" in value.lower()
    ]
    if preferred:
        return preferred[0]
    return next((value for value in strings if _is_material_rel(value)), "")


def _plate_material_texture_rel(root: Path, mesh_rel: str, material_path: Path) -> str:
    strings = [norm_slashes(text) for _path, text in collect_proto_strings(material_path.read_bytes())]
    textures = [value for value in strings if _is_texture_rel(value)]
    if not textures:
        return ""
    family = _plate_family_from_rel(mesh_rel)
    side = _plate_side_from_rel(mesh_rel)
    material_textures = [
        value
        for value in textures
        if not _looks_like_non_color_plate_texture(Path(norm_slashes(value).lower()).stem)
        and resolve_content_path(root, value).exists()
    ]
    if material_textures:
        return sorted(material_textures, key=lambda value: _plate_texture_score(value, family, side, 0))[0]
    family_textures = _plate_family_texture_rels(root, family)
    if family_textures:
        return sorted(family_textures, key=lambda value: _plate_texture_score(value, family, side, 1))[0]
    return sorted(textures, key=lambda value: _plate_texture_score(value, family, side, 2))[0]


def _plate_texture_score(value: str, family: str, side: str, source_rank: int) -> tuple[int, int, int, int, int, int, str]:
    lower = norm_slashes(value).lower()
    stem = Path(lower).stem
    family_rank = 0 if not family or _plate_family_from_rel(value) == family else 1
    bad_rank = 1 if _looks_like_non_color_plate_texture(stem) else 0

    side_rank = 1
    if side == "r" and stem in {"plate_r_c", "plater_c"}:
        side_rank = 0
    elif side == "f" and stem in {"plate_f_c", "platef_c"}:
        side_rank = 0
    elif stem in {"plate_c", "llate_c"}:
        side_rank = 1

    color_rank = 0 if stem.endswith("_c") or stem in {"plate_c", "llate_c"} else 1
    generic_rank = 0 if stem in {"plate_c", "llate_c"} else 1
    if side == "r" and stem in {"plate_r_c", "plater_c"}:
        name_rank = 0
    elif side == "f" and stem in {"plate_f_c", "platef_c"}:
        name_rank = 0
    elif stem == "plate_c":
        name_rank = 1
    elif stem == "llate_c":
        name_rank = 2
    else:
        name_rank = 3
    return (source_rank, family_rank, bad_rank, side_rank, color_rank + generic_rank, name_rank, lower)


def _plate_family_texture_rels(root: Path, family: str) -> list[str]:
    if not family:
        return []
    texture_dir = root / "content" / "cars" / "common_assets" / "parts" / "plates" / family / "textures"
    if not texture_dir.exists():
        return []
    rels: list[str] = []
    for texture_path in sorted(texture_dir.glob("*.texture")):
        if _looks_like_non_color_plate_texture(texture_path.stem):
            continue
        rel = game_rel_from_disk(root, texture_path)
        if rel:
            rels.append(rel)
    return rels


def _looks_like_non_color_plate_texture(stem: str) -> bool:
    lower = stem.lower()
    if lower in {"plate_e", "plate_ao"}:
        return True
    return any(token in lower for token in ("_nm", "_n", "normal", "rough", "metal", "mask", "alpha", "opacity", "_ao"))


def _plate_family_from_rel(value: str) -> str:
    parts = [part.lower() for part in split_content_path(value)]
    try:
        plates_index = parts.index("plates")
    except ValueError:
        return ""
    family_index = plates_index + 1
    if family_index >= len(parts):
        return ""
    family = parts[family_index]
    return family if family in {"eu", "jdm", "uk", "us"} else ""


def _plate_side_from_rel(value: str) -> str:
    stem = Path(norm_slashes(value).lower()).stem
    if "_r_" in stem or stem.endswith("_r") or stem in {"plate_r_c", "plater_c"}:
        return "r"
    if "_f_" in stem or stem.endswith("_f") or stem in {"plate_f_c", "platef_c"}:
        return "f"
    return ""


def _texture_descriptor_summary(texture_path: Path) -> dict[str, int]:
    try:
        fields = parse_fields(texture_path.read_bytes())
    except Exception:
        return {}
    values: dict[int, int] = {}
    for field in fields:
        if field.number in {1, 2, 3} and field.wire == 0:
            try:
                values[field.number] = int(field.value)
            except Exception:
                pass
    return {
        "width": values.get(1, 0),
        "height": values.get(2, 0),
        "mips": values.get(3, 0),
    }


def ensure_project_plate_png(
    entry: dict[str, Any],
    game_root: str | Path,
    project_folder: str | Path,
    *,
    texture_to_png: Callable[[str | Path, str | Path, bool], dict[str, Any]],
) -> dict[str, Any]:
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    actor_rel = normalize_game_rel_path(str(files.get("actor") or ""))
    actor_path = resolve_content_path(Path(game_root), actor_rel) if actor_rel else None
    support = inspect_actor_road_plate(game_root, actor_path)
    if not support.get("supported"):
        entry.pop("road_plate", None)
        return {"changed": False, "skipped": support.get("reason") or "not supported", "support": support}

    project = Path(project_folder)
    png = project / PROJECT_PLATE_PNG
    project.mkdir(parents=True, exist_ok=True)
    metadata = dict(entry.get("road_plate") or {}) if isinstance(entry.get("road_plate"), dict) else {}
    overwrite_png = (
        str(metadata.get("sourceTextureRel") or "").lower() != str(support.get("textureRel") or "").lower()
        or int(metadata.get("textureWidth") or 0) != int(support.get("textureWidth") or 0)
        or int(metadata.get("textureHeight") or 0) != int(support.get("textureHeight") or 0)
    )
    result = texture_to_png(Path(str(support["texturePath"])), png, overwrite_png)
    metadata.update(
        {
            "supported": True,
            "projectPng": PROJECT_PLATE_PNG,
            "sourceTextureRel": support.get("textureRel") or "",
            "sourceMaterialRel": support.get("materialRel") or "",
            "sourceMeshRel": support.get("meshRel") or "",
            "plateFamily": support.get("plateFamily") or "",
            "plateSide": support.get("plateSide") or "",
            "textureWidth": support.get("textureWidth") or 0,
            "textureHeight": support.get("textureHeight") or 0,
            "textureMips": support.get("textureMips") or 0,
        }
    )
    entry["road_plate"] = metadata
    return {
        "changed": not bool(result.get("skipped")),
        "status": "created" if not bool(result.get("skipped")) else "current",
        "png": str(png),
        "support": support,
        "conversion": result,
    }


def sync_entry_road_plate(
    entry: dict[str, Any],
    game_root: str | Path,
    project_folder: str | Path,
    writer: GameFileWriter,
    *,
    parse_texture_descriptor: Callable[[str | Path], dict[str, Any]],
    choose_auto_format: Callable[[str | Path, dict[str, Any], int, str | None], tuple[str, str]],
    convert_image_to_dds_with_texconv: Callable[..., Path],
    texturemips_bytes_from_dds: Callable[[str | Path, str | Path], bytes],
    project_png_path: str | Path | None = None,
    local_root: str | Path | None = None,
    savedcar_paths: list[str | Path] | None = None,
    mechanical_paths: list[str | Path] | None = None,
    managed_compatible_rels: list[str] | None = None,
) -> dict[str, Any]:
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    actor_rel = normalize_game_rel_path(str(files.get("actor") or ""))
    actor_path = resolve_content_path(Path(game_root), actor_rel) if actor_rel else None
    support = inspect_actor_road_plate(game_root, actor_path)
    if not support.get("supported"):
        return {"changed": False, "skipped": support.get("reason") or "not supported", "support": support}
    project_png = Path(project_png_path) if project_png_path else Path(project_folder) / PROJECT_PLATE_PNG
    if not project_png.exists():
        return {"changed": False, "skipped": "road plate PNG missing", "support": support}

    root = Path(game_root)
    local_root_path = Path(local_root) if local_root else None
    skin_folder_rel = normalize_game_rel_path(str(files.get("skin_folder") or ""))
    skin_id = str(entry.get("skin_id") or entry.get("display_name") or "skin").strip()
    car_content_path = normalize_game_rel_path(str(entry.get("car_content_path") or ""))
    if not skin_folder_rel or not car_content_path:
        return {"changed": False, "skipped": "skin folder metadata missing", "support": support}

    clean_id = _clean_part_id(skin_id)
    texture_rel = norm_slashes(f"{skin_folder_rel}\\plate_r.texture")
    material_rel = norm_slashes(f"{skin_folder_rel}\\plate_r.material")
    mesh_rel = norm_slashes(f"{skin_folder_rel}\\plate_r.mesh")
    carpart_rel = norm_slashes(f"{car_content_path}\\presets\\parts\\liverylab_{clean_id}_plate_r.carpart")
    compatible_rel = norm_slashes(f"{car_content_path}\\presets\\parts\\liverylab_{clean_id}_plate_r.compatiblepart")

    generated = _build_generated_plate_assets(
        support,
        project_png,
        texture_rel,
        material_rel,
        mesh_rel,
        carpart_rel,
        compatible_rel,
        parse_texture_descriptor=parse_texture_descriptor,
        choose_auto_format=choose_auto_format,
        convert_image_to_dds_with_texconv=convert_image_to_dds_with_texconv,
        texturemips_bytes_from_dds=texturemips_bytes_from_dds,
    )

    changed = False
    writes: list[dict[str, Any]] = []
    deleted: list[str] = []
    for rel, data, label in generated:
        target = root / Path(*split_content_path(rel))
        before = target.read_bytes() if target.exists() and target.is_file() else None
        if before != data:
            result = writer.write_bytes(target, data, label=label, backup=True)
            writes.append(result.to_dict())
            changed = True
        if local_root_path is not None and rel.lower().startswith(skin_folder_rel.lower() + "\\"):
            local_target = local_root_path / Path(*split_content_path(rel))
            local_target.parent.mkdir(parents=True, exist_ok=True)
            if not local_target.exists() or local_target.read_bytes() != data:
                local_target.write_bytes(data)
                changed = True

    protected_compatible_rels = [compatible_rel, *(managed_compatible_rels or [])]
    actor_removed_refs: list[str] = []
    mechanical_bind_refs: list[str] = [compatible_rel]
    if actor_path is not None and actor_path.exists():
        old_actor = actor_path.read_bytes()
        patched_actor, actor_removed_refs = prune_actor_liverylab_road_plate_refs(
            old_actor,
            car_content_path,
            protected_compatible_rels,
        )
        patched_actor, actor_changed = add_actor_resource_data(patched_actor, compatible_rel, ACTOR_COMPATIBLE_PART_FIELD)
        if actor_changed:
            result = writer.write_bytes(actor_path, patched_actor, label="actor road plate part registration", backup=True)
            writes.append(result.to_dict())
            changed = True
        elif actor_removed_refs and patched_actor != old_actor:
            result = writer.write_bytes(actor_path, patched_actor, label="actor stale road plate cleanup", backup=True)
            writes.append(result.to_dict())
            changed = True
        for rel in actor_liverylab_road_plate_refs(patched_actor, car_content_path):
            if rel.lower() == compatible_rel.lower() or rel.lower() not in {ref.lower() for ref in protected_compatible_rels}:
                continue
            target = root / Path(*split_content_path(rel))
            if target.exists() and target.is_file():
                mechanical_bind_refs.append(rel)
        stale_deleted = _delete_generated_plate_refs_for_compatible(
            root,
            actor_removed_refs,
            car_content_path,
            writer,
            local_root_path=local_root_path,
        )
        if stale_deleted:
            deleted.extend(stale_deleted)
            changed = True

    mechanical_results: list[dict[str, Any]] = []
    for mechanical in mechanical_paths or []:
        path = Path(mechanical)
        if not path.exists() or not path.is_file():
            continue
        old_mechanical = path.read_bytes()
        new_mechanical = old_mechanical
        mechanical_removed = 0
        for stale_rel in actor_removed_refs:
            new_mechanical, stale_changed = remove_mechanical_compatiblepart_data(new_mechanical, stale_rel)
            mechanical_removed += 1 if stale_changed else 0
        mechanical_added = 0
        for bind_rel in mechanical_bind_refs:
            new_mechanical, bind_changed = add_mechanical_compatiblepart_data(new_mechanical, bind_rel)
            mechanical_added += 1 if bind_changed else 0
        mechanical_changed = new_mechanical != old_mechanical
        if mechanical_changed:
            result = writer.write_bytes(path, new_mechanical, label="mechanical road plate part binding", backup=True)
            writes.append(result.to_dict())
            changed = True
        mechanical_results.append(
            {
                "path": str(path),
                "changed": mechanical_changed,
                "added": mechanical_added,
                "removed": mechanical_removed,
            }
        )

    finalstate_rels = _entry_finalstate_rels(entry)
    finalstate_results: list[dict[str, Any]] = []
    for rel in finalstate_rels:
        target = root / Path(*split_content_path(rel))
        if not target.exists() or not target.is_file():
            continue
        final_changed, write_result = _patch_finalstate_plate_part(target, compatible_rel, writer, "road plate car final state")
        if final_changed:
            changed = True
        finalstate_results.append({"path": str(target), "changed": final_changed, "write": write_result})
        if local_root_path is not None:
            local_target = local_root_path / Path(*split_content_path(rel))
            if local_target.exists() and local_target.is_file():
                patched = ensure_finalstate_plate_part(local_target.read_bytes(), compatible_rel, PLATE_SLOT_ID, unique_seed=str(local_target))
                if patched != local_target.read_bytes():
                    local_target.write_bytes(patched)
                    changed = True

    savedcar_results: list[dict[str, Any]] = []
    for savedcar in savedcar_paths or []:
        path = Path(savedcar)
        if not path.exists() or not path.is_file():
            continue
        saved_changed, write_result = _patch_finalstate_plate_part(path, compatible_rel, writer, "road plate saved car state")
        if saved_changed:
            changed = True
        savedcar_results.append({"path": str(path), "changed": saved_changed, "write": write_result})

    road_plate = dict(entry.get("road_plate") or {}) if isinstance(entry.get("road_plate"), dict) else {}
    road_plate.update(
        {
            "supported": True,
            "projectPng": PROJECT_PLATE_PNG,
            "compatiblePartRel": compatible_rel,
            "carPartRel": carpart_rel,
            "meshRel": mesh_rel,
            "materialRel": material_rel,
            "textureRel": texture_rel,
            "sourceTextureRel": support.get("textureRel") or "",
            "sourceMaterialRel": support.get("materialRel") or "",
            "sourceMeshRel": support.get("meshRel") or "",
            "plateFamily": support.get("plateFamily") or "",
            "plateSide": support.get("plateSide") or "",
            "textureWidth": support.get("textureWidth") or 0,
            "textureHeight": support.get("textureHeight") or 0,
            "textureMips": support.get("textureMips") or 0,
        }
    )
    entry["road_plate"] = road_plate
    return {
        "changed": changed,
        "status": "updated" if changed else "current",
        "compatiblePartRel": compatible_rel,
        "writes": writes,
        "writeCount": len(writes),
        "deleted": deleted,
        "actorRemovedRefs": actor_removed_refs,
        "mechanicals": mechanical_results,
        "finalstates": finalstate_results,
        "savedcars": savedcar_results,
        "support": support,
    }


def _build_generated_plate_assets(
    support: dict[str, Any],
    project_png: Path,
    texture_rel: str,
    material_rel: str,
    mesh_rel: str,
    carpart_rel: str,
    compatible_rel: str,
    *,
    parse_texture_descriptor: Callable[[str | Path], dict[str, Any]],
    choose_auto_format: Callable[[str | Path, dict[str, Any], int, str | None], tuple[str, str]],
    convert_image_to_dds_with_texconv: Callable[..., Path],
    texturemips_bytes_from_dds: Callable[[str | Path, str | Path], bytes],
) -> list[tuple[str, bytes, str]]:
    source_texture = Path(str(support["texturePath"]))
    source_mips = source_texture.with_suffix(".texturemips")
    source_material = Path(str(support["materialPath"]))
    source_mesh = Path(str(support["meshPath"]))
    texture_data = source_texture.read_bytes()
    material_data = patch_path_strings(source_material.read_bytes(), {str(support["textureRel"]): texture_rel})
    mesh_data = patch_path_strings(source_mesh.read_bytes(), {str(support["materialRel"]): material_rel})

    desc = parse_texture_descriptor(source_texture)
    format_name, _auto_note = choose_auto_format(source_texture, desc, source_mips.stat().st_size, "Base_Color")
    with tempfile.TemporaryDirectory(prefix="liverylab_road_plate_") as tmp:
        dds_path = Path(tmp) / "plate_r.dds"
        convert_image_to_dds_with_texconv(
            project_png,
            dds_path,
            format_name,
            None,
            int(desc["mips"]),
            (int(desc["width"]), int(desc["height"])),
        )
        mips_data = texturemips_bytes_from_dds(source_texture, dds_path)

    carpart_data = make_carpart_data(mesh_rel, PLATE_SLOT_ID)
    compatible_data = make_compatiblepart_data(carpart_rel, PLATE_LABEL)
    return [
        (texture_rel, texture_data, "road plate texture descriptor"),
        (texture_rel[:-len(".texture")] + ".texturemips", mips_data, "road plate texturemips"),
        (material_rel, material_data, "road plate material"),
        (mesh_rel, mesh_data, "road plate mesh"),
        (carpart_rel, carpart_data, "road plate car part"),
        (compatible_rel, compatible_data, "road plate compatible part"),
    ]


def patch_path_strings(raw: bytes, replacements: dict[str, str]) -> bytes:
    exact = {norm_slashes(old): norm_slashes(new) for old, new in replacements.items() if old and new}
    patched = patch_strings_recursive_exact(raw, exact)
    if patched != raw:
        return patched
    changed = False
    out: list[ProtoField] = []
    for field in parse_fields(raw):
        if field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            text = is_printable_utf8(bytes(field.value))
            if text is not None:
                next_text = text
                for old, new in exact.items():
                    next_text = next_text.replace(old, new)
                if next_text != text:
                    out.append(ProtoField(field.number, field.wire, next_text.encode("utf-8")))
                    changed = True
                    continue
            else:
                nested = patch_path_strings(bytes(field.value), exact)
                if nested != bytes(field.value):
                    out.append(ProtoField(field.number, field.wire, nested))
                    changed = True
                    continue
        out.append(field)
    return build_fields(out) if changed else raw


def make_carpart_data(mesh_rel: str, slot_id: int = PLATE_SLOT_ID) -> bytes:
    label = "LiveryLab Road Plate"
    return build_fields(
        [
            ProtoField(2, 2, label.encode("utf-8")),
            ProtoField(3, 0, int(slot_id)),
            ProtoField(5, 2, norm_slashes(mesh_rel).encode("utf-8")),
            ProtoField(14, 2, label.encode("utf-8")),
            ProtoField(53, 2, b""),
            ProtoField(105, 2, b"LiveryLab"),
        ]
    )


def make_compatiblepart_data(carpart_rel: str, label: str = PLATE_LABEL) -> bytes:
    return build_fields(
        [
            ProtoField(1, 2, norm_slashes(carpart_rel).encode("utf-8")),
            ProtoField(3, 2, str(label or PLATE_LABEL).encode("utf-8")),
            ProtoField(5, 2, build_fields([ProtoField(1, 2, b"")])),
            ProtoField(6, 2, build_fields([ProtoField(1, 2, b"")])),
        ]
    )


def add_mechanical_compatiblepart_data(mechanical_data: bytes, compatible_rel: str) -> tuple[bytes, bool]:
    normalized = normalize_game_rel_path(compatible_rel)
    fields = parse_fields(mechanical_data)
    existing = [
        normalize_game_rel_path(is_printable_utf8(field.value) or "")
        for field in fields
        if field.number == 10 and field.wire == 2
    ]
    if any(row.lower() == normalized.lower() for row in existing):
        return mechanical_data, False
    insert_at: int | None = None
    for index, field in enumerate(fields):
        if field.number == 10 and field.wire == 2:
            insert_at = index + 1
    if insert_at is None:
        for index, field in enumerate(fields):
            if field.number in {8, 9} and field.wire == 2:
                insert_at = index + 1
    new_field = ProtoField(10, 2, normalized.encode("utf-8"))
    if insert_at is None:
        fields.append(new_field)
    else:
        fields.insert(insert_at, new_field)
    return build_fields(fields), True


def remove_mechanical_compatiblepart_data(mechanical_data: bytes, compatible_rel: str) -> tuple[bytes, bool]:
    normalized = normalize_game_rel_path(compatible_rel)
    fields = parse_fields(mechanical_data)
    changed = False
    kept: list[ProtoField] = []
    for field in fields:
        if field.number == 10 and field.wire == 2:
            text = normalize_game_rel_path(is_printable_utf8(field.value) or "")
            if text.lower() == normalized.lower():
                changed = True
                continue
        kept.append(field)
    return (build_fields(kept), True) if changed else (mechanical_data, False)


def _generated_plate_rels_for_compatible(root: Path, compatible_rel: str, car_content_path: str) -> list[str]:
    car_prefix = normalize_game_rel_path(car_content_path).lower().rstrip("\\/")
    suffixes = (".compatiblepart", ".carpart", ".mesh", ".material", ".texture", ".texturemips")
    rels: list[str] = []
    seen: set[str] = set()

    def add_rel(value: str | None) -> None:
        rel = normalize_game_rel_path(value)
        lower = rel.lower()
        if not rel or lower in seen:
            return
        if not lower.startswith(car_prefix + "\\"):
            return
        if not lower.endswith(suffixes):
            return
        seen.add(lower)
        rels.append(rel)
        if lower.endswith(".texture"):
            add_rel(rel[:-len(".texture")] + ".texturemips")

    def add_strings_from_file(rel: str, depth: int = 0) -> None:
        if depth > 4:
            return
        path = root / Path(*split_content_path(rel))
        if not path.exists() or not path.is_file():
            return
        try:
            strings = [text for _path, text in collect_proto_strings(path.read_bytes())]
        except Exception:
            return
        for text in strings:
            before = len(rels)
            add_rel(text)
            if len(rels) != before and normalize_game_rel_path(text).lower().endswith((".carpart", ".mesh", ".material")):
                add_strings_from_file(text, depth + 1)

    add_rel(compatible_rel)
    add_strings_from_file(compatible_rel)
    for rel in list(rels):
        if rel.lower().endswith((".carpart", ".mesh", ".material")):
            add_strings_from_file(rel, 1)
    return rels


def _delete_generated_plate_refs_for_compatible(
    root: Path,
    compatible_refs: list[str],
    car_content_path: str,
    writer: GameFileWriter,
    *,
    local_root_path: Path | None = None,
) -> list[str]:
    deleted: list[str] = []
    seen: set[str] = set()
    for compatible_rel in compatible_refs:
        if not is_liverylab_road_plate_compatible_rel(compatible_rel, car_content_path):
            continue
        for rel in _generated_plate_rels_for_compatible(root, compatible_rel, car_content_path):
            key = rel.lower()
            if key in seen:
                continue
            seen.add(key)
            target = root / Path(*split_content_path(rel))
            if target.exists() and target.is_file():
                writer.delete_file(target, label="stale road plate generated file removal", backup=True)
                deleted.append(rel)
            if local_root_path is not None:
                local_target = local_root_path / Path(*split_content_path(rel))
                if local_target.exists() and local_target.is_file():
                    local_target.unlink()
    return deleted


def add_actor_resource_data(actor_data: bytes, new_rel: str, field_number: int) -> tuple[bytes, bool]:
    new_rel = norm_slashes(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 or not isinstance(field.value, (bytes, bytearray)):
            new_actor_fields.append(field)
            continue
        payload_fields = parse_fields(bytes(field.value))
        new_payload_fields: list[ProtoField] = []
        patched_resource_block = False
        for payload in payload_fields:
            if payload.number != 13 or payload.wire != 2 or not isinstance(payload.value, (bytes, bytearray)):
                new_payload_fields.append(payload)
                continue
            resource_fields = parse_fields(bytes(payload.value))
            existing = [
                norm_slashes(is_printable_utf8(bytes(resource.value)) or "")
                for resource in resource_fields
                if resource.number == field_number and resource.wire == 2
            ]
            if new_rel in existing:
                new_payload_fields.append(payload)
                patched_resource_block = 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, new_rel.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.number, payload.wire, build_fields(resource_fields)))
            changed = True
            patched_resource_block = True
        new_actor_fields.append(
            ProtoField(field.number, field.wire, build_fields(new_payload_fields))
            if patched_resource_block
            else field
        )
    return (build_fields(new_actor_fields), True) if changed else (actor_data, False)


def _entry_finalstate_rels(entry: dict[str, Any]) -> list[str]:
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    values: list[str] = []
    for key in ("carfinalstates", "carfinalstate"):
        raw = files.get(key)
        rows = raw if isinstance(raw, list) else [raw]
        for row in rows:
            rel = normalize_game_rel_path(str(row or ""))
            if rel and rel not in values:
                values.append(rel)
    return values


def _patch_finalstate_plate_part(path: Path, compatible_rel: str, writer: GameFileWriter, label: str) -> tuple[bool, dict[str, Any]]:
    old = path.read_bytes()
    new = ensure_finalstate_plate_part(old, compatible_rel, PLATE_SLOT_ID, unique_seed=str(path))
    if new == old:
        return False, {}
    result = writer.write_bytes(path, new, label=label, backup=True)
    return True, result.to_dict()


def cleanup_entry_road_plate(
    entry: dict[str, Any],
    game_root: str | Path,
    writer: GameFileWriter,
    *,
    local_root: str | Path | None = None,
    savedcar_paths: list[str | Path] | None = None,
    mechanical_paths: list[str | Path] | None = None,
    managed_compatible_rels: list[str] | None = None,
) -> dict[str, Any]:
    road_plate = dict(entry.get("road_plate") or {}) if isinstance(entry.get("road_plate"), dict) else {}
    compatible_rel = normalize_game_rel_path(str(road_plate.get("compatiblePartRel") or ""))
    if not compatible_rel:
        road_plate["source"] = "default"
        road_plate.pop("lastSyncedProjectPngHash", None)
        entry["road_plate"] = road_plate
        return {"changed": False, "skipped": "no generated road plate metadata"}

    root = Path(game_root)
    local_root_path = Path(local_root) if local_root else None
    changed = False
    writes: list[dict[str, Any]] = []
    deleted: list[str] = []

    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    car_content_path = normalize_game_rel_path(str(entry.get("car_content_path") or ""))
    actor_rel = normalize_game_rel_path(str(files.get("actor") or ""))
    actor_path = resolve_content_path(root, actor_rel) if actor_rel else None
    actor_removed_refs: list[str] = []
    if actor_path and actor_path.exists():
        old_actor = actor_path.read_bytes()
        new_actor, actor_removed_refs = prune_actor_liverylab_road_plate_refs(
            old_actor,
            car_content_path,
            managed_compatible_rels or [],
            remove_refs=[compatible_rel],
        )
        actor_changed = new_actor != old_actor
        if actor_changed:
            result = writer.write_bytes(actor_path, new_actor, label="actor road plate part removal", backup=True)
            writes.append(result.to_dict())
            changed = True

    mechanical_results: list[dict[str, Any]] = []
    for mechanical in mechanical_paths or []:
        path = Path(mechanical)
        if not path.exists() or not path.is_file():
            continue
        old_mechanical = path.read_bytes()
        new_mechanical = old_mechanical
        mechanical_removed = 0
        for remove_rel in [compatible_rel, *actor_removed_refs]:
            new_mechanical, removed_changed = remove_mechanical_compatiblepart_data(new_mechanical, remove_rel)
            mechanical_removed += 1 if removed_changed else 0
        mechanical_changed = new_mechanical != old_mechanical
        if mechanical_changed:
            result = writer.write_bytes(path, new_mechanical, label="mechanical road plate part removal", backup=True)
            writes.append(result.to_dict())
            changed = True
        mechanical_results.append({"path": str(path), "changed": mechanical_changed, "removed": mechanical_removed})

    finalstate_results: list[dict[str, Any]] = []
    for rel in _entry_finalstate_rels(entry):
        target = root / Path(*split_content_path(rel))
        if not target.exists() or not target.is_file():
            continue
        final_changed, write_result = _remove_finalstate_plate_part(target, compatible_rel, writer, "road plate car final state removal")
        if final_changed:
            changed = True
        finalstate_results.append({"path": str(target), "changed": final_changed, "write": write_result})
        if local_root_path is not None:
            local_target = local_root_path / Path(*split_content_path(rel))
            if local_target.exists() and local_target.is_file():
                old_local = local_target.read_bytes()
                new_local = remove_finalstate_plate_part(old_local, compatible_rel, PLATE_SLOT_ID)
                if new_local != old_local:
                    local_target.write_bytes(new_local)
                    changed = True

    savedcar_results: list[dict[str, Any]] = []
    for savedcar in savedcar_paths or []:
        path = Path(savedcar)
        if not path.exists() or not path.is_file():
            continue
        saved_changed, write_result = _remove_finalstate_plate_part(path, compatible_rel, writer, "road plate saved car state removal")
        if saved_changed:
            changed = True
        savedcar_results.append({"path": str(path), "changed": saved_changed, "write": write_result})

    generated_rels = [
        road_plate.get("textureRel"),
        str(road_plate.get("textureRel") or "")[:-len(".texture")] + ".texturemips" if str(road_plate.get("textureRel") or "").lower().endswith(".texture") else "",
        road_plate.get("materialRel"),
        road_plate.get("meshRel"),
        road_plate.get("carPartRel"),
        road_plate.get("compatiblePartRel"),
    ]
    stale_deleted = _delete_generated_plate_refs_for_compatible(
        root,
        actor_removed_refs,
        car_content_path,
        writer,
        local_root_path=local_root_path,
    )
    if stale_deleted:
        deleted.extend(stale_deleted)
        changed = True
    seen_delete: set[str] = set()
    for rel_raw in generated_rels:
        rel = normalize_game_rel_path(str(rel_raw or ""))
        if not rel:
            continue
        key = rel.lower()
        if key in seen_delete:
            continue
        seen_delete.add(key)
        target = root / Path(*split_content_path(rel))
        if target.exists() and target.is_file():
            writer.delete_file(target, label="road plate generated file removal", backup=True)
            deleted.append(rel)
            changed = True
        if local_root_path is not None:
            local_target = local_root_path / Path(*split_content_path(rel))
            if local_target.exists() and local_target.is_file():
                local_target.unlink()
                changed = True

    for key in ("compatiblePartRel", "carPartRel", "meshRel", "materialRel", "textureRel", "lastSyncedProjectPngHash"):
        road_plate.pop(key, None)
    road_plate["source"] = "default"
    entry["road_plate"] = road_plate
    return {
        "changed": changed,
        "status": "removed" if changed else "current",
        "compatiblePartRel": compatible_rel,
        "writes": writes,
        "writeCount": len(writes),
        "deleted": deleted,
        "actorRemovedRefs": actor_removed_refs,
        "mechanicals": mechanical_results,
        "finalstates": finalstate_results,
        "savedcars": savedcar_results,
    }


def _part_payload_info(raw: bytes) -> dict[str, Any]:
    info: dict[str, Any] = {"partRel": "", "slot": None}
    try:
        for field in parse_fields(raw):
            if field.number == 1 and field.wire == 2:
                info["partRel"] = norm_slashes(is_printable_utf8(bytes(field.value)) or "")
            elif field.number == 102 and field.wire == 0:
                info["slot"] = int(field.value)
    except Exception:
        pass
    return info


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-road-plate-v1|{unique_seed}|{normalized}|{slot}")
    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 _looks_like_carfinalstate(data: bytes) -> bool:
    try:
        fields = parse_fields(data)
    except Exception:
        return False
    return any(field.number in {10, 30} for field in fields)


def _patch_wrapped_carfinalstate(
    data: bytes,
    patcher: Callable[[bytes], bytes],
) -> bytes:
    try:
        fields = parse_fields(data)
    except Exception:
        return data
    out: list[ProtoField] = []
    changed = False
    for field in fields:
        if field.number == 2 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            nested_raw = bytes(field.value)
            if _looks_like_carfinalstate(nested_raw):
                nested = patcher(nested_raw)
                if nested != nested_raw:
                    out.append(ProtoField(field.number, field.wire, nested))
                    changed = True
                    continue
        out.append(field)
    return build_fields(out) if changed else data


def _ensure_plain_finalstate_plate_part(data: bytes, compatible_rel: str, slot: int, unique_seed: str) -> bytes:
    if not _looks_like_carfinalstate(data):
        return data
    normalized = normalize_game_rel_path(compatible_rel)
    out: list[ProtoField] = []
    has_foreign_slot = False
    for field in parse_fields(data):
        if field.number == 30 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            info = _part_payload_info(bytes(field.value))
            part_rel = str(info.get("partRel") or "")
            part_slot = info.get("slot")
            if part_rel.lower() == normalized.lower():
                return data
            if int(part_slot or -1) == int(slot):
                has_foreign_slot = True
        out.append(field)
    if has_foreign_slot:
        return data
    out.append(_make_finalstate_part_field(normalized, slot, unique_seed))
    return build_fields(out)


def ensure_finalstate_plate_part(data: bytes, compatible_rel: str, slot: int = PLATE_SLOT_ID, unique_seed: str = "") -> bytes:
    if _looks_like_carfinalstate(data):
        return _ensure_plain_finalstate_plate_part(data, compatible_rel, slot, unique_seed)
    nested = _patch_wrapped_carfinalstate(
        data,
        lambda raw: _ensure_plain_finalstate_plate_part(raw, compatible_rel, slot, unique_seed),
    )
    return nested if nested != data else data


def _remove_plain_finalstate_plate_part(data: bytes, compatible_rel: str, slot: int) -> bytes:
    normalized = normalize_game_rel_path(compatible_rel)
    out: list[ProtoField] = []
    changed = False
    for field in parse_fields(data):
        if field.number == 30 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            info = _part_payload_info(bytes(field.value))
            part_rel = str(info.get("partRel") or "")
            if part_rel.lower() == normalized.lower():
                changed = True
                continue
        out.append(field)
    return build_fields(out) if changed else data


def remove_finalstate_plate_part(data: bytes, compatible_rel: str, slot: int = PLATE_SLOT_ID) -> bytes:
    plain = _remove_plain_finalstate_plate_part(data, compatible_rel, slot)
    if plain != data:
        return plain
    return _patch_wrapped_carfinalstate(
        data,
        lambda raw: _remove_plain_finalstate_plate_part(raw, compatible_rel, slot),
    )


def _remove_finalstate_plate_part(path: Path, compatible_rel: str, writer: GameFileWriter, label: str) -> tuple[bool, dict[str, Any]]:
    old = path.read_bytes()
    new = remove_finalstate_plate_part(old, compatible_rel, PLATE_SLOT_ID)
    if new == old:
        return False, {}
    result = writer.write_bytes(path, new, label=label, backup=True)
    return True, result.to_dict()


def remove_actor_resource_data(actor_data: bytes, rel: str, field_number: int) -> tuple[bytes, bool]:
    normalized = norm_slashes(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 or not isinstance(field.value, (bytes, bytearray)):
            new_actor_fields.append(field)
            continue
        payload_fields = parse_fields(bytes(field.value))
        new_payload_fields: list[ProtoField] = []
        payload_changed = False
        for payload in payload_fields:
            if payload.number == field_number and payload.wire == 2 and isinstance(payload.value, (bytes, bytearray)):
                text = norm_slashes(is_printable_utf8(bytes(payload.value)) or "")
                if text.lower() == normalized.lower():
                    changed = True
                    payload_changed = True
                    continue
            if payload.number != 13 or payload.wire != 2 or not isinstance(payload.value, (bytes, bytearray)):
                new_payload_fields.append(payload)
                continue
            resource_fields = parse_fields(bytes(payload.value))
            next_resource_fields: list[ProtoField] = []
            resource_changed = False
            for resource in resource_fields:
                if resource.number == field_number and resource.wire == 2 and isinstance(resource.value, (bytes, bytearray)):
                    text = norm_slashes(is_printable_utf8(bytes(resource.value)) or "")
                    if text.lower() == normalized.lower():
                        changed = True
                        payload_changed = True
                        resource_changed = True
                        continue
                next_resource_fields.append(resource)
            new_payload_fields.append(
                ProtoField(payload.number, payload.wire, build_fields(next_resource_fields))
                if resource_changed
                else payload
            )
        new_actor_fields.append(
            ProtoField(field.number, field.wire, build_fields(new_payload_fields))
            if payload_changed
            else field
        )
    return (build_fields(new_actor_fields), True) if changed else (actor_data, False)


def _clean_part_id(value: str) -> str:
    clean = re.sub(r"[^A-Za-z0-9_\\-]+", "_", str(value or "").strip()).strip("_-").lower()
    return clean[:72] or "skin"
