"""Wrapper seguro sobre PUT /api/dashboard/:id que maneja los gotchas conocidos."""

from __future__ import annotations

import httpx

from .client import MetabaseClient


# Campos permitidos en un dashcard al enviar a la API de Metabase.
# Cualquier otro campo (especialmente 'card' denormalizado) causa 413.
_DASHCARD_ALLOWED_KEYS = {
    "id",
    "card_id",
    "dashboard_tab_id",
    "col",
    "row",
    "size_x",
    "size_y",
    "parameter_mappings",
    "visualization_settings",
    "series",
    "action_id",
    "inline_parameters",
}


def _strip_dashcard(dc: dict) -> dict:
    """Elimina campos denormalizados de un dashcard, conservando solo los permitidos.

    El campo ``card`` contiene el objeto completo de la question (con dataset_query,
    visualization_settings, etc.) y puede superar facilmente el limite de payload.
    Metabase lo añade en las respuestas GET pero lo rechaza (413) en PUT.

    Args:
        dc: Dict de dashcard tal como lo devuelve GET /api/dashboard/:id.

    Returns:
        Dict con solo los campos que acepta PUT /api/dashboard/:id.
    """
    return {k: v for k, v in dc.items() if k in _DASHCARD_ALLOWED_KEYS}


def metabase_update_dashboard_safe(
    client: MetabaseClient,
    dashboard_id: int,
    *,
    dashcards_update: list[dict] | None = None,
    dashcards_add: list[dict] | None = None,
    dashcards_remove: list[int] | None = None,
    extra_fields: dict | None = None,
) -> dict:
    """Actualiza un dashboard de Metabase manejando los tres gotchas conocidos.

    Gotchas que maneja automaticamente:
    1. **413 Payload Too Large**: strippea el campo ``.card`` denormalizado de
       cada dashcard antes de enviar. Metabase incluye ese objeto en GET pero
       lo rechaza en PUT.
    2. **500 FK violation (tabs)**: siempre incluye el array ``tabs`` actual en
       el body del PUT. Si se omite, Metabase borra las tabs y viola FK
       constraints de dashcards que las referencian.
    3. **IDs negativos para dashcards nuevos**: los dashcards sin ``id``
       (nuevos) reciben IDs negativos temporales (-1, -2, ...) que Metabase
       reemplaza con IDs reales en la respuesta.

    Flujo:
    1. GET /api/dashboard/:id — obtiene estado actual (dashcards + tabs).
    2. Construye la lista final de dashcards:
       - Si ``dashcards_update`` dado: usarla directamente (tras strip).
       - Si ``dashcards_add`` y/o ``dashcards_remove``: aplicar sobre existentes.
       - Sin ninguno: mantener existentes sin cambios.
    3. Strip de campos denormalizados en todos los dashcards.
    4. Asignar IDs negativos a dashcards nuevos (sin ``id``).
    5. PUT /api/dashboard/:id con ``{dashcards, tabs, **extra_fields}``.
    6. Devolver resumen de operacion.

    Args:
        client: Cliente autenticado con sesion activa.
        dashboard_id: ID del dashboard a actualizar.
        dashcards_update: Lista completa de dashcards que REEMPLAZA el estado
            actual. Si se da, ``dashcards_add`` y ``dashcards_remove`` se ignoran.
        dashcards_add: Dashcards a añadir sobre los existentes. Cada item
            no debe incluir ``id`` — la funcion asigna IDs negativos.
        dashcards_remove: Lista de IDs de dashcards existentes a eliminar.
        extra_fields: Campos adicionales del dashboard a actualizar
            (name, description, parameters, archived, collection_id, etc.).

    Returns:
        Dict con resumen de la operacion::

            {
                "added": [list of temp negative ids assigned],
                "updated": int,   # dashcards existentes conservados
                "removed": int,   # dashcards eliminados
                "response": dict, # respuesta raw de PUT /api/dashboard/:id
            }

    Raises:
        httpx.HTTPStatusError: Si la API devuelve 4xx/5xx con mensaje de
            diagnostico indicando que gotcha probablemente se activo.

    Example:
        >>> # Añadir una card al dashboard
        >>> result = metabase_update_dashboard_safe(
        ...     client,
        ...     dashboard_id=42,
        ...     dashcards_add=[{
        ...         "card_id": 100,
        ...         "col": 0,
        ...         "row": 0,
        ...         "size_x": 6,
        ...         "size_y": 4,
        ...         "parameter_mappings": [],
        ...         "visualization_settings": {},
        ...     }],
        ... )
        >>> print(result["added"])  # [-1]

        >>> # Reemplazar todos los dashcards y cambiar el nombre
        >>> result = metabase_update_dashboard_safe(
        ...     client,
        ...     dashboard_id=42,
        ...     dashcards_update=existing_dashcards,
        ...     extra_fields={"name": "Nuevo nombre"},
        ... )
        >>> print(result["updated"])
    """
    # ---- Paso 1: GET estado actual ------------------------------------------
    current = client.request("GET", f"/api/dashboard/{dashboard_id}")
    current_dashcards: list[dict] = current.get("dashcards") or []
    current_tabs: list[dict] = current.get("tabs") or []

    # ---- Paso 2: Construir lista final de dashcards -------------------------
    removed_count = 0
    added_ids: list[int] = []

    if dashcards_update is not None:
        # Reemplazo completo: usar tal cual (tras strip)
        final_dashcards = list(dashcards_update)
        # Contabilizar: existentes que siguen presentes vs removidos
        existing_ids = {dc.get("id") for dc in current_dashcards if dc.get("id")}
        final_pos_ids = {dc.get("id") for dc in final_dashcards if dc.get("id") and dc.get("id", 0) > 0}
        removed_count = len(existing_ids - final_pos_ids)
    else:
        # Operacion incremental: partir de existentes
        remove_set: set[int] = set(dashcards_remove or [])
        final_dashcards = [dc for dc in current_dashcards if dc.get("id") not in remove_set]
        removed_count = len(remove_set)

        if dashcards_add:
            final_dashcards = list(final_dashcards) + list(dashcards_add)

    # ---- Paso 3 & 4: Strip + asignar IDs negativos --------------------------
    cleaned: list[dict] = []
    next_neg_id = -1
    existing_count = 0

    for dc in final_dashcards:
        dc_clean = _strip_dashcard(dc)

        if "id" not in dc_clean or dc_clean.get("id") is None:
            # Dashcard nuevo sin id: asignar negativo
            dc_clean["id"] = next_neg_id
            added_ids.append(next_neg_id)
            next_neg_id -= 1
        elif isinstance(dc_clean.get("id"), int) and dc_clean["id"] < 0:
            # Ya tiene id negativo (caller lo asigno): registrar
            added_ids.append(dc_clean["id"])
        else:
            existing_count += 1

        cleaned.append(dc_clean)

    # ---- Paso 5: Construir body y PUT ----------------------------------------
    body: dict = {
        "dashcards": cleaned,
        "tabs": current_tabs,
    }
    if extra_fields:
        body.update(extra_fields)

    try:
        response = client.request("PUT", f"/api/dashboard/{dashboard_id}", json=body)
    except httpx.HTTPStatusError as exc:
        status = exc.response.status_code
        if status == 413:
            raise httpx.HTTPStatusError(
                f"413 Payload Too Large al actualizar dashboard {dashboard_id}. "
                "Probable causa: dashcard con campo '.card' denormalizado sin strippear. "
                "Revisar que dashcards_update no contenga el objeto 'card' completo.",
                request=exc.request,
                response=exc.response,
            ) from exc
        if status == 500:
            raise httpx.HTTPStatusError(
                f"500 Internal Server Error al actualizar dashboard {dashboard_id}. "
                "Posibles causas: (a) tabs FK violation — el body no incluyo 'tabs' "
                "(no deberia ocurrir con esta funcion); "
                "(b) dashcard referencia tab_id inexistente; "
                "(c) error interno de Metabase. "
                f"Body enviado tenia {len(cleaned)} dashcards y {len(current_tabs)} tabs.",
                request=exc.request,
                response=exc.response,
            ) from exc
        raise

    return {
        "added": added_ids,
        "updated": existing_count,
        "removed": removed_count,
        "response": response,
    }
