"""Constructor de dataset_query MBQL que envuelve una saved card con agregaciones,
joins, filtros y expressions en una segunda etapa."""

from __future__ import annotations

import uuid
from typing import Any


# ---------------------------------------------------------------------------
# Helpers internos
# ---------------------------------------------------------------------------


def _uuid() -> str:
    """Genera un UUID v4 fresco como string."""
    return str(uuid.uuid4())


def _inject_uuids(obj: Any) -> Any:
    """Recorre obj recursivamente e inyecta lib/uuid a dicts que no lo tengan."""
    if isinstance(obj, dict):
        if "lib/uuid" not in obj:
            obj["lib/uuid"] = _uuid()
        for k, v in obj.items():
            if k != "lib/uuid":
                obj[k] = _inject_uuids(v)
        return obj
    elif isinstance(obj, list):
        return [_inject_uuids(item) for item in obj]
    return obj


def _build_field_ref(
    field_name: str,
    base_type: str = "type/Decimal",
) -> list:
    """Construye un nodo MBQL ["field", meta, field_name] para campo de source-card."""
    return [
        "field",
        {"base-type": base_type, "lib/uuid": _uuid()},
        field_name,
    ]


def _build_field_ref_by_id(
    field_id: int,
    base_type: str = "type/Text",
    join_alias: str | None = None,
) -> list:
    """Construye un nodo MBQL ["field", meta, field_id] para campo por ID numerico."""
    meta: dict = {"base-type": base_type, "lib/uuid": _uuid()}
    if join_alias:
        meta["join-alias"] = join_alias
    return ["field", meta, field_id]


# ---------------------------------------------------------------------------
# Builders de aggregation
# ---------------------------------------------------------------------------

_OPS_WITHOUT_FIELD = {"count"}


def _build_aggregation(agg_spec: dict) -> list:
    """Convierte un spec de agregacion al formato MBQL.

    Formato input:
        {"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"}
        {"op": "count"}

    Formato output (con campo):
        ["sum", {"lib/uuid": ...}, ["field", {"base-type": "...", "lib/uuid": ...}, "PrecioVenta"]]

    Formato output (sin campo, ej. count):
        ["count", {"lib/uuid": ...}]
    """
    op = agg_spec["op"]
    meta = {"lib/uuid": _uuid()}

    if op in _OPS_WITHOUT_FIELD or "field" not in agg_spec:
        return [op, meta]

    field_node = _build_field_ref(
        agg_spec["field"],
        base_type=agg_spec.get("base_type", "type/Decimal"),
    )
    return [op, meta, field_node]


# ---------------------------------------------------------------------------
# Builders de join
# ---------------------------------------------------------------------------


def _build_join(join_spec: dict) -> dict:
    """Construye el dict de join MBQL a partir de un join_spec.

    join_spec esperado:
        {
            "alias": "Centros - idCentro",
            "source_card_id": 4076,
            "strategy": "left-join",       # default
            "fields": "none",              # opcional
            "local_field": "idCentro",
            "local_base_type": "type/Text",
            "foreign_field_id": 17316,
            "foreign_base_type": "type/Text",
        }
    """
    alias = join_spec["alias"]
    strategy = join_spec.get("strategy", "left-join")

    # Condicion de join: ["=", meta, local_field_ref, foreign_field_ref]
    local_ref = _build_field_ref(
        join_spec["local_field"],
        base_type=join_spec.get("local_base_type", "type/Text"),
    )
    foreign_ref = _build_field_ref_by_id(
        join_spec["foreign_field_id"],
        base_type=join_spec.get("foreign_base_type", "type/Text"),
        join_alias=alias,
    )
    condition_meta = {"lib/uuid": _uuid()}
    condition = ["=", condition_meta, local_ref, foreign_ref]

    join: dict = {
        "lib/type": "mbql/join",
        "lib/uuid": _uuid(),
        "alias": alias,
        "strategy": strategy,
        "stages": [
            {
                "lib/type": "mbql.stage/mbql",
                "source-card": join_spec["source_card_id"],
                "lib/options": {"lib/uuid": _uuid()},
            }
        ],
        "conditions": [condition],
    }

    if "fields" in join_spec:
        join["fields"] = join_spec["fields"]

    return join


# ---------------------------------------------------------------------------
# Builders de breakout
# ---------------------------------------------------------------------------


def _build_breakout(breakout_spec: dict) -> list:
    """Construye un nodo MBQL de breakout.

    Soporta:
        {"field": "fecha", "base_type": "type/Date", "temporal_unit": "month"}
        {"field_id": 156756, "base_type": "type/Text", "join_alias": "Centros - idCentro"}
    """
    if "field_id" in breakout_spec:
        meta: dict = {
            "base-type": breakout_spec.get("base_type", "type/Text"),
            "lib/uuid": _uuid(),
        }
        if "join_alias" in breakout_spec:
            meta["join-alias"] = breakout_spec["join_alias"]
        return ["field", meta, breakout_spec["field_id"]]
    else:
        meta = {
            "base-type": breakout_spec.get("base_type", "type/Date"),
            "lib/uuid": _uuid(),
        }
        if "temporal_unit" in breakout_spec:
            meta["temporal-unit"] = breakout_spec["temporal_unit"]
        return ["field", meta, breakout_spec["field"]]


# ---------------------------------------------------------------------------
# Builder de expressions (mini-DSL recursivo)
# ---------------------------------------------------------------------------


def _build_expr_node(node: Any, expr_name: str | None = None) -> Any:
    """Traduce recursivamente un nodo del mini-DSL a formato MBQL.

    Primitivos soportados:
        {"field": "nombre", "base_type": "type/Decimal"}  → field ref por nombre
        {"op": "sum_field", ...}                           → no aplica aqui
        {"op": "+", "args": [...]}                         → operacion MBQL
        {"ref": "NombreExpresion"}                         → expression ref
        int | float | bool | str                           → literal

    Nodo raiz recibe expr_name para incluir lib/expression-name en el meta.
    """
    if isinstance(node, (int, float, bool, str)) and not isinstance(node, bool):
        return node
    if isinstance(node, bool):
        return node

    if not isinstance(node, dict):
        return node

    # Referencia a otra expression nombrada en la misma stage
    if "ref" in node:
        ref_name = node["ref"]
        base_type = node.get("base_type", "type/Float")
        return [
            "expression",
            {
                "base-type": base_type,
                "effective-type": base_type,
                "lib/uuid": _uuid(),
            },
            ref_name,
        ]

    # Field ref por nombre
    if "field" in node and "op" not in node:
        return _build_field_ref(
            node["field"],
            base_type=node.get("base_type", "type/Decimal"),
        )

    # Operacion con args
    if "op" in node:
        op = node["op"]
        args = node.get("args", [])
        meta: dict = {"lib/uuid": _uuid()}
        if expr_name is not None:
            meta["lib/expression-name"] = expr_name

        translated_args = [_build_expr_node(a) for a in args]
        return [op, meta] + translated_args

    # Fallback: retornar el nodo tal cual
    return node


def _build_expression_entry(expr_spec: dict) -> list:
    """Construye el nodo MBQL completo para una expression con nombre.

    expr_spec:
        {"name": "Margen", "expr": {...mini-DSL...}}

    Retorna:
        [op, {"lib/uuid": ..., "lib/expression-name": "Margen"}, ...args]
    """
    name = expr_spec["name"]
    expr_node = expr_spec["expr"]

    # Si el nodo raiz es una operacion, trasladar el expr_name al meta
    if isinstance(expr_node, dict) and "op" in expr_node:
        op = expr_node["op"]
        args = expr_node.get("args", [])
        meta: dict = {
            "lib/uuid": _uuid(),
            "lib/expression-name": name,
        }
        translated_args = [_build_expr_node(a) for a in args]
        return [op, meta] + translated_args

    # Si es un field ref u otro primitivo, envolverlo
    built = _build_expr_node(expr_node, expr_name=name)
    if isinstance(built, list) and len(built) >= 2 and isinstance(built[1], dict):
        built[1]["lib/expression-name"] = name
    return built


# ---------------------------------------------------------------------------
# Funcion principal
# ---------------------------------------------------------------------------


def metabase_mbql_from_source_card(
    *,
    database_id: int,
    source_card_id: int,
    aggregations: list[dict],
    joins: list[dict] | None = None,
    filters: list[list] | None = None,
    breakouts: list[dict] | None = None,
    expressions: list[dict] | None = None,
) -> dict:
    """Construye un dataset_query MBQL que envuelve una saved card (source-card).

    Genera automaticamente todos los lib/uuid necesarios. Soporta agregaciones,
    joins, filtros, breakouts y una segunda etapa con expressions calculadas.
    Elimina la necesidad de construir el JSON MBQL manualmente para el patron
    comun "agregar sobre una card guardada y anadir expresiones derivadas".

    Args:
        database_id: ID de la database de Metabase contra la que se ejecuta.
        source_card_id: ID de la card guardada que actua como fuente de datos
            (equivalente a "pick a saved question" en el query builder).
        aggregations: Lista de specs de agregacion. Cada spec es un dict con
            "op" (sum|count|avg|min|max|count-distinct), "field" (nombre de
            columna en la source card) y "base_type" opcional (default
            "type/Decimal"). Para count sin campo, omitir "field".
        joins: Lista de specs de join. Cada spec incluye "alias", "source_card_id"
            (card a joinear), "strategy" (default "left-join"), "fields" opcional,
            "local_field" (campo en la etapa actual), "local_base_type",
            "foreign_field_id" (ID numerico del campo en la card joineada) y
            "foreign_base_type".
        filters: Lista de clausulas MBQL de filtro en formato raw. La funcion
            inyecta lib/uuid a cualquier dict que no lo tenga. Ejemplo:
            [["not-empty", {}, ["field", {"base-type": "type/Text"}, "nombre"]]].
        breakouts: Lista de specs de breakout. Soporta campo por nombre (con
            "field" y "base_type" opcionales y "temporal_unit" para fechas) o
            por ID numerico (con "field_id", "base_type" y "join_alias" opcionales).
        expressions: Lista de specs de expression para la segunda etapa MBQL.
            Cada spec tiene "name" (nombre de la expresion) y "expr" (nodo del
            mini-DSL recursivo). Nodos soportados: {"field": ..., "base_type": ...}
            para referencias a campos, {"op": ..., "args": [...]} para operaciones
            (+, -, *, /, coalesce, case, etc.), {"ref": ...} para referenciar
            otra expression nombrada en la misma etapa, y literales numericos.
            Si se proporcionan expressions, se crea una segunda stage MBQL.

    Returns:
        Dict con la estructura completa del dataset_query MBQL:
            {
                "lib/type": "mbql/query",
                "database": database_id,
                "stages": [stage0, stage1?]  # stage1 solo si hay expressions
            }
        Todos los dicts internos tienen lib/uuid unicos generados con uuid4.

    Example:
        >>> q = metabase_mbql_from_source_card(
        ...     database_id=6,
        ...     source_card_id=5305,
        ...     aggregations=[{"op": "count"}],
        ... )
        >>> q["stages"][0]["source-card"]
        5305
        >>> q["database"]
        6
    """
    # ---- Stage 0 -----------------------------------------------------------
    stage0: dict = {
        "lib/type": "mbql.stage/mbql",
        "source-card": source_card_id,
    }

    # Agregaciones
    agg_nodes = [_build_aggregation(a) for a in aggregations]
    if agg_nodes:
        stage0["aggregation"] = agg_nodes

    # Joins
    if joins:
        stage0["joins"] = [_build_join(j) for j in joins]

    # Filtros: inyectar uuids a dicts que no los tengan
    if filters:
        stage0["filters"] = [_inject_uuids(f) for f in filters]

    # Breakouts
    if breakouts:
        stage0["breakout"] = [_build_breakout(b) for b in breakouts]

    stages = [stage0]

    # ---- Stage 1 (solo si hay expressions) ---------------------------------
    if expressions:
        stage1: dict = {
            "lib/type": "mbql.stage/mbql",
            "expressions": [_build_expression_entry(e) for e in expressions],
        }
        stages.append(stage1)

    return {
        "lib/type": "mbql/query",
        "database": database_id,
        "stages": stages,
    }
