"""Lógica del módulo Tarifario: CRUD, importación por Excel y motor de costeo."""

import io
from datetime import date
from decimal import Decimal
from typing import Any

from fastapi import HTTPException, status
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session

from .dto import (
    CostChargeLine,
    CostOption,
    CostRequest,
    ImportConfirm,
    ImportPreview,
    ImportPreviewRow,
    RateLaneCreate,
    RateSheetCreate,
    RateSheetUpdate,
)
from ..common.pricing import air_volumetric_kg
from .models import RateBreak, RateCharge, RateLane, RateSheet

# Factor volumétrico aéreo: 1 m³ = 167 kg (equivale a 6000 cm³/kg).
# Respaldo cuando solo se conoce el volumen en m³ (sin dimensiones cm).
AIR_VOLUMETRIC_FACTOR = Decimal("167")


# ============================================================ CRUD tarifarios
def _sheet_query(db: Session, tenant_id: int, company_id: int):
    return db.query(RateSheet).filter(
        RateSheet.tenant_id == tenant_id,
        RateSheet.company_id == company_id,
        RateSheet.deleted_at.is_(None),
    )


def list_sheets(db: Session, tenant_id: int, company_id: int, mode: str | None = None,
                supplier_id: int | None = None) -> list[RateSheet]:
    q = _sheet_query(db, tenant_id, company_id)
    if mode:
        q = q.filter(RateSheet.mode == mode)
    if supplier_id:
        q = q.filter(RateSheet.supplier_id == supplier_id)
    return q.order_by(RateSheet.created_at.desc()).all()


def lane_count(db: Session, tenant_id: int, sheet_id: int) -> int:
    return (
        db.query(RateLane)
        .filter(RateLane.rate_sheet_id == sheet_id, RateLane.tenant_id == tenant_id,
                RateLane.deleted_at.is_(None))
        .count()
    )


def get_sheet(db: Session, tenant_id: int, company_id: int, sheet_id: int) -> RateSheet:
    sheet = _sheet_query(db, tenant_id, company_id).filter(RateSheet.id == sheet_id).first()
    if not sheet:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tarifario no encontrado")
    return sheet


def create_sheet(db: Session, tenant_id: int, company_id: int, data: RateSheetCreate,
                 user_id: str | None) -> RateSheet:
    sheet = RateSheet(
        tenant_id=tenant_id, company_id=company_id,
        **data.model_dump(),
        created_by=user_id, updated_by=user_id,
    )
    db.add(sheet)
    db.commit()
    db.refresh(sheet)
    return sheet


def update_sheet(db: Session, tenant_id: int, company_id: int, sheet_id: int,
                 data: RateSheetUpdate, user_id: str | None) -> RateSheet:
    sheet = get_sheet(db, tenant_id, company_id, sheet_id)
    for field, value in data.model_dump(exclude_unset=True).items():
        setattr(sheet, field, value)
    sheet.updated_by = user_id
    db.commit()
    db.refresh(sheet)
    return sheet


def delete_sheet(db: Session, tenant_id: int, company_id: int, sheet_id: int) -> None:
    from sqlalchemy import func
    sheet = get_sheet(db, tenant_id, company_id, sheet_id)
    sheet.deleted_at = func.now()
    db.commit()


# ============================================================ Rutas (lanes)
def list_lanes(db: Session, tenant_id: int, sheet_id: int) -> list[RateLane]:
    return (
        db.query(RateLane)
        .filter(RateLane.rate_sheet_id == sheet_id, RateLane.tenant_id == tenant_id,
                RateLane.deleted_at.is_(None))
        .order_by(RateLane.region, RateLane.destination)
        .all()
    )


def breaks_of(db: Session, lane_id: int) -> list[RateBreak]:
    return (
        db.query(RateBreak)
        .filter(RateBreak.rate_lane_id == lane_id, RateBreak.deleted_at.is_(None))
        .order_by(RateBreak.from_qty)
        .all()
    )


def _add_lane(db: Session, tenant_id: int, company_id: int, sheet_id: int,
              lane_data: RateLaneCreate) -> RateLane:
    payload = lane_data.model_dump(exclude={"breaks"})
    lane = RateLane(tenant_id=tenant_id, company_id=company_id, rate_sheet_id=sheet_id, **payload)
    db.add(lane)
    db.flush()  # id
    for br in lane_data.breaks:
        db.add(RateBreak(
            tenant_id=tenant_id, company_id=company_id, rate_lane_id=lane.id,
            from_qty=br.from_qty, rate=br.rate,
        ))
    return lane


def create_lane(db: Session, tenant_id: int, company_id: int, sheet_id: int,
                lane_data: RateLaneCreate) -> RateLane:
    get_sheet(db, tenant_id, company_id, sheet_id)  # valida pertenencia
    lane = _add_lane(db, tenant_id, company_id, sheet_id, lane_data)
    db.commit()
    db.refresh(lane)
    return lane


def delete_lane(db: Session, tenant_id: int, sheet_id: int, lane_id: int) -> None:
    from sqlalchemy import func
    lane = (
        db.query(RateLane)
        .filter(RateLane.id == lane_id, RateLane.rate_sheet_id == sheet_id,
                RateLane.tenant_id == tenant_id)
        .first()
    )
    if not lane:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ruta no encontrada")
    lane.deleted_at = func.now()
    db.commit()


# ============================================================ Cargos adicionales
def list_charges(db: Session, tenant_id: int, sheet_id: int) -> list[RateCharge]:
    return (
        db.query(RateCharge)
        .filter(RateCharge.rate_sheet_id == sheet_id, RateCharge.tenant_id == tenant_id,
                RateCharge.deleted_at.is_(None))
        .order_by(RateCharge.concept)
        .all()
    )


def create_charge(db: Session, tenant_id: int, company_id: int, sheet_id: int, data) -> RateCharge:
    get_sheet(db, tenant_id, company_id, sheet_id)
    ch = RateCharge(
        tenant_id=tenant_id, company_id=company_id, rate_sheet_id=sheet_id,
        rate_lane_id=data.rate_lane_id, concept=data.concept, charge_type=data.charge_type,
        value=data.value, condition=data.condition,
    )
    db.add(ch)
    db.commit()
    db.refresh(ch)
    return ch


def update_charge(db: Session, tenant_id: int, sheet_id: int, charge_id: int, data) -> RateCharge:
    ch = (
        db.query(RateCharge)
        .filter(RateCharge.id == charge_id, RateCharge.rate_sheet_id == sheet_id,
                RateCharge.tenant_id == tenant_id)
        .first()
    )
    if not ch:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cargo no encontrado")
    for field, value in data.model_dump(exclude_unset=True).items():
        setattr(ch, field, value)
    db.commit()
    db.refresh(ch)
    return ch


def delete_charge(db: Session, tenant_id: int, sheet_id: int, charge_id: int) -> None:
    from sqlalchemy import func
    ch = (
        db.query(RateCharge)
        .filter(RateCharge.id == charge_id, RateCharge.rate_sheet_id == sheet_id,
                RateCharge.tenant_id == tenant_id)
        .first()
    )
    if not ch:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cargo no encontrado")
    ch.deleted_at = func.now()
    db.commit()


# ============================================================ Importación Excel
# Plantillas por modo: encabezados esperados (orden libre, se detectan por nombre).
TEMPLATES: dict[str, list[str]] = {
    "aereo": ["Region", "Origen", "Destino", "IATA", "Min", "100", "300", "500", "1000"],
    "maritimo_fcl": ["Origen", "Destino", "Tipo contenedor", "Tarifa", "Transito", "Notas"],
    "maritimo_lcl": ["Origen", "Destino", "Tarifa W/M", "Minimo", "Notas"],
    "terrestre": ["Origen", "Destino", "Tarifa", "Transito", "Notas"],
}


def build_template(mode: str) -> bytes:
    """Genera un .xlsx con los encabezados del modo + una fila de ejemplo."""
    import openpyxl

    if mode not in TEMPLATES:
        raise HTTPException(status_code=400, detail=f"Modo '{mode}' no válido")
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = mode
    headers = TEMPLATES[mode]
    ws.append(headers)
    examples = {
        "aereo": ["EUROPA", "NLU", "Frankfurt", "FRA", 190, 1.00, 1.00, 0.95, 0.90],
        "maritimo_fcl": ["MXZLO", "CNSHA", "40HC", 2500, 28, "THC no incluido"],
        "maritimo_lcl": ["MXZLO", "USLAX", 45, 80, "1 W/M = 1 ton o 1 m3"],
        "terrestre": ["Monterrey", "Laredo", 850, 1, ""],
    }
    ws.append(examples[mode])
    buf = io.BytesIO()
    wb.save(buf)
    return buf.getvalue()


def _num(v: Any) -> Decimal | None:
    if v is None or v == "":
        return None
    try:
        return Decimal(str(v).replace("$", "").replace(",", "").strip())
    except Exception:
        return None


def parse_excel(mode: str, content: bytes) -> ImportPreview:
    """Lee el Excel y devuelve una vista previa con validaciones (no persiste)."""
    import openpyxl

    if mode not in TEMPLATES:
        raise HTTPException(status_code=400, detail=f"Modo '{mode}' no válido")
    try:
        wb = openpyxl.load_workbook(io.BytesIO(content), data_only=True, read_only=True)
    except Exception:
        raise HTTPException(status_code=400, detail="No se pudo leer el archivo Excel")
    ws = wb.active
    rows_iter = ws.iter_rows(values_only=True)
    header = next(rows_iter, None)
    if not header:
        raise HTTPException(status_code=400, detail="El archivo está vacío")
    cols = [str(c).strip() if c is not None else "" for c in header]
    idx = {name.lower(): i for i, name in enumerate(cols)}

    def cell(row, name):
        i = idx.get(name.lower())
        return row[i] if i is not None and i < len(row) else None

    preview_rows: list[ImportPreviewRow] = []
    valid = 0
    for n, row in enumerate(rows_iter, start=2):
        if row is None or all(c is None or str(c).strip() == "" for c in row):
            continue
        errors: list[str] = []
        warnings: list[str] = []
        data: dict = {}
        if mode == "aereo":
            data = {
                "region": cell(row, "Region"),
                "origin": cell(row, "Origen"),
                "destination": cell(row, "Destino") or cell(row, "IATA"),
                "iata": cell(row, "IATA"),
                "min_charge": _num(cell(row, "Min")),
                "breaks": {b: _num(cell(row, b)) for b in ("100", "300", "500", "1000")},
            }
            if not data["destination"]:
                errors.append("Falta destino/IATA")
            if not any(v is not None for v in data["breaks"].values()):
                errors.append("Sin tarifas por quiebre")
        elif mode == "maritimo_fcl":
            data = {
                "origin": cell(row, "Origen"),
                "destination": cell(row, "Destino"),
                "equipment_type": cell(row, "Tipo contenedor"),
                "flat_rate": _num(cell(row, "Tarifa")),
                "transit_days": _num(cell(row, "Transito")),
                "notes": cell(row, "Notas"),
            }
            if data["flat_rate"] is None:
                errors.append("Falta la tarifa")
            if not data["equipment_type"]:
                warnings.append("Sin tipo de contenedor")
        elif mode == "maritimo_lcl":
            data = {
                "origin": cell(row, "Origen"),
                "destination": cell(row, "Destino"),
                "wm_rate": _num(cell(row, "Tarifa W/M")),
                "min_charge": _num(cell(row, "Minimo")),
                "notes": cell(row, "Notas"),
            }
            if data["wm_rate"] is None:
                errors.append("Falta la tarifa W/M")
        else:  # terrestre
            data = {
                "origin": cell(row, "Origen"),
                "destination": cell(row, "Destino"),
                "flat_rate": _num(cell(row, "Tarifa")),
                "transit_days": _num(cell(row, "Transito")),
                "notes": cell(row, "Notas"),
            }
            if data["flat_rate"] is None:
                errors.append("Falta la tarifa")
        if not data.get("destination"):
            errors.append("Falta destino")
        ok = not errors
        if ok:
            valid += 1
        preview_rows.append(ImportPreviewRow(row=n, data=_jsonable(data), ok=ok,
                                             warnings=warnings, errors=errors))
    return ImportPreview(mode=mode, total=len(preview_rows), valid=valid,
                         rows=preview_rows, columns=cols)


def _jsonable(d: dict) -> dict:
    out = {}
    for k, v in d.items():
        if isinstance(v, Decimal):
            out[k] = float(v)
        elif isinstance(v, dict):
            out[k] = {kk: (float(vv) if isinstance(vv, Decimal) else vv) for kk, vv in v.items()}
        else:
            out[k] = v
    return out


def _rows_to_lanes(mode: str, rows: list[ImportPreviewRow], default_origin: str | None) -> list[RateLaneCreate]:
    lanes: list[RateLaneCreate] = []
    for r in rows:
        if not r.ok:
            continue
        d = r.data
        origin = d.get("origin") or default_origin
        if mode == "aereo":
            breaks = [
                {"from_qty": Decimal(b), "rate": Decimal(str(v))}
                for b, v in (d.get("breaks") or {}).items() if v is not None
            ]
            lanes.append(RateLaneCreate(
                origin=str(origin) if origin else None,
                destination=str(d.get("destination")),
                region=d.get("region"), rate_unit="per_kg",
                min_charge=_num(d.get("min_charge")),
                breaks=breaks,  # type: ignore[arg-type]
            ))
        elif mode == "maritimo_fcl":
            lanes.append(RateLaneCreate(
                origin=str(origin) if origin else None, destination=str(d.get("destination")),
                equipment_type=d.get("equipment_type"), rate_unit="per_container",
                flat_rate=_num(d.get("flat_rate")),
                transit_days=int(d["transit_days"]) if d.get("transit_days") else None,
                notes=d.get("notes"),
            ))
        elif mode == "maritimo_lcl":
            lanes.append(RateLaneCreate(
                origin=str(origin) if origin else None, destination=str(d.get("destination")),
                rate_unit="per_wm", min_charge=_num(d.get("min_charge")),
                breaks=[{"from_qty": Decimal(0), "rate": Decimal(str(d["wm_rate"]))}],  # type: ignore[arg-type]
                notes=d.get("notes"),
            ))
        else:
            lanes.append(RateLaneCreate(
                origin=str(origin) if origin else None, destination=str(d.get("destination")),
                rate_unit="flat", flat_rate=_num(d.get("flat_rate")),
                transit_days=int(d["transit_days"]) if d.get("transit_days") else None,
                notes=d.get("notes"),
            ))
    return lanes


def confirm_import(db: Session, tenant_id: int, company_id: int, data: ImportConfirm,
                   user_id: str | None) -> RateSheet:
    """Crea el tarifario + rutas a partir de la vista previa confirmada."""
    sheet = RateSheet(
        tenant_id=tenant_id, company_id=company_id,
        supplier_id=data.supplier_id, mode=data.mode, name=data.name,
        currency=data.currency, valid_from=data.valid_from, valid_to=data.valid_to,
        default_origin=data.default_origin, status=data.status or "borrador",
        notes=data.notes, created_by=user_id, updated_by=user_id,
    )
    db.add(sheet)
    db.flush()
    for lane in data.lanes:
        _add_lane(db, tenant_id, company_id, sheet.id, lane)
    db.commit()
    db.refresh(sheet)
    return sheet


def import_from_excel(db: Session, tenant_id: int, company_id: int, mode: str,
                      content: bytes, header: RateSheetCreate, user_id: str | None) -> RateSheet:
    """Atajo: parsea el Excel y crea el tarifario en un solo paso."""
    preview = parse_excel(mode, content)
    lanes = _rows_to_lanes(mode, preview.rows, header.default_origin)
    return confirm_import(
        db, tenant_id, company_id,
        ImportConfirm(**header.model_dump(), lanes=lanes), user_id,
    )


# ============================================================ Motor de costeo
def _volumetric_kg(volume_m3: Decimal | None) -> Decimal:
    return (volume_m3 or Decimal(0)) * AIR_VOLUMETRIC_FACTOR


def _rate_for(breaks: list[RateBreak], qty: Decimal) -> Decimal | None:
    """Tarifa aplicable al peso/wm 'qty' (mayor quiebre cuyo umbral <= qty)."""
    if not breaks:
        return None
    applicable = None
    for b in breaks:
        if b.from_qty <= qty:
            applicable = b.rate
    if applicable is None:
        applicable = breaks[0].rate  # por debajo del primer quiebre → tarifa base (gobierna el mínimo)
    return applicable


def _best_break_cost(breaks: list[RateBreak], qty: Decimal) -> Decimal:
    """Costo base con optimización de quiebre (declarar peso mayor si conviene)."""
    base_rate = _rate_for(breaks, qty)
    base = (qty * base_rate) if base_rate is not None else Decimal(0)
    for b in breaks:
        if b.from_qty > qty:
            candidate = b.from_qty * b.rate
            if candidate < base:
                base = candidate
    return base


def _apply_charges(db: Session, sheet: RateSheet, lane: RateLane, base: Decimal,
                   chargeable: Decimal, quantity: int, dangerous: bool) -> list[CostChargeLine]:
    charges = (
        db.query(RateCharge)
        .filter(
            RateCharge.deleted_at.is_(None),
            or_(RateCharge.rate_sheet_id == sheet.id, RateCharge.rate_lane_id == lane.id),
        )
        .all()
    )
    lines: list[CostChargeLine] = []
    for c in charges:
        if c.concept == "dgr" and not dangerous:
            continue
        v = c.value or Decimal(0)
        if c.charge_type == "fijo" or c.charge_type == "por_guia":
            amt = v
        elif c.charge_type == "por_kg":
            amt = v * chargeable
        elif c.charge_type == "por_contenedor":
            amt = v * quantity
        elif c.charge_type == "porcentaje":
            amt = base * v / Decimal(100)
        else:
            amt = v
        lines.append(CostChargeLine(concept=c.concept, amount=amt))
    return lines


def lane_locations(db: Session, tenant_id: int, company_id: int, mode: str) -> dict[str, list[str]]:
    """Orígenes/destinos existentes en los tarifarios activos de un modo.

    Alinea el cotizador con las rutas realmente cotizables (los códigos provienen
    de las lanes, por lo que el costeo siempre encontrará ruta).
    """
    sheets = _sheet_query(db, tenant_id, company_id).filter(
        RateSheet.mode == mode, RateSheet.status == "activo",
    ).all()
    origins: set[str] = set()
    destinations: set[str] = set()
    for sheet in sheets:
        lanes = db.query(RateLane).filter(
            RateLane.rate_sheet_id == sheet.id, RateLane.deleted_at.is_(None),
        ).all()
        for lane in lanes:
            origin = lane.origin or sheet.default_origin
            if origin:
                origins.add(origin)
            if lane.destination:
                destinations.add(lane.destination)
    return {"origins": sorted(origins), "destinations": sorted(destinations)}


def quote_cost(db: Session, tenant_id: int, company_id: int, req: CostRequest) -> list[CostOption]:
    on_date = req.on_date or date.today()
    sheets = _sheet_query(db, tenant_id, company_id).filter(
        RateSheet.mode == req.mode,
        RateSheet.status == "activo",
        or_(RateSheet.valid_from.is_(None), RateSheet.valid_from <= on_date),
        or_(RateSheet.valid_to.is_(None), RateSheet.valid_to >= on_date),
    ).all()

    gross = req.gross_weight_kg or Decimal(0)
    options: list[CostOption] = []
    for sheet in sheets:
        lanes_q = db.query(RateLane).filter(
            RateLane.rate_sheet_id == sheet.id, RateLane.deleted_at.is_(None),
        )
        if req.destination:
            lanes_q = lanes_q.filter(RateLane.destination == req.destination)
        for lane in lanes_q.all():
            # Origen: match exacto o el default del tarifario.
            lane_origin = lane.origin or sheet.default_origin
            if req.origin and lane_origin and lane_origin != req.origin:
                continue
            if req.mode == "maritimo_fcl":
                if req.equipment_type and lane.equipment_type and lane.equipment_type != req.equipment_type:
                    continue
                chargeable = Decimal(req.quantity)
                base = (lane.flat_rate or Decimal(0)) * req.quantity
                detail = f"{req.quantity} x {lane.equipment_type or 'contenedor'}"
            elif req.mode == "terrestre":
                chargeable = Decimal(req.quantity)
                base = (lane.flat_rate or Decimal(0)) * req.quantity
                detail = "tarifa por ruta"
            elif req.mode == "maritimo_lcl":
                tons = gross / Decimal(1000)
                wm = max(tons, req.volume_m3 or Decimal(0))
                brks = breaks_of(db, lane.id)
                base = _best_break_cost(brks, wm) if brks else Decimal(0)
                chargeable = wm
                base = max(base, lane.min_charge or Decimal(0))
                detail = f"W/M {wm.quantize(Decimal('0.01'))}"
            else:  # aereo
                # P/Vol por dimensiones (L×A×H×cant / 6000); si no hay dimensiones,
                # respaldo con el volumen en m³ × 167.
                vol_by_dims = air_volumetric_kg(req.length_cm, req.width_cm, req.height_cm, req.quantity)
                volumetric = vol_by_dims if vol_by_dims > 0 else _volumetric_kg(req.volume_m3)
                chargeable = max(gross, volumetric)
                brks = breaks_of(db, lane.id)
                base = _best_break_cost(brks, chargeable)
                base = max(base, lane.min_charge or Decimal(0))
                detail = f"facturable {chargeable.quantize(Decimal('0.01'))} kg (P/Vol)"

            charge_lines = _apply_charges(db, sheet, lane, base, chargeable, req.quantity, req.dangerous)
            total = base + sum((c.amount for c in charge_lines), Decimal(0))
            options.append(CostOption(
                rate_sheet_id=sheet.id, rate_sheet_name=sheet.name, supplier_id=sheet.supplier_id,
                currency=sheet.currency, chargeable=chargeable, base_cost=base,
                charges=charge_lines, total_cost=total, transit_days=lane.transit_days, detail=detail,
            ))
    options.sort(key=lambda o: o.total_cost)
    return options
