# FILE: src/apps/sales/services/opportunity_service.py
from __future__ import annotations

from datetime import datetime, timezone
from typing import Optional
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from fastapi import HTTPException

from src.core.constants import LOST_ELIGIBLE_STAGES, OpportunityStage, STAGE_ORDER, TERMINAL_STAGES
from src.core.exceptions import NotFoundError
from src.apps.auth.models.user import User
from src.apps.sales.models.opportunity import Opportunity
from src.apps.sales.models.activity import Activity
from src.apps.sales.models.contract import Contract


class OpportunityService:

    @staticmethod
    async def get_kanban(
        db: AsyncSession,
        tenant_id: str,
        search: Optional[str] = None,
        assigned_to: Optional[UUID] = None,
    ) -> dict:
        filters = [
            Opportunity.tenant_id == tenant_id,
            Opportunity.deleted_at.is_(None),
        ]
        if search:
            filters.append(Opportunity.family_name.ilike(f"%{search}%"))
        if assigned_to:
            filters.append(Opportunity.assigned_to == assigned_to)

        result = await db.execute(
            select(Opportunity)
            .where(*filters)
            .order_by(Opportunity.stage_entered_at.asc())
        )
        opportunities = result.scalars().all()

        opportunity_ids = [opp.id for opp in opportunities]
        contract_by_opportunity: dict = {}
        if opportunity_ids:
            contract_result = await db.execute(
                select(Contract)
                .where(
                    Contract.tenant_id == tenant_id,
                    Contract.deleted_at.is_(None),
                    Contract.opportunity_id.in_(opportunity_ids),
                )
                .order_by(Contract.opportunity_id, Contract.created_at.desc())
            )
            for contract in contract_result.scalars().all():
                # Most recent non-deleted contract per opportunity wins (first seen due to ORDER BY).
                contract_by_opportunity.setdefault(contract.opportunity_id, contract.id)

        now = datetime.now(timezone.utc)
        stages = [s.value for s in OpportunityStage]
        kanban: dict = {stage: [] for stage in stages}

        for opp in opportunities:
            stage_key = opp.stage if opp.stage in kanban else OpportunityStage.INQUIRY.value
            if opp.stage_entered_at:
                entered = opp.stage_entered_at
                if entered.tzinfo is None:
                    entered = entered.replace(tzinfo=timezone.utc)
                days_in_stage = (now - entered).days
            else:
                days_in_stage = 0

            kanban[stage_key].append({
                "id": opp.id,
                "family_name": opp.family_name,
                "care_type": opp.care_type,
                "section_id": opp.section_id,
                "plot_id": opp.plot_id,
                "estimated_value": opp.estimated_value,
                "contract_id": contract_by_opportunity.get(opp.id),
                "stage": opp.stage,
                "stage_entered_at": opp.stage_entered_at,
                "assigned_to": opp.assigned_to,
                "days_in_stage": days_in_stage,
                "next_action": opp.next_action,
                "notes": opp.notes,
                "created_at": opp.created_at,
                "updated_at": opp.updated_at,
            })

        return kanban

    @staticmethod
    async def get_by_id(
        db: AsyncSession,
        tenant_id: str,
        opp_id: UUID,
    ) -> Opportunity:
        result = await db.execute(
            select(Opportunity)
            .options(selectinload(Opportunity.activities))
            .where(
                Opportunity.id == opp_id,
                Opportunity.tenant_id == tenant_id,
                Opportunity.deleted_at.is_(None),
            )
        )
        opp = result.scalar_one_or_none()
        if not opp:
            raise NotFoundError("Opportunity not found")
        return opp

    @staticmethod
    async def get_activities(
        db: AsyncSession,
        tenant_id: str,
        opp_id: UUID,
    ) -> list[dict]:
        # Confirms the opportunity exists (and is tenant-scoped) before listing its activities.
        await OpportunityService.get_by_id(db, tenant_id, opp_id)

        result = await db.execute(
            select(Activity, User)
            .outerjoin(User, Activity.user_id == User.id)
            .where(
                Activity.tenant_id == tenant_id,
                Activity.opportunity_id == opp_id,
            )
            .order_by(Activity.created_at.desc())
        )
        return [
            {
                "id": activity.id,
                "activity_type": activity.activity_type,
                "subject": activity.subject,
                "notes": activity.notes,
                "activity_date": activity.activity_date,
                "created_at": activity.created_at,
                "user_name": user.full_name if user else None,
            }
            for activity, user in result.all()
        ]

    @staticmethod
    async def create(
        db: AsyncSession,
        tenant_id: str,
        data: dict,
        current_user,
    ) -> Opportunity:
        opp = Opportunity(
            tenant_id=tenant_id,
            stage=OpportunityStage.INQUIRY.value,
            stage_entered_at=datetime.now(timezone.utc),
            **data,
        )
        db.add(opp)
        await db.flush()

        activity = Activity(
            tenant_id=tenant_id,
            opportunity_id=opp.id,
            user_id=current_user.id,
            activity_type="created",
            subject="Opportunity created",
        )
        db.add(activity)
        await db.flush()
        await db.refresh(opp)
        return opp

    @staticmethod
    def _validate_stage_transition(old_stage: str, new_stage: str) -> None:
        """Enforce forward-only pipeline movement and block 'lost' once the contract is signed."""
        if old_stage in TERMINAL_STAGES:
            raise HTTPException(
                status_code=422,
                detail=f"Opportunity is in a terminal stage ('{old_stage}') and cannot be transitioned.",
            )

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

        if new_stage in STAGE_ORDER and old_stage in STAGE_ORDER:
            if STAGE_ORDER[new_stage] < STAGE_ORDER[old_stage]:
                raise HTTPException(
                    status_code=422,
                    detail=f"Cannot move from '{old_stage}' back to '{new_stage}'.",
                )

    @staticmethod
    async def update(
        db: AsyncSession,
        tenant_id: str,
        opp_id: UUID,
        data: dict,
        current_user,
    ) -> Opportunity:
        opp = await OpportunityService.get_by_id(db, tenant_id, opp_id)

        old_stage = opp.stage
        new_stage = data.get("stage")
        if new_stage is not None:
            new_stage = OpportunityStage(new_stage).value
            data["stage"] = new_stage
        stage_changed = new_stage is not None and new_stage != old_stage

        if stage_changed:
            OpportunityService._validate_stage_transition(old_stage, new_stage)

        for field, value in data.items():
            setattr(opp, field, value)

        if stage_changed:
            opp.stage_entered_at = datetime.now(timezone.utc)

        await db.flush()

        activity_type = "stage_changed" if stage_changed else "updated"
        subject = f"Stage changed to {new_stage}" if stage_changed else "Opportunity updated"
        activity = Activity(
            tenant_id=tenant_id,
            opportunity_id=opp.id,
            user_id=current_user.id,
            activity_type=activity_type,
            subject=subject,
        )
        db.add(activity)
        await db.flush()
        await db.refresh(opp)
        return opp

    @staticmethod
    async def soft_delete(
        db: AsyncSession,
        tenant_id: str,
        opp_id: UUID,
        lost_reason: Optional[str],
        current_user,
    ) -> Opportunity:
        opp = await OpportunityService.get_by_id(db, tenant_id, opp_id)

        opp.soft_delete()
        opp.stage = OpportunityStage.LOST.value
        opp.lost_reason = lost_reason

        activity = Activity(
            tenant_id=tenant_id,
            opportunity_id=opp.id,
            user_id=current_user.id,
            activity_type="marked_lost",
            subject="Opportunity marked as lost",
            notes=lost_reason,
        )
        db.add(activity)
        await db.flush()
        await db.refresh(opp)
        return opp
