from datetime import datetime, timezone

from fastapi import HTTPException, status
from sqlalchemy import func
from sqlalchemy.orm import Session

from api.v1.modules.crm.accounts.models import Account
from api.v1.modules.crm.cases import service as cases_service
from api.v1.modules.crm.common.folios import next_folio
from api.v1.modules.crm.quotes.models import Quote
from api.v1.modules.crm.service_requests.models import ServiceRequest
from api.v1.modules.crm.suppliers.models import Supplier

# Direcciones válidas de la operación (para validar y sembrar hitos).
_OPERATION_TYPES = ("importacion", "exportacion")

from .dto import (
    ShipmentCloseInput,
    ShipmentCreate,
    ShipmentDocumentCreate,
    ShipmentDocumentUpdate,
    ShipmentEventCreate,
    ShipmentEventDecisionInput,
    ShipmentEventUpdate,
    ShipmentRescheduleInput,
    ShipmentUpdate,
)
from .models import Shipment, ShipmentDocument, ShipmentEvent

# Hitos por defecto según el tipo de operación (Diagramas 2 y 3).
# Tupla: (event_type, título, kind). kind="decision" son puntos de decisión (rombos)
# que se resuelven con autorizado/rechazado y disparan el ciclo de corrección.
_DEFAULT_MILESTONES = {
    # Diagrama 2 — Proceso operativo de exportación
    "exportacion": [
        ("coordinacion_fecha_cliente", "Coordinar fecha de operación con el cliente", "hito"),
        ("revision_salidas", "Revisar disponibilidad de salidas del transporte", "hito"),
        ("validacion_cutoff", "Validar Cut Off del transportista", "hito"),
        ("decision_cutoff", "¿Se alcanza el Cut Off?", "decision"),
        ("programacion_transporte_terrestre", "Programar transporte terrestre y recolección", "hito"),
        ("recoleccion", "Recolección de mercancía", "hito"),
        ("traslado_puerto", "Trasladar la mercancía al puerto / aeropuerto", "hito"),
        ("entrega_terminal", "Entregar la mercancía en la terminal", "hito"),
        ("entrega_docs_agente", "Entregar documentación al agente aduanal", "hito"),
        ("despacho_exportacion", "Despacho de exportación", "hito"),
        ("decision_despacho_exportacion", "¿Despacho de exportación autorizado?", "decision"),
        ("emision_docs_internacionales", "Emitir documentación internacional (MBL/HBL, MAWB/HAWB, CMR)", "hito"),
        ("embarque", "Embarque", "hito"),
        ("zarpe", "Zarpe / Salida del transporte", "hito"),
        ("coordinacion_corresponsal", "Coordinar con el agente corresponsal en destino", "hito"),
        ("arribo", "Arribo a destino", "hito"),
        ("despacho_destino", "Despacho de importación en destino (corresponsal)", "hito"),
        ("entrega", "Entrega al consignatario", "hito"),
        ("cierre_operativo", "Cierre operativo (registrar costos finales)", "hito"),
    ],
    # Diagrama 3 — Proceso de importación
    "importacion": [
        ("aviso_llegada", "Aviso de llegada", "hito"),
        ("recepcion_docs", "Recepción de documentos (MBL/MAWB)", "hito"),
        ("coordinacion_agente_aduanal", "Coordinar con el agente aduanal el despacho", "hito"),
        ("entrega_docs_agente", "Entregar documentos y requisitos al agente aduanal", "hito"),
        ("despacho_importacion", "Despacho de importación", "hito"),
        ("decision_despacho_importacion", "¿Despacho de importación autorizado?", "decision"),
        ("liberacion", "Liberación de mercancía", "hito"),
        ("retiro", "Retiro en puerto / aeropuerto", "hito"),
        ("traslado", "Traslado a bodega del importador", "hito"),
        ("entrega", "Entrega final al cliente", "hito"),
        ("cierre_operativo", "Cierre operativo (registrar costos finales)", "hito"),
    ],
}


def _exists(db: Session, model, _id: int | None, tenant_id: int, company_id: int) -> bool:
    if _id is None:
        return True
    return (
        db.query(model.id)
        .filter(
            model.id == _id,
            model.tenant_id == tenant_id,
            model.company_id == company_id,
            model.deleted_at.is_(None),
        )
        .first()
        is not None
    )


def _validate_refs(db: Session, data: dict, tenant_id: int, company_id: int) -> None:
    checks = [
        ("account_id", Account, "El cliente asociado no existe"),
        ("quote_id", Quote, "La cotización asociada no existe"),
        ("service_request_id", ServiceRequest, "La solicitud asociada no existe"),
        ("carrier_supplier_id", Supplier, "El transportista/naviera no existe"),
        ("ground_carrier_supplier_id", Supplier, "El transportista terrestre no existe"),
        ("customs_agent_id", Supplier, "El agente aduanal no existe"),
        ("destination_agent_id", Supplier, "El agente en destino no existe"),
    ]
    for field, model, msg in checks:
        if field in data and not _exists(db, model, data.get(field), tenant_id, company_id):
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=msg)


def get_shipments(
    db: Session,
    tenant_id: int,
    company_id: int,
    search: str | None = None,
    shipment_status: str | None = None,
    account_id: int | None = None,
) -> list[Shipment]:
    query = db.query(Shipment).filter(
        Shipment.tenant_id == tenant_id,
        Shipment.company_id == company_id,
        Shipment.deleted_at.is_(None),
    )
    if shipment_status:
        query = query.filter(Shipment.status == shipment_status)
    if account_id is not None:
        query = query.filter(Shipment.account_id == account_id)
    if search:
        pattern = f"%{search}%"
        query = query.filter(
            Shipment.reference.ilike(pattern)
            | Shipment.booking_number.ilike(pattern)
            | Shipment.origin.ilike(pattern)
            | Shipment.destination.ilike(pattern)
        )
    return query.order_by(Shipment.created_at.desc()).all()


def get_shipment(db: Session, shipment_id: int, tenant_id: int, company_id: int) -> Shipment:
    obj = (
        db.query(Shipment)
        .filter(
            Shipment.id == shipment_id,
            Shipment.tenant_id == tenant_id,
            Shipment.company_id == company_id,
            Shipment.deleted_at.is_(None),
        )
        .first()
    )
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Embarque no encontrado")
    return obj


def create_shipment(
    db: Session, payload: ShipmentCreate, tenant_id: int, company_id: int, user_id: str | None = None
) -> Shipment:
    data = payload.model_dump()
    _validate_refs(db, data, tenant_id, company_id)
    obj = Shipment(**data, tenant_id=tenant_id, company_id=company_id, created_by=user_id, updated_by=user_id)
    db.add(obj)
    db.commit()
    db.refresh(obj)
    return obj


def update_shipment(
    db: Session, shipment_id: int, payload: ShipmentUpdate, tenant_id: int, company_id: int, user_id: str | None = None
) -> Shipment:
    obj = get_shipment(db, shipment_id, tenant_id, company_id)
    data = payload.model_dump(exclude_unset=True)
    _validate_refs(db, data, tenant_id, company_id)
    for field, value in data.items():
        setattr(obj, field, value)
    obj.updated_by = user_id
    db.commit()
    db.refresh(obj)
    return obj


def delete_shipment(db: Session, shipment_id: int, tenant_id: int, company_id: int) -> None:
    obj = get_shipment(db, shipment_id, tenant_id, company_id)
    obj.deleted_at = datetime.now(timezone.utc)
    db.commit()


def create_shipment_from_quote(
    db: Session, quote_id: int, tenant_id: int, company_id: int, user_id: str | None = None,
    operation_type: str | None = None,
) -> Shipment:
    """Liberar a Operaciones: crea el embarque a partir de una cotización aceptada.

    La dirección impo/expo se confirma al liberar (``operation_type``) y, si no se
    envía, se hereda de la solicitud. Con la dirección resuelta se genera el folio
    ``OP...`` y se siembran automáticamente los hitos del proceso (Diagramas 2 y 3).
    """
    if operation_type is not None and operation_type not in _OPERATION_TYPES:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Tipo de operación inválido: usa 'importacion' o 'exportacion'",
        )
    quote = (
        db.query(Quote)
        .filter(
            Quote.id == quote_id,
            Quote.tenant_id == tenant_id,
            Quote.company_id == company_id,
            Quote.deleted_at.is_(None),
        )
        .first()
    )
    if not quote:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cotización no encontrada")
    if quote.status != "aceptada":
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="La cotización debe estar aceptada para liberarse a Operaciones",
        )

    sr = None
    if quote.service_request_id:
        sr = db.query(ServiceRequest).filter(ServiceRequest.id == quote.service_request_id).first()

    # La dirección enviada al liberar manda; si no viene, se hereda de la solicitud
    resolved = operation_type or (sr.operation_type if sr else None)

    shipment = Shipment(
        reference=next_folio(db, tenant_id, company_id, "OP", resolved),
        case_id=quote.case_id,
        quote_id=quote.id,
        service_request_id=quote.service_request_id,
        account_id=quote.account_id,
        operation_type=resolved,
        transport_mode=sr.transport_mode if sr else None,
        service_type=sr.service_type if sr else None,
        incoterm=sr.incoterm if sr else None,
        origin=sr.origin if sr else None,
        destination=sr.destination if sr else None,
        destination_agent_id=sr.destination_agent_id if sr else None,
        status="abierta",
        owner_user_id=quote.owner_user_id,
        tenant_id=tenant_id,
        company_id=company_id,
        created_by=user_id,
        updated_by=user_id,
    )
    db.add(shipment)
    if sr:
        sr.status = "liberada"
    cases_service.advance_stage(db, quote.case_id, "operacion")
    db.flush()
    # Siembra automática de hitos si ya se conoce la dirección de la operación
    for position, (event_type, title, kind) in enumerate(_DEFAULT_MILESTONES.get(resolved or "", [])):
        db.add(ShipmentEvent(
            shipment_id=shipment.id, event_type=event_type, title=title, kind=kind,
            status="pendiente", position=position, tenant_id=tenant_id, company_id=company_id,
        ))
    db.commit()
    db.refresh(shipment)
    return shipment


# ----- Documentos del embarque -----

def get_shipment_documents(
    db: Session, tenant_id: int, company_id: int, shipment_id: int | None = None
) -> list[ShipmentDocument]:
    query = db.query(ShipmentDocument).filter(
        ShipmentDocument.tenant_id == tenant_id,
        ShipmentDocument.company_id == company_id,
        ShipmentDocument.deleted_at.is_(None),
    )
    if shipment_id is not None:
        query = query.filter(ShipmentDocument.shipment_id == shipment_id)
    return query.order_by(ShipmentDocument.id.asc()).all()


def _get_document(db: Session, doc_id: int, tenant_id: int, company_id: int) -> ShipmentDocument:
    obj = (
        db.query(ShipmentDocument)
        .filter(
            ShipmentDocument.id == doc_id,
            ShipmentDocument.tenant_id == tenant_id,
            ShipmentDocument.company_id == company_id,
            ShipmentDocument.deleted_at.is_(None),
        )
        .first()
    )
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Documento no encontrado")
    return obj


def create_shipment_document(
    db: Session, payload: ShipmentDocumentCreate, tenant_id: int, company_id: int
) -> ShipmentDocument:
    get_shipment(db, payload.shipment_id, tenant_id, company_id)  # valida scope
    obj = ShipmentDocument(**payload.model_dump(), tenant_id=tenant_id, company_id=company_id)
    db.add(obj)
    db.commit()
    db.refresh(obj)
    return obj


def update_shipment_document(
    db: Session, doc_id: int, payload: ShipmentDocumentUpdate, tenant_id: int, company_id: int
) -> ShipmentDocument:
    obj = _get_document(db, doc_id, tenant_id, company_id)
    for field, value in payload.model_dump(exclude_unset=True).items():
        setattr(obj, field, value)
    db.commit()
    db.refresh(obj)
    return obj


def delete_shipment_document(db: Session, doc_id: int, tenant_id: int, company_id: int) -> None:
    obj = _get_document(db, doc_id, tenant_id, company_id)
    obj.deleted_at = datetime.now(timezone.utc)
    db.commit()


# ----- Bitácora / hitos del embarque -----

def get_shipment_events(db: Session, tenant_id: int, company_id: int, shipment_id: int | None = None) -> list[ShipmentEvent]:
    query = db.query(ShipmentEvent).filter(
        ShipmentEvent.tenant_id == tenant_id,
        ShipmentEvent.company_id == company_id,
        ShipmentEvent.deleted_at.is_(None),
    )
    if shipment_id is not None:
        query = query.filter(ShipmentEvent.shipment_id == shipment_id)
    return query.order_by(ShipmentEvent.position.asc(), ShipmentEvent.id.asc()).all()


def _get_event(db: Session, event_id: int, tenant_id: int, company_id: int) -> ShipmentEvent:
    obj = (
        db.query(ShipmentEvent)
        .filter(
            ShipmentEvent.id == event_id,
            ShipmentEvent.tenant_id == tenant_id,
            ShipmentEvent.company_id == company_id,
            ShipmentEvent.deleted_at.is_(None),
        )
        .first()
    )
    if not obj:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hito no encontrado")
    return obj


def create_shipment_event(db: Session, payload: ShipmentEventCreate, tenant_id: int, company_id: int) -> ShipmentEvent:
    get_shipment(db, payload.shipment_id, tenant_id, company_id)
    obj = ShipmentEvent(**payload.model_dump(), tenant_id=tenant_id, company_id=company_id)
    db.add(obj)
    db.commit()
    db.refresh(obj)
    return obj


def update_shipment_event(db: Session, event_id: int, payload: ShipmentEventUpdate, tenant_id: int, company_id: int) -> ShipmentEvent:
    obj = _get_event(db, event_id, tenant_id, company_id)
    for field, value in payload.model_dump(exclude_unset=True).items():
        setattr(obj, field, value)
    db.commit()
    db.refresh(obj)
    return obj


def complete_shipment_event(db: Session, event_id: int, tenant_id: int, company_id: int) -> ShipmentEvent:
    obj = _get_event(db, event_id, tenant_id, company_id)
    # Un punto de decisión no se "completa" a mano: se resuelve con decide_shipment_event
    if obj.kind == "decision":
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Este hito es un punto de decisión: resuélvelo como autorizado o rechazado",
        )
    obj.status = "completado"
    obj.actual_date = datetime.now(timezone.utc)
    db.commit()
    db.refresh(obj)
    return obj


def decide_shipment_event(
    db: Session, event_id: int, payload: ShipmentEventDecisionInput, tenant_id: int, company_id: int
) -> ShipmentEvent:
    """Resuelve un punto de decisión del flujo (Cut Off / despacho autorizado).

    - autorizado  → la decisión queda completada y el flujo continúa.
    - rechazado   → la decisión queda 'rechazada' y se genera automáticamente un hito
      de corrección (rehacer trámite) que apunta a esta decisión, implementando el
      ciclo de corrección de los diagramas 2 (R-E-14) y 3 (R-I-07).
    """
    obj = _get_event(db, event_id, tenant_id, company_id)
    if obj.kind != "decision":
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Solo los puntos de decisión aceptan un resultado (autorizado/rechazado)",
        )
    obj.outcome = payload.outcome
    obj.actual_date = datetime.now(timezone.utc)
    if payload.notes:
        obj.notes = payload.notes

    if payload.outcome == "autorizado":
        obj.status = "completado"
        db.commit()
        db.refresh(obj)
        return obj

    # Rechazado: se abre el ciclo de corrección
    obj.status = "rechazado"
    correction = ShipmentEvent(
        shipment_id=obj.shipment_id,
        event_type=f"{obj.event_type or 'tramite'}_correccion",
        title=f"Corrección: rehacer trámite — {obj.title}",
        kind="hito",
        status="en_correccion",
        parent_event_id=obj.id,
        attempt=(obj.attempt or 1) + 1,
        # Se inserta justo después de la decisión rechazada para conservar el orden del flujo
        position=obj.position,
        tenant_id=tenant_id,
        company_id=company_id,
    )
    # Empuja una posición los hitos posteriores para dejar hueco a la corrección
    db.query(ShipmentEvent).filter(
        ShipmentEvent.shipment_id == obj.shipment_id,
        ShipmentEvent.tenant_id == tenant_id,
        ShipmentEvent.company_id == company_id,
        ShipmentEvent.deleted_at.is_(None),
        ShipmentEvent.position > obj.position,
    ).update({ShipmentEvent.position: ShipmentEvent.position + 1})
    correction.position = obj.position + 1
    db.add(correction)
    db.commit()
    db.refresh(obj)
    return obj


def delete_shipment_event(db: Session, event_id: int, tenant_id: int, company_id: int) -> None:
    obj = _get_event(db, event_id, tenant_id, company_id)
    obj.deleted_at = datetime.now(timezone.utc)
    db.commit()


def reschedule_departure(
    db: Session, shipment_id: int, payload: ShipmentRescheduleInput, tenant_id: int, company_id: int,
    user_id: str | None = None,
) -> Shipment:
    """Reprograma la salida cuando no se alcanza el Cut Off (R-E-06).

    Conserva la salida anterior en ``previous_etd`` y deja constancia en la bitácora.
    """
    shipment = get_shipment(db, shipment_id, tenant_id, company_id)
    if payload.etd is not None:
        shipment.previous_etd = shipment.etd
        shipment.etd = payload.etd
    if payload.cutoff_date is not None:
        shipment.cutoff_date = payload.cutoff_date
    shipment.updated_by = user_id

    last_pos = (
        db.query(func.max(ShipmentEvent.position))
        .filter(
            ShipmentEvent.shipment_id == shipment_id,
            ShipmentEvent.tenant_id == tenant_id,
            ShipmentEvent.company_id == company_id,
            ShipmentEvent.deleted_at.is_(None),
        )
        .scalar()
    )
    detail = payload.reason or "Reprogramación de salida por Cut Off no alcanzado"
    db.add(
        ShipmentEvent(
            shipment_id=shipment_id,
            event_type="reprogramacion",
            title="Reprogramación de salida (nuevo Cut Off / ETD)",
            kind="hito",
            status="completado",
            actual_date=datetime.now(timezone.utc),
            position=(last_pos or 0) + 1,
            notes=detail,
            tenant_id=tenant_id,
            company_id=company_id,
        )
    )
    db.commit()
    db.refresh(shipment)
    return shipment


def close_shipment(
    db: Session, shipment_id: int, payload: ShipmentCloseInput, tenant_id: int, company_id: int,
    user_id: str | None = None,
) -> Shipment:
    """Cierre operativo del embarque con costos finales (R-E-22).

    Marca el embarque como 'cerrada' y registra los costos reales; el cierre es el
    disparador válido de la facturación (R-F-01). No permite cerrar si quedan puntos
    de decisión sin resolver.
    """
    shipment = get_shipment(db, shipment_id, tenant_id, company_id)
    if shipment.status == "cancelada":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT, detail="El embarque está cancelado"
        )
    pending_decision = (
        db.query(ShipmentEvent.id)
        .filter(
            ShipmentEvent.shipment_id == shipment_id,
            ShipmentEvent.tenant_id == tenant_id,
            ShipmentEvent.company_id == company_id,
            ShipmentEvent.deleted_at.is_(None),
            ShipmentEvent.kind == "decision",
            ShipmentEvent.status.in_(["pendiente", "rechazado", "en_correccion"]),
        )
        .first()
    )
    if pending_decision:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="No se puede cerrar: hay puntos de decisión pendientes o en corrección",
        )
    shipment.actual_cost_total = payload.actual_cost_total
    shipment.cost_currency = payload.cost_currency
    shipment.status = "cerrada"
    shipment.closed_at = datetime.now(timezone.utc)
    shipment.closed_by = user_id
    shipment.updated_by = user_id
    db.commit()
    db.refresh(shipment)
    return shipment


def seed_default_milestones(db: Session, shipment_id: int, tenant_id: int, company_id: int) -> list[ShipmentEvent]:
    """Crea los hitos por defecto del embarque según su tipo de operación (import/export)."""
    shipment = get_shipment(db, shipment_id, tenant_id, company_id)
    existing = get_shipment_events(db, tenant_id, company_id, shipment_id)
    if existing:
        return existing
    milestones = _DEFAULT_MILESTONES.get(shipment.operation_type or "", [])
    if not milestones:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Define el tipo de operación (importación/exportación) para generar los hitos",
        )
    created = []
    for position, (event_type, title, kind) in enumerate(milestones):
        ev = ShipmentEvent(
            shipment_id=shipment_id, event_type=event_type, title=title, kind=kind,
            status="pendiente", position=position, tenant_id=tenant_id, company_id=company_id,
        )
        db.add(ev)
        created.append(ev)
    db.commit()
    for ev in created:
        db.refresh(ev)
    return created
