"""Phase 6.2 Opportunity + Pipeline application services."""
from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal
from uuid import UUID, uuid4

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import CrmEventType
from app.models.foundation import Opportunity, Pipeline, PipelineStage
from app.models.sales_engine import (
    OpportunityWinLoss,
    SalesAudit,
    SalesStageHistory,
)
from app.models.types import OpportunityStatus, WinLossOutcome
from app.repositories.sales_engine import (
    OpportunityRepository,
    OpportunityWinLossRepository,
    PipelineRepository,
    PipelineStageRepository,
    SalesAuditRepository,
    SalesStageHistoryRepository,
)
from app.schemas.sales_engine import (
    OpportunityCreate,
    OpportunityLoseRequest,
    OpportunityStageChangeRequest,
    OpportunityUpdate,
    OpportunityWinRequest,
    PipelineCreate,
    PipelineStageCreate,
    PipelineStageUpdate,
    PipelineUpdate,
)
from app.validators import (
    validate_amount,
    validate_currency_code,
    validate_non_empty,
    validate_win_probability,
)
from app.validators.sales_engine import (
    ensure_not_reopen_won,
    ensure_opportunity_mutable,
    ensure_pipeline_active,
    ensure_stage_active,
    ensure_stage_belongs_to_pipeline,
    validate_lost_requires_reason,
    validate_probability,
    validate_stage_flags,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


def _next_opp_number() -> str:
    return f"OPP-{uuid4().hex[:10].upper()}"


class SalesAuditService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = SalesAuditRepository(session)

    async def record(
        self,
        *,
        tenant_id: UUID,
        entity_type: str,
        entity_id: UUID,
        action: str,
        actor_user_id: str | None = None,
        changes: dict | None = None,
        message: str | None = None,
    ) -> SalesAudit:
        entry = SalesAudit(
            tenant_id=tenant_id,
            entity_type=entity_type,
            entity_id=entity_id,
            action=action,
            actor_user_id=actor_user_id,
            changes=changes,
            message=message,
        )
        await self.repo.add(entry)
        return entry


class PipelineService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = PipelineRepository(session)
        self.stage_repo = PipelineStageRepository(session)
        self.audit = SalesAuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(self, tenant_id: UUID, body: PipelineCreate) -> Pipeline:
        name = validate_non_empty(body.name, field="name")
        code = validate_non_empty(body.code, field="code")
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError(
                "کد پایپ‌لاین تکراری است",
                status_code=409,
                error_code="pipeline_code_exists",
            )
        if body.is_default:
            for existing in await self.repo.list_by_tenant(tenant_id, limit=500):
                existing.is_default = False
        entity = Pipeline(
            tenant_id=tenant_id,
            name=name,
            code=code,
            description=body.description,
            sort_order=body.sort_order,
            is_default=body.is_default,
            is_active=body.is_active,
            status=body.status if body.is_active else "inactive",
        )
        await self.repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.PIPELINE_CREATED,
            aggregate_type="pipeline",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

    async def update(
        self, tenant_id: UUID, pipeline_id: UUID, body: PipelineUpdate
    ) -> Pipeline:
        entity = await self.repo.get(tenant_id, pipeline_id)
        if entity is None:
            raise NotFoundError("پایپ‌لاین یافت نشد", error_code="pipeline_not_found")
        data = body.model_dump(exclude_unset=True)
        if "name" in data:
            data["name"] = validate_non_empty(data["name"], field="name")
        if data.get("is_default") is True:
            for existing in await self.repo.list_by_tenant(tenant_id, limit=500):
                if existing.id != pipeline_id:
                    existing.is_default = False
        if "is_active" in data:
            data["status"] = "active" if data["is_active"] else "inactive"
        _apply_update(entity, data)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.PIPELINE_CHANGED,
            aggregate_type="pipeline",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "is_active": entity.is_active},
        )
        return entity

    async def get(self, tenant_id: UUID, pipeline_id: UUID) -> Pipeline:
        entity = await self.repo.get(tenant_id, pipeline_id)
        if entity is None:
            raise NotFoundError("پایپ‌لاین یافت نشد", error_code="pipeline_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def create_stage(
        self, tenant_id: UUID, body: PipelineStageCreate
    ) -> PipelineStage:
        pipeline = await self.repo.get(tenant_id, body.pipeline_id)
        if pipeline is None:
            raise NotFoundError("پایپ‌لاین یافت نشد", error_code="pipeline_not_found")
        validate_stage_flags(
            is_start_stage=body.is_start_stage,
            is_won_stage=body.is_won_stage,
            is_lost_stage=body.is_lost_stage,
        )
        existing = await self.stage_repo.list_by_pipeline(tenant_id, body.pipeline_id)
        if body.is_start_stage and any(s.is_start_stage for s in existing):
            raise AppError(
                "فقط یک مرحله شروع مجاز است",
                status_code=422,
                error_code="duplicate_start_stage",
            )
        if body.is_won_stage and any(s.is_won_stage for s in existing):
            raise AppError(
                "فقط یک مرحله برد مجاز است",
                status_code=422,
                error_code="duplicate_won_stage",
            )
        if body.is_lost_stage and any(s.is_lost_stage for s in existing):
            raise AppError(
                "فقط یک مرحله باخت مجاز است",
                status_code=422,
                error_code="duplicate_lost_stage",
            )
        name = validate_non_empty(body.name, field="name")
        code = validate_non_empty(body.code, field="code")
        validate_win_probability(body.win_probability)
        entity = PipelineStage(
            tenant_id=tenant_id,
            pipeline_id=body.pipeline_id,
            name=name,
            code=code,
            position=body.position,
            win_probability=body.win_probability,
            color=body.color,
            is_start_stage=body.is_start_stage,
            is_won_stage=body.is_won_stage,
            is_lost_stage=body.is_lost_stage,
            is_closed_stage=body.is_closed_stage or body.is_won_stage or body.is_lost_stage,
            required_fields=body.required_fields,
            exit_validation=body.exit_validation,
            is_active=body.is_active,
        )
        await self.stage_repo.add(entity)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.PIPELINE_STAGE_CREATED,
            aggregate_type="pipeline_stage",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"pipeline_id": str(entity.pipeline_id), "code": entity.code},
        )
        return entity

    async def update_stage(
        self, tenant_id: UUID, stage_id: UUID, body: PipelineStageUpdate
    ) -> PipelineStage:
        entity = await self.stage_repo.get(tenant_id, stage_id)
        if entity is None:
            raise NotFoundError("مرحله پایپ‌لاین یافت نشد", error_code="stage_not_found")
        data = body.model_dump(exclude_unset=True)
        if "name" in data:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "win_probability" in data and data["win_probability"] is not None:
            validate_win_probability(data["win_probability"])
        validate_stage_flags(
            is_start_stage=data.get("is_start_stage", entity.is_start_stage),
            is_won_stage=data.get("is_won_stage", entity.is_won_stage),
            is_lost_stage=data.get("is_lost_stage", entity.is_lost_stage),
        )
        _apply_update(entity, data)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_stages(
        self, tenant_id: UUID, pipeline_id: UUID, *, offset: int, limit: int
    ):
        pipeline = await self.repo.get(tenant_id, pipeline_id)
        if pipeline is None:
            raise NotFoundError("پایپ‌لاین یافت نشد", error_code="pipeline_not_found")
        return await self.stage_repo.list_by_pipeline(
            tenant_id, pipeline_id, offset=offset, limit=limit
        )

    async def get_stage(self, tenant_id: UUID, stage_id: UUID) -> PipelineStage:
        entity = await self.stage_repo.get(tenant_id, stage_id)
        if entity is None:
            raise NotFoundError("مرحله پایپ‌لاین یافت نشد", error_code="stage_not_found")
        return entity


class OpportunityService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = OpportunityRepository(session)
        self.pipeline_repo = PipelineRepository(session)
        self.stage_repo = PipelineStageRepository(session)
        self.history_repo = SalesStageHistoryRepository(session)
        self.win_loss_repo = OpportunityWinLossRepository(session)
        self.audit = SalesAuditService(session)
        self.publisher = publisher or get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: OpportunityCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Opportunity:
        actor_id = actor.user_id if actor else None
        title = validate_non_empty(body.title or body.name, field="title")
        name = validate_non_empty(body.name or body.title, field="name")
        amount = validate_amount(body.amount)
        expected_revenue = validate_amount(
            body.expected_revenue if body.expected_revenue is not None else amount
        )
        currency = validate_currency_code(body.currency_code)
        probability = validate_probability(body.probability)
        opp_number = (body.opportunity_number or "").strip() or _next_opp_number()
        if await self.repo.get_by_number(tenant_id, opp_number) is not None:
            raise AppError(
                "شماره فرصت تکراری است",
                status_code=409,
                error_code="opportunity_number_exists",
            )
        pipeline = await self.pipeline_repo.get(tenant_id, body.pipeline_id)
        if pipeline is None:
            raise NotFoundError("پایپ‌لاین یافت نشد", error_code="pipeline_not_found")
        ensure_pipeline_active(pipeline)
        stage = await self.stage_repo.get(tenant_id, body.stage_id)
        if stage is None:
            raise NotFoundError("مرحله پایپ‌لاین یافت نشد", error_code="stage_not_found")
        ensure_stage_active(stage)
        ensure_stage_belongs_to_pipeline(stage, body.pipeline_id)
        if probability == 0 and stage.win_probability:
            probability = stage.win_probability
        entity = Opportunity(
            tenant_id=tenant_id,
            opportunity_number=opp_number,
            name=name,
            title=title,
            pipeline_id=body.pipeline_id,
            stage_id=body.stage_id,
            status=OpportunityStatus.OPEN,
            amount=amount,
            expected_revenue=expected_revenue,
            currency_code=currency,
            probability=probability,
            priority=body.priority,
            expected_close_date=body.expected_close_date,
            organization_id=body.organization_id,
            contact_id=body.contact_id,
            lead_id=body.lead_id,
            owner_user_id=body.owner_user_id,
            description=body.description,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        now = datetime.now(timezone.utc)
        await self.history_repo.add(
            SalesStageHistory(
                tenant_id=tenant_id,
                opportunity_id=entity.id,
                previous_stage_id=None,
                next_stage_id=entity.stage_id,
                changed_by=actor_id,
                changed_at=now,
                duration_seconds=None,
                reason="created",
            )
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="opportunity",
            entity_id=entity.id,
            action="create",
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.OPPORTUNITY_CREATED,
            aggregate_type="opportunity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "opportunity_number": entity.opportunity_number,
                "amount": str(entity.amount),
            },
        )
        return entity

    async def update(
        self,
        tenant_id: UUID,
        opportunity_id: UUID,
        body: OpportunityUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Opportunity:
        entity = await self.repo.get(tenant_id, opportunity_id)
        if entity is None:
            raise NotFoundError("فرصت یافت نشد", error_code="opportunity_not_found")
        ensure_opportunity_mutable(entity)
        ensure_not_reopen_won(entity)
        actor_id = actor.user_id if actor else None
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
            data.setdefault("title", data["name"])
        if "title" in data and data["title"] is not None:
            data["title"] = validate_non_empty(data["title"], field="title")
            data.setdefault("name", data["title"])
        if "amount" in data and data["amount"] is not None:
            data["amount"] = validate_amount(data["amount"])
        if "expected_revenue" in data and data["expected_revenue"] is not None:
            data["expected_revenue"] = validate_amount(data["expected_revenue"])
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        if "probability" in data and data["probability"] is not None:
            data["probability"] = validate_probability(data["probability"])
        if "stage_id" in data and data["stage_id"] is not None:
            stage = await self.stage_repo.get(tenant_id, data["stage_id"])
            if stage is None:
                raise NotFoundError(
                    "مرحله پایپ‌لاین یافت نشد", error_code="stage_not_found"
                )
            ensure_stage_active(stage)
            ensure_stage_belongs_to_pipeline(stage, entity.pipeline_id)
        _apply_update(entity, data)
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="opportunity",
            entity_id=entity.id,
            action="update",
            actor_user_id=actor_id,
            changes=data,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.OPPORTUNITY_UPDATED,
            aggregate_type="opportunity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value},
        )
        return entity

    async def change_stage(
        self,
        tenant_id: UUID,
        opportunity_id: UUID,
        body: OpportunityStageChangeRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Opportunity:
        entity = await self.repo.get(tenant_id, opportunity_id)
        if entity is None:
            raise NotFoundError("فرصت یافت نشد", error_code="opportunity_not_found")
        ensure_opportunity_mutable(entity)
        stage = await self.stage_repo.get(tenant_id, body.stage_id)
        if stage is None:
            raise NotFoundError("مرحله پایپ‌لاین یافت نشد", error_code="stage_not_found")
        ensure_stage_active(stage)
        ensure_stage_belongs_to_pipeline(stage, entity.pipeline_id)
        actor_id = actor.user_id if actor else None
        previous = entity.stage_id
        now = datetime.now(timezone.utc)
        duration = None
        latest = await self.history_repo.latest_for_opportunity(tenant_id, opportunity_id)
        if latest is not None and latest.changed_at is not None:
            previous_at = latest.changed_at
            if previous_at.tzinfo is None:
                previous_at = previous_at.replace(tzinfo=timezone.utc)
            duration = int((now - previous_at).total_seconds())
        await self.history_repo.add(
            SalesStageHistory(
                tenant_id=tenant_id,
                opportunity_id=entity.id,
                previous_stage_id=previous,
                next_stage_id=body.stage_id,
                changed_by=actor_id,
                changed_at=now,
                duration_seconds=duration,
                reason=body.reason,
            )
        )
        entity.stage_id = body.stage_id
        entity.probability = stage.win_probability
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="opportunity",
            entity_id=entity.id,
            action="stage_change",
            actor_user_id=actor_id,
            changes={
                "previous_stage_id": str(previous),
                "next_stage_id": str(body.stage_id),
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.STAGE_CHANGED,
            aggregate_type="opportunity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "previous_stage_id": str(previous),
                "stage_id": str(entity.stage_id),
            },
        )
        return entity

    async def mark_won(
        self,
        tenant_id: UUID,
        opportunity_id: UUID,
        body: OpportunityWinRequest | None = None,
        *,
        actor: CurrentUser | None = None,
    ) -> Opportunity:
        entity = await self.repo.get(tenant_id, opportunity_id)
        if entity is None:
            raise NotFoundError("فرصت یافت نشد", error_code="opportunity_not_found")
        ensure_opportunity_mutable(entity)
        actor_id = actor.user_id if actor else None
        now = datetime.now(timezone.utc)
        entity.status = OpportunityStatus.WON
        entity.won_at = now
        entity.probability = 100
        entity.updated_by = actor_id
        win_body = body or OpportunityWinRequest()
        await self.win_loss_repo.add(
            OpportunityWinLoss(
                tenant_id=tenant_id,
                opportunity_id=entity.id,
                outcome=WinLossOutcome.WON,
                reason_id=win_body.reason_id,
                category_id=win_body.category_id,
                sub_category=win_body.sub_category,
                root_cause=win_body.root_cause,
                competitor_name=win_body.competitor_name,
                price_factor=win_body.price_factor,
                feature_gap=win_body.feature_gap,
                timing_factor=win_body.timing_factor,
                relationship_factor=win_body.relationship_factor,
                approval_factor=win_body.approval_factor,
                custom_reason=win_body.custom_reason,
                recorded_by=actor_id,
                recorded_at=now,
                is_locked=True,
            )
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="opportunity",
            entity_id=entity.id,
            action="won",
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.OPPORTUNITY_WON,
            aggregate_type="opportunity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"amount": str(entity.amount)},
        )
        return entity

    async def mark_lost(
        self,
        tenant_id: UUID,
        opportunity_id: UUID,
        body: OpportunityLoseRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Opportunity:
        entity = await self.repo.get(tenant_id, opportunity_id)
        if entity is None:
            raise NotFoundError("فرصت یافت نشد", error_code="opportunity_not_found")
        ensure_opportunity_mutable(entity)
        validate_lost_requires_reason(body.reason_id, body.custom_reason)
        actor_id = actor.user_id if actor else None
        now = datetime.now(timezone.utc)
        entity.status = OpportunityStatus.LOST
        entity.lost_at = now
        entity.probability = 0
        entity.updated_by = actor_id
        await self.win_loss_repo.add(
            OpportunityWinLoss(
                tenant_id=tenant_id,
                opportunity_id=entity.id,
                outcome=WinLossOutcome.LOST,
                reason_id=body.reason_id,
                category_id=body.category_id,
                sub_category=body.sub_category,
                root_cause=body.root_cause,
                competitor_name=body.competitor_name,
                price_factor=body.price_factor,
                feature_gap=body.feature_gap,
                timing_factor=body.timing_factor,
                relationship_factor=body.relationship_factor,
                approval_factor=body.approval_factor,
                custom_reason=body.custom_reason,
                recorded_by=actor_id,
                recorded_at=now,
                is_locked=True,
            )
        )
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="opportunity",
            entity_id=entity.id,
            action="lost",
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=CrmEventType.OPPORTUNITY_LOST,
            aggregate_type="opportunity",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"amount": str(entity.amount)},
        )
        return entity

    async def get(self, tenant_id: UUID, opportunity_id: UUID) -> Opportunity:
        entity = await self.repo.get(tenant_id, opportunity_id)
        if entity is None:
            raise NotFoundError("فرصت یافت نشد", error_code="opportunity_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
