from __future__ import annotations

import re
from collections.abc import Iterable
from datetime import datetime

from app.rips.application.catalogs import (
    CONCEPTO_RECAUDO,
    LST_SI_NO,
    RIPS_TIPO_USUARIO_VERSION2,
    TIPO_OTRO_SERVICIO,
    ZONA_VERSION2,
)
from app.rips.application.models import RipsValidationFinding, RipsValidationReport
from app.rips.domain.models import (
    RipsHospitalizacion,
    RipsMedicamento,
    RipsOtroServicio,
    RipsPayload,
    RipsProcedimiento,
    RipsUsuario,
)


_COUNTRY_CODE_PATTERN = re.compile(r"^\d{3}$")
_MUNICIPALITY_CODE_PATTERN = re.compile(r"^\d{5}$")
_USER_SERVICE_NOTIFICATION_FIELDS = (
    ("consultas", "RIPS-V1-NOTIFY-001"),
    ("urgencias", "RIPS-V1-NOTIFY-002"),
    ("recienNacidos", "RIPS-V1-NOTIFY-004"),
)


def _reject(
    *,
    rule_code: str,
    message: str,
    field_path: str | None = None,
) -> RipsValidationFinding:
    return RipsValidationFinding(
        rule_code=rule_code,
        severity="reject",
        message=message,
        field_path=field_path,
    )


def _notify(
    *,
    rule_code: str,
    message: str,
    field_path: str | None = None,
) -> RipsValidationFinding:
    return RipsValidationFinding(
        rule_code=rule_code,
        severity="notify",
        message=message,
        field_path=field_path,
    )


def _validate_user(user: RipsUsuario, index: int) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    base_path = f"usuarios[{index}]"

    if user.tipoUsuario not in RIPS_TIPO_USUARIO_VERSION2:
        findings.append(
            _reject(
                rule_code="RIPS-USR-001",
                message="tipoUsuario debe existir en la tabla RIPSTipoUsuarioVersion2.",
                field_path=f"{base_path}.tipoUsuario",
            )
        )
    if not _COUNTRY_CODE_PATTERN.fullmatch(user.codPaisResidencia):
        findings.append(
            _reject(
                rule_code="RIPS-USR-002",
                message="codPaisResidencia debe corresponder a un código ISO 3166-1 de tres caracteres.",
                field_path=f"{base_path}.codPaisResidencia",
            )
        )
    if user.codPaisOrigen and not _COUNTRY_CODE_PATTERN.fullmatch(user.codPaisOrigen):
        findings.append(
            _reject(
                rule_code="RIPS-USR-003",
                message="codPaisOrigen debe corresponder a un código ISO 3166-1 de tres caracteres.",
                field_path=f"{base_path}.codPaisOrigen",
            )
        )
    if not _MUNICIPALITY_CODE_PATTERN.fullmatch(user.codMunicipioResidencia):
        findings.append(
            _reject(
                rule_code="RIPS-USR-004",
                message="codMunicipioResidencia debe informarse con cinco dígitos DANE.",
                field_path=f"{base_path}.codMunicipioResidencia",
            )
        )
    if user.codZonaTerritorialResidencia not in ZONA_VERSION2:
        findings.append(
            _reject(
                rule_code="RIPS-USR-005",
                message="codZonaTerritorialResidencia debe existir en la tabla ZonaVersion2.",
                field_path=f"{base_path}.codZonaTerritorialResidencia",
            )
        )
    if user.incapacidad not in LST_SI_NO:
        findings.append(
            _reject(
                rule_code="RIPS-USR-006",
                message="incapacidad debe corresponder a un valor de la tabla LstSiNo.",
                field_path=f"{base_path}.incapacidad",
            )
        )

    for service_name, rule_code in _USER_SERVICE_NOTIFICATION_FIELDS:
        service_items = getattr(user.servicios, service_name)
        if not service_items:
            findings.append(
                _notify(
                    rule_code=rule_code,
                    message=(
                        f"El segmento {service_name} no fue generado ni validado en V1. "
                        "Revise si el caso requiere este bloque antes de radicar."
                    ),
                    field_path=f"{base_path}.servicios.{service_name}",
                )
            )

    findings.extend(_validate_consecutivos(user.servicios.hospitalizacion, f"{base_path}.servicios.hospitalizacion"))
    findings.extend(_validate_consecutivos(user.servicios.procedimientos, f"{base_path}.servicios.procedimientos"))
    findings.extend(_validate_consecutivos(user.servicios.medicamentos, f"{base_path}.servicios.medicamentos"))
    findings.extend(_validate_consecutivos(user.servicios.otrosServicios, f"{base_path}.servicios.otrosServicios"))
    findings.extend(_validate_hospitalizacion(user.servicios.hospitalizacion, base_path))
    findings.extend(_validate_procedimientos(user.servicios.procedimientos, base_path))
    findings.extend(_validate_medicamentos(user.servicios.medicamentos, base_path))
    findings.extend(_validate_otros_servicios(user.servicios.otrosServicios, base_path))
    findings.extend(_validate_service_relations(user, index))
    return findings


def _validate_consecutivos(items: Iterable[object], base_path: str) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    expected = 1
    for index, item in enumerate(items):
        consecutive = getattr(item, "consecutivo", None)
        if consecutive != expected:
            findings.append(
                _reject(
                    rule_code="RIPS-CNS-001",
                    message="Los consecutivos deben iniciar en 1 e incrementarse de uno en uno sin repetirse.",
                    field_path=f"{base_path}[{index}].consecutivo",
                )
            )
        expected += 1
    return findings


def _validate_payment_fields(
    *,
    concepto_recaudo: str,
    valor_pago_moderador: int,
    num_fev_pago_moderador: str | None,
    field_path: str,
) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    if concepto_recaudo not in CONCEPTO_RECAUDO:
        findings.append(
            _reject(
                rule_code="RIPS-PAY-001",
                message="conceptoRecaudo debe corresponder a un valor soportado por el anexo técnico.",
                field_path=f"{field_path}.conceptoRecaudo",
            )
        )
    if valor_pago_moderador == 0 and num_fev_pago_moderador:
        findings.append(
            _reject(
                rule_code="RIPS-PAY-002",
                message="numFEVPagoModerador no debe informarse cuando valorPagoModerador es 0.",
                field_path=f"{field_path}.numFEVPagoModerador",
            )
        )
    if valor_pago_moderador > 0 and not num_fev_pago_moderador:
        findings.append(
            _reject(
                rule_code="RIPS-PAY-003",
                message="numFEVPagoModerador es obligatorio cuando valorPagoModerador es mayor a 0.",
                field_path=f"{field_path}.numFEVPagoModerador",
            )
        )
    if concepto_recaudo in {"04", "05"} and valor_pago_moderador != 0:
        findings.append(
            _reject(
                rule_code="RIPS-PAY-004",
                message="valorPagoModerador debe ser 0 cuando conceptoRecaudo corresponde a ANTICIPO o NO APLICA.",
                field_path=f"{field_path}.valorPagoModerador",
            )
        )
    return findings


def _validate_procedimientos(
    procedimientos: list[RipsProcedimiento], user_base_path: str
) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    for index, item in enumerate(procedimientos):
        base_path = f"{user_base_path}.servicios.procedimientos[{index}]"
        if item.vrServicio < 0:
            findings.append(
                _reject(
                    rule_code="RIPS-PROC-001",
                    message="vrServicio no puede ser negativo.",
                    field_path=f"{base_path}.vrServicio",
                )
            )
        findings.extend(
            _validate_payment_fields(
                concepto_recaudo=item.conceptoRecaudo,
                valor_pago_moderador=item.valorPagoModerador,
                num_fev_pago_moderador=item.numFEVPagoModerador,
                field_path=base_path,
            )
        )
    return findings


def _validate_medicamentos(
    medicamentos: list[RipsMedicamento], user_base_path: str
) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    for index, item in enumerate(medicamentos):
        base_path = f"{user_base_path}.servicios.medicamentos[{index}]"
        if item.cantidadMedicamento <= 0:
            findings.append(
                _reject(
                    rule_code="RIPS-MED-001",
                    message="cantidadMedicamento debe ser mayor a 0.",
                    field_path=f"{base_path}.cantidadMedicamento",
                )
            )
        if item.vrUnitMedicamento < 0 or item.vrServicio < 0:
            findings.append(
                _reject(
                    rule_code="RIPS-MED-002",
                    message="Los valores monetarios del medicamento no pueden ser negativos.",
                    field_path=f"{base_path}.vrServicio",
                )
            )
        findings.extend(
            _validate_payment_fields(
                concepto_recaudo=item.conceptoRecaudo,
                valor_pago_moderador=item.valorPagoModerador,
                num_fev_pago_moderador=item.numFEVPagoModerador,
                field_path=base_path,
            )
        )
    return findings


def _validate_hospitalizacion(
    hospitalizaciones: list[RipsHospitalizacion], user_base_path: str
) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    for index, item in enumerate(hospitalizaciones):
        base_path = f"{user_base_path}.servicios.hospitalizacion[{index}]"
        admission = _parse_datetime(item.fechaInicioAtencion)
        discharge = _parse_datetime(item.fechaEgreso)
        if admission and discharge and discharge < admission:
            findings.append(
                _reject(
                    rule_code="RIPS-HOS-001",
                    message="fechaEgreso no puede ser menor a fechaInicioAtencion en hospitalizacion.",
                    field_path=f"{base_path}.fechaEgreso",
                )
            )
    return findings


def _validate_otros_servicios(
    otros_servicios: list[RipsOtroServicio], user_base_path: str
) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    for index, item in enumerate(otros_servicios):
        base_path = f"{user_base_path}.servicios.otrosServicios[{index}]"
        if item.tipoOS not in TIPO_OTRO_SERVICIO:
            findings.append(
                _reject(
                    rule_code="RIPS-OS-001",
                    message="tipoOS debe existir en la tabla TipoOtrosServicios.",
                    field_path=f"{base_path}.tipoOS",
                )
            )
        if item.cantidadOS <= 0:
            findings.append(
                _reject(
                    rule_code="RIPS-OS-002",
                    message="cantidadOS debe ser mayor a 0.",
                    field_path=f"{base_path}.cantidadOS",
                )
            )
        if item.vrUnitOS < 0 or item.vrServicio < 0:
            findings.append(
                _reject(
                    rule_code="RIPS-OS-003",
                    message="Los valores monetarios de otrosServicios no pueden ser negativos.",
                    field_path=f"{base_path}.vrServicio",
                )
            )
        findings.extend(
            _validate_payment_fields(
                concepto_recaudo=item.conceptoRecaudo,
                valor_pago_moderador=item.valorPagoModerador,
                num_fev_pago_moderador=item.numFEVPagoModerador,
                field_path=base_path,
            )
        )
    return findings


def _validate_service_relations(user: RipsUsuario, user_index: int) -> list[RipsValidationFinding]:
    findings: list[RipsValidationFinding] = []
    procedure_base = f"usuarios[{user_index}].servicios.procedimientos"
    medication_base = f"usuarios[{user_index}].servicios.medicamentos"
    other_service_base = f"usuarios[{user_index}].servicios.otrosServicios"

    for index, item in enumerate(user.servicios.procedimientos):
        if item.tipoDocumentoIdentificacion != user.tipoDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-001",
                    message="El tipo de documento del procedimiento debe coincidir con el del usuario.",
                    field_path=f"{procedure_base}[{index}].tipoDocumentoIdentificacion",
                )
            )
        if item.numDocumentoIdentificacion != user.numDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-002",
                    message="El número de documento del procedimiento debe coincidir con el del usuario.",
                    field_path=f"{procedure_base}[{index}].numDocumentoIdentificacion",
                )
            )

    for index, item in enumerate(user.servicios.medicamentos):
        if item.tipoDocumentoIdentificacion != user.tipoDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-003",
                    message="El tipo de documento del medicamento debe coincidir con el del usuario.",
                    field_path=f"{medication_base}[{index}].tipoDocumentoIdentificacion",
                )
            )
        if item.numDocumentoIdentificacion != user.numDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-004",
                    message="El número de documento del medicamento debe coincidir con el del usuario.",
                    field_path=f"{medication_base}[{index}].numDocumentoIdentificacion",
                )
            )

    for index, item in enumerate(user.servicios.otrosServicios):
        if item.tipoOS == "03" and not (user.servicios.hospitalizacion or user.servicios.urgencias):
            findings.append(
                _reject(
                    rule_code="RIPS-REL-016",
                    message="Los otrosServicios de tipo estancia requieren hospitalizacion u urgencias asociadas.",
                    field_path=f"{other_service_base}[{index}].tipoOS",
                )
            )
        if item.tipoDocumentoIdentificacion and item.tipoDocumentoIdentificacion != user.tipoDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-017",
                    message="El tipo de documento de otrosServicios debe coincidir con el del usuario cuando se informe.",
                    field_path=f"{other_service_base}[{index}].tipoDocumentoIdentificacion",
                )
            )
        if item.numDocumentoIdentificacion and item.numDocumentoIdentificacion != user.numDocumentoIdentificacion:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-018",
                    message="El número de documento de otrosServicios debe coincidir con el del usuario cuando se informe.",
                    field_path=f"{other_service_base}[{index}].numDocumentoIdentificacion",
                )
            )

    return findings


def _parse_int(value: object) -> int | None:
    text = str(value or "").strip()
    if not text:
        return None
    digits = re.sub(r"[^\d\-]", "", text)
    if not digits:
        return None
    try:
        return int(digits)
    except ValueError:
        return None


def _parse_datetime(value: str) -> datetime | None:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        return datetime.strptime(text, "%Y-%m-%d %H:%M")
    except ValueError:
        return None


def _as_object_dict(value: object) -> dict[str, object] | None:
    if not isinstance(value, dict):
        return None
    return {str(key): item for key, item in value.items()}


def _resolve_source_invoice_number(source_context: dict[str, object]) -> str | None:
    factura = _as_object_dict(source_context.get("factura"))
    if factura is None:
        return None
    factura_json = _as_object_dict(factura.get("factura_json"))
    if factura_json is None:
        return None
    invoice_info = _as_object_dict(factura_json.get("informacion_factura"))
    if invoice_info is None:
        return None
    prefijo = str(invoice_info.get("prefijo") or "").strip()
    numero = str(invoice_info.get("numero_factura") or "").strip()
    if prefijo and numero and not numero.startswith(prefijo):
        return f"{prefijo}{numero}"
    return numero or None


def _source_other_service_items(servicios: dict[str, object]) -> list[dict[str, object]]:
    items: list[dict[str, object]] = []
    for section in (
        "examenes_laboratorio",
        "imagenologia",
        "hospitalizacion",
        "honorarios_medicos",
        "otros_servicios",
    ):
        raw_items = servicios.get(section)
        if not isinstance(raw_items, list):
            continue
        for item in raw_items:
            normalized_item = _as_object_dict(item)
            if normalized_item is not None:
                items.append(normalized_item)
    return items


def _section_items(servicios: dict[str, object], section: str) -> list[dict[str, object]]:
    raw_items = servicios.get(section)
    if not isinstance(raw_items, list):
        return []

    items: list[dict[str, object]] = []
    for item in raw_items:
        normalized_item = _as_object_dict(item)
        if normalized_item is not None:
            items.append(normalized_item)
    return items


def _validate_source_relations(
    payload: RipsPayload,
    source_context: dict[str, object] | None,
) -> list[RipsValidationFinding]:
    if not isinstance(source_context, dict):
        return []

    findings: list[RipsValidationFinding] = []
    factura = _as_object_dict(source_context.get("factura"))
    if factura is None:
        return findings

    factura_json = _as_object_dict(factura.get("factura_json"))
    if factura_json is None:
        return findings

    provider = _as_object_dict(factura_json.get("proveedor"))
    if provider is not None:
        provider_nit = re.sub(r"\D", "", str(provider.get("nit") or ""))
        if provider_nit and provider_nit != payload.numDocumentoIdObligado:
            findings.append(
                _reject(
                    rule_code="RIPS-REL-010",
                    message="numDocumentoIdObligado debe coincidir con el NIT del proveedor en la factura.",
                    field_path="numDocumentoIdObligado",
                )
            )

    expected_invoice = _resolve_source_invoice_number(source_context)
    if expected_invoice and expected_invoice != payload.numFactura:
        findings.append(
            _reject(
                rule_code="RIPS-REL-011",
                message="numFactura debe coincidir con la factura fuente del caso.",
                field_path="numFactura",
            )
        )

    servicios = _as_object_dict(factura_json.get("servicios_procedimientos"))
    if servicios is None or not payload.usuarios:
        return findings

    procedimientos_src = _section_items(servicios, "procedimientos_quirurgicos")
    medicamentos_src = _section_items(servicios, "medicamentos")
    hospitalizacion_src = _section_items(servicios, "hospitalizacion")
    otros_servicios_src = _source_other_service_items(servicios)
    procedimientos = payload.usuarios[0].servicios.procedimientos
    hospitalizaciones = payload.usuarios[0].servicios.hospitalizacion
    medicamentos = payload.usuarios[0].servicios.medicamentos
    otros_servicios = payload.usuarios[0].servicios.otrosServicios

    if len(procedimientos) != len(procedimientos_src):
        findings.append(
            _reject(
                rule_code="RIPS-REL-012",
                message="La cantidad de procedimientos RIPS debe coincidir con las líneas quirúrgicas facturadas.",
                field_path="usuarios[0].servicios.procedimientos",
            )
        )
    if len(medicamentos) != len(medicamentos_src):
        findings.append(
            _reject(
                rule_code="RIPS-REL-013",
                message="La cantidad de medicamentos RIPS debe coincidir con las líneas de medicamentos facturadas.",
                field_path="usuarios[0].servicios.medicamentos",
            )
        )
    if hospitalizacion_src and not hospitalizaciones:
        findings.append(
            _reject(
                rule_code="RIPS-REL-019",
                message="La factura fuente contiene estancia/hospitalización y el segmento hospitalizacion del RIPS está ausente.",
                field_path="usuarios[0].servicios.hospitalizacion",
            )
        )
    if len(otros_servicios) != len(otros_servicios_src):
        findings.append(
            _reject(
                rule_code="RIPS-REL-020",
                message=(
                    "La cantidad de otrosServicios RIPS debe coincidir con las líneas facturadas de "
                    "laboratorio, imagenología, hospitalización, honorarios y otros servicios."
                ),
                field_path="usuarios[0].servicios.otrosServicios",
            )
        )

    proc_total_source = sum(_parse_int(item.get("total")) or 0 for item in procedimientos_src)
    med_total_source = sum(_parse_int(item.get("total")) or 0 for item in medicamentos_src)
    other_total_source = sum(_parse_int(item.get("total")) or 0 for item in otros_servicios_src)
    proc_total_payload = sum(item.vrServicio for item in procedimientos)
    med_total_payload = sum(item.vrServicio for item in medicamentos)
    other_total_payload = sum(item.vrServicio for item in otros_servicios)

    if procedimientos_src and proc_total_payload != proc_total_source:
        findings.append(
            _reject(
                rule_code="RIPS-REL-014",
                message="La suma de vrServicio de procedimientos debe coincidir con el total facturado de procedimientos.",
                field_path="usuarios[0].servicios.procedimientos",
            )
        )
    if medicamentos_src and med_total_payload != med_total_source:
        findings.append(
            _reject(
                rule_code="RIPS-REL-015",
                message="La suma de vrServicio de medicamentos debe coincidir con el total facturado de medicamentos.",
                field_path="usuarios[0].servicios.medicamentos",
            )
        )
    if otros_servicios_src and other_total_payload != other_total_source:
        findings.append(
            _reject(
                rule_code="RIPS-REL-021",
                message=(
                    "La suma de vrServicio de otrosServicios debe coincidir con el total facturado de "
                    "laboratorio, imagenología, hospitalización, honorarios y otros servicios."
                ),
                field_path="usuarios[0].servicios.otrosServicios",
            )
        )

    return findings


def validate_rips_payload(
    payload: RipsPayload,
    *,
    source_context: dict[str, object] | None = None,
) -> RipsValidationReport:
    findings: list[RipsValidationFinding] = []

    if not payload.numDocumentoIdObligado:
        findings.append(
            _reject(
                rule_code="RIPS-TXN-001",
                message="numDocumentoIdObligado es obligatorio en la transacción.",
                field_path="numDocumentoIdObligado",
            )
        )
    if not payload.numFactura:
        findings.append(
            _reject(
                rule_code="RIPS-TXN-002",
                message="numFactura es obligatorio en la transacción.",
                field_path="numFactura",
            )
        )

    for index, user in enumerate(payload.usuarios):
        if user.consecutivo != index + 1:
            findings.append(
                _reject(
                    rule_code="RIPS-USR-007",
                    message="Los usuarios deben iniciar en consecutivo 1 y continuar sin saltos.",
                    field_path=f"usuarios[{index}].consecutivo",
                )
            )
        findings.extend(_validate_user(user, index))
        for hospitalization_index, item in enumerate(user.servicios.hospitalizacion):
            if item.codPrestador != payload.numDocumentoIdObligado:
                findings.append(
                    _reject(
                        rule_code="RIPS-REL-007",
                        message="codPrestador de hospitalizacion debe coincidir con numDocumentoIdObligado.",
                        field_path=f"usuarios[{index}].servicios.hospitalizacion[{hospitalization_index}].codPrestador",
                    )
                )
        for procedure_index, item in enumerate(user.servicios.procedimientos):
            if item.codPrestador != payload.numDocumentoIdObligado:
                findings.append(
                    _reject(
                        rule_code="RIPS-REL-005",
                        message="codPrestador del procedimiento debe coincidir con numDocumentoIdObligado.",
                        field_path=f"usuarios[{index}].servicios.procedimientos[{procedure_index}].codPrestador",
                    )
                )
        for medication_index, item in enumerate(user.servicios.medicamentos):
            if item.codPrestador != payload.numDocumentoIdObligado:
                findings.append(
                    _reject(
                        rule_code="RIPS-REL-006",
                        message="codPrestador del medicamento debe coincidir con numDocumentoIdObligado.",
                        field_path=f"usuarios[{index}].servicios.medicamentos[{medication_index}].codPrestador",
                    )
                )
        for other_service_index, item in enumerate(user.servicios.otrosServicios):
            if item.codPrestador != payload.numDocumentoIdObligado:
                findings.append(
                    _reject(
                        rule_code="RIPS-REL-009",
                        message="codPrestador de otrosServicios debe coincidir con numDocumentoIdObligado.",
                        field_path=f"usuarios[{index}].servicios.otrosServicios[{other_service_index}].codPrestador",
                    )
                )

    findings.extend(_validate_source_relations(payload, source_context))

    has_rejects = any(item.severity == "reject" for item in findings)
    has_notifications = any(item.severity == "notify" for item in findings)
    status = "rejected" if has_rejects else "notified" if has_notifications else "approved"
    return RipsValidationReport(
        status=status,
        ready_for_cuv=not has_rejects,
        findings=findings,
    )
