import secrets
import base64
import hashlib
import hmac
import httpx
import logging
import time
from datetime import datetime, timezone
from urllib.parse import urlparse
from app.core.config import get_settings

logger = logging.getLogger(__name__)

PAYMENT_HTTP_USER_AGENT = "Documenta-Pagos/1.8.0 (+https://documenta.cl)"


def klap_webhook_apikey(api_key: str) -> str:
    """Calcula el valor del header Apikey usado por los webhooks de Klap."""
    return hashlib.sha256((api_key or "").encode()).hexdigest()


def getnet_notification_signature(request_id: str | int, status: str, date: str,
                                  secret_key: str) -> str:
    """Firma documentada por Getnet: requestId + status + date + secretKey."""
    canonical = f"{request_id}{status}{date}{secret_key}"
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def getnet_notification_signature_is_valid(signature: str, request_id: str | int,
                                            status: str, date: str,
                                            secret_key: str) -> bool:
    """Compara la firma sin filtrar el secreto ni permitir diferencias temporales."""
    received = (signature or "").strip()
    if received.lower().startswith("sha256:"):
        received = received[7:]
    if len(received) != 64:
        return False
    try:
        int(received, 16)
    except ValueError:
        return False
    expected = getnet_notification_signature(request_id, status, date, secret_key)
    return hmac.compare_digest(received.lower(), expected)


def _safe_provider_error(response: httpx.Response) -> tuple[str | None, str | None]:
    """Extrae codigo/mensaje del proveedor sin incluir HTML ni secretos."""
    try:
        payload = response.json()
    except ValueError:
        return None, None
    if not isinstance(payload, dict):
        return None, None
    status = payload.get("status") if isinstance(payload.get("status"), dict) else {}
    code = payload.get("code") or status.get("reason")
    message = payload.get("message") or status.get("message")
    return (str(code)[:40] if code else None,
            str(message).replace("\n", " ")[:240] if message else None)


class GesnotIntegrationError(RuntimeError):
    def __init__(self, message: str, *, http_status: int | None = None,
                 codigo: int | None = None, duration_ms: int | None = None,
                 response_body: str | None = None):
        super().__init__(message)
        self.http_status = http_status
        self.codigo = codigo
        self.duration_ms = duration_ms
        self.response_body = (response_body or "")[:1000] or None


class WebpayAdapter:
    API_PATH = "/rswebpaytransaction/api/webpay/v1.2/transactions"

    def __init__(self, commerce_code: str | None = None, api_key: str | None = None,
                 base_url: str | None = None, environment: str | None = None):
        self.commerce_code = commerce_code
        self.api_key = api_key
        env = (environment or get_settings().webpay_env).upper()
        canonical = "https://webpay3g.transbank.cl" if env in ("PRODUCCION","PRODUCTION") else "https://webpay3gint.transbank.cl"
        candidate = (base_url or canonical).split("/rswebpaytransaction", 1)[0].rstrip("/")
        host = urlparse(candidate).hostname
        if host not in {"webpay3g.transbank.cl", "webpay3gint.transbank.cl"}:
            raise ValueError("Endpoint Webpay no permitido")
        if host != urlparse(canonical).hostname:
            raise ValueError("El endpoint Webpay no corresponde al ambiente configurado")
        self.base_url = candidate

    def _headers(self) -> dict[str, str]:
        if not self.commerce_code or not self.api_key:
            raise ValueError("Credencial Webpay incompleta")
        return {"Tbk-Api-Key-Id": self.commerce_code,
                "Tbk-Api-Key-Secret": self.api_key,
                "Content-Type": "application/json", "Accept": "application/json"}

    def create(self, buy_order: str, session_id: str, amount: int, return_url: str) -> dict:
        if get_settings().webpay_env.lower() == "mock":
            token = f"mock_{secrets.token_urlsafe(18)}"
            return {"token": token, "url": return_url, "mock": True}
        try:
            response = httpx.post(f"{self.base_url}{self.API_PATH}", headers=self._headers(),
                                  json={"buy_order": buy_order, "session_id": session_id,
                                        "amount": amount, "return_url": return_url}, timeout=25.0)
        except httpx.RequestError as exc:
            raise RuntimeError("No fue posible conectar con Transbank") from exc
        if response.status_code not in (200, 201):
            # No registrar headers: contienen el secreto de la credencial.
            detail = response.text[:500]
            logger.warning("Transbank create rechazado status=%s body=%s", response.status_code, detail)
            message = "Credenciales rechazadas" if response.status_code in (401, 403) else "Solicitud rechazada"
            raise RuntimeError(f"Transbank: {message} (HTTP {response.status_code})")
        result = response.json()
        if not result.get("token") or not result.get("url"):
            raise RuntimeError("Transbank no devolvió token y URL de pago")
        return result

    def commit(self, token: str) -> dict:
        response = httpx.put(f"{self.base_url}{self.API_PATH}/{token}",
                             headers=self._headers(), timeout=25.0)
        response.raise_for_status()
        return response.json()


class GetnetAdapter:
    """Web Checkout 2.3. La aprobación siempre se obtiene consultando la sesión."""

    def __init__(self, login: str, secret_key: str, base_url: str | None,
                 environment: str = "INTEGRACION"):
        self.login = (login or "").strip()
        self.secret_key = secret_key or ""
        canonical = ("https://checkout.getnet.cl" if environment.upper() == "PRODUCCION"
                     else "https://checkout.test.getnet.cl")
        candidate = (base_url or canonical).rstrip("/")
        if urlparse(candidate).hostname != urlparse(canonical).hostname:
            raise ValueError("El endpoint Getnet no corresponde al ambiente configurado")
        if not self.login or not self.secret_key:
            raise ValueError("Credencial Getnet incompleta")
        self.base_url = candidate

    def _auth(self) -> dict:
        nonce_raw = secrets.token_bytes(16)
        seed = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
        digest = hashlib.sha256(nonce_raw + seed.encode() + self.secret_key.encode()).digest()
        return {"login": self.login, "tranKey": base64.b64encode(digest).decode(),
                "nonce": base64.b64encode(nonce_raw).decode(), "seed": seed}

    @staticmethod
    def _headers() -> dict[str, str]:
        # El WAF de Getnet rechaza el User-Agent predeterminado python-httpx con HTTP 403.
        return {"Accept": "application/json", "Content-Type": "application/json",
                "User-Agent": PAYMENT_HTTP_USER_AGENT}

    def create(self, reference: str, amount: int, return_url: str, cancel_url: str,
               ip_address: str, user_agent: str, buyer: dict | None = None) -> dict:
        expiration = datetime.fromtimestamp(time.time() + 30 * 60, timezone.utc).isoformat()
        payload = {"auth": self._auth(), "locale": "es_CL",
                   "payment": {"reference": reference,
                               "description": f"Trámite Documenta {reference}",
                               "amount": {"currency": "CLP", "total": amount},
                               "allowPartial": False},
                   "expiration": expiration, "returnUrl": return_url,
                   "cancelUrl": cancel_url, "ipAddress": ip_address or "127.0.0.1",
                   "userAgent": (user_agent or "Documenta")[:250], "skipResult": False}
        if buyer:
            payload["buyer"] = buyer
        try:
            response = httpx.post(f"{self.base_url}/api/session/", json=payload,
                                  headers=self._headers(), timeout=25.0)
        except httpx.RequestError as exc:
            raise RuntimeError("No fue posible conectar con Getnet") from exc
        if response.status_code not in (200, 201):
            code, message = _safe_provider_error(response)
            logger.warning("Getnet create rechazado status=%s content_type=%s code=%s message=%s",
                           response.status_code, response.headers.get("content-type"), code, message)
            detail = f"; código {code}: {message}" if code and message else ""
            raise RuntimeError(f"Getnet rechazó la solicitud (HTTP {response.status_code}{detail})")
        result = response.json()
        if str((result.get("status") or {}).get("status", "")).upper() != "OK":
            raise RuntimeError(f"Getnet: {(result.get('status') or {}).get('message', 'sesión rechazada')}")
        if not result.get("requestId") or not result.get("processUrl"):
            raise RuntimeError("Getnet no devolvió requestId y processUrl")
        return result

    def status(self, request_id: str) -> dict:
        response = httpx.post(f"{self.base_url}/api/session/{request_id}",
                              json={"auth": self._auth()},
                              headers=self._headers(), timeout=25.0)
        response.raise_for_status()
        return response.json()


class KlapAdapter:
    """Checkout FLEX: crea una orden; la aprobación llega por webhook autenticado."""

    def __init__(self, api_key: str, base_url: str | None, environment: str = "INTEGRACION"):
        self.api_key = api_key or ""
        canonical = ("https://api.pasarela.multicaja.cl/payment-gateway/v1/orders"
                     if environment.upper() == "PRODUCCION" else
                     "https://api-pasarela-sandbox.mcdesaqa.cl/payment-gateway/v1/orders")
        candidate = (base_url or canonical).rstrip("/")
        allowed = {"api.pasarela.multicaja.cl", "api-pasarela-sandbox.mcdesaqa.cl"}
        if urlparse(candidate).hostname not in allowed:
            raise ValueError("Endpoint Klap no permitido")
        if not self.api_key:
            raise ValueError("API Key Klap incompleta")
        self.base_url = candidate

    def create(self, merchant_order_id: str, amount: int, return_url: str,
               webhook_url: str, customer: dict | None = None) -> dict:
        payload = {
            "reference_id": merchant_order_id,
            "generate_token": "none",
            "amount": {"currency": "CLP", "total": amount},
            "methods": ["tarjetas"],
            "description": f"Trámite Documenta {merchant_order_id}",
            "customs": [
                {"key": "tarjetas_expiration_minutes", "value": "30"},
                {"key": "tarjetas_delivery_type", "value": "4"},
            ],
            "urls": {"return_url": return_url, "cancel_url": return_url},
            "webhooks": {
                "webhook_confirm": webhook_url,
                "webhook_reject": webhook_url,
            },
        }
        if customer:
            allowed = {"email", "rut", "first_name", "last_name", "phone",
                       "address_line", "address_city", "address_state", "country",
                       "postal_code"}
            payload["user"] = {key: value for key, value in customer.items()
                               if key in allowed and value not in (None, "")}
        try:
            response = httpx.post(self.base_url, json=payload,
                                  headers={"Apikey": self.api_key,
                                           "Content-Type": "application/json",
                                           "Accept": "application/json",
                                           "User-Agent": PAYMENT_HTTP_USER_AGENT}, timeout=25.0)
        except httpx.RequestError as exc:
            raise RuntimeError("No fue posible conectar con Klap") from exc
        if response.status_code not in (200, 201):
            code, message = _safe_provider_error(response)
            logger.warning("Klap create rechazado status=%s code=%s message=%s",
                           response.status_code, code, message)
            detail = f"; código {code}: {message}" if code and message else ""
            raise RuntimeError(f"Klap rechazó la solicitud (HTTP {response.status_code}{detail})")
        result = response.json()
        order_id = result.get("order_id") or result.get("orderId")
        if not order_id:
            raise RuntimeError("Klap no devolvió order_id")
        result["order_id"] = str(order_id)
        redirect_url = result.get("redirect_url") or result.get("redirectUrl")
        if redirect_url:
            redirect_host = urlparse(str(redirect_url)).hostname
            allowed_redirect = bool(redirect_host) and (
                redirect_host == "klap.cl" or redirect_host.endswith(".klap.cl") or
                redirect_host == "multicaja.cl" or redirect_host.endswith(".multicaja.cl") or
                redirect_host == "mcdesaqa.cl" or redirect_host.endswith(".mcdesaqa.cl")
            )
            if not allowed_redirect:
                raise RuntimeError("Klap devolvió una URL de pago no permitida")
            result["redirect_url"] = str(redirect_url)
        return result

    def status(self, token: str) -> dict:
        try:
            response = httpx.get(f"{self.base_url}/{token}",
                                 headers={"Apikey":self.api_key,"Accept":"application/json",
                                          "User-Agent":PAYMENT_HTTP_USER_AGENT},timeout=20.0)
        except httpx.RequestError as exc:
            raise RuntimeError("No fue posible consultar la orden Klap") from exc
        if response.status_code != 200:
            code,message=_safe_provider_error(response)
            logger.warning("Klap status rechazado status=%s code=%s message=%s",
                           response.status_code,code,message)
            raise RuntimeError(f"Klap no pudo consultar la orden (HTTP {response.status_code})")
        return response.json()


class GesnotAdapter:
    def create_order(self, sale_id: int, payload: dict, key_cliente: str | None = None,
                     id_notaria: int | None = None, base_url: str = "https://api.gesnot.cl/ot") -> dict:
        if get_settings().gesnot_mode.lower() == "mock":
            return {"comprobante": f"OT-SBX-{sale_id:08d}", "codigo": 0, "error": False}
        if not key_cliente or not id_notaria:
            raise ValueError("Credencial o idNotaria GesNot no configurados")
        request_payload = {"keyCliente": key_cliente, "idNotaria": id_notaria, **payload}
        endpoint = f"{base_url.rstrip('/').removesuffix('/registrar')}/registrar"
        started = time.perf_counter()
        try:
            response = httpx.post(endpoint, headers={"Content-Type": "application/json"},
                                  json=request_payload, timeout=30.0)
        except httpx.RequestError as exc:
            duration = int((time.perf_counter() - started) * 1000)
            raise GesnotIntegrationError("GESNOT_CONNECT_ERROR", duration_ms=duration) from exc
        duration = int((time.perf_counter() - started) * 1000)
        if response.status_code < 200 or response.status_code >= 300:
            raise GesnotIntegrationError(f"GESNOT_HTTP_{response.status_code}",
                                         http_status=response.status_code, duration_ms=duration,
                                         response_body=response.text)
        try:
            result = response.json()
        except ValueError as exc:
            raise GesnotIntegrationError("GESNOT_INVALID_JSON", http_status=response.status_code,
                                         duration_ms=duration) from exc
        if result.get("error") or int(result.get("codigo", -1)) != 0:
            raise GesnotIntegrationError(f"GESNOT_REJECTED_{result.get('codigo')}",
                                         http_status=response.status_code,
                                         codigo=result.get("codigo"), duration_ms=duration,
                                         response_body=response.text)
        result["_http_status"] = response.status_code
        result["_duration_ms"] = duration
        result["_endpoint"] = endpoint
        return result

    def status(self, comprobante: str, base_url: str = "https://api.gesnot.cl/ot") -> dict:
        response = httpx.get(f"{base_url.rstrip('/')}/consulta-estado",
                             headers={"Content-Type": "application/json"},
                             params={"comprobante": comprobante}, timeout=15.0)
        response.raise_for_status()
        return response.json()
