import secrets
import json
import re
from datetime import date
from decimal import Decimal, InvalidOperation
from datetime import datetime
from sqlalchemy import bindparam, text
from app.core.config import get_settings
from app.core.db import db_session
from app.core.security import encrypt_text, format_rut, keyed_hash, new_access_token, normalize_email, normalize_rut, valid_rut
from app.core.timezone import SANTIAGO
from app.services.service_configuration import effective_entity_id
from app.services.validation import looks_like_beneficiary_rut, normalize_chilean_phone


def validate_field_value(field: dict, value: str):
    if field["largo_minimo"] is not None and len(value) < field["largo_minimo"]: raise ValueError(f"{field['etiqueta']}: valor demasiado corto")
    if field["largo_maximo"] is not None and len(value) > field["largo_maximo"]: raise ValueError(f"{field['etiqueta']}: valor demasiado largo")
    if field["patron_regex"]:
        try: matches = re.fullmatch(field["patron_regex"], value)
        except re.error as exc: raise ValueError(f"Configuración inválida en {field['etiqueta']}") from exc
        if not matches: raise ValueError(f"{field['etiqueta']}: formato inválido")
    kind=field["tipo_dato"]
    if kind in {"RUT","RUT_CHILENO","RUT CHILENO"} and not valid_rut(value): raise ValueError(f"{field['etiqueta']}: RUT inválido")
    if kind=="TELEFONO":
        try: normalize_chilean_phone(value)
        except ValueError as exc: raise ValueError(f"{field['etiqueta']}: {exc}") from exc
    if kind=="TEXTO" and "nombre completo" in str(field["etiqueta"]).lower() and (len(value.split())<2 or any(len(part)<2 for part in value.split())):
        raise ValueError(f"{field['etiqueta']}: ingresa nombre y apellido")
    if looks_like_beneficiary_rut(str(field.get("codigo") or ""), str(field["etiqueta"]), value) and not valid_rut(value):
        raise ValueError(f"{field['etiqueta']}: RUT inválido; si es una empresa, escribe su razón social")
    if kind=="EMAIL" and not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+",value): raise ValueError(f"{field['etiqueta']}: correo inválido")
    if kind in {"ENTERO","DECIMAL"}:
        try: number=Decimal(value)
        except InvalidOperation as exc: raise ValueError(f"{field['etiqueta']}: debe ser un número") from exc
        if kind=="ENTERO" and number!=number.to_integral_value(): raise ValueError(f"{field['etiqueta']}: debe ser entero")
        if field["valor_minimo"] is not None and number<field["valor_minimo"]: raise ValueError(f"{field['etiqueta']}: valor bajo el mínimo")
        if field["valor_maximo"] is not None and number>field["valor_maximo"]: raise ValueError(f"{field['etiqueta']}: valor sobre el máximo")
    if kind=="FECHA":
        try: date.fromisoformat(value)
        except ValueError as exc: raise ValueError(f"{field['etiqueta']}: fecha inválida") from exc
    if kind=="LISTA" and field["opciones_json"]:
        options=json.loads(field["opciones_json"])
        if isinstance(options,list):
            values=[str(item.get("value",item.get("codigo",item.get("label", "")))) if isinstance(item,dict) else str(item) for item in options]
            if value not in values: raise ValueError(f"{field['etiqueta']}: opción inválida")
        elif value not in options: raise ValueError(f"{field['etiqueta']}: opción inválida")


def list_materias(search: str | None = None, category: str | None = None,
                  capability: str | None = None):
    normalized_capability = capability.upper() if capability else None
    if normalized_capability and normalized_capability not in ENTITY_CAPABILITIES:
        raise ValueError("Capacidad de entidad inválida")
    with db_session() as db:
        rows = db.execute(text("""
          SELECT m.id,m.codigo,m.nombre,m.slug,m.descripcion_corta,c.nombre categoria,c.slug categoria_slug,
                 (SELECT GROUP_CONCAT(te2.codigo ORDER BY sc2.es_principal DESC,te2.orden SEPARATOR ',')
                    FROM servicio_capacidades sc2 JOIN tipos_entidad te2 ON te2.id=sc2.tipo_entidad_id
                   WHERE sc2.materia_id=m.id AND sc2.activo=TRUE AND te2.activo=TRUE) capacidades_csv
          FROM materias m JOIN categorias_materia c ON c.id=m.categoria_materia_id
          JOIN estados_materia e ON e.id=m.estado_materia_id
          WHERE e.codigo='ACTIVA'
            AND (:search IS NULL OR m.nombre LIKE CONCAT('%',:search,'%'))
            AND (:category IS NULL OR c.slug=:category)
            AND (:capability IS NULL OR EXISTS (
              SELECT 1 FROM servicio_capacidades sc
              JOIN tipos_entidad te ON te.id=sc.tipo_entidad_id
              WHERE sc.materia_id=m.id AND sc.activo=TRUE AND te.activo=TRUE
                AND te.codigo=:capability
            ))
          ORDER BY c.orden,m.nombre
        """), {"search": search, "category": category,
                  "capability": normalized_capability}).mappings().all()
        result=[]
        for row in rows:
            item=dict(row); raw=item.pop("capacidades_csv","") or ""
            item["capacidades"]=[value for value in raw.split(",") if value]
            result.append(item)
        return result


def list_services(search: str | None = None, category: str | None = None,
                  capability: str | None = None):
    """Contrato neutral H2; mantiene materias como almacenamiento compatible."""
    return list_materias(search, category, capability)


def get_materia(slug: str, entity_uuid: str | None = None):
    with db_session() as db:
        materia = db.execute(text("""
          SELECT m.id,m.codigo,m.nombre,m.slug,m.descripcion,m.descripcion_corta,m.requiere_comparecientes
          FROM materias m JOIN estados_materia e ON e.id=m.estado_materia_id
          WHERE m.slug=:slug AND e.codigo='ACTIVA'
        """), {"slug": slug}).mappings().first()
        if not materia:
            return None
        entity_id = None
        if entity_uuid:
            entity_id = db.execute(text("""
              SELECT n.id FROM notarias n
              JOIN directorio d ON d.notaria_id=n.id AND d.materia_id=:matter AND d.activo=TRUE
              WHERE n.uuid=UNHEX(REPLACE(:uuid,'-',''))
            """), {"matter": materia["id"], "uuid": entity_uuid}).scalar()
            if not entity_id:
                return None
        field_entity = effective_entity_id(db, materia["id"], entity_id, "fields")
        requirement_entity = effective_entity_id(db, materia["id"], entity_id, "requirements")
        fields = db.execute(text("""
          SELECT cm.id,cm.codigo,cm.etiqueta,cm.ayuda,cm.placeholder,cm.requerido,
                 cm.aplica_a,cm.orden,td.codigo tipo_dato,cm.opciones_json,cm.largo_minimo,
                 cm.largo_maximo,cm.valor_minimo,cm.valor_maximo,cm.patron_regex
          FROM campos_materias cm JOIN tipo_de_dato td ON td.id=cm.tipo_de_dato_id
          WHERE cm.materia_id=:id AND cm.notaria_id <=> :entity AND cm.activo=TRUE
          ORDER BY cm.aplica_a,cm.orden
        """), {"id": materia["id"], "entity": field_entity}).mappings().all()
        requirements = db.execute(text("""
          SELECT rd.id,rd.codigo,rd.nombre,rd.descripcion,rd.requerido,rd.cantidad_maxima,
                 rd.mime_permitidos,rd.tamano_maximo_bytes,tr.codigo tipo
          FROM requisitos_documentos rd JOIN tipos_requisito tr ON tr.id=rd.tipo_requisito_id
          WHERE rd.materia_id=:id AND rd.notaria_id <=> :entity AND rd.activo=TRUE ORDER BY rd.orden
        """), {"id": materia["id"], "entity": requirement_entity}).mappings().all()
        result = dict(materia)
        result["campos"] = [dict(x) for x in fields]
        result["requisitos"] = [dict(x) for x in requirements]
        result["configuracion_entidad_id"] = entity_id
        result["personalizacion"] = {
            "campos_plantilla": field_entity is not None,
            "requisitos": requirement_entity is not None,
        }
        if any(x["tipo_dato"] == "COMUNA" for x in fields):
            communes = db.execute(text("""
              SELECT c.codigo_gesnot codigo,c.nombre,r.nombre region
              FROM comunas c JOIN regiones r ON r.id=c.region_id
              WHERE c.activo=TRUE AND c.codigo_gesnot IS NOT NULL
              ORDER BY c.nombre
            """)).mappings().all()
            result["comunas"] = [dict(x) for x in communes]
        return result


ENTITY_CAPABILITIES = {"NOTARIA", "CONSERVADOR", "ARCHIVERO"}


def list_notarias(materia_slug: str, lat: float | None = None, lng: float | None = None,
                  notaria_uuid: str | None = None, capacidad: str | None = None,
                  comuna_id: int | None = None, jurisdiccion_id: int | None = None,
                  corte_id: int | None = None):
    capability = capacidad.upper() if capacidad else None
    if capability and capability not in ENTITY_CAPABILITIES:
        raise ValueError("Capacidad de entidad inválida")
    with db_session() as db:
        rows = db.execute(text("""
          SELECT d.id directorio_id,n.id notaria_id,
                 LOWER(CONCAT(SUBSTR(HEX(n.uuid),1,8),'-',SUBSTR(HEX(n.uuid),9,4),'-',
                   SUBSTR(HEX(n.uuid),13,4),'-',SUBSTR(HEX(n.uuid),17,4),'-',SUBSTR(HEX(n.uuid),21,12))) entidad_uuid,
                 n.nombre_fantasia,n.nombre_notario,n.direccion,n.telefono,
                 COALESCE(eu.latitud,n.latitud) latitud,COALESCE(eu.longitud,n.longitud) longitud,
                 co.id comuna_id,co.nombre comuna,r.nombre region,d.precio,d.moneda,d.plazo_estimado_horas,d.tipo_cobertura,
                 (SELECT GROUP_CONCAT(te.codigo ORDER BY te.orden SEPARATOR ',')
                    FROM entidad_capacidades ec
                    JOIN tipos_entidad te ON te.id=ec.tipo_entidad_id
                   WHERE ec.entidad_id=n.id AND ec.activo=TRUE AND te.activo=TRUE) capacidades_csv,
                 (SELECT GROUP_CONCAT(DISTINCT pi.codigo
                           ORDER BY FIELD(pi.codigo,'WEBPAY','KLAP','GETNET') SEPARATOR ',')
                    FROM notaria_credenciales nc
                   JOIN proveedores_integracion pi ON pi.id=nc.proveedor_integracion_id
                  WHERE nc.notaria_id=n.id AND nc.activa=TRUE
                    AND pi.codigo IN ('WEBPAY','KLAP','GETNET')
                    AND (nc.valida_hasta IS NULL OR nc.valida_hasta>UTC_TIMESTAMP(6))) medios_pago_csv,
                 CASE WHEN :lat IS NOT NULL AND :lng IS NOT NULL
                           AND COALESCE(eu.latitud,n.latitud) IS NOT NULL
                           AND COALESCE(eu.longitud,n.longitud) IS NOT NULL
                   THEN ROUND(6371*2*ASIN(SQRT(
                     POWER(SIN(RADIANS(COALESCE(eu.latitud,n.latitud)-:lat)/2),2)
                     +COS(RADIANS(:lat))*COS(RADIANS(COALESCE(eu.latitud,n.latitud)))
                     *POWER(SIN(RADIANS(COALESCE(eu.longitud,n.longitud)-:lng)/2),2)
                   )),1)
                   ELSE NULL END distancia_km
          FROM directorio d JOIN notarias n ON n.id=d.notaria_id
          LEFT JOIN entidad_ubicaciones eu ON eu.entidad_id=n.id
          JOIN estados_notaria en ON en.id=n.estado_notaria_id
          JOIN materias m ON m.id=d.materia_id JOIN comunas co ON co.id=n.comuna_id
          JOIN regiones r ON r.id=co.region_id
          WHERE m.slug=:slug AND d.activo=TRUE AND en.codigo='ACTIVA'
            AND (:notaria_uuid IS NULL OR LOWER(HEX(n.uuid))=REPLACE(LOWER(:notaria_uuid),'-',''))
            AND EXISTS (
              SELECT 1 FROM entidad_capacidades ec2
              JOIN servicio_capacidades sc2
                ON sc2.tipo_entidad_id=ec2.tipo_entidad_id
              JOIN tipos_entidad te2 ON te2.id=ec2.tipo_entidad_id
              WHERE ec2.entidad_id=n.id AND sc2.materia_id=m.id
                AND ec2.activo=TRUE AND sc2.activo=TRUE AND te2.activo=TRUE
            )
            AND (:capacidad IS NULL OR EXISTS (
              SELECT 1 FROM entidad_capacidades ec3
              JOIN tipos_entidad te3 ON te3.id=ec3.tipo_entidad_id
              WHERE ec3.entidad_id=n.id AND ec3.activo=TRUE AND te3.activo=TRUE
                AND te3.codigo=:capacidad
            ))
            AND (:capacidad IS NULL OR EXISTS (
              SELECT 1 FROM servicio_capacidades sc3
              JOIN tipos_entidad ts3 ON ts3.id=sc3.tipo_entidad_id
              WHERE sc3.materia_id=m.id AND sc3.activo=TRUE AND ts3.activo=TRUE
                AND ts3.codigo=:capacidad
            ))
            AND (
              (:comuna_id IS NULL AND :jurisdiccion_id IS NULL AND :corte_id IS NULL)
              OR d.tipo_cobertura='NACIONAL'
              OR (d.tipo_cobertura='ENTIDAD' AND EXISTS (
                SELECT 1 FROM entidad_jurisdicciones ej
                JOIN jurisdicciones je ON je.id=ej.jurisdiccion_id
                LEFT JOIN jurisdiccion_comunas jce ON jce.jurisdiccion_id=ej.jurisdiccion_id
                WHERE ej.entidad_id=n.id AND ej.activo=TRUE
                  AND (:comuna_id IS NULL OR jce.comuna_id=:comuna_id)
                  AND (:jurisdiccion_id IS NULL OR ej.jurisdiccion_id=:jurisdiccion_id)
                  AND (:corte_id IS NULL OR je.corte_apelaciones_id=:corte_id)
              ))
              OR (d.tipo_cobertura='PERSONALIZADA'
                AND (:comuna_id IS NULL OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_comunas occ
                  WHERE occ.oferta_id=d.id AND occ.comuna_id=:comuna_id
                ) OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_jurisdicciones ocj
                  JOIN jurisdiccion_comunas jc ON jc.jurisdiccion_id=ocj.jurisdiccion_id
                  WHERE ocj.oferta_id=d.id AND jc.comuna_id=:comuna_id
                ) OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_cortes oct
                  JOIN jurisdicciones jt ON jt.corte_apelaciones_id=oct.corte_apelaciones_id
                  JOIN jurisdiccion_comunas jct ON jct.jurisdiccion_id=jt.id
                  WHERE oct.oferta_id=d.id AND jct.comuna_id=:comuna_id
                ))
                AND (:jurisdiccion_id IS NULL OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_jurisdicciones ocj2
                  WHERE ocj2.oferta_id=d.id AND ocj2.jurisdiccion_id=:jurisdiccion_id
                ) OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_cortes oct2
                  JOIN jurisdicciones jt2 ON jt2.corte_apelaciones_id=oct2.corte_apelaciones_id
                  WHERE oct2.oferta_id=d.id AND jt2.id=:jurisdiccion_id
                ))
                AND (:corte_id IS NULL OR EXISTS (
                  SELECT 1 FROM oferta_cobertura_cortes oct3
                  WHERE oct3.oferta_id=d.id AND oct3.corte_apelaciones_id=:corte_id
                ))
              )
            )
            AND EXISTS (
              SELECT 1 FROM notaria_credenciales nc2
              JOIN proveedores_integracion pi2 ON pi2.id=nc2.proveedor_integracion_id
              WHERE nc2.notaria_id=n.id AND nc2.activa=TRUE
                AND pi2.codigo IN ('WEBPAY','KLAP','GETNET')
                AND (nc2.valida_hasta IS NULL OR nc2.valida_hasta>UTC_TIMESTAMP(6))
            )
          ORDER BY distancia_km IS NULL,distancia_km,d.precio,n.nombre_fantasia
        """), {"slug": materia_slug,"lat":lat,"lng":lng,
                 "notaria_uuid":notaria_uuid,"capacidad":capability,
                 "comuna_id":comuna_id,"jurisdiccion_id":jurisdiccion_id,
                 "corte_id":corte_id}).mappings().all()
        items = []
        for row in rows:
            item = dict(row)
            item["entidad_id"] = item["notaria_id"]
            item["nombre_entidad"] = item["nombre_fantasia"]
            csv = item.pop("medios_pago_csv", "") or ""
            item["medios_pago"] = [value for value in csv.split(",") if value]
            capabilities_csv = item.pop("capacidades_csv", "") or ""
            item["capacidades"] = [value for value in capabilities_csv.split(",") if value]
            items.append(item)
        if items:
            return {"items": items, "availability": "AVAILABLE", "message": None}

        diagnostic = db.execute(text("""
          SELECT
            COUNT(d.id) ofertas_totales,
            COALESCE(SUM(d.activo=TRUE),0) ofertas_activas,
            COALESCE(SUM(d.activo=TRUE AND en.codigo='ACTIVA'),0) ofertas_publicables
          FROM materias m
          LEFT JOIN directorio d ON d.materia_id=m.id
          LEFT JOIN notarias n ON n.id=d.notaria_id
          LEFT JOIN estados_notaria en ON en.id=n.estado_notaria_id
          WHERE m.slug=:slug
        """), {"slug": materia_slug}).mappings().first()
        if not diagnostic or diagnostic["ofertas_totales"] == 0:
            code, message = "NO_OFFERS", "Este servicio todavía no tiene una oferta configurada."
        elif diagnostic["ofertas_activas"] == 0:
            code, message = "INACTIVE_OFFERS", "La oferta existe, pero está desactivada en su configuración."
        else:
            code, message = "INACTIVE_ENTITIES", "La oferta existe, pero la entidad asociada no está activa o no cubre el territorio solicitado."
        return {"items": [], "availability": code, "message": message}


def list_entities(service_slug: str, lat: float | None = None, lng: float | None = None,
                  entity_uuid: str | None = None, capability: str | None = None,
                  commune_id: int | None = None, jurisdiction_id: int | None = None,
                  court_id: int | None = None):
    """Contrato neutral H2 con elegibilidad por servicio y territorio."""
    return list_notarias(service_slug, lat, lng, entity_uuid, capability,
                         commune_id, jurisdiction_id, court_id)


def embedded_notary_catalog(notaria_uuid: str):
    if not re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", notaria_uuid):
        return None
    with db_session() as db:
        notary = db.execute(text("""
          SELECT n.nombre_fantasia,n.nombre_notario,n.direccion,c.nombre comuna
          FROM notarias n
          JOIN comunas c ON c.id=n.comuna_id
          JOIN estados_notaria en ON en.id=n.estado_notaria_id
          WHERE LOWER(HEX(n.uuid))=REPLACE(LOWER(:uuid),'-','')
            AND en.codigo='ACTIVA' AND n.eliminado_en IS NULL
        """), {"uuid":notaria_uuid}).mappings().first()
        if not notary:
            return None
        offers = db.execute(text("""
          SELECT m.nombre,m.slug,m.descripcion_corta,c.nombre categoria,
                 d.precio,d.moneda,d.plazo_estimado_horas
          FROM directorio d
          JOIN notarias n ON n.id=d.notaria_id
          JOIN materias m ON m.id=d.materia_id
          JOIN categorias_materia c ON c.id=m.categoria_materia_id
          JOIN estados_materia em ON em.id=m.estado_materia_id
          WHERE LOWER(HEX(n.uuid))=REPLACE(LOWER(:uuid),'-','')
            AND d.activo=TRUE AND em.codigo='ACTIVA'
            AND EXISTS (
              SELECT 1 FROM notaria_credenciales nc
              JOIN proveedores_integracion pi ON pi.id=nc.proveedor_integracion_id
              WHERE nc.notaria_id=n.id AND nc.activa=TRUE
                AND pi.codigo IN ('WEBPAY','KLAP','GETNET')
                AND (nc.valida_hasta IS NULL OR nc.valida_hasta>UTC_TIMESTAMP(6))
            )
          ORDER BY c.orden,m.nombre
        """), {"uuid":notaria_uuid}).mappings().all()
        result = dict(notary)
        capabilities = db.execute(text("""
          SELECT te.codigo FROM entidad_capacidades ec
          JOIN tipos_entidad te ON te.id=ec.tipo_entidad_id
          JOIN notarias n ON n.id=ec.entidad_id
          WHERE LOWER(HEX(n.uuid))=REPLACE(LOWER(:uuid),'-','')
            AND ec.activo=TRUE AND te.activo=TRUE
          ORDER BY te.orden
        """), {"uuid":notaria_uuid}).scalars().all()
        result["capacidades"] = list(capabilities)
        return {"entidad":result,"notaria":result,"items":[dict(row) for row in offers]}


def embedded_entity_catalog(entity_uuid: str):
    return embedded_notary_catalog(entity_uuid)


def public_sale_status(folio: str, rut: str):
    settings=get_settings()
    with db_session() as db:
        row=db.execute(text("""
          SELECT v.numero_publico folio,ev.codigo estado,es.codigo estado_solicitud,
                 v.total,v.moneda,v.creada_en,v.pagada_en,v.realizada_en,v.vence_en,
                 m.nombre documento,m.nombre servicio,n.nombre_fantasia notaria,
                 n.nombre_fantasia entidad,n.direccion,co.nombre comuna,te.codigo capacidad,
                 p.id pago_id,ep.codigo estado_pago,p.codigo_autorizacion,p.tipo_pago,p.cuotas,p.confirmado_en,
                 og.numero_ot
          FROM ventas v JOIN clientes c ON c.id=v.cliente_id
          JOIN estados_venta ev ON ev.id=v.estado_venta_id JOIN materias m ON m.id=v.materia_id
          JOIN notarias n ON n.id=v.notaria_id JOIN comunas co ON co.id=n.comuna_id
          LEFT JOIN solicitudes s ON s.venta_id=v.id
          LEFT JOIN estados_solicitud es ON es.id=s.estado_solicitud_id
          LEFT JOIN tipos_entidad te ON te.id=s.capacidad_destino_id
          LEFT JOIN pagos p ON p.id=(SELECT MAX(p2.id) FROM pagos p2 WHERE p2.venta_id=v.id)
          LEFT JOIN estados_pago ep ON ep.id=p.estado_pago_id LEFT JOIN ordenes_gesnot og ON og.venta_id=v.id
          WHERE v.numero_publico=:folio AND c.rut_hmac=:rut
        """),{"folio":folio,"rut":keyed_hash(normalize_rut(rut),settings.pii_hmac_key)}).mappings().first()
        return dict(row) if row else None


def create_sale(payload, authenticated_user_id: int | None = None):
    settings = get_settings()
    token = new_access_token()
    folio = f"DOC-{datetime.now(SANTIAGO):%Y%m%d}-{secrets.token_hex(3).upper()}"
    customer = payload.cliente
    offer_id = getattr(payload, "oferta_id", None) or getattr(payload, "directorio_id", None)
    with db_session() as db:
        offer = db.execute(text("""
          SELECT d.id,d.notaria_id,d.materia_id,d.precio,n.dias_vencimiento,
                 te.id capacidad_id,te.codigo capacidad
          FROM directorio d JOIN notarias n ON n.id=d.notaria_id
          JOIN servicio_capacidades sc ON sc.materia_id=d.materia_id AND sc.activo=TRUE
          JOIN entidad_capacidades ec ON ec.entidad_id=d.notaria_id
            AND ec.tipo_entidad_id=sc.tipo_entidad_id AND ec.activo=TRUE
          JOIN tipos_entidad te ON te.id=sc.tipo_entidad_id AND te.activo=TRUE
          WHERE d.id=:id AND d.activo=TRUE
          ORDER BY ec.es_principal DESC,sc.es_principal DESC,te.orden
          LIMIT 1 FOR UPDATE
        """), {"id": offer_id}).mappings().first()
        if not offer:
            raise ValueError("Oferta no disponible")
        cliente_id = db.execute(text("""
          INSERT INTO clientes
           (uuid,rut_hmac,rut_cifrado,nombres_cifrado,apellidos_cifrado,email_hmac,
            email_cifrado,telefono_cifrado,clave_cifrado_id)
          VALUES (UNHEX(REPLACE(UUID(),'-','')),:rut_h,:rut,:nombres,:apellidos,:email_h,
                  :email,:telefono,'APP_KEY_V1')
        """), {
          "rut_h": keyed_hash(normalize_rut(customer.rut), settings.pii_hmac_key),
          "rut": encrypt_text(normalize_rut(customer.rut)), "nombres": encrypt_text(customer.nombres),
          "apellidos": encrypt_text(customer.apellidos),
          "email_h": keyed_hash(normalize_email(str(customer.email)), settings.pii_hmac_key),
          "email": encrypt_text(normalize_email(str(customer.email))),
          "telefono": encrypt_text(customer.telefono) if customer.telefono else None,
        }).lastrowid
        db.execute(text("SET @venta_id=0"))
        db.execute(text("CALL sp_generar_venta(:cliente,:directorio,:folio,:token,:precio,0,'public',@venta_id)"), {
            "cliente": cliente_id, "directorio": offer["id"], "folio": folio,
            "token": keyed_hash(token, settings.access_token_hmac_key), "precio": offer["precio"],
        })
        sale_id = db.execute(text("SELECT @venta_id")).scalar_one()
        if authenticated_user_id:
            db.execute(text("UPDATE ventas SET usuario_id=:user WHERE id=:sale"),
                       {"user": authenticated_user_id, "sale": sale_id})
        field_entity = effective_entity_id(db, offer["materia_id"], offer["notaria_id"], "fields")
        configured = db.execute(text("""
          SELECT cm.id,cm.codigo,cm.etiqueta,cm.requerido,cm.largo_minimo,cm.largo_maximo,cm.valor_minimo,
                 cm.valor_maximo,cm.patron_regex,cm.opciones_json,td.codigo tipo_dato
          FROM campos_materias cm JOIN tipo_de_dato td ON td.id=cm.tipo_de_dato_id
          WHERE cm.materia_id=:materia AND cm.notaria_id <=> :entity AND cm.activo=TRUE
        """), {"materia":offer["materia_id"], "entity":field_entity}).mappings().all()
        configured_by_id={row["id"]:dict(row) for row in configured}
        answers_by_id={answer.campo_id:answer.valor.strip() for answer in payload.respuestas}
        if len(answers_by_id)!=len(payload.respuestas) or not set(answers_by_id).issubset(configured_by_id): raise ValueError("Uno o más campos no pertenecen al trámite seleccionado")
        system_codes={"cliente_nombre_completo","cliente_email","cliente_rut","cliente_telefono"}
        missing=[field["etiqueta"] for field in configured
                 if field["requerido"] and field["codigo"] not in system_codes and field["tipo_dato"]!="ARCHIVO"
                 and not answers_by_id.get(field["id"])]
        if missing: raise ValueError("Faltan campos obligatorios: "+", ".join(missing))
        normalized_answers={}
        for field_id,value in answers_by_id.items():
            if configured_by_id[field_id]["tipo_dato"] == "ARCHIVO":
                raise ValueError("Los archivos deben cargarse como adjuntos")
            validate_field_value(configured_by_id[field_id],value)
            normalized_answers[field_id] = format_rut(value) if configured_by_id[field_id]["tipo_dato"] in {"RUT","RUT_CHILENO","RUT CHILENO"} else value
            if configured_by_id[field_id]["tipo_dato"] == "COMUNA":
                if not value.isdigit() or not db.execute(text("""
                  SELECT 1 FROM comunas
                  WHERE codigo_gesnot=:codigo AND activo=TRUE
                """), {"codigo": int(value)}).scalar():
                    raise ValueError(f"{configured_by_id[field_id]['etiqueta']}: comuna inválida")
        if payload.respuestas:
            for answer in payload.respuestas:
                db.execute(text("""
                  INSERT INTO respuestas_campos
                    (venta_id,campo_materia_id,valor_cifrado,clave_cifrado_id)
                  VALUES (:venta,:campo,:valor,'APP_KEY_V1')
                """), {"venta": sale_id, "campo": answer.campo_id,
                         "valor": encrypt_text(normalized_answers[answer.campo_id])})
        request_id = db.execute(text("""
          SELECT id FROM solicitudes WHERE venta_id=:id
        """), {"id": sale_id}).scalar_one()
        row = db.execute(text("SELECT total,vence_en FROM ventas WHERE id=:id"), {"id": sale_id}).mappings().one()
        return {"folio": folio, "access_token": token, "estado": "BORRADOR",
                "solicitud_id": request_id, "entidad_destino_id": offer["notaria_id"],
                "capacidad": offer["capacidad"], **dict(row)}


def get_sale(folio: str, access_token: str):
    token_hash = keyed_hash(access_token, get_settings().access_token_hmac_key)
    with db_session() as db:
        row = db.execute(text("""
          SELECT v.id,v.numero_publico folio,ev.codigo estado,es.codigo estado_solicitud,
                 s.id solicitud_id,te.codigo capacidad,v.total,v.moneda,v.vence_en,
                 v.pagada_en,v.realizada_en,m.nombre materia,m.nombre servicio,
                 n.nombre_fantasia notaria,n.nombre_fantasia entidad,
                 p.id pago_id,ep.codigo estado_pago,p.codigo_autorizacion,p.confirmado_en,
                 og.numero_ot
          FROM ventas v JOIN estados_venta ev ON ev.id=v.estado_venta_id
          JOIN materias m ON m.id=v.materia_id JOIN notarias n ON n.id=v.notaria_id
          LEFT JOIN solicitudes s ON s.venta_id=v.id
          LEFT JOIN estados_solicitud es ON es.id=s.estado_solicitud_id
          LEFT JOIN tipos_entidad te ON te.id=s.capacidad_destino_id
          LEFT JOIN pagos p ON p.id=(SELECT MAX(p2.id) FROM pagos p2 WHERE p2.venta_id=v.id)
          LEFT JOIN estados_pago ep ON ep.id=p.estado_pago_id
          LEFT JOIN ordenes_gesnot og ON og.venta_id=v.id
          WHERE v.numero_publico=:folio AND v.acceso_token_hmac=:token
        """), {"folio": folio, "token": token_hash}).mappings().first()
        return dict(row) if row else None
