from __future__ import annotations

import math
import os
import struct
from pathlib import Path
from typing import Any

from liverylab_core import official_saved_car_generation, official_saved_car_state, project_capabilities
from liverylab_core.proto import ProtoField, build_fields, is_printable_utf8, parse_fields


BLOCKED_LABEL_MARKERS = ("DESIGN", "PRESENTATION", "PRES")


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


def _field_text(fields: list[ProtoField], number: int) -> str:
    for field in fields:
        if field.number == number and field.wire == 2 and isinstance(field.value, bytes):
            return is_printable_utf8(field.value) or ""
    return ""


def _float_value(field: ProtoField) -> float | None:
    if field.wire != 5:
        return None
    raw = field.value if isinstance(field.value, bytes) else struct.pack("<I", int(field.value))
    return struct.unpack("<f", raw)[0]


def _vector(fields: list[ProtoField]) -> tuple[float, float, float] | None:
    values: dict[int, float] = {}
    for field in fields:
        value = _float_value(field)
        if field.number in (1, 2, 3) and value is not None:
            values[field.number] = value
    return (values[1], values[2], values[3]) if len(values) == 3 else None


def _linear_to_srgb(value: float) -> float:
    value = max(0.0, min(1.0, value))
    return 12.92 * value if value <= 0.0031308 else 1.055 * math.pow(value, 1.0 / 2.4) - 0.055


def _srgb_to_linear(value: float) -> float:
    value = max(0.0, min(1.0, value))
    return value / 12.92 if value <= 0.04045 else math.pow((value + 0.055) / 1.055, 2.4)


def _hex_from_linear(values: tuple[float, float, float]) -> str:
    rgb = [round(_linear_to_srgb(value) * 255) for value in values]
    return "#" + "".join(f"{value:02x}" for value in rgb)


def _linear_from_hex(color: str) -> tuple[float, float, float]:
    text = str(color or "").strip().lower()
    if len(text) != 7 or not text.startswith("#"):
        raise ValueError(f"Invalid body color: {color}")
    try:
        return tuple(_srgb_to_linear(int(text[index:index + 2], 16) / 255.0) for index in (1, 3, 5))
    except ValueError as exc:
        raise ValueError(f"Invalid body color: {color}") from exc


def find_saved_car(car_model: str, car_guid: str) -> Path | None:
    filename = f"{str(car_model or '').strip()}_{official_saved_car_state.normalize_guid(car_guid)}{official_saved_car_state.SAVED_CAR_SUFFIX}"
    if not filename.strip("_"):
        return None
    for root in project_capabilities.saved_cars_roots():
        candidate = root / filename
        if candidate.is_file():
            return candidate
    return None


def _resolve_oem_color(game_root: Path, reference: str) -> tuple[str, str] | None:
    raw = str(reference or "").replace("\\", "/").strip()
    relative = Path(*raw.split("/"))
    candidates = [game_root / relative]
    if relative.parts and relative.parts[0].lower() == "content":
        candidates.append(game_root / Path(*relative.parts[1:]))
    path = next((candidate for candidate in candidates if candidate.is_file()), None)
    if path is None:
        return None
    fields = _exact_fields(path.read_bytes()) or []
    vector_field = next((field for field in fields if field.number == 3 and field.wire == 2 and isinstance(field.value, bytes)), None)
    vector = _vector(_exact_fields(vector_field.value) or []) if vector_field else None
    return (_hex_from_linear(vector), _field_text(fields, 2) or path.stem) if vector else None


def _control_details(control_raw: bytes, game_root: Path) -> dict[str, Any]:
    fields = _exact_fields(control_raw) or []
    vector_field = next((field for field in fields if field.number == 1 and field.wire == 2 and isinstance(field.value, bytes)), None)
    vector = _vector(_exact_fields(vector_field.value) or []) if vector_field else None
    if vector:
        return {"available": True, "color": _hex_from_linear(vector), "source": "saved_car_custom", "oemReference": "", "oemName": ""}
    oem_ref = _field_text(fields, 10)
    resolved = _resolve_oem_color(game_root, oem_ref) if oem_ref else None
    if resolved:
        return {"available": True, "color": resolved[0], "source": "saved_car_oem", "oemReference": oem_ref, "oemName": resolved[1]}
    return {"available": False, "color": "", "source": "", "oemReference": oem_ref, "oemName": ""}


def _find_control(raw: bytes, game_root: Path) -> dict[str, Any]:
    fields = _exact_fields(raw)
    if fields is None:
        return {}
    label = _field_text(fields, 1).strip().upper()
    if label.startswith("EXT SKIN"):
        blocked = any(marker in label for marker in BLOCKED_LABEL_MARKERS)
        wrapper_field = next((field for field in fields if field.number == 2 and field.wire == 2 and isinstance(field.value, bytes)), None)
        wrapper = _exact_fields(wrapper_field.value) if wrapper_field else None
        control_field = next((field for field in wrapper or [] if field.number == 2 and field.wire == 2 and isinstance(field.value, bytes)), None)
        details = _control_details(control_field.value, game_root) if control_field else {"available": False}
        return {**details, "label": label, "blocked": blocked, "supported": bool(details.get("available") and not blocked)}
    for field in fields:
        if field.wire == 2 and isinstance(field.value, bytes):
            found = _find_control(field.value, game_root)
            if found:
                return found
    return {}


def inspect(path: str | Path, game_root: str | Path) -> dict[str, Any]:
    saved_car = Path(path)
    inspection = official_saved_car_state.inspect_saved_car_file(saved_car)
    if not inspection.get("valid"):
        return {"supported": False, "available": False, "reason": "SavedCar is invalid", "path": str(saved_car)}
    inner = bytes(official_saved_car_state.unwrap_saved_car_state(saved_car.read_bytes()).get("innerData") or b"")
    details = _find_control(inner, Path(game_root).expanduser())
    return {**details, "path": str(saved_car), "carGuid": inspection.get("carGuid") or "", "carModel": inspection.get("carModel") or "", "reason": "" if details.get("supported") else "No editable native EXT SKIN body-color control was found"}


def _color_control(color: str) -> bytes:
    values = _linear_from_hex(color)
    vector = build_fields([ProtoField(index, 5, struct.pack("<f", value)) for index, value in enumerate(values, 1)])
    return build_fields([ProtoField(1, 2, vector), ProtoField(3, 5, struct.pack("<f", 0.3)), ProtoField(4, 5, struct.pack("<f", 0.6)), ProtoField(20, 2, b"")])


def _patch_control(raw: bytes, control: bytes) -> tuple[bytes, int]:
    fields = _exact_fields(raw)
    if fields is None:
        return raw, 0
    label = _field_text(fields, 1).strip().upper()
    if label.startswith("EXT SKIN") and not any(marker in label for marker in BLOCKED_LABEL_MARKERS):
        changed = 0
        output: list[ProtoField] = []
        for field in fields:
            value = field.value
            if field.number == 2 and field.wire == 2 and isinstance(value, bytes):
                wrapper = _exact_fields(value) or []
                next_wrapper: list[ProtoField] = []
                for item in wrapper:
                    if item.number == 2 and item.wire == 2:
                        next_wrapper.append(ProtoField(2, 2, control))
                        changed += 1
                    else:
                        next_wrapper.append(item)
                value = build_fields(next_wrapper)
            output.append(ProtoField(field.number, field.wire, value))
        return build_fields(output), changed
    changed = 0
    output = []
    for field in fields:
        value = field.value
        if field.wire == 2 and isinstance(value, bytes):
            value, count = _patch_control(value, control)
            changed += count
        output.append(ProtoField(field.number, field.wire, value))
    return build_fields(output), changed


def write(path: str | Path, game_root: str | Path, color: str) -> dict[str, Any]:
    saved_car = Path(path)
    before = inspect(saved_car, game_root)
    if not before.get("supported"):
        raise ValueError(str(before.get("reason") or "SavedCar body color is not editable"))
    raw = saved_car.read_bytes()
    inner = bytes(official_saved_car_state.unwrap_saved_car_state(raw).get("innerData") or b"")
    patched_inner, count = _patch_control(inner, _color_control(color))
    if count != 1:
        raise RuntimeError(f"Expected one SavedCar body-color control, patched {count}")
    patched = official_saved_car_generation.wrap_car_final_state(patched_inner)
    validated = official_saved_car_state.inspect_saved_car_bytes(patched, saved_car.name)
    if not validated.get("valid") or validated.get("carGuid") != before.get("carGuid") or validated.get("carModel") != before.get("carModel"):
        raise RuntimeError("Patched SavedCar failed validation")
    temp = saved_car.with_name(saved_car.name + ".body-color.tmp")
    temp.write_bytes(patched)
    os.replace(temp, saved_car)
    after = inspect(saved_car, game_root)
    expected = str(color).lower()
    if not after.get("supported") or str(after.get("color") or "").lower() != expected:
        raise RuntimeError("SavedCar body color verification failed")
    return {"changed": str(before.get("color") or "").lower() != expected, "before": before, "after": after, "path": str(saved_car)}
