from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from liverylab_core.proto import ProtoField, build_fields, is_printable_utf8, parse_fields


SAVED_CAR_SUFFIX = ".carfinalstatewithconsumable"
GUID_RE = re.compile(
    r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"
)
OFFICIAL_EXTERNAL_AUTHORED_SKIN_MARKERS = ("DESIGN", "PRESENTATION")
LIVERY_LINKER_FIELD_NUMBER = 52


def official_external_material_compatibility(inspection: dict[str, Any] | None) -> dict[str, Any]:
    refs = inspection.get("materialRefs") if isinstance(inspection, dict) else []
    material_refs: list[str] = []
    material_stems: list[str] = []
    neutral_stems: list[str] = []
    for value in refs or []:
        ref = str(value or "").strip()
        stem = Path(ref.replace("\\", "/")).stem.strip().upper()
        if not stem.startswith("EXT_SKIN"):
            continue
        if ref and ref not in material_refs:
            material_refs.append(ref)
        if stem and stem not in material_stems:
            material_stems.append(stem)
        normalized_ref = ref.replace("\\", "/").lower()
        authored = (
            "/skins/" in normalized_ref
            or any(marker in stem for marker in OFFICIAL_EXTERNAL_AUTHORED_SKIN_MARKERS)
            or stem.endswith("_PRES")
        )
        if not authored and stem not in neutral_stems:
            neutral_stems.append(stem)
    compatible = bool(neutral_stems)
    reason = ""
    if not material_stems:
        reason = "The selected variant has no EXT_SKIN material reference."
    elif not compatible:
        reason = "The selected variant uses only pre-authored skin/design/presentation materials, which AC EVO does not expose to Official External Liveries."
    return {
        "compatible": compatible,
        "reason": reason,
        "materialRefs": material_refs,
        "materialStems": material_stems,
        "neutralMaterialStems": neutral_stems,
    }


def normalize_guid(value: Any) -> str:
    match = GUID_RE.search(str(value or ""))
    return match.group(1).lower() if match else ""


def guid_from_linker_parts(high: Any, low: Any) -> str:
    try:
        high_value = int(high)
        low_value = int(low)
    except (TypeError, ValueError):
        return ""
    if not 0 <= high_value < (1 << 64) or not 0 <= low_value < (1 << 64):
        return ""
    compact = f"{high_value:016x}{low_value:016x}"
    return f"{compact[:8]}-{compact[8:12]}-{compact[12:16]}-{compact[16:20]}-{compact[20:]}"


def official_livery_guid(inspection: dict[str, Any] | None) -> str:
    if not isinstance(inspection, dict):
        return ""
    if inspection.get("liveryLinkerPresent"):
        return normalize_guid(inspection.get("liveryLinkerGuid"))
    return normalize_guid(inspection.get("carGuid"))


def matches_official_livery_guid(inspection: dict[str, Any] | None, expected_guid: Any) -> bool:
    expected = normalize_guid(expected_guid)
    return bool(expected and official_livery_guid(inspection) == expected)


def guid_from_filename(path: str | Path) -> str:
    return normalize_guid(Path(path).name)


def car_model_from_filename(path: str | Path) -> str:
    item = Path(path)
    stem = item.name
    if stem.lower().endswith(SAVED_CAR_SUFFIX):
        stem = stem[: -len(SAVED_CAR_SUFFIX)]
    guid = normalize_guid(stem)
    if guid and stem.lower().endswith(f"_{guid}"):
        return stem[: -(len(guid) + 1)]
    match = GUID_RE.search(stem)
    if match:
        return stem[: match.start()].rstrip("_")
    return item.stem


def _parse_message(raw: bytes) -> list[ProtoField] | None:
    try:
        fields = parse_fields(raw)
        if build_fields(fields) != raw:
            return None
        return fields
    except Exception:
        return None


def _string_value(raw: Any) -> str:
    if not isinstance(raw, (bytes, bytearray)):
        return ""
    return is_printable_utf8(bytes(raw)) or ""


def _string_field(fields: list[ProtoField], number: int) -> str:
    for field in fields:
        if field.number == number and field.wire == 2:
            text = _string_value(field.value)
            if text:
                return text
    return ""


def _message_fields(fields: list[ProtoField], number: int) -> list[list[ProtoField]]:
    messages: list[list[ProtoField]] = []
    for field in fields:
        if field.number != number or field.wire != 2:
            continue
        parsed = _parse_message(bytes(field.value))
        if parsed is not None:
            messages.append(parsed)
    return messages


def _walk_strings(raw: bytes, *, depth: int = 0, max_depth: int = 8) -> list[str]:
    if depth > max_depth:
        return []
    fields = _parse_message(raw)
    if fields is None:
        text = _string_value(raw)
        return [text] if text else []
    strings: list[str] = []
    for field in fields:
        if field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
            continue
        value = bytes(field.value)
        nested = _parse_message(value)
        if nested is not None:
            strings.extend(_walk_strings(value, depth=depth + 1, max_depth=max_depth))
            continue
        text = _string_value(value)
        if text:
            strings.append(text)
    return strings


def _unique(values: list[str]) -> list[str]:
    seen: set[str] = set()
    out: list[str] = []
    for value in values:
        key = value.lower()
        if key in seen:
            continue
        seen.add(key)
        out.append(value)
    return out


def _car_model_from_content_path(path: str) -> str:
    normalized = path.replace("/", "\\")
    marker = "content\\cars\\"
    lower = normalized.lower()
    start = lower.find(marker)
    if start < 0:
        return ""
    rest = normalized[start + len(marker) :]
    return rest.split("\\", 1)[0]


def _guids_from_strings(strings: list[str]) -> list[str]:
    guids: list[str] = []
    for text in strings:
        guids.extend(match.group(1).lower() for match in GUID_RE.finditer(text))
    return _unique(guids)


def _content_refs(strings: list[str], suffix: str) -> list[str]:
    suffix = suffix.lower()
    return _unique(
        [
            text
            for text in strings
            if text.replace("/", "\\").lower().startswith("content\\") and text.lower().endswith(suffix)
        ]
    )


def looks_like_carfinalstate(data: bytes) -> bool:
    fields = _parse_message(data)
    if fields is None:
        return False
    car_data = _string_field(fields, 1).replace("/", "\\").lower()
    actor = _string_field(fields, 2).replace("/", "\\").lower()
    if car_data.endswith(".car") and actor.endswith(".actor"):
        return True
    if "\\data\\cardata.car" in car_data and actor.endswith(".actor"):
        return True
    for preset_fields in _message_fields(fields, 10):
        mechanical = _string_field(preset_fields, 1).lower()
        visual = _string_field(preset_fields, 2).lower()
        if mechanical.endswith(".mechanicalcarpreset") or visual.endswith(".visualcarpreset"):
            return True
    return False


def unwrap_saved_car_state(data: bytes) -> dict[str, Any]:
    if looks_like_carfinalstate(data):
        return {
            "valid": True,
            "wrapped": False,
            "fieldPath": "",
            "innerData": data,
            "errors": [],
        }

    fields = _parse_message(data)
    if fields is None:
        return {
            "valid": False,
            "wrapped": False,
            "fieldPath": "",
            "innerData": b"",
            "errors": ["Saved car file is not a supported protobuf message."],
        }

    for field in fields:
        if field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
            continue
        value = bytes(field.value)
        if looks_like_carfinalstate(value):
            return {
                "valid": True,
                "wrapped": True,
                "fieldPath": str(field.number),
                "innerData": value,
                "errors": [],
            }

    return {
        "valid": False,
        "wrapped": False,
        "fieldPath": "",
        "innerData": b"",
        "errors": ["Could not find an inner car final state message."],
    }


def _inspect_compatible_parts(fields: list[ProtoField]) -> list[dict[str, str]]:
    parts: list[dict[str, str]] = []
    for part_fields in _message_fields(fields, 30):
        path = _string_field(part_fields, 1)
        guid = normalize_guid(_string_field(part_fields, 100))
        if path or guid:
            parts.append({"path": path, "guid": guid})
    return parts


def _inspect_livery_linker(fields: list[ProtoField]) -> dict[str, Any]:
    present = False
    for field in fields:
        if field.number != LIVERY_LINKER_FIELD_NUMBER or field.wire != 2:
            continue
        present = True
        nested = _parse_message(bytes(field.value))
        if nested is None:
            continue
        values = {
            child.number: int(child.value)
            for child in nested
            if child.wire == 0 and child.number in (1, 2)
        }
        guid = guid_from_linker_parts(values.get(1), values.get(2))
        if guid:
            return {
                "present": True,
                "fieldPath": str(LIVERY_LINKER_FIELD_NUMBER),
                "a": str(values[1]),
                "b": str(values[2]),
                "guid": guid,
            }
    return {
        "present": present,
        "fieldPath": str(LIVERY_LINKER_FIELD_NUMBER) if present else "",
        "a": "",
        "b": "",
        "guid": "",
    }


def inspect_saved_car_bytes(data: bytes, filename: str | Path = "") -> dict[str, Any]:
    filename_guid = guid_from_filename(filename) if filename else ""
    filename_car_model = car_model_from_filename(filename) if filename else ""
    unwrapped = unwrap_saved_car_state(data)
    result: dict[str, Any] = {
        "valid": bool(unwrapped.get("valid")),
        "wrapped": bool(unwrapped.get("wrapped")),
        "fieldPath": unwrapped.get("fieldPath") or "",
        "size": len(data),
        "filenameGuid": filename_guid,
        "filenameCarModel": filename_car_model,
        "carGuid": filename_guid,
        "liveryLinkerPresent": False,
        "liveryLinkerFieldPath": "",
        "liveryLinker": {},
        "liveryLinkerGuid": "",
        "carModel": filename_car_model,
        "carData": "",
        "actorPath": "",
        "mechanicalPreset": "",
        "visualPreset": "",
        "compatibleRim": "",
        "rimDesign": "",
        "rimGuid": "",
        "materialRefs": [],
        "compatibleParts": [],
        "contentRefs": [],
        "guids": [],
        "warnings": [],
        "errors": list(unwrapped.get("errors") or []),
    }
    if not result["valid"]:
        return result

    inner = bytes(unwrapped["innerData"])
    fields = _parse_message(inner) or []
    strings = _walk_strings(inner)
    guids = _guids_from_strings(strings)
    livery_linker = _inspect_livery_linker(fields)
    result["liveryLinkerPresent"] = bool(livery_linker.get("present"))
    result["liveryLinkerFieldPath"] = str(livery_linker.get("fieldPath") or "")
    result["liveryLinker"] = {
        "a": str(livery_linker.get("a") or ""),
        "b": str(livery_linker.get("b") or ""),
    } if livery_linker.get("guid") else {}
    result["liveryLinkerGuid"] = str(livery_linker.get("guid") or "")

    result["carData"] = _string_field(fields, 1)
    result["actorPath"] = _string_field(fields, 2)
    result["carModel"] = (
        _car_model_from_content_path(result["carData"])
        or _car_model_from_content_path(result["actorPath"])
        or filename_car_model
    )

    for preset_fields in _message_fields(fields, 10):
        result["mechanicalPreset"] = result["mechanicalPreset"] or _string_field(preset_fields, 1)
        result["visualPreset"] = result["visualPreset"] or _string_field(preset_fields, 2)
        result["carGuid"] = normalize_guid(_string_field(preset_fields, 100)) or result["carGuid"]

    for rim_fields in _message_fields(fields, 20):
        result["compatibleRim"] = result["compatibleRim"] or _string_field(rim_fields, 1)
        result["rimDesign"] = result["rimDesign"] or _string_field(rim_fields, 2)
        result["rimGuid"] = normalize_guid(_string_field(rim_fields, 100)) or result["rimGuid"]

    result["compatibleParts"] = _inspect_compatible_parts(fields)
    result["guids"] = guids
    result["carGuid"] = normalize_guid(result["carGuid"]) or (guids[0] if guids else "")
    result["materialRefs"] = _content_refs(strings, ".material")
    result["contentRefs"] = _unique([text for text in strings if text.replace("/", "\\").lower().startswith("content\\")])
    if result["liveryLinkerPresent"] and not result["liveryLinkerGuid"]:
        result["warnings"].append("SavedCar livery linker is incomplete or invalid.")
    if filename_guid and result["liveryLinkerGuid"] and filename_guid != result["liveryLinkerGuid"]:
        result["warnings"].append("Filename GUID does not match the SavedCar livery linker.")
    elif not result["liveryLinkerPresent"] and filename_guid and result["carGuid"] and filename_guid != result["carGuid"]:
        result["warnings"].append("Filename GUID does not match the inner saved car GUID.")
    return result


def inspect_saved_car_file(path: str | Path) -> dict[str, Any]:
    item = Path(path).expanduser()
    try:
        return inspect_saved_car_bytes(item.read_bytes(), item.name)
    except Exception as exc:
        return {
            "valid": False,
            "wrapped": False,
            "fieldPath": "",
            "size": 0,
            "filenameGuid": guid_from_filename(item),
            "filenameCarModel": car_model_from_filename(item),
            "carGuid": "",
            "liveryLinkerPresent": False,
            "liveryLinkerFieldPath": "",
            "liveryLinker": {},
            "liveryLinkerGuid": "",
            "carModel": car_model_from_filename(item),
            "errors": [str(exc)],
            "warnings": [],
        }
