"""Creature model and appearance resolution for UTC (2DA appearance, body/head models, textures)."""

from __future__ import annotations

from typing import TYPE_CHECKING

from loggerplus import RobustLogger
from pykotor.common.misc import EquipmentSlot  # pyright: ignore[reportMissingImports]
from pykotor.resource.formats.twoda import read_2da  # pyright: ignore[reportMissingImports]
from pykotor.resource.generics.uti import UTI, read_uti  # pyright: ignore[reportMissingImports]
from pykotor.resource.type import ResourceType  # pyright: ignore[reportMissingImports]

if TYPE_CHECKING:
    from pykotor.common.misc import InventoryItem, ResRef  # pyright: ignore[reportMissingImports]
    from pykotor.extract.file import ResourceResult  # pyright: ignore[reportMissingImports]
    from pykotor.extract.installation import Installation  # pyright: ignore[reportMissingImports]
    from pykotor.extract.twoda import TwoDARow  # pyright: ignore[reportMissingImports]
    from pykotor.resource.formats.twoda import TwoDA  # pyright: ignore[reportMissingImports]
    from pykotor.resource.generics.utc import UTC  # pyright: ignore[reportMissingImports]
    from pykotor.resource.generics.uti import UTI  # pyright: ignore[reportMissingImports]


def _load_installation_2da(
    installation: Installation,
    resname: str,
    *,
    raise_if_missing: bool = False,
) -> TwoDA | None:
    """Load a 2DA from installation resources with optional strict failure behavior."""
    lookup: ResourceResult | None = installation.resource(resname, ResourceType.TwoDA)
    if lookup:
        return read_2da(lookup.data)

    message = f"{resname}.2da missing from installation."
    if raise_if_missing:
        raise ValueError(message)
    RobustLogger().error(message)
    return None


def _get_2da_row(table: TwoDA, row_id: int, *, context: str = "") -> TwoDARow | None:
    """Get a 2DA row by integer index or by string label. Returns None if not found.

    Tries index first (for 2DAs where row count > row_id), then label lookup (for sparse or
    alternate 2DAs). Handles appearance_id/head_id out of range or missing from the table.
    """
    if table.has_row(row_id):
        return table.get_row(row_id)
    return table.find_row(str(row_id))


def _get_default_body_values(
    utc: UTC,
    utc_appearance_row: TwoDARow,
    context_base: str,
) -> tuple[str, str, str, str]:
    model_column = "modela"
    tex_column = "texaevil" if utc.alignment <= 25 else "texa"
    body_model = utc_appearance_row.get_string(model_column, context=f"Fetching model 'modela'{context_base}")
    override_texture = utc_appearance_row.get_string(tex_column, context=f"Fetching default texture{context_base}")
    return body_model, override_texture, model_column, tex_column


def get_body_model(  # noqa: C901, PLR0912, PLR0915
    utc: UTC,
    installation: Installation,
    *,
    appearance: TwoDA | None = None,
    baseitems: TwoDA | None = None,
) -> tuple[str | None, str | None]:
    """Return the body model and texture names for the given creature UTC.

    Args:
    ----
        utc: UTC object of the target creature.
        installation: The relevant installation.

    Returns:
    -------
        Returns a tuple containing the name of the body model and the texture to apply to the model. The texture value may be None, in which case the default texture provided by the model should be used.

    References:
    ----------
        Original BioWare engine binaries (from swkotor.exe, swkotor2.exe)
        Original BioWare engine binaries
        Derivations and Other Implementations:
        ----------
        https://github.com/th3w1zard1/KotOR.js/tree/master/src/module/ModuleCreature.ts (Creature appearance handling)
        https://github.com/th3w1zard1/kotorblender/tree/master/io_scene_kotor/format/mdl/reader.py (MDL model loading)


        Note: Appearance lookup uses appearance.2da for model associations
    """
    log = RobustLogger()

    # Load appearance.2da if not provided
    if appearance is None:
        appearance = _load_installation_2da(installation, "appearance", raise_if_missing=True)
        assert appearance is not None

    # Load baseitems.2da if not provided
    if baseitems is None:
        baseitems = _load_installation_2da(installation, "baseitems", raise_if_missing=True)
        assert baseitems is not None

    # Prepare context for logging and error messages
    first_name: str = installation.string(utc.first_name)
    context_base: str = f" for UTC '{first_name}'"

    log.debug("Lookup appearance row %s for get_body_model call.", utc.appearance_id)
    utc_appearance_row: TwoDARow | None = _get_2da_row(appearance, utc.appearance_id, context=context_base)
    if utc_appearance_row is None:
        log.warning(
            "Creature '%s' references appearance_id %s which does not exist in appearance.2da. Using default model 'unknown'.",
            first_name,
            utc.appearance_id,
        )
        return ("unknown", None)
    body_model: str | None = None
    override_texture: str | None = None

    # Determine body model and texture based on modeltype
    modeltype: str = utc_appearance_row.get_string("modeltype", context=f"Fetching model type{context_base}")
    if modeltype != "B":
        log.debug("appearance.2da: utc 'modeltype' is '%s', fetching 'race' model%s", modeltype, context_base)
        body_model = utc_appearance_row.get_string("race", context=context_base)
    else:
        log.debug("appearance.2da: utc 'modeltype' is 'B'")

        # Handle armor or default model/texture
        # tex_column: Literal["texaevil", "texa", "texbevil", "texb", "texc", "texd", "texi", "texlevil", "texl", "texnevil", "texn", "texfevil", "texf", "texgevil", "texg", "texhevil", "texh", "texivevil", "texi", "texjevil", "texj", "texkevil", "texk", "texlevil", "texl", "texmevil", "texm", "texnevil", "texn", "texovevil", "texo", "texpevil", "texp", "texqevil", "texq", "texrevevil", "texr", "texsevil", "texs", "textevil", "text", "texuevil", "texu", "texvevil", "texv", "texwevil", "texw", "texxevil", "texx", "texyevil", "texy", "texzevil", "texz"]
        if EquipmentSlot.ARMOR not in utc.equipment or not utc.equipment[EquipmentSlot.ARMOR].resref:
            tex_append = "01"
            body_model, override_texture, model_column, tex_column = _get_default_body_values(utc, utc_appearance_row, context_base)
        else:
            # Handle armor-specific model and texture
            armor_resref: ResRef = utc.equipment[EquipmentSlot.ARMOR].resref
            log.debug("utc is wearing armor, fetch '%s.uti'", armor_resref)

            # Attempt to load armor UTI
            armor_res_lookup: ResourceResult | None = installation.resource(str(armor_resref), ResourceType.UTI)
            if armor_res_lookup is None:
                log.debug("'%s.uti' missing from installation%s", armor_resref, context_base)
                # Fallback to default values if armor UTI is missing
                tex_append = "01"
                body_model, override_texture, model_column, tex_column = _get_default_body_values(utc, utc_appearance_row, context_base)
            else:
                # Process armor-specific model and texture
                armor_uti: UTI = read_uti(armor_res_lookup.data)
                log.debug("baseitems.2da: get body row %s for their armor", armor_uti.base_item)

                body_row: TwoDARow = baseitems.get_row(armor_uti.base_item, context=f"Fetching armor base item row{context_base}")
                body_cell: str = body_row.get_string("bodyvar", context=f"Fetching 'bodyvar'{context_base}")
                log.debug("baseitems.2da: 'bodyvar' cell: %s", body_cell)

                # Determine model and texture columns
                armor_variation: str = body_cell.lower()
                model_column = f"model{armor_variation}"
                evil_tex_column = f"tex{armor_variation}evil"
                tex_column = (
                    evil_tex_column
                    if utc.alignment <= 25  # noqa: PLR2004
                    and evil_tex_column in appearance.get_headers()
                    else f"tex{armor_variation}"
                )
                tex_append = str(armor_uti.texture_variation).rjust(2, "0")

                body_model = utc_appearance_row.get_string(model_column, context=f"Fetching model column{context_base}")
                override_texture = utc_appearance_row.get_string(tex_column, context=f"Fetching texture column{context_base}")

        log.debug("appearance.2da's texture column: '%s'", tex_column)
        log.debug("override_texture name: '%s'", override_texture)

        # Process override texture
        if override_texture and override_texture.strip() and override_texture != "****":
            fallback_override_texture: str = override_texture + tex_append
            if tex_append != "01" and installation.texture(fallback_override_texture) is None:
                fallback_override_texture = f"{override_texture}01"
                log.debug(
                    "override texture '%s' not found, appending '01' to the end like the game itself would do.",
                    fallback_override_texture,
                )
            override_texture = fallback_override_texture
        else:
            override_texture = None
        log.debug(
            "Final override texture name (from appearance.2da's '%s' column): '%s'",
            tex_column,
            override_texture,
        )
        log.debug(
            "Final body model name (from appearance.2da's '%s' column): '%s'",
            model_column,
            body_model,
        )

    # Fallback to 'race' column if body_model is empty or invalid
    if not body_model or not body_model.strip() or body_model == "****":
        body_model = utc_appearance_row.get_string("race", context=f"Fetching 'race' column{context_base}")
        log.debug("body model name (from appearance.2da's 'race' column): '%s'", body_model)

    normalized_model = body_model.strip() if body_model and body_model.strip() else None
    normalized_texture = override_texture.strip() if override_texture and override_texture.strip() else None
    return normalized_model, normalized_texture


def get_weapon_models(
    utc: UTC,
    installation: Installation,
    *,
    appearance: TwoDA | None = None,
    baseitems: TwoDA | None = None,
) -> tuple[str | None, str | None]:
    """Returns a tuple containing the right-hand weapon model and the left-hand weapon model (in that order).

    If no weapon is equipped in a particular hand the value will return None.

    If no value is specified for the appearance or baseitem parameters then they will be loaded from the given
    installation.

    Args:
    ----
        utc: UTC object of the target creature.
        installation: The relevant installation.
        appearance: The appearance.2da loaded into a TwoDA object.
        baseitems: The baseitems.2da loaded into a TwoDA object.

    Returns:
    -------
        Returns a tuple containing right-hand and left-hand weapon model names.
    """
    if appearance is None:
        appearance = _load_installation_2da(installation, "appearance")
        if appearance is None:
            return None, None
    if baseitems is None:
        baseitems = _load_installation_2da(installation, "baseitems")
        if baseitems is None:
            return None, None

    right_hand_model: str | None = _weapon_model_for_slot(utc, EquipmentSlot.RIGHT_HAND, installation, baseitems)
    left_hand_model: str | None = _weapon_model_for_slot(utc, EquipmentSlot.LEFT_HAND, installation, baseitems)
    return right_hand_model, left_hand_model


def _weapon_model_for_slot(
    utc: UTC,
    slot: EquipmentSlot,
    installation: Installation,
    baseitems: TwoDA,
) -> str | None:
    """Resolve equipped weapon model name for a specific slot."""
    equipped_item = utc.equipment.get(slot)
    if equipped_item is None or not equipped_item.resref:
        return None
    return _load_hand_uti(installation, str(equipped_item.resref), baseitems)


def _load_hand_uti(
    installation: Installation,
    hand_resref: str,
    baseitems: TwoDA,
) -> str | None:
    hand_lookup: ResourceResult | None = installation.resource(hand_resref, ResourceType.UTI)
    if not hand_lookup:
        RobustLogger().debug(f"{hand_resref}.uti missing from installation.")
        return None
    hand_uti: UTI = read_uti(hand_lookup.data)
    default_model: str = baseitems.get_row(hand_uti.base_item).get_string("defaultmodel")
    return default_model.replace(
        "001",
        str(hand_uti.model_variation).rjust(3, "0"),
    ).strip()


def get_head_model(  # noqa: C901, PLR0912
    utc: UTC,
    installation: Installation,
    *,
    appearance: TwoDA | None = None,
    heads: TwoDA | None = None,
) -> tuple[str | None, str | None]:
    """Returns the model and texture names for the head used by a creature.

    The value for the texture may be None and the default texture provided by the model should be used instead.

    If no value is specified for the appearance or heads parameters then they will be loaded from the given
    installation.

    Args:
    ----
        utc: UTC object of the target creature.
        installation: The relevant installation.
        appearance: The appearance.2da loaded into a TwoDA object.
        heads: The heads.2da loaded into a TwoDA object.

    Returns:
    -------
        Returns a tuple containing the name of the model and the texture to apply to the model.
    """
    if appearance is None:
        appearance = _load_installation_2da(installation, "appearance")
        if appearance is None:
            return None, None
    if heads is None:
        heads = _load_installation_2da(installation, "heads")
        if heads is None:
            return None, None

    model: str | None = None
    texture: str | None = None

    utc_appearance_row: TwoDARow | None = _get_2da_row(appearance, utc.appearance_id)
    if utc_appearance_row is None:
        return None, None
    head_id: int | None = utc_appearance_row.get_integer("normalhead")
    if head_id is not None:
        head_row: TwoDARow | None = _get_2da_row(heads, head_id)
        if head_row is None:
            RobustLogger().warning(
                "Row %s missing from heads.2da, defined in appearance.2da column 'normalhead' for appearance_id %s",
                head_id,
                utc.appearance_id,
            )
        else:
            model = head_row.get_string("head")
            head_column_name: str | None = None
            if utc.alignment < 10:  # noqa: PLR2004
                head_column_name = "headtexvvve"
            elif utc.alignment < 20:  # noqa: PLR2004
                head_column_name = "headtexvve"
            elif utc.alignment < 30:  # noqa: PLR2004
                head_column_name = "headtexve"
            elif utc.alignment < 40:  # noqa: PLR2004
                head_column_name = "headtexe"
            elif "alttexture" in heads.get_headers():
                if not installation.game().is_k2():  # TSL only override.
                    RobustLogger().error("'alttexture' column in heads.2da should never exist in a K1 installation.")
                else:
                    head_column_name = "alttexture"
            if head_column_name is not None:
                try:
                    texture = head_row.get_string(head_column_name)
                    texture = texture if texture and texture.strip() else None
                except KeyError:
                    RobustLogger().error("Cannot find %s in heads.2da", head_column_name, exc_info=True)

    return model, texture


def get_mask_model(
    utc: UTC,
    installation: Installation,
) -> str | None:
    model: str | None = None

    head_equip: InventoryItem | None = utc.equipment.get(EquipmentSlot.HEAD)
    if head_equip is not None:
        resref: ResRef = head_equip.resref
        if not resref:
            return None
        resource: ResourceResult | None = installation.resource(str(resref), ResourceType.UTI)
        if resource is None:
            return None
        uti: UTI = read_uti(resource.data)
        model = "I_Mask_" + str(uti.model_variation).rjust(3, "0")

    return model and model.strip()
