from __future__ import annotations

from pathlib import Path
from typing import Any


CLASS_NONE = "none"
CLASS_SHARED_ADDITIVE = "shared_additive_parts"
CLASS_SHARED_ADDITIVE_VARIANT_AO = "shared_additive_parts_variant_ao"
CLASS_FULL_VARIANT = "full_variant_model"
CLASS_HYBRID_REPLACEMENT = "hybrid_replacement_parts"
CLASS_NON_LIVERY = "non_livery_parts"
CLASS_NEEDS_REVIEW = "needs_review"

POLICY_VERSION = 1
UV_REPLACEMENT_LIKE_SLOTS = {"body", "front_bumper", "rear_bumper", "tailgate", "hood"}
COMPONENT_HIDE_REPLACEMENT_SLOTS = {"front_bumper", "rear_bumper", "tailgate", "hood", "wing"}


def is_generated_liverylab_part_ref(value: Any) -> bool:
    stem = Path(str(value or "").replace("\\", "/")).stem.lower()
    return stem.startswith("liverylab_") or "_liverylab_" in stem


def unique_compatible_part_refs(refs: list[str]) -> list[str]:
    unique: list[str] = []
    seen: set[str] = set()
    for ref in refs:
        text = str(ref or "").strip()
        if not text:
            continue
        key = text.lower()
        if key in seen or is_generated_liverylab_part_ref(text):
            continue
        seen.add(key)
        unique.append(text)
    return unique


def part_slot(compatible_rel: str, carpart_rel: str) -> str:
    text = f"{compatible_rel} {carpart_rel}".lower()
    for token, slot in (
        ("diffusor", "diffusor"),
        ("diffuser", "diffusor"),
        ("front_bumper", "front_bumper"),
        ("fbumper", "front_bumper"),
        ("rear_bumper", "rear_bumper"),
        ("rbumper", "rear_bumper"),
        ("tailgate", "tailgate"),
        ("hardtop", "hardtop"),
        ("splitter", "splitter"),
        ("spoiler", "spoiler"),
        ("wing", "wing"),
        ("body", "body"),
        ("hood", "hood"),
        ("bonnet", "hood"),
        ("light", "light"),
        ("lumirank", "display"),
        ("display", "display"),
        ("disc", "brake"),
        ("caliper", "brake"),
        ("susp", "mechanical"),
        ("engine", "mechanical"),
        ("gear", "mechanical"),
        ("diff", "mechanical"),
        ("exhaust", "exhaust"),
        ("steer", "interior"),
        ("rim", "wheel"),
        ("tyre", "wheel"),
        ("tire", "wheel"),
        ("cockpit", "interior"),
        ("int_", "interior"),
    ):
        if token in text:
            return slot
    return ""


def is_uv_replacement_like_slot(slot: str) -> bool:
    return str(slot or "").strip().lower() in UV_REPLACEMENT_LIKE_SLOTS


def is_component_hide_replacement_slot(slot: str) -> bool:
    return str(slot or "").strip().lower() in COMPONENT_HIDE_REPLACEMENT_SLOTS


def ignored_reason(slot: str, mesh_rel: str, carpart_rel: str, skin_relevant: bool) -> str:
    text = f"{slot} {mesh_rel} {carpart_rel}".lower()
    slot_key = str(slot or "").strip().lower()
    mechanical_text = text.replace("diffusor", "").replace("diffuser", "")
    if any(token in text for token in ("display", "lumirank", "racelogic", "sas")):
        return "display/electronics part"
    if any(token in text for token in ("light", "aux")):
        return "light part"
    if any(token in text for token in ("brake", "disc", "caliper", "rim", "tyre", "tire", "wheel")):
        return "wheel/brake part"
    if slot_key == "diffusor":
        return ""
    if any(token in mechanical_text for token in ("mechanical", "engine", "susp", "gear", "diff", "exhaust")):
        return "mechanical part"
    if any(token in text for token in ("interior", "cockpit", "int_", "steer")):
        return "interior part"
    if skin_relevant:
        return ""
    return ""


def _surface_policy(
    *,
    key: str,
    label: str,
    model_policy: str,
    ao_policy: str,
    uv_policy: str,
    requires_separate_layers: bool = False,
    merge_policy: str = "normal",
) -> dict[str, Any]:
    return {
        "key": key,
        "label": label,
        "modelPolicy": model_policy,
        "aoPolicy": ao_policy,
        "uvPolicy": uv_policy,
        "requiresSeparateLayers": bool(requires_separate_layers),
        "mergePolicy": merge_policy,
    }


def surface_policies_for_classification(classification: str) -> list[dict[str, Any]]:
    cls = str(classification or "").strip() or CLASS_NONE
    if cls == CLASS_SHARED_ADDITIVE:
        return [
            _surface_policy(
                key="ext_skin_c",
                label="EXT_SKIN_C",
                model_policy="base_plus_additive_parts",
                ao_policy="base",
                uv_policy="base_plus_shared_parts",
            )
        ]
    if cls == CLASS_SHARED_ADDITIVE_VARIANT_AO:
        return [
            _surface_policy(
                key="ext_skin_c",
                label="EXT_SKIN_C",
                model_policy="base_plus_additive_parts",
                ao_policy="variant",
                uv_policy="base_plus_shared_parts",
            )
        ]
    if cls == CLASS_FULL_VARIANT:
        return [
            _surface_policy(
                key="ext_skin_c",
                label="EXT_SKIN_C",
                model_policy="variant_model_set",
                ao_policy="variant",
                uv_policy="variant_model_set",
            )
        ]
    if cls == CLASS_HYBRID_REPLACEMENT:
        return [
            _surface_policy(
                key="ext_skin_c",
                label="EXT_SKIN_C",
                model_policy="base_model",
                ao_policy="base",
                uv_policy="base",
                merge_policy="merge_with_spec_parts",
            ),
            _surface_policy(
                key="ext_skin_c_spec_parts",
                label="Spec Parts",
                model_policy="spec_parts_only",
                ao_policy="variant",
                uv_policy="spec_parts_only",
                requires_separate_layers=True,
                merge_policy="merge_into_ext_skin_c",
            ),
        ]
    return [
        _surface_policy(
            key="ext_skin_c",
            label="EXT_SKIN_C",
            model_policy="base_model",
            ao_policy="base",
            uv_policy="base",
        )
    ]


def build_policy(classification: str, confidence: str = "low") -> dict[str, Any]:
    cls = str(classification or CLASS_NONE).strip()
    return {
        "variantPolicyVersion": POLICY_VERSION,
        "classification": cls,
        "confidence": str(confidence or "low"),
        "surfaces": surface_policies_for_classification(cls),
    }


def classify_variant_report(report: dict[str, Any]) -> dict[str, Any]:
    variant = report.get("variant", {}) if isinstance(report.get("variant"), dict) else {}
    parts = variant.get("compatibleParts", []) if isinstance(variant.get("compatibleParts"), list) else []
    mesh_refs = variant.get("meshRefs", []) if isinstance(variant.get("meshRefs"), list) else []
    relevant = [row for row in mesh_refs if row.get("liveryRelevant") or row.get("previewRelevant")]
    skin_relevant = [row for row in mesh_refs if row.get("liveryRelevant")]
    replacing = [row for row in mesh_refs if row.get("replacesExterior")]
    ignored = [row for row in mesh_refs if row.get("ignoredReason")]
    base = report.get("base", {}) if isinstance(report.get("base"), dict) else {}
    active = report.get("activeSkin", {}) if isinstance(report.get("activeSkin"), dict) else {}
    base_ao = set(base.get("aoTextureRefs", []) or [])
    active_ao = set(active.get("aoTextureRefs", []) or [])
    variant_ao = set(variant.get("aoTextureRefs", []) or [])
    base_color = set(base.get("colorTextureRefs", []) or [])
    active_color = set(active.get("colorTextureRefs", []) or [])
    source = str(variant.get("compatiblePartSource") or "").strip()
    material_refs = set(str(row or "").lower() for row in active.get("materialRefs", []) or [])
    base_material_refs = set(str(row or "").lower() for row in base.get("materialRefs", []) or [])
    different_material = bool(material_refs and material_refs != base_material_refs)
    different_ao = bool(active_ao and base_ao and active_ao != base_ao)
    extra_part_ao = variant_ao - (base_ao | active_ao)
    extra_part_skin_ao = {
        ref for ref in extra_part_ao
        if "ext_skin" in str(ref or "").lower()
    }
    different_color = bool(active_color and base_color and active_color != base_color)
    mesh_text = " ".join(str(row.get("relPath") or "") for row in mesh_refs).lower()
    material_text = " ".join([*material_refs, *active_ao, *active_color]).lower()

    reasons: list[str] = []
    classification = CLASS_NONE
    confidence = "low"
    if not parts:
        reasons.append("No active compatible parts found.")
        classification = CLASS_NONE
        confidence = "high"
    elif not relevant:
        reasons.append("Compatible parts exist, but none appear livery-relevant.")
        classification = CLASS_NON_LIVERY
        confidence = "medium" if ignored else "low"
    elif (replacing or "drift" in mesh_text or "drift" in material_text) and source not in {"carfinalstate"}:
        reasons.append("Replacement-like variant parts were only found through fallback/static preset data, not the active skin final state.")
        classification = CLASS_NONE
        confidence = "medium"
    elif replacing or "drift" in mesh_text or "drift" in material_text:
        reasons.append("Variant includes replacement-like body parts or drift-specific resources.")
        classification = CLASS_HYBRID_REPLACEMENT
        confidence = "medium"
    elif skin_relevant and extra_part_skin_ao:
        reasons.append("Livery-relevant additive parts reference skin AO textures outside the base/active skin AO set.")
        classification = CLASS_NEEDS_REVIEW
        confidence = "low"
    elif skin_relevant and different_ao:
        reasons.append("Livery-relevant additive parts use the active skin texture set with a variant AO.")
        classification = CLASS_SHARED_ADDITIVE_VARIANT_AO
        confidence = "medium"
    elif different_material and (different_ao or "gnose" in mesh_text or "g_nose" in material_text):
        reasons.append("Variant uses a distinct skin material/texture set.")
        classification = CLASS_FULL_VARIANT
        confidence = "medium"
    elif skin_relevant and not different_ao:
        reasons.append("Livery-relevant parts appear to share the base texture/AO set.")
        classification = CLASS_SHARED_ADDITIVE
        confidence = "medium"
    else:
        reasons.append("Variant data is mixed or incomplete and needs manual review.")
        classification = CLASS_NEEDS_REVIEW
        confidence = "low"

    policy = build_policy(classification, confidence)
    return {
        "variantClassification": classification,
        "confidence": confidence,
        "reasons": reasons,
        "counts": {
            "compatibleParts": len(parts),
            "meshRefs": len(mesh_refs),
            "relevantMeshRefs": len(relevant),
            "skinRelevantMeshRefs": len(skin_relevant),
            "ignoredMeshRefs": len(ignored),
        },
        "variantPolicy": policy,
        "variantPolicyVersion": policy["variantPolicyVersion"],
        "surfacePolicies": policy["surfaces"],
    }
