"""Cálculo de cuotas (amortización a cuota fija), resumen de deuda y generación del acuerdo de préstamo en PDF."""
import calendar
import hashlib
import io
import json
import os
from datetime import datetime

from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet


# Catálogo de cláusulas modulares de bajo riesgo legal que el prestamista puede activar.
# Cada una se agrega al PDF solo si viene activa=true en prestamo["clausulas"][id].
# Los gastos de cobranza tienen un tope de referencia (no una ley con cifra fija, sino buena
# práctica frente a la Ley 2300 de 2023 de protección al deudor en cobranza).
TOPE_GASTOS_COBRANZA_PCT = 20

CLAUSULAS_CATALOGO = {
    "conciliacion": {
        "nombre": "Conciliación previa",
        "texto": lambda c: (
            "Las partes acuerdan que, en caso de controversia derivada del presente contrato, intentarán "
            "resolverla mediante conciliación ante un centro de conciliación autorizado, antes de acudir "
            "a la vía judicial."
        ),
    },
    "gastos_cobranza": {
        "nombre": "Gastos de cobranza a cargo del deudor",
        "texto": lambda c: (
            f"En caso de incumplimiento, los gastos razonables en que incurra el prestamista para el cobro "
            f"del préstamo (incluyendo gestión extrajudicial) serán asumidos por el deudor, hasta un "
            f"máximo del {c.get('tope_pct', 10)}% del saldo insoluto, en los términos permitidos por la "
            f"Ley 2300 de 2023."
        ),
    },
    "garantia_real": {
        "nombre": "Garantía sobre un bien",
        "texto": lambda c: (
            f"El deudor ofrece como garantía del cumplimiento de esta obligación el siguiente bien: "
            f"{c.get('texto', '')}. Esta cláusula deja constancia del acuerdo entre las partes, pero "
            f"<b>no reemplaza el registro formal de la garantía</b> (ej. prenda con registro, hipoteca), "
            f"el cual, según el tipo de bien, puede requerir trámite ante notaría o la entidad de registro "
            f"correspondiente para ser oponible a terceros. Se recomienda asesoría legal para formalizar "
            f"la garantía si el valor del bien es significativo."
        ),
    },
}


def sanitizar_clausulas(raw):
    """Filtra lo que venga del cliente para quedarse solo con claves conocidas del catálogo
    y coaccionar los campos configurables a tipos seguros."""
    raw = raw or {}
    limpio = {}
    for clave in CLAUSULAS_CATALOGO:
        config = raw.get(clave)
        if not isinstance(config, dict) or not config.get("activa"):
            continue
        entrada = {"activa": True}
        if clave == "gastos_cobranza":
            entrada["tope_pct"] = max(0, min(TOPE_GASTOS_COBRANZA_PCT, float(config.get("tope_pct") or 10)))
        elif clave == "garantia_real":
            entrada["texto"] = str(config.get("texto") or "").strip()[:300]
        limpio[clave] = entrada
    return limpio


def clausulas_para_pdf(clausulas):
    """Devuelve la lista de textos de las cláusulas activas, en un orden estable."""
    textos = []
    for clave, definicion in CLAUSULAS_CATALOGO.items():
        config = (clausulas or {}).get(clave)
        if config and config.get("activa"):
            textos.append(definicion["texto"](config))
    return textos


# Tasa de usura certificada por la Superfinanciera (1.5x el interés bancario corriente),
# vigente para julio 2026: 28.79% efectivo anual. Cobrar por encima de este tope es el
# delito de usura (Código Penal Art. 305). ACTUALIZAR cada mes según la resolución vigente
# en https://www.superfinanciera.gov.co/publicaciones/10115999/
TASA_USURA_EA = 28.79


def tasa_mensual_a_efectiva_anual(tasa_mensual_pct):
    return (((1 + tasa_mensual_pct / 100) ** 12) - 1) * 100


def efectiva_anual_a_mensual(tasa_efectiva_anual_pct):
    return ((1 + tasa_efectiva_anual_pct / 100) ** (1 / 12) - 1) * 100


def tasa_referencia_mercado():
    """Nota puramente informativa (no tiene efecto legal ni se usa para validar nada): a partir
    de la tasa de usura certificada por la Superfinanciera (que por ley es 1.5x la tasa de
    interés bancario corriente), se despeja la tasa de interés bancario corriente misma, que es
    la tasa promedio que cobran los bancos en créditos de consumo. Se expresa también en
    mensual efectiva para comparar directo con lo que el prestamista está por pactar."""
    tasa_ea = round(TASA_USURA_EA / 1.5, 2)
    return {"tasa_ea": tasa_ea, "tasa_mensual": round(efectiva_anual_a_mensual(tasa_ea), 2)}


def _parse_fecha(fecha, nombre_campo):
    """Convierte 'YYYY-MM-DD' a datetime, rechazando valores mal formados (p.ej. escritos a mano
    en el input de fecha con un año de más o menos dígitos) con un mensaje claro en vez de un 500."""
    if not isinstance(fecha, str) or len(fecha) != 10:
        raise ValueError(f"{nombre_campo} inválida: '{fecha}'")
    try:
        return datetime.strptime(fecha, "%Y-%m-%d")
    except ValueError:
        raise ValueError(f"{nombre_campo} inválida: '{fecha}'")


def _sumar_periodo(fecha_dt, cantidad, tipo_periodo):
    """Suma 'cantidad' periodos (meses o años) a una fecha, ajustando el día si el mes
    resultante tiene menos días (ej. 31 de enero + 1 mes = 28/29 de febrero)."""
    meses_a_sumar = int(cantidad) * (12 if tipo_periodo == "anio" else 1)
    mes_index = fecha_dt.month - 1 + meses_a_sumar
    anio = fecha_dt.year + mes_index // 12
    mes = mes_index % 12 + 1
    dia = min(fecha_dt.day, calendar.monthrange(anio, mes)[1])
    return fecha_dt.replace(year=anio, month=mes, day=dia)


def calcular_fecha_vencimiento(fecha_inicio, numero_cuotas, tipo_periodo):
    """La fecha de vencimiento no la elige el usuario: se calcula sumando a la fecha de
    desembolso tantos períodos (meses o años) como cuotas se hayan definido."""
    inicio = _parse_fecha(fecha_inicio, "fecha_inicio")
    fin = _sumar_periodo(inicio, max(1, int(numero_cuotas)), tipo_periodo)
    return fin.strftime("%Y-%m-%d")


def validar_usura(tasa_interes, tipo_tasa, tasa_interes_mora):
    """Verifica si la tasa pactada (ordinaria y de mora) supera el tope legal de usura,
    expresando la tasa ordinaria como efectiva anual según como fue pactada: si ya es
    efectiva anual se usa tal cual, si es mes vencido se convierte por composición mensual
    (la misma fórmula que ya se usaba para la mora), para poder compararla contra TASA_USURA_EA."""
    if tipo_tasa == "efectiva_anual":
        ea_interes = tasa_interes
    else:
        ea_interes = tasa_mensual_a_efectiva_anual(tasa_interes)
    ea_mora = tasa_mensual_a_efectiva_anual(tasa_interes_mora)

    return {
        "tope_legal_ea": TASA_USURA_EA,
        "tasa_interes_ea": round(ea_interes, 2),
        "tasa_mora_ea": round(ea_mora, 2),
        "excede_interes": ea_interes > TASA_USURA_EA,
        "excede_mora": ea_mora > TASA_USURA_EA,
    }


def hash_acuerdo(prestamo):
    """Huella de los términos centrales del préstamo. Sirve como evidencia de que el
    acuerdo firmado electrónicamente corresponde exactamente a estos términos."""
    base = "|".join(
        str(v)
        for v in (
            prestamo["id"],
            prestamo.get("ciudad") or "",
            prestamo["prestatario_nombre"],
            prestamo.get("prestatario_tipo_documento") or "",
            prestamo.get("prestatario_numero_documento") or "",
            prestamo.get("prestatario_direccion") or "",
            prestamo["monto"],
            prestamo.get("moneda") or "COP",
            prestamo["tasa_interes"],
            prestamo.get("tipo_tasa") or "mes_vencido",
            prestamo["tasa_interes_mora"],
            prestamo["fecha_inicio"],
            prestamo["fecha_vencimiento"],
            prestamo["numero_cuotas"],
            prestamo.get("tipo_periodo") or "mes",
            json.dumps(prestamo.get("clausulas") or {}, sort_keys=True),
            prestamo.get("codeudor_nombre") or "",
            prestamo.get("codeudor_tipo_documento") or "",
            prestamo.get("codeudor_numero_documento") or "",
            prestamo.get("codeudor_direccion") or "",
        )
    )
    return hashlib.sha256(base.encode("utf-8")).hexdigest()


def calcular_cuotas(monto, tasa_interes, tipo_tasa, fecha_inicio, numero_cuotas, tipo_periodo):
    """Sistema de cuota fija por amortización (el mismo método que usan los bancos): todas las
    cuotas valen lo mismo, pero el interés de cada una se calcula sobre el saldo de capital que
    queda pendiente, así que con cada pago se abona más capital y menos interés que en la cuota
    anterior. La fórmula es:
        cuota = P × [ i × (1+i)^n ] / [ (1+i)^n − 1 ]
    donde P es el monto, n el número de cuotas e i la tasa del período (mensual si tipo_periodo
    es 'mes', anual si es 'anio'), llevada a ese período según cómo se pactó:
    - mes_vencido: la tasa ya es la tasa del período tal cual se ingresó (mensual o anual).
    - efectiva_anual: se convierte por composición desde efectiva anual al período de pago."""
    numero_cuotas = max(1, int(numero_cuotas))
    inicio = _parse_fecha(fecha_inicio, "fecha_inicio")

    if tipo_periodo == "anio":
        if tipo_tasa == "efectiva_anual":
            i = tasa_interes / 100
        else:
            i = (1 + tasa_interes / 100) ** 12 - 1
    else:
        if tipo_tasa == "efectiva_anual":
            i = efectiva_anual_a_mensual(tasa_interes) / 100
        else:
            i = tasa_interes / 100

    if i > 0:
        factor = (1 + i) ** numero_cuotas
        valor_cuota = round(monto * i * factor / (factor - 1), 2)
    else:
        valor_cuota = round(monto / numero_cuotas, 2)

    cuotas = []
    saldo = monto
    for numero in range(1, numero_cuotas + 1):
        fecha_cuota = _sumar_periodo(inicio, numero, tipo_periodo)
        interes_periodo = round(saldo * i, 2)
        if numero == numero_cuotas:
            monto_esta_cuota = round(saldo + interes_periodo, 2)
        else:
            monto_esta_cuota = valor_cuota
        saldo = round(saldo - (monto_esta_cuota - interes_periodo), 2)
        cuotas.append(
            {
                "numero": numero,
                "fecha_vencimiento": fecha_cuota.strftime("%Y-%m-%d"),
                "monto": monto_esta_cuota,
            }
        )
    return cuotas


def calcular_resumen_deuda(prestamo, fecha_consulta=None):
    """Explica en términos simples cuánto se debe hoy: por cada cuota pendiente,
    si está al día no hay nada adicional; si está vencida, se prorratea el interés
    de mora mensual según los días de atraso."""
    hoy = fecha_consulta or datetime.now().strftime("%Y-%m-%d")
    hoy_dt = datetime.strptime(hoy, "%Y-%m-%d")
    tasa_mora = prestamo.get("tasa_interes_mora", 0)

    cuotas_pendientes = [c for c in prestamo["cuotas"] if not c["pagada"]]
    detalle = []
    total_a_pagar_todo = 0

    for c in cuotas_pendientes:
        vencimiento_dt = datetime.strptime(c["fecha_vencimiento"], "%Y-%m-%d")
        dias_atraso = max(0, (hoy_dt - vencimiento_dt).days)
        interes_mora = round(c["monto"] * (tasa_mora / 100) * (dias_atraso / 30), 2) if dias_atraso else 0
        total_cuota_hoy = round(c["monto"] + interes_mora, 2)
        total_a_pagar_todo += total_cuota_hoy
        detalle.append(
            {
                "cuota_id": c["id"],
                "numero": c["numero"],
                "fecha_vencimiento": c["fecha_vencimiento"],
                "monto_base": c["monto"],
                "dias_atraso": dias_atraso,
                "interes_mora": interes_mora,
                "total_a_pagar": total_cuota_hoy,
            }
        )

    proxima = detalle[0] if detalle else None

    return {
        "fecha_consulta": hoy,
        "tasa_interes_mora": tasa_mora,
        "saldo_total_pendiente": round(total_a_pagar_todo, 2),
        "monto_inicial": prestamo["monto"],
        "saldo_total": calcular_saldo_total(prestamo, hoy_dt),
        "proxima_cuota": proxima,
        "cuotas_pendientes": detalle,
        "al_dia": all(d["dias_atraso"] == 0 for d in detalle),
    }


def calcular_saldo_total(prestamo, hoy_dt):
    """Saldo si se quisiera saldar el préstamo HOY: capital inicial más el interés ya
    causado según el tiempo que realmente ha transcurrido desde el desembolso (no el
    interés del plazo completo pactado), menos lo que ya se ha pagado, más la mora de
    las cuotas que estén vencidas."""
    monto_inicial = prestamo["monto"]
    monto_total_contratado = sum(c["monto"] for c in prestamo["cuotas"])
    inicio_dt = datetime.strptime(prestamo["fecha_inicio"], "%Y-%m-%d")
    fin_dt = datetime.strptime(prestamo["fecha_vencimiento"], "%Y-%m-%d")
    dias_totales = max((fin_dt - inicio_dt).days, 1)
    dias_transcurridos = min(max((hoy_dt - inicio_dt).days, 0), dias_totales)

    factor_total = (monto_total_contratado / monto_inicial) - 1 if monto_inicial else 0
    factor_transcurrido = factor_total * (dias_transcurridos / dias_totales)

    total_pagado = sum(c["monto"] for c in prestamo["cuotas"] if c["pagada"])
    tasa_mora = prestamo.get("tasa_interes_mora", 0)
    mora_vencida = 0
    for c in prestamo["cuotas"]:
        if c["pagada"]:
            continue
        vencimiento_dt = datetime.strptime(c["fecha_vencimiento"], "%Y-%m-%d")
        dias_atraso = max(0, (hoy_dt - vencimiento_dt).days)
        if dias_atraso:
            mora_vencida += c["monto"] * (tasa_mora / 100) * (dias_atraso / 30)

    saldo = monto_inicial * (1 + factor_transcurrido) - total_pagado + mora_vencida
    return round(max(saldo, 0), 2)


PERIODOS_DASHBOARD = ("mes", "semestre", "anio", "anio_calendario", "todo")


def fecha_desde_periodo(periodo, hoy_dt=None):
    """Convierte el filtro de período elegido en el dashboard en una fecha ISO de corte
    ('desde cuándo' se cuentan los préstamos), o None si el período es 'todo' el histórico.
    'anio_calendario' es desde el 1 de enero del año en curso, los demás son ventanas móviles."""
    hoy_dt = hoy_dt or datetime.now()
    if periodo == "mes":
        return _sumar_periodo(hoy_dt, -1, "mes").isoformat()
    if periodo == "semestre":
        return _sumar_periodo(hoy_dt, -6, "mes").isoformat()
    if periodo == "anio":
        return _sumar_periodo(hoy_dt, -1, "anio").isoformat()
    if periodo == "anio_calendario":
        return hoy_dt.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0).isoformat()
    return None


def calcular_estadisticas_admin(prestamos, periodo="todo", hoy_dt=None):
    """Estadísticas del negocio para el dashboard de administrador. Los montos se agrupan
    por moneda porque no tiene sentido sumar COP con USD. 'Interés pactado' es el interés
    total que se ganó/ganará según lo contratado (capital total de cuotas menos el monto
    inicial); 'saldo causado' es cuánto de eso ya se ha generado según el tiempo transcurrido
    (ver calcular_saldo_total), aplicable solo a la cartera activa (préstamos vigentes hoy)."""
    hoy_dt = hoy_dt or datetime.now()
    desde = fecha_desde_periodo(periodo, hoy_dt)

    def moneda_de(p):
        return p.get("moneda") or "COP"

    def interes_pactado(p):
        return sum(c["monto"] for c in p["cuotas"]) - p["monto"]

    def plazo_dias(p):
        inicio = datetime.strptime(p["fecha_inicio"], "%Y-%m-%d")
        fin = datetime.strptime(p["fecha_vencimiento"], "%Y-%m-%d")
        return (fin - inicio).days

    def tasa_ea(p):
        if p.get("tipo_tasa") == "efectiva_anual":
            return p["tasa_interes"]
        return tasa_mensual_a_efectiva_anual(p["tasa_interes"])

    def nueva_moneda():
        return {"numero_prestamos": 0, "capital": 0.0, "interes_pactado": 0.0, "saldo_causado": 0.0}

    activos = [p for p in prestamos if p["estado"] in ("activo", "mora")]
    en_periodo = [p for p in prestamos if not desde or p["creado_en"] >= desde] if periodo != "todo" else prestamos

    cartera_activa = {}
    for p in activos:
        m = moneda_de(p)
        acc = cartera_activa.setdefault(m, nueva_moneda())
        acc["numero_prestamos"] += 1
        acc["capital"] += p["monto"]
        acc["interes_pactado"] += interes_pactado(p)
        acc["saldo_causado"] += calcular_saldo_total(p, hoy_dt)

    historico_total = {}
    for p in prestamos:
        m = moneda_de(p)
        acc = historico_total.setdefault(m, nueva_moneda())
        acc["numero_prestamos"] += 1
        acc["capital"] += p["monto"]
        acc["interes_pactado"] += interes_pactado(p)

    del_periodo = {}
    for p in en_periodo:
        m = moneda_de(p)
        acc = del_periodo.setdefault(m, nueva_moneda())
        acc["numero_prestamos"] += 1
        acc["capital"] += p["monto"]
        acc["interes_pactado"] += interes_pactado(p)

    n_periodo = len(en_periodo)

    # El plazo y la tasa de interés son comparables sin importar la moneda (son días y
    # porcentajes, no montos), pero el monto promedio del préstamo sí depende de la
    # moneda, así que se agrupa igual que el resto de montos.
    promedio_monto_por_moneda = {}
    for p in en_periodo:
        m = moneda_de(p)
        acc = promedio_monto_por_moneda.setdefault(m, {"suma": 0.0, "n": 0})
        acc["suma"] += p["monto"]
        acc["n"] += 1

    promedios = {
        "monto_prestamo_por_moneda": {
            m: round(d["suma"] / d["n"], 2) for m, d in promedio_monto_por_moneda.items()
        },
        "plazo_dias": round((sum(plazo_dias(p) for p in en_periodo) / n_periodo), 2) if n_periodo else 0,
        "tasa_interes_ea": round((sum(tasa_ea(p) for p in en_periodo) / n_periodo), 2) if n_periodo else 0,
    }

    def redondear(por_moneda):
        return {
            m: {k: (round(v, 2) if k != "numero_prestamos" else v) for k, v in datos.items()}
            for m, datos in por_moneda.items()
        }

    return {
        "periodo": periodo,
        "desde": desde,
        "cartera_activa": redondear(cartera_activa),
        "historico_total": redondear(historico_total),
        "del_periodo": redondear(del_periodo),
        "promedios_periodo": promedios,
        "numero_prestamos_periodo": n_periodo,
    }


def calcular_credit_score(prestamos, hoy_dt=None):
    """Score de crédito PROPIO e inventado por PrestaRed (no es un score de una central
    de riesgo real ni tiene validez regulatoria), pensado como base para un futuro producto
    de scoring. Parte de una línea base de 600 puntos (escala 0-1000) y suma o resta según
    el comportamiento de pago de la persona como deudor, en TODOS sus préstamos
    registrados en la plataforma, sin importar cuál prestamista se los otorgó."""
    hoy_dt = hoy_dt or datetime.now()
    hoy = hoy_dt.strftime("%Y-%m-%d")

    cuotas_a_tiempo = 0
    cuotas_tarde = 0
    cuotas_vencidas_sin_pagar = 0
    prestamos_pagados = 0
    prestamos_en_mora = 0

    for p in prestamos:
        if p["estado"] == "pagado":
            prestamos_pagados += 1
        if p["estado"] == "mora":
            prestamos_en_mora += 1
        for c in p["cuotas"]:
            if c["pagada"]:
                fecha_pago = (c.get("fecha_pago") or "")[:10]
                if fecha_pago and fecha_pago <= c["fecha_vencimiento"]:
                    cuotas_a_tiempo += 1
                else:
                    cuotas_tarde += 1
            elif c["fecha_vencimiento"] < hoy:
                cuotas_vencidas_sin_pagar += 1

    cuotas_evaluadas = cuotas_a_tiempo + cuotas_tarde
    sin_historial = not prestamos

    # Ingreso y gastos mensuales que el deudor reporto al firmar (pueden cambiar de un
    # prestamo a otro); se usa el dato mas reciente. Es una señal simple de capacidad de
    # pago: si los gastos reportados igualan o superan el ingreso, no le queda margen para
    # asumir mas deuda, asi que se penaliza el puntaje.
    ingreso_mensual = None
    gastos_mensuales = None
    for p in prestamos:
        if p.get("prestatario_ingreso_mensual") is not None:
            ingreso_mensual = p["prestatario_ingreso_mensual"]
        if p.get("prestatario_gastos_mensuales") is not None:
            gastos_mensuales = p["prestatario_gastos_mensuales"]

    capacidad_pago = None
    if ingreso_mensual is not None and gastos_mensuales is not None:
        capacidad_pago = round(ingreso_mensual - gastos_mensuales, 2)

    puntaje = 600
    if cuotas_evaluadas > 0:
        pct_a_tiempo = cuotas_a_tiempo / cuotas_evaluadas
        puntaje += round(pct_a_tiempo * 250)
    puntaje += min(150, prestamos_pagados * 30)
    puntaje -= cuotas_tarde * 25
    puntaje -= prestamos_en_mora * 100
    puntaje -= cuotas_vencidas_sin_pagar * 20
    if capacidad_pago is not None and capacidad_pago <= 0:
        puntaje -= 50
    puntaje = max(0, min(1000, round(puntaje)))

    if sin_historial:
        categoria = "Sin historial"
    elif puntaje >= 800:
        categoria = "Excelente"
    elif puntaje >= 650:
        categoria = "Bueno"
    elif puntaje >= 500:
        categoria = "Regular"
    else:
        categoria = "Riesgoso"

    return {
        "puntaje": puntaje,
        "categoria": categoria,
        "detalle": {
            "numero_prestamos": len(prestamos),
            "prestamos_pagados_completos": prestamos_pagados,
            "prestamos_en_mora": prestamos_en_mora,
            "cuotas_pagadas_a_tiempo": cuotas_a_tiempo,
            "cuotas_pagadas_tarde": cuotas_tarde,
            "cuotas_vencidas_sin_pagar": cuotas_vencidas_sin_pagar,
            "ingreso_mensual_reportado": ingreso_mensual,
            "gastos_mensuales_reportados": gastos_mensuales,
            "capacidad_pago_mensual": capacidad_pago,
        },
        "aviso": "Score experimental calculado por PrestaRed a partir del historial de pagos en la plataforma. No es un score de una central de riesgo autorizada y no tiene validez regulatoria.",
    }


ETIQUETAS_TIPO_TASA = {"mes_vencido": "mensual (mes vencido)", "efectiva_anual": "efectiva anual (E.A.)"}
ETIQUETAS_PERIODO = {"mes": "mensuales", "anio": "anuales"}
ETIQUETAS_DOCUMENTO = {"CC": "Cédula de ciudadanía", "CE": "Cédula de extranjería", "PA": "Pasaporte"}


def _formato_monto(valor, moneda):
    return f"${valor:,.0f} {moneda or 'COP'}"


_UNIDADES = ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]
_DIEZ_A_DIECINUEVE = [
    "diez", "once", "doce", "trece", "catorce", "quince",
    "dieciséis", "diecisiete", "dieciocho", "diecinueve",
]
_VEINTIS = [
    "veinte", "veintiuno", "veintidós", "veintitrés", "veinticuatro",
    "veinticinco", "veintiséis", "veintisiete", "veintiocho", "veintinueve",
]
_DECENAS = ["", "", "veinte", "treinta", "cuarenta", "cincuenta", "sesenta", "setenta", "ochenta", "noventa"]
_CENTENAS = [
    "", "ciento", "doscientos", "trescientos", "cuatrocientos", "quinientos",
    "seiscientos", "setecientos", "ochocientos", "novecientos",
]


def _tres_digitos_a_letras(n):
    """n entre 0 y 999."""
    if n == 0:
        return ""
    if n == 100:
        return "cien"
    centena, resto = divmod(n, 100)
    partes = []
    if centena:
        partes.append(_CENTENAS[centena])
    if resto:
        if resto < 10:
            partes.append(_UNIDADES[resto])
        elif resto < 20:
            partes.append(_DIEZ_A_DIECINUEVE[resto - 10])
        elif resto < 30:
            partes.append(_VEINTIS[resto - 20])
        else:
            decena, unidad = divmod(resto, 10)
            partes.append(f"{_DECENAS[decena]} y {_UNIDADES[unidad]}" if unidad else _DECENAS[decena])
    return " ".join(partes)


def _apocope_uno(texto):
    """'uno'/'veintiuno' pierden la 'o' final justo antes de 'mil', 'millón' o al cierre de
    la frase (ej. 'veintiún mil', 'un millón'), como exige la gramática española."""
    if texto == "uno":
        return "un"
    if texto == "veintiuno":
        return "veintiún"
    if texto.endswith(" uno"):
        return texto[:-3] + "un"
    return texto


def numero_a_letras(numero):
    """Convierte un entero no negativo (hasta 999.999.999.999) a su forma en palabras, en
    español, para usarlo en el pagaré (donde el monto debe constar tanto en cifras como en
    letras). Se usa solo con fines de redacción del documento, no para cálculos."""
    numero = int(round(numero))
    if numero == 0:
        return "cero"

    millones, resto = divmod(numero, 1_000_000)
    miles, unidades = divmod(resto, 1000)
    partes = []

    if millones:
        if millones == 1:
            partes.append("un millón")
        else:
            partes.append(f"{_apocope_uno(_tres_digitos_a_letras(millones))} millones")
    if miles:
        if miles == 1:
            partes.append("mil")
        else:
            partes.append(f"{_apocope_uno(_tres_digitos_a_letras(miles))} mil")
    if unidades:
        partes.append(_apocope_uno(_tres_digitos_a_letras(unidades)))

    return " ".join(partes)


def _tabla_identificacion(titulo, nombre, tipo_doc, num_doc, direccion, telefono, correo):
    """Tabla con los datos de identificación de una de las partes del contrato (nombre,
    documento, dirección, teléfono, correo), requeridos para que el contrato sea válido
    como evidencia frente a un juez o en un trámite de cobro."""
    doc_label = ETIQUETAS_DOCUMENTO.get(tipo_doc, tipo_doc or "")
    documento_texto = f"{doc_label} No. {num_doc}" if (tipo_doc and num_doc) else "No especificado"
    filas = [
        [titulo, ""],
        ["Nombre", nombre or "No especificado"],
        ["Documento", documento_texto],
        ["Dirección", direccion or "No especificado"],
        ["Teléfono", telefono or "No especificado"],
        ["Correo electrónico", correo or "No especificado"],
    ]
    tabla = Table(filas, colWidths=[140, 300])
    tabla.setStyle(
        TableStyle(
            [
                ("SPAN", (0, 0), (1, 0)),
                ("BACKGROUND", (0, 0), (1, 0), colors.HexColor("#2c3e50")),
                ("TEXTCOLOR", (0, 0), (1, 0), colors.white),
                ("FONTNAME", (0, 0), (1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9.5),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
                ("BACKGROUND", (0, 1), (0, -1), colors.HexColor("#f5f5f5")),
            ]
        )
    )
    return tabla


def generar_acuerdo_pdf(prestamo, prestamista, uploads_dir=None):
    buffer = io.BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=letter)
    styles = getSampleStyleSheet()
    elementos = []
    moneda = prestamo.get("moneda") or "COP"

    elementos.append(Paragraph("CONTRATO DE MUTUO O PRÉSTAMO", styles["Title"]))
    elementos.append(Spacer(1, 12))

    fecha_contrato = (prestamo.get("creado_en") or "")[:10] or prestamo["fecha_inicio"]
    ciudad = prestamo.get("ciudad") or "No especificada"
    preambulo = (
        f"En la ciudad de <b>{ciudad}</b>, el <b>{fecha_contrato}</b>, se celebra el presente contrato "
        f"de mutuo o préstamo de dinero entre las partes que se identifican a continuación, quienes "
        f"acuerdan regirse por las cláusulas que se detallan en este documento:"
    )
    elementos.append(Paragraph(preambulo, styles["Normal"]))
    elementos.append(Spacer(1, 14))

    elementos.append(
        _tabla_identificacion(
            "PRESTAMISTA",
            prestamista.get("nombre"),
            prestamista.get("tipo_documento"),
            prestamista.get("numero_documento"),
            prestamista.get("direccion"),
            prestamista.get("telefono"),
            prestamista.get("email"),
        )
    )
    elementos.append(Spacer(1, 10))
    elementos.append(
        _tabla_identificacion(
            "DEUDOR",
            prestamo.get("prestatario_nombre"),
            prestamo.get("prestatario_tipo_documento"),
            prestamo.get("prestatario_numero_documento"),
            prestamo.get("prestatario_direccion"),
            prestamo.get("prestatario_telefono"),
            prestamo.get("prestatario_email"),
        )
    )
    elementos.append(Spacer(1, 10))
    if prestamo.get("codeudor_nombre"):
        elementos.append(
            _tabla_identificacion(
                "CODEUDOR SOLIDARIO",
                prestamo.get("codeudor_nombre"),
                prestamo.get("codeudor_tipo_documento"),
                prestamo.get("codeudor_numero_documento"),
                prestamo.get("codeudor_direccion"),
                prestamo.get("codeudor_telefono"),
                prestamo.get("codeudor_email"),
            )
        )
        elementos.append(Spacer(1, 10))
    elementos.append(Spacer(1, 10))

    monto_total = sum(c["monto"] for c in prestamo["cuotas"])
    tipo_tasa_label = ETIQUETAS_TIPO_TASA.get(prestamo.get("tipo_tasa"), ETIQUETAS_TIPO_TASA["mes_vencido"])
    periodo_label = ETIQUETAS_PERIODO.get(prestamo.get("tipo_periodo"), ETIQUETAS_PERIODO["mes"])
    datos = [
        ["Monto prestado", _formato_monto(prestamo["monto"], moneda)],
        ["Tasa de interés", f"{prestamo['tasa_interes']}% {tipo_tasa_label}"],
        ["Monto total a pagar", _formato_monto(monto_total, moneda)],
        ["Fecha de desembolso", prestamo["fecha_inicio"]],
        ["Fecha de vencimiento", prestamo["fecha_vencimiento"]],
        ["Número de cuotas", f"{prestamo['numero_cuotas']} {periodo_label}"],
        ["Interés de mora (si se paga una cuota tarde)", f"{prestamo.get('tasa_interes_mora', 0)}% mensual, prorrateado por días de atraso"],
    ]
    tabla_datos = Table(datos, colWidths=[180, 260])
    tabla_datos.setStyle(
        TableStyle(
            [
                ("FONTSIZE", (0, 0), (-1, -1), 10),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
                ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#f5f5f5")),
            ]
        )
    )
    elementos.append(tabla_datos)
    elementos.append(Spacer(1, 20))

    elementos.append(Paragraph("Plan de pagos", styles["Heading3"]))
    cuotas_data = [["Cuota", "Fecha de vencimiento", "Monto"]]
    for c in prestamo["cuotas"]:
        cuotas_data.append([str(c["numero"]), c["fecha_vencimiento"], _formato_monto(c["monto"], moneda)])
    tabla_cuotas = Table(cuotas_data, repeatRows=1, colWidths=[60, 200, 180])
    tabla_cuotas.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2c3e50")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f5f5")]),
            ]
        )
    )
    elementos.append(tabla_cuotas)
    elementos.append(Spacer(1, 20))

    textos_clausulas = clausulas_para_pdf(prestamo.get("clausulas"))
    tiene_codeudor = bool(prestamo.get("codeudor_nombre"))
    if tiene_codeudor:
        textos_clausulas = textos_clausulas + [
            f"Codeudor solidario: <b>{prestamo['codeudor_nombre']}</b> se constituye en codeudor "
            f"solidario de esta obligación, respondiendo junto con el deudor, en los mismos "
            f"términos y con el mismo alcance, por la totalidad de la deuda (capital, intereses "
            f"ordinarios y moratorios), conforme al artículo 1568 y siguientes del Código Civil."
        ]

    if textos_clausulas:
        elementos.append(Paragraph("Cláusulas adicionales", styles["Heading3"]))
        for i, texto in enumerate(textos_clausulas, start=1):
            elementos.append(Paragraph(f"{i}. {texto}", styles["Normal"]))
            elementos.append(Spacer(1, 8))
        elementos.append(Spacer(1, 12))

    elementos.append(Paragraph(
        "Pagaré anexo: junto con este contrato se suscribe un pagaré a la orden del prestamista, "
        "por el monto total adeudado, el cual constituye un título valor independiente conforme a "
        "los artículos 709 y siguientes del Código de Comercio. <b>Al firmar electrónicamente el "
        "presente contrato, el deudor (y el codeudor solidario, si lo hay) acepta también los "
        "términos de dicho pagaré anexo</b>, disponible por separado en el detalle de este préstamo.",
        styles["Normal"],
    ))
    elementos.append(Spacer(1, 14))

    elementos.append(Paragraph("Firmas", styles["Heading3"]))
    firma_prestamista = next((f for f in prestamo["firmas"] if f["rol"] == "prestamista"), None)
    firma_prestatario = next((f for f in prestamo["firmas"] if f["rol"] == "prestatario"), None)
    firma_codeudor = next((f for f in prestamo["firmas"] if f["rol"] == "codeudor"), None)

    elementos.extend(_bloque_firma(prestamista["nombre"], "prestamista", firma_prestamista, styles, uploads_dir))
    elementos.append(Spacer(1, 16))
    elementos.extend(_bloque_firma(prestamo["prestatario_nombre"], "deudor", firma_prestatario, styles, uploads_dir))
    if tiene_codeudor:
        elementos.append(Spacer(1, 16))
        elementos.extend(_bloque_firma(prestamo["codeudor_nombre"], "codeudor", firma_codeudor, styles, uploads_dir))

    doc.build(elementos, onFirstPage=_dibujar_pie_pagina, onLaterPages=_dibujar_pie_pagina)
    return buffer.getvalue()


def generar_pagare_pdf(prestamo, prestamista, uploads_dir=None):
    """Pagaré anexo al contrato: título valor independiente (Art. 709 y ss. del Código de
    Comercio) que documenta la misma obligación. Solo lo firma quien se obliga a pagar (el
    deudor y el codeudor solidario, si lo hay) — el prestamista es el beneficiario, no un
    otorgante, así que no firma el pagaré."""
    buffer = io.BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=letter)
    styles = getSampleStyleSheet()
    elementos = []
    moneda = prestamo.get("moneda") or "COP"
    monto_total = sum(c["monto"] for c in prestamo["cuotas"])
    tiene_codeudor = bool(prestamo.get("codeudor_nombre"))

    elementos.append(Paragraph(f"PAGARÉ No. {prestamo['id']}", styles["Title"]))
    elementos.append(Spacer(1, 12))

    ciudad = prestamo.get("ciudad") or "No especificada"
    fecha_contrato = (prestamo.get("creado_en") or "")[:10] or prestamo["fecha_inicio"]

    doc_deudor = ETIQUETAS_DOCUMENTO.get(prestamo.get("prestatario_tipo_documento"), "")
    doc_prestamista = ETIQUETAS_DOCUMENTO.get(prestamista.get("tipo_documento"), "")

    otorgantes = (
        f"Yo, <b>{prestamo.get('prestatario_nombre')}</b>, identificado con {doc_deudor} No. "
        f"{prestamo.get('prestatario_numero_documento')},"
    )
    if tiene_codeudor:
        doc_codeudor = ETIQUETAS_DOCUMENTO.get(prestamo.get("codeudor_tipo_documento"), "")
        otorgantes += (
            f" junto con <b>{prestamo['codeudor_nombre']}</b>, identificado con {doc_codeudor} No. "
            f"{prestamo.get('codeudor_numero_documento')}, quien se obliga solidariamente,"
        )

    monto_letras = numero_a_letras(monto_total).upper()
    unidad_moneda = "PESOS COLOMBIANOS (COP)" if moneda == "COP" else moneda
    tipo_tasa_label = ETIQUETAS_TIPO_TASA.get(prestamo.get("tipo_tasa"), ETIQUETAS_TIPO_TASA["mes_vencido"])

    cuerpo = (
        f"{otorgantes} debo(emos) y pagaré(mos) incondicionalmente a la orden de "
        f"<b>{prestamista.get('nombre')}</b>, identificado con {doc_prestamista} No. "
        f"{prestamista.get('numero_documento')}, o a quien sus derechos represente, en la ciudad de "
        f"{ciudad}, la suma de <b>{monto_letras} {unidad_moneda}</b> ({_formato_monto(monto_total, moneda)}), "
        f"correspondiente al capital más los intereses pactados del {prestamo['tasa_interes']}% "
        f"{tipo_tasa_label}, según el plan de pagos que consta en el contrato de préstamo suscrito "
        f"entre las partes el {fecha_contrato}, del cual este pagaré es anexo y forma parte integral."
    )
    elementos.append(Paragraph(cuerpo, styles["Normal"]))
    elementos.append(Spacer(1, 10))

    vencimiento = (
        f"Fecha de vencimiento: <b>{prestamo['fecha_vencimiento']}</b>. En caso de mora, se causará "
        f"un interés moratorio del {prestamo.get('tasa_interes_mora', 0)}% mensual sobre las cuotas "
        f"vencidas, prorrateado por días de atraso."
    )
    elementos.append(Paragraph(vencimiento, styles["Normal"]))
    elementos.append(Spacer(1, 10))

    legal = (
        "Este pagaré constituye un título valor conforme a los artículos 709 y siguientes del "
        "Código de Comercio colombiano y podrá hacerse exigible mediante proceso ejecutivo en caso "
        "de incumplimiento. Es anexo del contrato de préstamo referido y no un instrumento "
        "independiente de la deuda que en él se documenta."
    )
    elementos.append(Paragraph(legal, styles["Normal"]))
    elementos.append(Spacer(1, 16))

    elementos.append(Paragraph(f"Lugar y fecha de suscripción: {ciudad}, {fecha_contrato}", styles["Normal"]))
    elementos.append(Spacer(1, 20))

    elementos.append(Paragraph("Firma(s) del(los) otorgante(s)", styles["Heading3"]))
    firmas = prestamo.get("firmas") or []
    firma_prestatario = next((f for f in firmas if f["rol"] == "prestatario"), None)
    firma_codeudor = next((f for f in firmas if f["rol"] == "codeudor"), None)

    elementos.extend(_bloque_firma(prestamo["prestatario_nombre"], "deudor", firma_prestatario, styles, uploads_dir))
    if tiene_codeudor:
        elementos.append(Spacer(1, 16))
        elementos.extend(_bloque_firma(prestamo["codeudor_nombre"], "codeudor", firma_codeudor, styles, uploads_dir))

    doc.build(elementos, onFirstPage=_dibujar_pie_pagina, onLaterPages=_dibujar_pie_pagina)
    return buffer.getvalue()


def _dibujar_pie_pagina(canvas, doc):
    """Pie de página impreso en cada hoja del contrato (se registra vía onFirstPage/
    onLaterPages en doc.build, ya que reportlab no repite Paragraphs automáticamente)."""
    canvas.saveState()
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(colors.grey)
    ancho_pagina, _ = letter
    canvas.drawCentredString(ancho_pagina / 2, 30, "Contrato realizado por PrestaRed")
    canvas.restoreState()


def _bloque_firma(nombre, rol, firma, styles, uploads_dir=None):
    """Devuelve una lista de flowables (nunca uno solo) para poder incluir, ademas del
    texto de la firma electronica, la imagen de la firma dibujada a mano cuando exista."""
    if not firma:
        return [Paragraph(
            f"_____________________________<br/>{nombre} ({rol}) - pendiente de firma", styles["Normal"]
        )]

    texto = (
        f"Firmado electrónicamente por <b>{firma['nombre_firmante']}</b> ({rol})<br/>"
        f"Fecha: {firma['fecha_firma'][:19].replace('T', ' ')} &nbsp;|&nbsp; "
        f"IP: {firma['ip'] or 'N/A'} &nbsp;|&nbsp; Hash: {firma['hash_acuerdo'][:16]}..."
    )
    elementos = [Paragraph(texto, styles["Normal"])]

    ruta_firma = uploads_dir and firma.get("firma_dibujada") and os.path.join(uploads_dir, firma["firma_dibujada"])
    if ruta_firma and os.path.exists(ruta_firma):
        try:
            elementos.append(Spacer(1, 4))
            elementos.append(Image(ruta_firma, width=140, height=52))
        except Exception:
            pass  # imagen de firma corrupta o ilegible: el contrato sigue siendo valido sin ella

    return elementos
