"""Coercion de valores de un dict a tipos esperados segun un schema declarativo."""

from datetime import datetime, timezone


def coerce_types(
    data: dict, schema: dict[str, str]
) -> tuple[dict, list[str]]:
    """Convierte valores de un dict a los tipos esperados segun el schema.

    Schema es un dict de {campo: tipo} donde tipo es uno de:
    "int", "float", "str", "bool", "datetime", "list[str]".

    Coerciones soportadas (todas desde str):
    - str → int: int(v), warning si tenia decimales
    - str → float: float(v)
    - str → bool: "true/1/yes" → True, "false/0/no" → False (case-insensitive)
    - str → datetime: ISO 8601 parse
    - str → list[str]: split por "," y strip de cada elemento
    - Valor ya del tipo correcto → pass through
    - Campo ausente en schema → pass through sin tocar
    - Coercion imposible → mantener original + warning

    Args:
        data: Dict con los valores a coercionar.
        schema: Dict de {campo: tipo_esperado}.

    Returns:
        (coerced_data, warnings) — nuevo dict con tipos corregidos (no muta el
        original), lista de warnings para coerciones lossy o fallidas.
    """
    result = dict(data)
    warnings: list[str] = []

    for field, target_type in schema.items():
        if field not in data:
            continue

        value = data[field]
        try:
            result[field] = _coerce_value(value, target_type, field, warnings)
        except Exception as exc:
            warnings.append(
                f"{field}: cannot coerce {value!r} to {target_type}: {exc}"
            )
            result[field] = value

    return result, warnings


_BOOL_TRUE = {"true", "1", "yes"}
_BOOL_FALSE = {"false", "0", "no"}


def _coerce_value(
    value: object, target: str, field: str, warnings: list[str]
) -> object:
    # --- int ---
    if target == "int":
        if isinstance(value, int) and not isinstance(value, bool):
            return value
        if isinstance(value, float):
            if value != int(value):
                warnings.append(
                    f"{field}: lossy coercion float→int: {value} → {int(value)}"
                )
            return int(value)
        if isinstance(value, str):
            stripped = value.strip()
            # detectar si tiene parte decimal no cero
            try:
                as_float = float(stripped)
                if as_float != int(as_float):
                    warnings.append(
                        f"{field}: lossy coercion str→int: {value!r} → {int(as_float)}"
                    )
                return int(as_float)
            except ValueError:
                raise ValueError(f"cannot parse {value!r} as int")
        raise TypeError(f"cannot coerce {type(value).__name__} to int")

    # --- float ---
    if target == "float":
        if isinstance(value, float):
            return value
        if isinstance(value, int) and not isinstance(value, bool):
            return float(value)
        if isinstance(value, str):
            return float(value.strip())
        raise TypeError(f"cannot coerce {type(value).__name__} to float")

    # --- str ---
    if target == "str":
        if isinstance(value, str):
            return value
        return str(value)

    # --- bool ---
    if target == "bool":
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            low = value.strip().lower()
            if low in _BOOL_TRUE:
                return True
            if low in _BOOL_FALSE:
                return False
            raise ValueError(
                f"cannot parse {value!r} as bool; expected true/false/1/0/yes/no"
            )
        if isinstance(value, int):
            return bool(value)
        raise TypeError(f"cannot coerce {type(value).__name__} to bool")

    # --- datetime ---
    if target == "datetime":
        if isinstance(value, datetime):
            return value
        if isinstance(value, str):
            s = value.strip()
            # Intentar parse ISO 8601 con y sin Z
            if s.endswith("Z"):
                s = s[:-1] + "+00:00"
            return datetime.fromisoformat(s)
        raise TypeError(f"cannot coerce {type(value).__name__} to datetime")

    # --- list[str] ---
    if target == "list[str]":
        if isinstance(value, list):
            return [str(item) for item in value]
        if isinstance(value, str):
            return [item.strip() for item in value.split(",")]
        raise TypeError(f"cannot coerce {type(value).__name__} to list[str]")

    raise ValueError(f"unknown target type: {target!r}")
