"""Service for opportunity stage transitions."""
from datetime import datetime, timezone
from uuid import UUID

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.core.constants import LOST_ELIGIBLE_STAGES, STAGE_ORDER, STAGE_TRANSITIONS, TERMINAL_STAGES
from src.apps.sales.models.opportunity import Opportunity


class StageService:
    @staticmethod
    async def _get_opportunity(
        db: AsyncSession, tenant_id: UUID, opportunity_id: UUID
    ) -> Opportunity:
        result = await db.execute(
            select(Opportunity).where(
                Opportunity.id == opportunity_id,
                Opportunity.tenant_id == tenant_id,
                Opportunity.deleted_at.is_(None),
            )
        )
        opp = result.scalar_one_or_none()
        if not opp:
            raise HTTPException(status_code=404, detail="Opportunity not found")
        return opp

    @staticmethod
    async def transition_stage(
        db: AsyncSession,
        tenant_id: UUID,
        opportunity_id: UUID,
        new_stage: str,
    ) -> Opportunity:
        opp = await StageService._get_opportunity(db, tenant_id, opportunity_id)
        current = opp.stage

        if current in TERMINAL_STAGES:
            raise HTTPException(
                status_code=422,
                detail=f"Opportunity is in a terminal stage ('{current}') and cannot be transitioned.",
            )

        allowed = STAGE_TRANSITIONS.get(current, [])
        if new_stage not in allowed:
            allowed_str = ", ".join(allowed) if allowed else "none"
            raise HTTPException(
                status_code=422,
                detail=f"Cannot transition from '{current}' to '{new_stage}'. Allowed next stages: {allowed_str}.",
            )

        # Forward-only guard: never allow moving back to an earlier pipeline stage,
        # even if it were ever added to STAGE_TRANSITIONS by mistake.
        if new_stage in STAGE_ORDER and current in STAGE_ORDER:
            if STAGE_ORDER[new_stage] < STAGE_ORDER[current]:
                raise HTTPException(
                    status_code=422,
                    detail=f"Cannot move from '{current}' back to '{new_stage}'.",
                )

        opp.stage = new_stage
        opp.stage_entered_at = datetime.now(timezone.utc)
        await db.flush()
        return opp

    @staticmethod
    async def mark_lost(
        db: AsyncSession,
        tenant_id: UUID,
        opportunity_id: UUID,
        loss_reason: str,
        loss_notes: str | None = None,
    ) -> Opportunity:
        opp = await StageService._get_opportunity(db, tenant_id, opportunity_id)

        if opp.stage == "lost":
            raise HTTPException(
                status_code=422, detail="Opportunity is already marked as lost."
            )

        if opp.stage not in LOST_ELIGIBLE_STAGES:
            raise HTTPException(
                status_code=422,
                detail=f"Opportunity is in '{opp.stage}' — it can no longer be marked as lost once the contract is signed.",
            )

        opp.lost_from_stage = opp.stage
        opp.lost_reason = loss_reason
        opp.lost_notes = loss_notes
        opp.lost_at = datetime.now(timezone.utc)
        opp.stage = "lost"
        opp.stage_entered_at = datetime.now(timezone.utc)
        await db.flush()
        return opp
