import base64
import hashlib
import ipaddress
import json
import logging
import re
import socket
import time
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from typing import Any
from urllib.parse import urlparse

import httpx
from sqlalchemy import text

from app.core.config import get_settings
from app.core.db import db_session
from app.core.security import decrypt_platform_secret, keyed_hash


log = logging.getLogger(__name__)
CODE_PATTERN = re.compile(r"^[A-Z0-9][A-Z0-9._:/-]{5,119}$")
RESULT_VALUES = {"VERIFICADO", "INVALIDO", "NO_ENCONTRADO", "ERROR", "OMITIDO"}


def normalize_verification_code(value: str) -> str:
    normalized = re.sub(r"\s+", "", value.strip().upper())
    if not CODE_PATTERN.fullmatch(normalized):
        raise ValueError("El código debe tener entre 6 y 120 caracteres y usar letras, números, punto, guion o barra")
    return normalized


def mask_code(value: str) -> str:
    if len(value) <= 10:
        return value[:3] + "…" + value[-2:]
    return value[:6] + "…" + value[-4:]


def _json_config(value: str | None) -> dict[str, Any]:
    try:
        parsed = json.loads(value or "{}")
        return parsed if isinstance(parsed, dict) else {}
    except json.JSONDecodeError:
        return {}


def _json_path(payload: Any, path: str | None) -> Any:
    if not path:
        return None
    current = payload
    for part in path.split("."):
        if isinstance(current, dict):
            current = current.get(part)
        elif isinstance(current, list) and part.isdigit() and int(part) < len(current):
            current = current[int(part)]
        else:
            return None
    return current


def _safe_https_url(value: Any) -> str | None:
    if not isinstance(value, str):
        return None
    parsed = urlparse(value)
    if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
        return None
    return value[:700]


def validate_public_https_endpoint(url: str) -> str:
    parsed = urlparse(url.strip())
    if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
        raise ValueError("El endpoint debe ser una URL HTTPS sin credenciales embebidas")
    if parsed.port not in (None, 443):
        raise ValueError("El endpoint de verificación sólo puede usar el puerto HTTPS 443")
    try:
        addresses = {item[4][0] for item in socket.getaddrinfo(parsed.hostname, 443, type=socket.SOCK_STREAM)}
    except OSError as exc:
        raise ValueError("No fue posible resolver el endpoint") from exc
    if not addresses or any(not ipaddress.ip_address(address).is_global for address in addresses):
        raise ValueError("El endpoint debe resolver únicamente a direcciones públicas")
    return url.strip()


def _canonical_hash(value: dict[str, Any]) -> bytes:
    body = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str).encode()
    return hashlib.sha256(body).digest()


def _utc_datetime(value: Any) -> datetime | None:
    if isinstance(value, datetime):
        return value.replace(tzinfo=value.tzinfo or timezone.utc).astimezone(timezone.utc)
    if isinstance(value, str):
        try:
            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
            return parsed.replace(tzinfo=parsed.tzinfo or timezone.utc).astimezone(timezone.utc)
        except ValueError:
            return None
    return None


def _internal_result(repository: dict[str, Any], code: str) -> dict[str, Any]:
    with db_session() as db:
        row = db.execute(text("""
          SELECT dv.codigo_publico,dv.estado_documento,dv.estado_firma,dv.algoritmo_hash,
                 LOWER(HEX(dv.hash_documento)) hash_documento,dv.emitido_en,dv.firmado_en,
                 dv.revocado_en,dv.motivo_revocacion,d.tipo,d.version,
                 s.numero_publico folio,m.nombre servicio,n.nombre_fantasia emisor
          FROM documento_verificaciones dv
          JOIN documentos d ON d.id=dv.documento_id
          JOIN solicitudes s ON s.id=d.solicitud_id
          JOIN materias m ON m.id=s.servicio_id
          JOIN notarias n ON n.id=dv.emisor_entidad_id
          WHERE dv.codigo_publico=:code LIMIT 1
        """), {"code": code}).mappings().first()
    if not row:
        return {"resultado": "NO_ENCONTRADO", "evidencia": None}
    invalid = row["estado_documento"] != "VIGENTE" or row["estado_firma"] == "INVALIDO"
    evidence = {
        "codigo": row["codigo_publico"],
        "documento": row["servicio"],
        "tipo": row["tipo"],
        "version": int(row["version"]),
        "emisor": row["emisor"],
        "emitido_en": row["emitido_en"],
        "firmado_en": row["firmado_en"],
        "estado_documento": row["estado_documento"],
        "estado_firma": row["estado_firma"],
        "algoritmo_hash": row["algoritmo_hash"],
        "hash_documento": row["hash_documento"],
        "revocado_en": row["revocado_en"],
        "motivo_revocacion": row["motivo_revocacion"],
        "fuente_url": "https://documenta.cl/verificar",
    }
    return {"resultado": "INVALIDO" if invalid else "VERIFICADO", "evidencia": evidence}


def _http_headers(repository: dict[str, Any], credential: dict[str, Any] | None) -> dict[str, str]:
    headers = {"Accept": "application/json", "User-Agent": "Documenta-Verificador/1.0"}
    auth_type = repository["auth_tipo"]
    if auth_type == "NINGUNA":
        return headers
    if not credential:
        raise ValueError("CREDENCIAL_NO_CONFIGURADA")
    secret = decrypt_platform_secret(credential["secreto_cifrado"])
    if auth_type == "BEARER":
        headers["Authorization"] = f"Bearer {secret}"
    elif auth_type == "API_KEY_HEADER":
        headers[repository.get("auth_header") or "X-API-Key"] = secret
    elif auth_type == "BASIC":
        identity = credential.get("identificador_publico") or ""
        headers["Authorization"] = "Basic " + base64.b64encode(f"{identity}:{secret}".encode()).decode()
    return headers


def _external_result(repository: dict[str, Any], code: str) -> dict[str, Any]:
    endpoint = validate_public_https_endpoint(repository["endpoint_base"])
    config = _json_config(repository.get("configuracion_json"))
    with db_session() as db:
        credential = db.execute(text("""
          SELECT identificador_publico,secreto_cifrado
          FROM repositorio_verificacion_credenciales
          WHERE repositorio_id=:repository AND activa=TRUE
            AND valida_desde<=UTC_TIMESTAMP(6)
            AND (valida_hasta IS NULL OR valida_hasta>UTC_TIMESTAMP(6))
          ORDER BY version DESC LIMIT 1
        """), {"repository": repository["id"]}).mappings().first()
    headers = _http_headers(repository, dict(credential) if credential else None)
    timeout = max(0.5, min(10.0, int(repository["timeout_ms"]) / 1000))
    max_attempts = max(1, min(3, int(repository["max_intentos"])))
    response = None
    for attempt in range(1, max_attempts + 1):
        try:
            with httpx.Client(timeout=timeout, follow_redirects=False, verify=True) as client:
                if repository["metodo_http"] == "POST":
                    response = client.post(endpoint, json={repository["codigo_parametro"]: code}, headers=headers)
                else:
                    response = client.get(endpoint, params={repository["codigo_parametro"]: code}, headers=headers)
            if response.status_code < 500:
                break
        except httpx.RequestError:
            if attempt >= max_attempts:
                raise
    if response is None:
        raise httpx.RequestError("SIN_RESPUESTA")
    if response.status_code == 404:
        return {"resultado": "NO_ENCONTRADO", "evidencia": None,
                "http_status": response.status_code, "intentos": attempt}
    if response.status_code < 200 or response.status_code >= 300:
        raise RuntimeError(f"HTTP_{response.status_code}")
    if len(response.content) > 1024 * 1024:
        raise ValueError("RESPUESTA_DEMASIADO_GRANDE")
    try:
        payload = response.json()
    except ValueError as exc:
        raise ValueError("RESPUESTA_NO_JSON") from exc
    found_value = _json_path(payload, config.get("found_path"))
    status_value = _json_path(payload, config.get("status_path"))
    normalized_status = str(status_value).strip().upper() if status_value is not None else ""
    valid_values = {str(item).upper() for item in config.get("valid_values", ["VALIDO", "VALID", "VERIFIED", "VIGENTE"])}
    invalid_values = {str(item).upper() for item in config.get("invalid_values", ["INVALIDO", "INVALID", "REVOKED", "REVOCADO"])}
    missing_values = {str(item).upper() for item in config.get("not_found_values", ["NO_ENCONTRADO", "NOT_FOUND", "MISSING"])}
    if found_value is False or normalized_status in missing_values:
        result = "NO_ENCONTRADO"
    elif normalized_status in invalid_values:
        result = "INVALIDO"
    elif found_value is True or normalized_status in valid_values:
        result = "VERIFICADO"
    else:
        raise ValueError("RESPUESTA_SIN_ESTADO_MAPEABLE")
    evidence = None
    if result in {"VERIFICADO", "INVALIDO"}:
        evidence = {
            "codigo": _json_path(payload, config.get("code_path")) or code,
            "documento": _json_path(payload, config.get("document_name_path")),
            "emisor": _json_path(payload, config.get("issuer_path")),
            "emitido_en": _json_path(payload, config.get("issued_at_path")),
            "firmado_en": _json_path(payload, config.get("signed_at_path")),
            "estado_documento": _json_path(payload, config.get("document_status_path")) or normalized_status,
            "estado_firma": _json_path(payload, config.get("signature_status_path")),
            "hash_documento": _json_path(payload, config.get("hash_path")),
            "fuente_url": _safe_https_url(_json_path(payload, config.get("document_url_path"))) or endpoint,
        }
        evidence = {key: value for key, value in evidence.items() if value is not None}
    return {"resultado": result, "evidencia": evidence,
            "http_status": response.status_code, "intentos": attempt}


def _run_repository(repository: dict[str, Any], code: str) -> dict[str, Any]:
    started = time.monotonic()
    result: dict[str, Any] = {"repositorio_id": repository["id"], "repositorio": repository["nombre"],
                              "codigo_repositorio": repository["codigo"], "resultado": "ERROR",
                              "intentos": 1, "http_status": None, "evidencia": None,
                              "error_codigo": None}
    open_until = _utc_datetime(repository.get("circuito_abierto_hasta"))
    if repository.get("estado_circuito") == "ABIERTO" and open_until and open_until > datetime.now(timezone.utc):
        result.update({"resultado": "OMITIDO", "error_codigo": "CIRCUITO_ABIERTO"})
    else:
        try:
            if repository["tipo_conector"] == "INTERNO_DOCUMENTA":
                outcome = _internal_result(repository, code)
            else:
                outcome = _external_result(repository, code)
            result.update(outcome)
        except (httpx.RequestError, RuntimeError, ValueError) as exc:
            safe = str(exc).upper().replace(" ", "_")[:100]
            result["error_codigo"] = safe if re.fullmatch(r"[A-Z0-9_:-]+", safe) else type(exc).__name__.upper()
            log.warning("verification_repository_failed repository=%s error=%s", repository["codigo"], result["error_codigo"])
        except Exception:
            result["error_codigo"] = "ERROR_CONECTOR"
            log.exception("verification_repository_unexpected repository=%s", repository["codigo"])
    result["duracion_ms"] = max(0, round((time.monotonic() - started) * 1000))
    evidence = result.get("evidencia")
    result["evidencia_hash"] = _canonical_hash(evidence) if evidence else None
    return result


def _update_circuit(db, repository: dict[str, Any], outcome: dict[str, Any]) -> None:
    if outcome["resultado"] in {"VERIFICADO", "INVALIDO", "NO_ENCONTRADO"}:
        db.execute(text("""
          UPDATE repositorios_verificacion SET estado_circuito='CERRADO',fallos_consecutivos=0,
            circuito_abierto_hasta=NULL,ultimo_error_sanitizado=NULL WHERE id=:id
        """), {"id": repository["id"]})
        return
    if outcome["resultado"] == "OMITIDO":
        return
    threshold = int(repository["umbral_circuito"])
    db.execute(text("""
      UPDATE repositorios_verificacion
      SET fallos_consecutivos=fallos_consecutivos+1,
          estado_circuito=IF(fallos_consecutivos+1>=:threshold,'ABIERTO',estado_circuito),
          circuito_abierto_hasta=IF(fallos_consecutivos+1>=:threshold,
            DATE_ADD(UTC_TIMESTAMP(6),INTERVAL 5 MINUTE),circuito_abierto_hasta),
          ultimo_error_sanitizado=:error
      WHERE id=:id
    """), {"threshold": threshold, "error": outcome["error_codigo"], "id": repository["id"]})


def verify_document(code_value: str, ip: str | None, user_agent: str | None) -> dict[str, Any]:
    code = normalize_verification_code(code_value)
    settings = get_settings()
    code_hmac = keyed_hash(code, settings.access_token_hmac_key)
    ip_hmac = keyed_hash(ip, settings.access_token_hmac_key) if ip else None
    agent_hash = hashlib.sha256((user_agent or "").encode()).digest() if user_agent else None
    started = time.monotonic()
    public_uuid = uuid.uuid4()
    with db_session() as db:
        if ip_hmac:
            recent = db.execute(text("""
              SELECT COUNT(*) FROM verificaciones_documentales
              WHERE ip_hmac=:ip AND iniciada_en>DATE_SUB(UTC_TIMESTAMP(6),INTERVAL 5 MINUTE)
            """), {"ip": ip_hmac}).scalar() or 0
            if recent >= 20:
                raise PermissionError("RATE_LIMIT")
        verification_id = db.execute(text("""
          INSERT INTO verificaciones_documentales
            (uuid,codigo_hmac,codigo_mascara,ip_hmac,agente_hash)
          VALUES (:uuid,:code,:mask,:ip,:agent)
        """), {"uuid": public_uuid.bytes, "code": code_hmac, "mask": mask_code(code),
                "ip": ip_hmac, "agent": agent_hash}).lastrowid
        repositories = [dict(row) for row in db.execute(text("""
          SELECT * FROM repositorios_verificacion
          WHERE activo=TRUE ORDER BY prioridad,id LIMIT 20
        """)).mappings().all()]
    outcomes: list[dict[str, Any]] = []
    if repositories:
        with ThreadPoolExecutor(max_workers=min(4, len(repositories))) as executor:
            future_map = {executor.submit(_run_repository, repository, code): repository for repository in repositories}
            for future in as_completed(future_map):
                outcomes.append(future.result())
    order = {repository["id"]: index + 1 for index, repository in enumerate(repositories)}
    outcomes.sort(key=lambda item: order[item["repositorio_id"]])
    verified = [item for item in outcomes if item["resultado"] == "VERIFICADO"]
    invalid = [item for item in outcomes if item["resultado"] == "INVALIDO"]
    errors = [item for item in outcomes if item["resultado"] in {"ERROR", "OMITIDO"}]
    healthy = [item for item in outcomes if item["resultado"] in {"VERIFICADO", "INVALIDO", "NO_ENCONTRADO"}]
    if verified:
        status, summary = "VERIFICADO", "El código fue verificado por al menos una fuente configurada."
    elif invalid:
        status, summary = "INVALIDO", "La fuente encontró el documento, pero informó que no está vigente o su evidencia no es válida."
    elif healthy and not errors:
        status, summary = "NO_ENCONTRADO", "Ningún repositorio activo encontró el código consultado."
    else:
        status, summary = "INDETERMINADO", "No fue posible completar la consulta en todas las fuentes necesarias."
    duration = max(0, round((time.monotonic() - started) * 1000))
    repository_map = {item["id"]: item for item in repositories}
    with db_session() as db:
        for outcome in outcomes:
            evidence = outcome.get("evidencia")
            db.execute(text("""
              INSERT INTO verificacion_intentos
                (verificacion_id,repositorio_id,orden,resultado,intentos,http_status,duracion_ms,
                 evidencia_normalizada_json,evidencia_hash,error_codigo)
              VALUES (:verification,:repository,:position,:result,:attempts,:http,:duration,
                      :evidence,:hash,:error)
            """), {"verification": verification_id, "repository": outcome["repositorio_id"],
                    "position": order[outcome["repositorio_id"]], "result": outcome["resultado"],
                    "attempts": outcome.get("intentos", 1), "http": outcome.get("http_status"),
                    "duration": outcome["duracion_ms"],
                    "evidence": json.dumps(evidence, ensure_ascii=False, default=str) if evidence else None,
                    "hash": outcome.get("evidencia_hash"), "error": outcome.get("error_codigo")})
            _update_circuit(db, repository_map[outcome["repositorio_id"]], outcome)
        db.execute(text("""
          UPDATE verificaciones_documentales
          SET estado=:status,resumen=:summary,repositorios_consultados=:checked,
              repositorios_exitosos=:successful,repositorios_fallidos=:failed,
              duracion_ms=:duration,finalizada_en=UTC_TIMESTAMP(6)
          WHERE id=:id
        """), {"status": status, "summary": summary, "checked": len(outcomes),
                "successful": len(healthy), "failed": len(errors), "duration": duration,
                "id": verification_id})
    sources = [{"repositorio": item["repositorio"], "codigo": item["codigo_repositorio"],
                "resultado": item["resultado"], "duracion_ms": item["duracion_ms"],
                "evidencia": item.get("evidencia"),
                "error": "FUENTE_NO_DISPONIBLE" if item.get("error_codigo") else None}
               for item in outcomes]
    return {"verificacion_id": str(public_uuid), "codigo": code, "estado": status,
            "resumen": summary, "consultado_en": datetime.now(timezone.utc),
            "duracion_ms": duration, "fuentes": sources,
            "kpis": {"consultadas": len(outcomes), "respondieron": len(healthy), "fallaron": len(errors)}}


def test_repository(repository: dict[str, Any], code_value: str) -> dict[str, Any]:
    code = normalize_verification_code(code_value)
    result = _run_repository(repository, code)
    with db_session() as db:
        db.execute(text("""
          UPDATE repositorios_verificacion SET ultima_prueba_en=UTC_TIMESTAMP(6),
            ultima_prueba_ok=:ok,ultimo_error_sanitizado=:error WHERE id=:id
        """), {"ok": result["resultado"] in {"VERIFICADO", "INVALIDO", "NO_ENCONTRADO"},
                "error": result.get("error_codigo"), "id": repository["id"]})
        _update_circuit(db, repository, result)
    return {"repositorio": result["repositorio"], "resultado": result["resultado"],
            "duracion_ms": result["duracion_ms"], "http_status": result.get("http_status"),
            "evidencia": result.get("evidencia"), "error": result.get("error_codigo")}
