# /apps/aroflo_connector_app/client.py
import logging
from typing import Any, Dict, Optional, Union, List, Tuple, Sequence
from urllib.parse import urlencode

import requests

from .config import AroFloSettings
from .auth import (
    build_authorization_value,
    build_timestamp,
    build_hmac_signature,
)

log = logging.getLogger(__name__)


class AroFloError(RuntimeError):
    """Error genérico al hablar con la API de AroFlo."""


# AroFlo permite parámetros repetidos (where/join/order), así que soportamos:
# - dict (casos simples)
# - lista de tuplas (casos con repetidos)
ParamsType = Union[Dict[str, Any], List[Tuple[str, str]]]
MultiStr = Union[str, Sequence[str]]


class AroFloClient:
    """
    Cliente para la API de AroFlo.

    - Carga settings desde .env
    - Construye headers HMAC (igual Postman)
    - Hace request genérico y parsea JSON
    """

    def __init__(
        self,
        settings: Optional[AroFloSettings] = None,
        session: Optional[requests.Session] = None,
    ) -> None:
        self.settings = settings or AroFloSettings.from_env()
        self.session = session or requests.Session()
        self.session.headers.update({"User-Agent": "aroflo-connector-app/0.0.1"})

    def request(
        self,
        path: str,
        method: str = "GET",
        *,
        params: Optional[ParamsType] = None,
        data: Any = None,  # ✅ permite str (XML) o dict (form)
        json: Any = None,
        var_string: str = "",
        # ✅ Soporte nativo de AroFlo:
        order: Optional[MultiStr] = None,
        # ✅ Compatibilidad: algunas zonas/CLIs pueden mandar esto
        order_by: Optional[MultiStr] = None,
        orderby: Optional[MultiStr] = None,
        **kwargs: Any,
    ) -> Any:
        method_u = (method or "GET").upper()

        # 1) Normalizar alias de order (AroFlo usa "order=")
        if order is None:
            order = order_by or orderby

        # 2) Por seguridad: si alguien manda order_by/orderby vía kwargs (legacy),
        # lo consumimos aquí para que NO se pase a requests (y rompa).
        if "order_by" in kwargs and order is None:
            order = kwargs.pop("order_by")
        else:
            kwargs.pop("order_by", None)

        if "orderby" in kwargs and order is None:
            order = kwargs.pop("orderby")
        else:
            kwargs.pop("orderby", None)

        # 3) Si nos pasan order, lo inyectamos en params SIN perder repetidos.
        if order is not None:
            params = self._inject_multi_param(params, "order", order)

        url = self.settings.base_url.rstrip("/")
        if path:
            url += "/" + path.lstrip("/")

        # 4) Si no nos pasan var_string, lo derivamos del payload real
        if not var_string:
            if params:
                # urlencode soporta dict o secuencia de tuplas
                var_string = urlencode(params)  # type: ignore[arg-type]
            elif isinstance(data, dict):
                var_string = urlencode(data)
            # Si data es str (XML), NO lo usamos para var_string.

        authorization = build_authorization_value(self.settings)
        iso_ts = build_timestamp()
        _, hmac_headers = build_hmac_signature(
            request_type=method_u,
            var_string=var_string or "",
            authorization=authorization,
            iso_timestamp=iso_ts,
            settings=self.settings,
        )

        extra_headers = kwargs.pop("headers", {}) or {}
        headers = {**hmac_headers, **extra_headers}

        # Si usamos data=form (dict), forzamos Content-Type correcto
        if isinstance(data, dict) and "Content-Type" not in headers:
            headers["Content-Type"] = "application/x-www-form-urlencoded"

        if json is not None and "Content-Type" not in headers:
            headers["Content-Type"] = "application/json"

        timeout = kwargs.pop("timeout", self.settings.timeout)

        log.debug(
            "AroFlo request %s %s params=%s data=%s json=%s var_string=%s",
            method_u,
            url,
            params,
            (type(data).__name__ if data is not None else None),
            ("<json>" if json is not None else None),
            var_string,
        )

        try:
            resp = self.session.request(
                method_u,
                url,
                params=params,  # puede ser dict o lista de tuplas
                data=data,
                json=json,
                headers=headers,
                timeout=timeout,
                **kwargs,
            )
        except requests.RequestException as exc:
            raise AroFloError(f"Error de red al conectar con AroFlo: {exc}") from exc

        try:
            data_resp = resp.json()
        except ValueError as exc:
            raise AroFloError(
                f"Respuesta no JSON desde AroFlo (status={resp.status_code}): {resp.text!r}"
            ) from exc

        status = data_resp.get("status")
        if resp.status_code >= 400 or (status not in (None, "0", 0, "OK")):
            raise AroFloError(
                f"Error en respuesta de AroFlo. HTTP={resp.status_code}, payload={data_resp!r}"
            )

        return data_resp

    def _inject_multi_param(
        self,
        params: Optional[ParamsType],
        key: str,
        value: MultiStr,
    ) -> ParamsType:
        """
        Inserta un parámetro que puede repetirse (where/join/order) de forma segura.

        - Si params es None: crea lista de tuplas.
        - Si params es dict: si value es str -> set simple; si value es lista -> convierte a lista de tuplas.
        - Si params es lista de tuplas: agrega una o varias tuplas.
        """
        # Normalizar value a lista
        values: List[str]
        if isinstance(value, (list, tuple)):
            values = [str(v) for v in value if v is not None and str(v).strip()]
        else:
            values = [str(value)] if str(value).strip() else []

        if not values:
            return params or {}

        # Caso 1: params None -> lista de tuplas
        if params is None:
            out: List[Tuple[str, str]] = []
            for v in values:
                out.append((key, v))
            return out

        # Caso 2: params es lista de tuplas -> append
        if isinstance(params, list):
            out2 = list(params)
            for v in values:
                out2.append((key, v))
            return out2

        # Caso 3: params es dict
        if len(values) == 1:
            params[key] = values[0]
            return params

        # Si vienen múltiples valores y params es dict, convertimos a lista de tuplas
        out3: List[Tuple[str, str]] = [(k, str(v)) for k, v in params.items()]
        for v in values:
            out3.append((key, v))
        return out3

    # Helpers (opcionales pero útiles para zonas/CLI)
    def get(self, *, params: ParamsType, path: str = "") -> Any:
        return self.request(path, method="GET", params=params)


def get_client() -> "AroFloClient":
    return AroFloClient()

