from __future__ import annotations

import copy
from pathlib import Path
from typing import Any, Iterable


VARIANT_AO_STATUS_READY = "ready"
VARIANT_AO_STATUS_MISSING_SOURCE = "missing_source"
VARIANT_AO_STATUS_MISSING_MIPS = "missing_mips"


def _search_text(values: Iterable[Any]) -> str:
    parts: list[str] = []
    for value in values:
        text = str(value or "").strip().lower()
        if not text:
            continue
        text = text.replace("\\", " ").replace("/", " ").replace("_", " ").replace("-", " ")
        parts.append(" ".join(text.split()))
    return " ".join(parts)


def _mentions_g_nose(text: str) -> bool:
    compact = str(text or "").replace(" ", "")
    return "gnose" in compact or "g nose" in text


def compatible_part_allowed_for_context(
    compatible_ref: Any,
    carpart_ref: Any,
    context_values: Iterable[Any],
) -> bool:
    part_text = _search_text((compatible_ref, carpart_ref))
    if _mentions_g_nose(part_text):
        return _mentions_g_nose(_search_text(context_values))
    return True


def _normalize_ref(value: Any) -> str:
    return str(value or "").strip().replace("\\", "/").lower()


def _path_stem(value: Any) -> str:
    text = str(value or "").strip().replace("\\", "/")
    return Path(text).stem.replace("_", " ").replace("-", " ").strip()


def _clean_label(value: Any) -> str:
    text = str(value or "").strip()
    if not text:
        return ""
    return " ".join(text.replace("_", " ").replace("-", " ").split())


def _titleish(value: str) -> str:
    text = _clean_label(value)
    if not text:
        return ""
    return " ".join(part.upper() if len(part) <= 3 and any(ch.isalpha() for ch in part) else part[:1].upper() + part[1:] for part in text.split())


def _identity_tokens(value: Any) -> tuple[str, ...]:
    text = _search_text((value,))
    compact = text.replace(" ", "")
    tokens: list[str] = []
    for key, aliases in (
        ("drift", ("drift",)),
        ("weissach", ("weissach",)),
        ("g_nose", ("gnose", "g nose")),
        ("tuned", ("tuned", "tune")),
        ("abs", ("abs",)),
        ("tc", ("tc", "traction control")),
        ("clubsport", ("clubsport", "club sport")),
        ("cup", ("cup",)),
        ("rs", ("rs",)),
        ("standard", ("standard", "stock", "default")),
    ):
        for alias in aliases:
            if " " in alias:
                found = alias in text
            else:
                found = alias in compact
            if found:
                tokens.append(key)
                break
    return tuple(sorted(set(tokens)))


def _visible_spec_name(row: dict[str, Any]) -> str:
    values: list[str] = []
    for key in (
        "label",
        "mechanicalPresetShortName",
        "mechanicalPresetName",
        "visualPresetPresentation",
        "visualPresetName",
    ):
        value = _clean_label(row.get(key))
        if value and value.lower() not in {item.lower() for item in values}:
            values.append(value)
    return " | ".join(values)


def spec_identity_signature(row: dict[str, Any]) -> tuple[Any, ...]:
    visible = _visible_spec_name(row)
    token_values = [
        visible,
        row.get("mechanicalPreset"),
        row.get("visualPreset"),
        row.get("carFinalState"),
        row.get("cardesign"),
    ]
    return (
        _clean_label(visible).lower(),
        _identity_tokens(" ".join(str(value or "") for value in token_values)),
    )


def _variant_mesh_refs(row: dict[str, Any]) -> tuple[str, ...]:
    preview = row.get("variantPartPreview") if isinstance(row.get("variantPartPreview"), dict) else {}
    refs: list[str] = []
    for item in preview.get("meshRefs", []) or []:
        if not isinstance(item, dict):
            continue
        if not (item.get("previewRelevant") or item.get("skinRelevant")):
            continue
        ref = _normalize_ref(item.get("relPath") or item.get("path"))
        if ref:
            refs.append(ref)
    return tuple(sorted(set(refs)))


def _variant_aux_model_refs(row: dict[str, Any]) -> tuple[str, ...]:
    refs: list[str] = []
    preview = row.get("variantPartPreview") if isinstance(row.get("variantPartPreview"), dict) else {}
    sources = [preview.get("auxiliaryModels"), row.get("auxiliaryModels")]
    for source in sources:
        for item in source or []:
            if not isinstance(item, dict):
                continue
            ref = _normalize_ref(item.get("sourceRelPath") or item.get("compatiblePartRel") or item.get("path"))
            if ref:
                refs.append(ref)
    return tuple(sorted(set(refs)))


def livery_variant_signature(row: dict[str, Any]) -> tuple[Any, ...]:
    policy = row.get("variantPolicy") if isinstance(row.get("variantPolicy"), dict) else {}
    preview = row.get("variantPartPreview") if isinstance(row.get("variantPartPreview"), dict) else {}
    ao = row.get("variantAoTexture") if isinstance(row.get("variantAoTexture"), dict) else {}
    surfaces = row.get("surfacePolicies") if isinstance(row.get("surfacePolicies"), list) else []
    surface_sig = tuple(
        sorted(
            (
                str(surface.get("key") or ""),
                str(surface.get("modelPolicy") or ""),
                str(surface.get("aoPolicy") or ""),
                str(surface.get("uvPolicy") or ""),
                str(surface.get("mergePolicy") or ""),
            )
            for surface in surfaces
            if isinstance(surface, dict)
        )
    )
    return (
        str(policy.get("classification") or ""),
        str(row.get("aoPolicy") or ""),
        _normalize_ref(row.get("aoSourceTexturePath") or ao.get("sourceTexturePath") or ao.get("sourceRel")),
        _variant_mesh_refs(row) or _variant_aux_model_refs(row),
        tuple(sorted(str(item or "").strip() for item in row.get("hiddenExteriorMaterialNames", []) or [] if str(item or "").strip())),
        bool(preview.get("hideExteriorModel")),
        surface_sig,
    )


def variant_display_label(row: dict[str, Any], duplicate_label_count: int = 1) -> str:
    base = _clean_label(row.get("label")) or _clean_label(row.get("mechanicalPresetShortName")) or _clean_label(row.get("mechanicalPresetName"))
    if not base:
        base = _clean_label(row.get("visualPresetPresentation")) or _clean_label(row.get("visualPresetName"))
    if not base:
        base = _titleish(_path_stem(row.get("mechanicalPreset") or row.get("visualPreset"))) or "Default"
    details: list[str] = []
    visual = _clean_label(row.get("visualPresetPresentation")) or _clean_label(row.get("visualPresetName"))
    mechanical = _clean_label(row.get("mechanicalPresetShortName")) or _clean_label(row.get("mechanicalPresetName"))
    if visual and visual.lower() != base.lower():
        details.append(visual)
    if mechanical and mechanical.lower() != base.lower() and mechanical.lower() not in {item.lower() for item in details}:
        details.append(mechanical)
    if duplicate_label_count > 1:
        for candidate in (_titleish(_path_stem(row.get("cardesign"))), _titleish(_path_stem(row.get("rimDesign"))), _titleish(_path_stem(row.get("visualPreset")))):
            if candidate and candidate.lower() != base.lower() and candidate.lower() not in {item.lower() for item in details}:
                details.append(candidate)
                break
    return f"{base} - {details[0]}" if details else base


def annotate_and_dedupe_livery_variants(rows: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
    clean_rows = [copy.deepcopy(row) for row in rows if isinstance(row, dict)]
    for row in clean_rows:
        row["liveryVariantSignature"] = list(livery_variant_signature(row))
        row["specIdentitySignature"] = list(spec_identity_signature(row))

    merged: list[dict[str, Any]] = []
    by_signature: dict[tuple[Any, ...], dict[str, Any]] = {}
    for row in clean_rows:
        signature = (spec_identity_signature(row), livery_variant_signature(row))
        existing = by_signature.get(signature)
        if not existing:
            row["sourceVariantCount"] = 1
            row["sourceVariantIds"] = [str(row.get("id") or "")]
            row["sourceVariants"] = [_variant_alias(row)]
            by_signature[signature] = row
            merged.append(row)
            continue
        existing["sourceVariantCount"] = int(existing.get("sourceVariantCount") or 1) + 1
        ids = [str(item or "") for item in existing.get("sourceVariantIds", []) or [] if str(item or "")]
        row_id = str(row.get("id") or "")
        if row_id and row_id not in ids:
            ids.append(row_id)
        existing["sourceVariantIds"] = ids
        aliases = [item for item in existing.get("sourceVariants", []) or [] if isinstance(item, dict)]
        aliases.append(_variant_alias(row))
        existing["sourceVariants"] = aliases

    label_counts: dict[str, int] = {}
    for row in merged:
        label = _clean_label(row.get("label")) or "Default"
        label_counts[label.lower()] = label_counts.get(label.lower(), 0) + 1
    for row in merged:
        label = _clean_label(row.get("label")) or "Default"
        row["displayLabel"] = variant_display_label(row, label_counts.get(label.lower(), 1))
        aliases = [item for item in row.get("sourceVariants", []) or [] if isinstance(item, dict)]
        row["displayLabel"] = _merged_display_label(row, aliases)
    return merged


def _variant_alias(row: dict[str, Any]) -> dict[str, Any]:
    return {
        "id": str(row.get("id") or ""),
        "label": str(row.get("label") or ""),
        "displayLabel": str(row.get("displayLabel") or row.get("label") or ""),
        "mechanicalPreset": str(row.get("mechanicalPreset") or ""),
        "mechanicalPresetName": str(row.get("mechanicalPresetName") or ""),
        "mechanicalPresetShortName": str(row.get("mechanicalPresetShortName") or ""),
        "visualPreset": str(row.get("visualPreset") or ""),
        "visualPresetName": str(row.get("visualPresetName") or ""),
        "visualPresetPresentation": str(row.get("visualPresetPresentation") or ""),
        "carFinalState": str(row.get("carFinalState") or ""),
        "cardesign": str(row.get("cardesign") or ""),
        "rimDesign": str(row.get("rimDesign") or ""),
        "specIdentitySignature": list(spec_identity_signature(row)),
    }


def _merged_display_label(row: dict[str, Any], aliases: list[dict[str, Any]]) -> str:
    label = str(row.get("displayLabel") or row.get("label") or "Default").strip() or "Default"
    count = len(aliases)
    if count <= 1:
        return label
    if f"({count} presets)" in label:
        return label
    return f"{label} ({count} presets)"


def default_variant_id(rows: Iterable[dict[str, Any]]) -> str:
    variants = [row for row in rows if isinstance(row, dict) and str(row.get("id") or "").strip()]
    if not variants:
        return ""

    def score(row: dict[str, Any]) -> tuple[int, str]:
        policy = row.get("variantPolicy") if isinstance(row.get("variantPolicy"), dict) else {}
        text = _search_text(
            (
                row.get("displayLabel"),
                row.get("label"),
                row.get("mechanicalPresetShortName"),
                row.get("mechanicalPresetName"),
                row.get("visualPresetPresentation"),
                row.get("visualPresetName"),
                row.get("mechanicalPreset"),
                row.get("visualPreset"),
                row.get("cardesign"),
            )
        )
        compact = text.replace(" ", "")
        standard = any(token in compact for token in ("standard", "default", "stock", "base"))
        non_variant = str(policy.get("classification") or "").strip() in {"", "none"}
        no_parts = not row.get("variantPartPreview") and not row.get("compatibleParts")
        base_ao = str(row.get("aoPolicy") or "").strip().lower() in {"", "base"}
        variant_penalty = 0 if (non_variant or no_parts) else 1
        ao_penalty = 0 if base_ao else 1
        standard_penalty = 0 if standard else 1
        return (standard_penalty, variant_penalty, ao_penalty, str(row.get("displayLabel") or row.get("label") or "").lower())

    return str(sorted(variants, key=score)[0].get("id") or "")


def normalize_texture_ref(value: Any) -> str:
    text = str(value or "").strip().replace("\\", "/").lower()
    while text.startswith("./"):
        text = text[2:]
    return text


def _slot_ref(slot: dict[str, Any]) -> str:
    for key in ("resolvedRel", "value", "resolvedPath"):
        ref = normalize_texture_ref(slot.get(key))
        if ref:
            return ref
    return ""


def _slot_score(slot: dict[str, Any], base_refs: set[str], index: int) -> tuple[int, str, int]:
    ref = _slot_ref(slot)
    stem = Path(ref).stem.lower()
    is_base_ref = ref in base_refs
    skin_ao = stem.startswith("ext_skin") and "_ao" in stem
    skin_ref = "ext_skin" in ref or "/skins/" in ref
    if not is_base_ref and skin_ao:
        return (0, ref, index)
    if not is_base_ref and skin_ref:
        return (1, ref, index)
    if not is_base_ref:
        return (2, ref, index)
    return (9, ref, index)


def select_variant_ao_source(base_refs: Iterable[Any], material_summaries: Iterable[dict[str, Any]]) -> dict[str, Any]:
    normalized_base = {normalize_texture_ref(ref) for ref in base_refs if normalize_texture_ref(ref)}
    candidates: list[tuple[tuple[int, str, int], dict[str, Any]]] = []
    index = 0
    for material in material_summaries:
        if not isinstance(material, dict):
            continue
        for slot in material.get("aoTextureRefs", []) or []:
            if not isinstance(slot, dict):
                continue
            ref = _slot_ref(slot)
            if not ref:
                continue
            score = _slot_score(slot, normalized_base, index)
            index += 1
            if score[0] >= 9:
                continue
            source_path = str(slot.get("resolvedPath") or "").strip()
            source_rel = str(slot.get("resolvedRel") or slot.get("value") or "").strip()
            texturemips_path = str(slot.get("texturemipsPath") or "").strip()
            candidates.append(
                (
                    score,
                    {
                        "sourceRef": ref,
                        "sourceRel": source_rel,
                        "sourceTexturePath": source_path,
                        "sourceTexturemipsPath": texturemips_path,
                        "sourceValue": str(slot.get("value") or ""),
                        "materialPath": str(material.get("path") or ""),
                        "materialRelPath": str(material.get("relPath") or ""),
                        "materialSource": str(material.get("source") or ""),
                    },
                )
            )
    if not candidates:
        return {}
    return copy.deepcopy(sorted(candidates, key=lambda row: row[0])[0][1])


def variant_ao_metadata(source: dict[str, Any], target_path: str | Path | None = None) -> dict[str, Any]:
    if not isinstance(source, dict) or not source:
        return {}
    source_path = Path(str(source.get("sourceTexturePath") or "")) if str(source.get("sourceTexturePath") or "").strip() else None
    source_mips = Path(str(source.get("sourceTexturemipsPath") or "")) if str(source.get("sourceTexturemipsPath") or "").strip() else None
    if source_path and not source_mips:
        source_mips = source_path.with_suffix(".texturemips")
    source_exists = bool(source_path and source_path.exists() and source_path.is_file())
    mips_exists = bool(source_mips and source_mips.exists() and source_mips.is_file())
    status = VARIANT_AO_STATUS_READY if source_exists and mips_exists else VARIANT_AO_STATUS_MISSING_SOURCE
    if source_exists and not mips_exists:
        status = VARIANT_AO_STATUS_MISSING_MIPS
    target = Path(target_path) if target_path else None
    return {
        **copy.deepcopy(source),
        "path": str(target) if target else "",
        "name": target.name if target else "",
        "exists": bool(target and target.exists() and target.is_file()),
        "sourceTexturePath": str(source_path) if source_path else "",
        "sourceTexturemipsPath": str(source_mips) if source_mips else "",
        "sourceTextureExists": source_exists,
        "sourceTexturemipsExists": mips_exists,
        "sourceTexturemipsSize": int(source_mips.stat().st_size) if mips_exists and source_mips else None,
        "sourceTexturemipsMtimeMs": int(source_mips.stat().st_mtime * 1000) if mips_exists and source_mips else None,
        "status": status,
        "ready": status == VARIANT_AO_STATUS_READY,
    }


def policy_with_variant_ao(policy: dict[str, Any], has_variant_ao: bool) -> dict[str, Any]:
    updated = copy.deepcopy(policy) if isinstance(policy, dict) else {}
    if not updated:
        return updated
    updated["aoPolicy"] = "variant" if has_variant_ao else updated.get("aoPolicy") or "base"
    if has_variant_ao:
        surfaces = updated.get("surfaces") if isinstance(updated.get("surfaces"), list) else []
        for surface in surfaces:
            if isinstance(surface, dict):
                surface["aoPolicy"] = "variant"
    return updated


def surface_policies_with_variant_ao(surfaces: Iterable[Any], has_variant_ao: bool) -> list[dict[str, Any]]:
    rows = copy.deepcopy([row for row in surfaces if isinstance(row, dict)])
    if has_variant_ao:
        for row in rows:
            row["aoPolicy"] = "variant"
    return rows
