"""Carga y lectura del CSD (Certificado de Sello Digital) de la empresa.

El ``.key`` es material con el que se puede firmar a nombre de la empresa ante el SAT: no se
devuelve nunca por la API, ni entero ni en partes. Sólo entra (al subirlo) y se usa del lado
del servidor (al timbrar).
"""

from datetime import datetime, timezone

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
from fastapi import HTTPException, status
from sqlalchemy.orm import Session

from core.crypto import SecretsNotConfigured, encrypt_secret
from core.s3_keys import tenant_company_prefix

from ...fin.stamping import sealer
from .models import IssuerSettings
from .service import get_issuer_settings

# Tamaño máximo razonable: un .cer del SAT ronda los 2 KB y un .key los 2 KB. El tope evita
# que alguien suba un archivo enorme por error o a propósito.
_MAX_BYTES = 64 * 1024


def _csd_keys(tenant_id: int, company_id: int) -> tuple[str, str]:
    """Claves de almacenamiento del par. Estables: subir de nuevo reemplaza el anterior."""
    prefijo = tenant_company_prefix(tenant_id, company_id) + "certificates/"
    return prefijo + "cfdi.cer", prefijo + "cfdi.key"


def _verify_pair(cer_bytes: bytes, key_bytes: bytes, password: str) -> str:
    """Comprueba que la llave privada corresponde al certificado. Devuelve el NoCertificado.

    Se hace firmando un dato de prueba con la llave y verificándolo con la pública del
    certificado. Es la única forma de saberlo antes de timbrar: si no cuadran, el error
    aparecería hasta que el PAC rechace el comprobante, con un mensaje que no menciona el CSD.
    """
    try:
        cert_number, _ = sealer.read_certificate(cer_bytes)
        private_key = sealer.load_private_key(key_bytes, password)
    except sealer.SealingError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
        ) from exc

    try:
        cert = load_der_x509_certificate(cer_bytes)
    except ValueError:
        cert = load_pem_x509_certificate(cer_bytes)

    reto = b"verificacion-de-par-csd"
    firma = private_key.sign(reto, padding.PKCS1v15(), hashes.SHA256())
    try:
        cert.public_key().verify(firma, reto, padding.PKCS1v15(), hashes.SHA256())
    except Exception as exc:  # noqa: BLE001 — cualquier fallo aquí es "no corresponden"
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=(
                "La llave privada (.key) no corresponde al certificado (.cer). "
                "Verifica que ambos archivos sean del mismo CSD."
            ),
        ) from exc

    return cert_number


def upload_csd(
    db: Session,
    tenant_id: int,
    company_id: int,
    cer_bytes: bytes,
    key_bytes: bytes,
    password: str,
    user_id: str | None = None,
) -> IssuerSettings:
    """Valida el par, lo guarda en almacenamiento y cifra la contraseña."""
    # Exige que ya existan los datos fiscales: el CSD pertenece a un emisor, no al aire.
    obj = get_issuer_settings(db, tenant_id, company_id)

    if not password:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="La contraseña de la llave privada es obligatoria.",
        )
    for etiqueta, datos in (("certificado (.cer)", cer_bytes), ("llave privada (.key)", key_bytes)):
        if not datos:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Falta el archivo del {etiqueta}.",
            )
        if len(datos) > _MAX_BYTES:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"El archivo del {etiqueta} es demasiado grande para ser un CSD.",
            )

    cert_number = _verify_pair(cer_bytes, key_bytes, password)

    # La contraseña se cifra ANTES de subir los archivos: si no hay clave maestra, no se deja
    # material criptográfico en el almacenamiento a medio configurar.
    try:
        password_enc = encrypt_secret(password)
    except SecretsNotConfigured as exc:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc)
        ) from exc

    from core.storage_s3 import (
        put_object_bytes,
    )  # noqa: PLC0415  (import diferido: tests sin MinIO)

    cer_key, key_key = _csd_keys(tenant_id, company_id)
    put_object_bytes(cer_key, cer_bytes, content_type="application/x-x509-ca-cert")
    put_object_bytes(key_key, key_bytes, content_type="application/octet-stream")

    obj.csd_cer_file_key = cer_key
    obj.csd_key_file_key = key_key
    obj.csd_password_enc = password_enc
    obj.csd_cert_number = cert_number
    obj.csd_uploaded_at = datetime.now(timezone.utc)
    obj.updated_by = user_id
    db.commit()
    db.refresh(obj)
    return obj


def delete_csd(
    db: Session, tenant_id: int, company_id: int, user_id: str | None = None
) -> IssuerSettings:
    """Desvincula el CSD de la empresa.

    Se borran también los objetos del almacenamiento: dejar una llave privada huérfana es
    justo lo que no se quiere. Si el borrado remoto falla, la referencia se limpia igual —
    sin ella el sistema ya no puede firmar.
    """
    obj = get_issuer_settings(db, tenant_id, company_id)
    claves = [k for k in (obj.csd_cer_file_key, obj.csd_key_file_key) if k]
    if claves:
        try:
            from core.storage_s3 import delete_object_if_exists  # noqa: PLC0415

            for k in claves:
                delete_object_if_exists(k)
        except Exception:  # noqa: BLE001
            # No se propaga: la referencia se limpia igual y el CSD queda inutilizable.
            pass

    obj.csd_cer_file_key = None
    obj.csd_key_file_key = None
    obj.csd_password_enc = None
    obj.csd_cert_number = None
    obj.csd_uploaded_at = None
    obj.updated_by = user_id
    db.commit()
    db.refresh(obj)
    return obj
