from __future__ import annotations

from typing import Iterable

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

EXT_BANNER_SLOT_ID = "EXT_BANNER"
EXT_BANNER_LABEL = "EXT BANNER"
FINALSTATE_SNAPSHOT_FIELDS = {3, 4}
COMMON_CLEAR_GLASS_COLOR = "content\\cars\\common_assets\\customization\\common\\race_glass_clear.oemsinglelayercolor"


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


def _field_text(field: ProtoField) -> str | None:
    if field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
        return None
    return is_printable_utf8(bytes(field.value))


def _collect_texts(raw: bytes, depth: int = 0) -> list[str]:
    if depth > 8:
        return []
    try:
        fields = parse_fields(raw)
    except Exception:
        return []
    texts: list[str] = []
    for field in fields:
        text = _field_text(field)
        if text is not None:
            texts.append(text)
        elif field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            texts.extend(_collect_texts(bytes(field.value), depth + 1))
    return texts


def _text_matches_material(text: str, material_rel: str) -> bool:
    return norm_slashes(text).lower() == norm_slashes(material_rel).lower()


def _is_material_ref(text: str) -> bool:
    return norm_slashes(text).lower().endswith(".material") and "content\\cars\\" in norm_slashes(text).lower()


def _replace_material_texts(raw: bytes, material_rel: str) -> tuple[bytes, bool]:
    try:
        fields = parse_fields(raw)
    except Exception:
        return raw, False
    changed = False
    out: list[ProtoField] = []
    for field in fields:
        if field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
            out.append(field)
            continue
        value = bytes(field.value)
        nested, nested_changed = _replace_material_texts(value, material_rel)
        if nested_changed:
            out.append(ProtoField(field.number, field.wire, nested))
            changed = True
            continue
        text = is_printable_utf8(value)
        if text is not None:
            if _is_material_ref(text) and not _text_matches_material(text, material_rel):
                out.append(ProtoField(field.number, field.wire, norm_slashes(material_rel).encode("utf-8")))
                changed = True
            else:
                out.append(field)
            continue
        out.append(field)
    return (build_fields(out), True) if changed else (raw, False)


def _slot_id(slot_raw: bytes) -> str:
    try:
        for field in parse_fields(slot_raw):
            if field.number == 1:
                text = _field_text(field)
                if text:
                    return text
    except Exception:
        return ""
    return ""


def _slot_label(slot_raw: bytes) -> str:
    try:
        for field in parse_fields(slot_raw):
            if field.number == 2:
                text = _field_text(field)
                if text:
                    return text
    except Exception:
        return ""
    return ""


def _is_ext_skin_slot(slot_raw: bytes) -> bool:
    return _slot_id(slot_raw).upper().startswith("EXT_SKIN")


def design_ext_banner_label(data: bytes) -> str | None:
    try:
        fields = parse_fields(data)
    except Exception:
        return None
    for field in fields:
        if field.number == 3 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            slot_raw = bytes(field.value)
            if _slot_id(slot_raw) == EXT_BANNER_SLOT_ID or _raw_has_ext_banner(slot_raw):
                label = _slot_label(slot_raw)
                if label:
                    return label
    return None


def design_ext_banner_material(data: bytes) -> str | None:
    try:
        fields = parse_fields(data)
    except Exception:
        return None
    for field in fields:
        if field.number != 3 or field.wire != 2 or not isinstance(field.value, (bytes, bytearray)):
            continue
        slot_raw = bytes(field.value)
        if _slot_id(slot_raw) != EXT_BANNER_SLOT_ID and not _raw_has_ext_banner(slot_raw):
            continue
        try:
            for slot_field in parse_fields(slot_raw):
                if slot_field.number == 3:
                    text = _field_text(slot_field)
                    if text and _is_material_ref(text):
                        return norm_slashes(text)
        except Exception:
            continue
    return None


def _text_is_ext_banner(text: str) -> bool:
    normalized = norm_slashes(text).lower()
    return (
        normalized == EXT_BANNER_SLOT_ID.lower()
        or normalized == EXT_BANNER_LABEL.lower()
        or "ext_banner" in normalized
        or "ext banner" in normalized
    )


def _raw_has_ext_banner(raw: bytes) -> bool:
    return any(_text_is_ext_banner(text) for text in _collect_texts(raw))


def remove_design_ext_banner_slot(data: bytes) -> tuple[bytes, bool]:
    fields = parse_fields(data)
    out: list[ProtoField] = []
    changed = False
    for field in fields:
        if field.number == 3 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            slot_raw = bytes(field.value)
            if _slot_id(slot_raw) == EXT_BANNER_SLOT_ID or _raw_has_ext_banner(slot_raw):
                changed = True
                continue
        out.append(field)
    return (build_fields(out), True) if changed else (data, False)


def remove_visualpreset_ext_banner_entry(data: bytes) -> tuple[bytes, bool]:
    fields = parse_fields(data)
    out: list[ProtoField] = []
    changed = False
    for field in fields:
        if field.number in {8, 9} and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            if _raw_has_ext_banner(bytes(field.value)):
                changed = True
                continue
        out.append(field)
    return (build_fields(out), True) if changed else (data, False)


def remove_carfinalstate_ext_banner_snapshot(data: bytes) -> tuple[bytes, bool]:
    fields = parse_fields(data)
    out: list[ProtoField] = []
    changed = False
    for top in fields:
        if top.number != 10 or top.wire != 2 or not isinstance(top.value, (bytes, bytearray)):
            out.append(top)
            continue
        field10 = bytes(top.value)
        f10_fields = parse_fields(field10)
        new_f10: list[ProtoField] = []
        field10_changed = False
        for field in f10_fields:
            if field.number in FINALSTATE_SNAPSHOT_FIELDS and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
                if _raw_has_ext_banner(bytes(field.value)):
                    field10_changed = True
                    continue
            new_f10.append(field)
        if field10_changed:
            out.append(ProtoField(top.number, top.wire, build_fields(new_f10)))
            changed = True
        else:
            out.append(top)
    return (build_fields(out), True) if changed else (data, False)


def _minimal_design_slot(material_rel: str, label: str | None = None) -> bytes:
    clean_label = str(label or "").strip() or EXT_BANNER_LABEL
    empty_payload = build_fields([
        ProtoField(1, 2, b""),
        ProtoField(2, 2, b""),
        ProtoField(3, 2, b""),
    ])
    return build_fields([
        ProtoField(1, 2, EXT_BANNER_SLOT_ID.encode("utf-8")),
        ProtoField(2, 2, clean_label.encode("utf-8")),
        ProtoField(3, 2, norm_slashes(material_rel).encode("utf-8")),
        ProtoField(6, 2, empty_payload),
    ])


def ensure_design_ext_banner_slot(data: bytes, material_rel: str, label: str | None = None) -> tuple[bytes, bool]:
    material_rel = norm_slashes(material_rel)
    fields = parse_fields(data)
    desired_label = str(label or "").strip() or EXT_BANNER_LABEL
    banner_label = desired_label
    without_banner: list[ProtoField] = []

    for field in fields:
        if field.number == 3 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            slot_raw = bytes(field.value)
            if _slot_id(slot_raw) == EXT_BANNER_SLOT_ID or _raw_has_ext_banner(slot_raw):
                banner_label = _slot_label(slot_raw) or banner_label
                continue
        without_banner.append(field)

    banner_field = ProtoField(3, 2, _minimal_design_slot(material_rel, banner_label))
    last_skin_index = -1
    for index, field in enumerate(without_banner):
        if field.number == 3 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            if _is_ext_skin_slot(bytes(field.value)):
                last_skin_index = index

    out: list[ProtoField] = []
    inserted = False
    for index, field in enumerate(without_banner):
        out.append(field)
        if index == last_skin_index:
            out.append(banner_field)
            inserted = True
    if not inserted:
        out.append(banner_field)

    new_data = build_fields(out)
    return (new_data, True) if new_data != data else (data, False)


def _visual_entry_has_ext_banner(raw: bytes, material_rel: str) -> bool:
    texts = _collect_texts(raw)
    return any(_text_matches_material(text, material_rel) for text in texts) or any(
        "ext_banner" in norm_slashes(text).lower() for text in texts
    )


def ensure_visualpreset_ext_banner_entry(data: bytes, material_rel: str) -> tuple[bytes, bool]:
    material_rel = norm_slashes(material_rel)
    fields = parse_fields(data)
    out: list[ProtoField] = []
    changed = False
    for field in fields:
        if field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            raw = bytes(field.value)
            if field.number == 9 and _visual_entry_has_ext_banner(raw, material_rel):
                patched, did_patch = _replace_material_texts(raw, material_rel)
                out.append(ProtoField(field.number, field.wire, patched) if did_patch else field)
                changed = changed or did_patch
                continue
        out.append(field)
    return (build_fields(out), True) if changed else (data, False)


def _finalstate_snapshot_label(snapshot_raw: bytes) -> str:
    try:
        for field in parse_fields(snapshot_raw):
            if field.wire == 2 and field.number == 1:
                text = _field_text(field)
                if text:
                    return text
    except Exception:
        return ""
    return ""


def carstate_ext_banner_label(data: bytes) -> str | None:
    try:
        fields = parse_fields(data)
    except Exception:
        return None
    for top in fields:
        if top.number == 10 and top.wire == 2 and isinstance(top.value, (bytes, bytearray)):
            try:
                f10_fields = parse_fields(bytes(top.value))
            except Exception:
                continue
            for field in f10_fields:
                if field.number in FINALSTATE_SNAPSHOT_FIELDS and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
                    snapshot = bytes(field.value)
                    if _raw_has_ext_banner(snapshot):
                        label = _finalstate_snapshot_label(snapshot)
                        if label:
                            return label
        elif top.number == 2 and top.wire == 2 and isinstance(top.value, (bytes, bytearray)):
            nested_label = carstate_ext_banner_label(bytes(top.value))
            if nested_label:
                return nested_label
    return None


def _patch_finalstate_snapshot(snapshot_raw: bytes, material_rel: str, label: str | None = None) -> bytes:
    fields = parse_fields(snapshot_raw)
    out: list[ProtoField] = []
    wrote_label = False
    label = str(label or "").strip() or _finalstate_snapshot_label(snapshot_raw) or EXT_BANNER_LABEL
    for field in fields:
        if field.wire == 2 and field.number == 1:
            out.append(ProtoField(field.number, field.wire, label.encode("utf-8")))
            wrote_label = True
        elif field.wire == 2 and field.number == 2 and isinstance(field.value, (bytes, bytearray)):
            nested, _did_patch = _replace_material_texts(bytes(field.value), material_rel)
            out.append(ProtoField(field.number, field.wire, nested))
        else:
            out.append(field)
    if not wrote_label:
        out.insert(0, ProtoField(1, 2, label.encode("utf-8")))
    return build_fields(out)


def _minimal_finalstate_material_snapshot(material_rel: str, label: str | None = None) -> bytes:
    clean_label = str(label or "").strip() or EXT_BANNER_LABEL
    return build_fields([
        ProtoField(1, 2, clean_label.encode("utf-8")),
        ProtoField(2, 2, build_fields([
            ProtoField(1, 2, norm_slashes(material_rel).encode("utf-8")),
        ])),
    ])


def _minimal_finalstate_selected_snapshot(material_rel: str, label: str | None = None) -> bytes:
    clean_label = str(label or "").strip() or EXT_BANNER_LABEL
    clear_choice = build_fields([
        ProtoField(1, 2, b""),
        ProtoField(10, 2, COMMON_CLEAR_GLASS_COLOR.encode("utf-8")),
        ProtoField(20, 2, b""),
    ])
    return build_fields([
        ProtoField(1, 2, clean_label.encode("utf-8")),
        ProtoField(2, 2, build_fields([
            ProtoField(1, 2, norm_slashes(material_rel).encode("utf-8")),
            ProtoField(2, 2, clear_choice),
        ])),
    ])


def _snapshot_is_ext_banner(snapshot_raw: bytes, material_rel: str) -> bool:
    material_rel = norm_slashes(material_rel)
    texts = _collect_texts(snapshot_raw)
    return (
        any(_text_matches_material(text, material_rel) for text in texts)
        or _raw_has_ext_banner(snapshot_raw)
    )


def ensure_carfinalstate_ext_banner_snapshot(data: bytes, material_rel: str, label: str | None = None) -> tuple[bytes, bool]:
    material_rel = norm_slashes(material_rel)
    fields = parse_fields(data)
    out: list[ProtoField] = []
    changed = False
    for top in fields:
        if top.number != 10 or top.wire != 2 or not isinstance(top.value, (bytes, bytearray)):
            out.append(top)
            continue
        field10 = bytes(top.value)
        f10_fields = parse_fields(field10)
        banner_snapshots: dict[int, bytes] = {}
        remaining_f10: list[ProtoField] = []
        banner_label = str(label or "").strip()
        for field in f10_fields:
            if field.number in FINALSTATE_SNAPSHOT_FIELDS and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
                snapshot = bytes(field.value)
                if _snapshot_is_ext_banner(snapshot, material_rel):
                    banner_snapshots.setdefault(field.number, snapshot)
                    banner_label = banner_label or _finalstate_snapshot_label(snapshot)
                    continue
            remaining_f10.append(field)

        banner_label = banner_label or EXT_BANNER_LABEL
        material_snapshot = (
            _patch_finalstate_snapshot(banner_snapshots[3], material_rel, banner_label)
            if 3 in banner_snapshots
            else _minimal_finalstate_material_snapshot(material_rel, banner_label)
        )
        selected_snapshot = (
            _patch_finalstate_snapshot(banner_snapshots[4], material_rel, banner_label)
            if 4 in banner_snapshots
            else _minimal_finalstate_selected_snapshot(material_rel, banner_label)
        )

        new_f10: list[ProtoField] = []
        inserted_material = False
        inserted_selected = False
        for field in remaining_f10:
            if (
                not inserted_material
                and field.number == 3
                and field.wire == 2
                and isinstance(field.value, (bytes, bytearray))
            ):
                new_f10.append(ProtoField(3, 2, material_snapshot))
                inserted_material = True
            if (
                not inserted_selected
                and field.number == 4
                and field.wire == 2
                and isinstance(field.value, (bytes, bytearray))
            ):
                new_f10.append(ProtoField(4, 2, selected_snapshot))
                inserted_selected = True
            new_f10.append(field)

        if not inserted_material:
            new_f10.append(ProtoField(3, 2, material_snapshot))
        if not inserted_selected:
            new_f10.append(ProtoField(4, 2, selected_snapshot))

        new_field10 = build_fields(new_f10)
        if new_field10 != field10:
            out.append(ProtoField(top.number, top.wire, build_fields(new_f10)))
            changed = True
        else:
            out.append(top)
    return (build_fields(out), True) if changed else (data, False)


def ensure_carstate_ext_banner_snapshot(data: bytes, material_rel: str, label: str | None = None) -> tuple[bytes, bool]:
    """Ensure an EXT_BANNER snapshot in plain or consumable-wrapped car state data."""
    patched, changed = ensure_carfinalstate_ext_banner_snapshot(data, material_rel, label)
    if changed:
        return patched, True

    try:
        fields = parse_fields(data)
    except Exception:
        return data, False
    out: list[ProtoField] = []
    wrapped_changed = False
    for field in fields:
        if field.number == 2 and field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            try:
                nested, nested_changed = ensure_carfinalstate_ext_banner_snapshot(bytes(field.value), material_rel, label)
            except Exception:
                out.append(field)
                continue
            if nested_changed:
                out.append(ProtoField(field.number, field.wire, nested))
                wrapped_changed = True
                continue
        out.append(field)
    return (build_fields(out), True) if wrapped_changed else (data, False)


def ensure_many_carfinalstates(
    items: Iterable[tuple[str, bytes]],
    material_rel: str,
) -> list[tuple[str, bytes, bool]]:
    results: list[tuple[str, bytes, bool]] = []
    for key, data in items:
        patched, changed = ensure_carfinalstate_ext_banner_snapshot(data, material_rel)
        results.append((key, patched, changed))
    return results
