from datetime import date, datetime, timezone

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

from ..accounts.models import Account
from ..cases import service as cases_service
from ..common.folios import next_folio
from ..contacts.models import Contact
from ..pipelines.models import Pipeline, PipelineStage
from .dto import OpportunityCreate, OpportunityUpdate
from .models import Opportunity


def _get_scoped_stage(
    db: Session,
    stage_id: int,
    tenant_id: int,
    company_id: int,
    code: int = status.HTTP_404_NOT_FOUND,
    detail: str = "Etapa no encontrada",
) -> PipelineStage:
    stage = (
        db.query(PipelineStage)
        .filter(
            PipelineStage.id == stage_id,
            PipelineStage.tenant_id == tenant_id,
            PipelineStage.company_id == company_id,
            PipelineStage.deleted_at.is_(None),
        )
        .first()
    )
    if not stage:
        raise HTTPException(status_code=code, detail=detail)
    return stage


def _apply_stage_state(opportunity: Opportunity, stage: PipelineStage) -> None:
    """Deriva estado/probabilidad/cierre desde la etapa destino.

    Fuente única de verdad para create/update/move: la etapa manda. Así una
    oportunidad nunca queda 'open' en una etapa ganada/perdida (evita conteos
    inconsistentes en métricas).
    """
    opportunity.stage_id = stage.id
    opportunity.pipeline_id = stage.pipeline_id
    if stage.is_won:
        opportunity.status = "won"
        opportunity.probability = 100
        opportunity.closed_at = datetime.now(timezone.utc)
        opportunity.won_date = opportunity.won_date or date.today()
        opportunity.lost_date = None
    elif stage.is_lost:
        opportunity.status = "lost"
        opportunity.probability = 0
        opportunity.closed_at = datetime.now(timezone.utc)
        opportunity.lost_date = opportunity.lost_date or date.today()
        opportunity.won_date = None
    else:
        opportunity.status = "open"
        opportunity.probability = stage.probability
        opportunity.closed_at = None
        opportunity.won_date = None
        opportunity.lost_date = None


def _validate_refs(db: Session, data: dict, tenant_id: int, company_id: int) -> None:
    """Valida que cuenta, contacto y embudo existan en el tenant/company.

    La etapa se valida por separado (necesitamos el objeto para derivar estado),
    verificando además que pertenezca al embudo indicado cuando ambos vienen.
    """
    def scope(model, _id):
        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()
        )

    checks = [
        ("account_id", Account, "La cuenta asociada no existe"),
        ("contact_id", Contact, "El contacto asociado no existe"),
        ("pipeline_id", Pipeline, "El embudo asociado no existe"),
    ]
    for field, model, message in checks:
        value = data.get(field)
        if value is not None and not scope(model, value):
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=message)

    stage_id = data.get("stage_id")
    if stage_id is not None:
        stage = _get_scoped_stage(
            db, stage_id, tenant_id, company_id,
            code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="La etapa asociada no existe",
        )
        pipeline_id = data.get("pipeline_id")
        if pipeline_id is not None and stage.pipeline_id != pipeline_id:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="La etapa no pertenece al embudo indicado",
            )


def get_opportunities(
    db: Session,
    tenant_id: int,
    company_id: int,
    pipeline_id: int | None = None,
    stage_id: int | None = None,
    opp_status: str | None = None,
    search: str | None = None,
) -> list[Opportunity]:
    query = db.query(Opportunity).filter(
        Opportunity.tenant_id == tenant_id,
        Opportunity.company_id == company_id,
        Opportunity.deleted_at.is_(None),
    )
    if pipeline_id is not None:
        query = query.filter(Opportunity.pipeline_id == pipeline_id)
    if stage_id is not None:
        query = query.filter(Opportunity.stage_id == stage_id)
    if opp_status:
        query = query.filter(Opportunity.status == opp_status)
    if search:
        query = query.filter(Opportunity.name.ilike(f"%{search}%"))
    return query.order_by(Opportunity.created_at.desc()).all()


def get_opportunity(db: Session, opportunity_id: int, tenant_id: int, company_id: int) -> Opportunity:
    opportunity = (
        db.query(Opportunity)
        .filter(
            Opportunity.id == opportunity_id,
            Opportunity.tenant_id == tenant_id,
            Opportunity.company_id == company_id,
            Opportunity.deleted_at.is_(None),
        )
        .first()
    )
    if not opportunity:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Oportunidad no encontrada")
    return opportunity


def create_opportunity(
    db: Session, payload: OpportunityCreate, tenant_id: int, company_id: int
) -> Opportunity:
    data = payload.model_dump()
    _validate_refs(db, data, tenant_id, company_id)
    opportunity = Opportunity(**data, tenant_id=tenant_id, company_id=company_id)
    # Si se asigna etapa, esta manda sobre estado/probabilidad/cierre
    if opportunity.stage_id is not None:
        stage = _get_scoped_stage(db, opportunity.stage_id, tenant_id, company_id)
        _apply_stage_state(opportunity, stage)
    # Folio O... auto-generado (mensual). La dirección impo/expo se hereda al ciclo.
    if not opportunity.reference:
        opportunity.reference = next_folio(db, tenant_id, company_id, "O", opportunity.operation_type)
    # Expediente: nace con la oportunidad y se hereda a solicitud/cotización/operación/factura
    if not opportunity.case_id:
        case = cases_service.create_case(
            db, tenant_id, company_id, account_id=opportunity.account_id, title=opportunity.name, stage="oportunidad",
        )
        opportunity.case_id = case.id
    db.add(opportunity)
    db.commit()
    db.refresh(opportunity)
    return opportunity


def update_opportunity(
    db: Session, opportunity_id: int, payload: OpportunityUpdate, tenant_id: int, company_id: int
) -> Opportunity:
    opportunity = get_opportunity(db, opportunity_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(opportunity, field, value)
    # Cambiar de etapa vía PATCH también deriva estado (misma regla que move)
    if data.get("stage_id") is not None:
        stage = _get_scoped_stage(db, data["stage_id"], tenant_id, company_id)
        _apply_stage_state(opportunity, stage)
    db.commit()
    db.refresh(opportunity)
    return opportunity


def move_opportunity(
    db: Session, opportunity_id: int, stage_id: int, tenant_id: int, company_id: int
) -> Opportunity:
    """Mueve la oportunidad a una etapa y deriva estado/probabilidad de la etapa destino."""
    opportunity = get_opportunity(db, opportunity_id, tenant_id, company_id)
    stage = _get_scoped_stage(db, stage_id, tenant_id, company_id)
    _apply_stage_state(opportunity, stage)
    db.commit()
    db.refresh(opportunity)
    return opportunity


def delete_opportunity(db: Session, opportunity_id: int, tenant_id: int, company_id: int) -> None:
    opportunity = get_opportunity(db, opportunity_id, tenant_id, company_id)
    opportunity.deleted_at = datetime.now(timezone.utc)
    db.commit()
