from __future__ import annotations

from typing import Any

from sqlalchemy import select

from services.sales_service import sales_events as sales_event_types
from services.sales_service.event_publisher import SalesEventPublisher
from services.shared.core import new_id, utc_now_iso
from services.shared.sales_sql_models import (
    SalesConditionRow,
    SalesCounterpartyRow,
    SalesDealRow,
    SalesDocumentRow,
    SalesInvoiceRow,
    SalesOfferRow,
    SalesPaymentRow,
    SalesPipelineStageRow,
    SalesStageHistoryRow,
)


ALLOWED_TRANSITIONS: dict[str, list[str]] = {
    "new_qualified_lead": [
        "warm_lead",
        "hot_lead",
        "enrichment_required",
        "active_text_communication",
        "active_voice_communication",
        "lost",
    ],
    "warm_lead": [
        "active_text_communication",
        "active_voice_communication",
        "enrichment_required",
        "lost",
    ],
    "hot_lead": [
        "active_text_communication",
        "active_voice_communication",
        "offer_selection",
        "lost",
    ],
    "enrichment_required": [
        "active_text_communication",
        "active_voice_communication",
        "need_clarification",
        "lost",
    ],
    "active_text_communication": [
        "waiting_customer_reply",
        "need_clarification",
        "need_confirmed",
        "active_voice_communication",
        "follow_up_scheduled",
        "transferred_to_support",
        "lost",
    ],
    "active_voice_communication": [
        "waiting_customer_reply",
        "need_clarification",
        "need_confirmed",
        "active_text_communication",
        "follow_up_scheduled",
        "transferred_to_support",
        "lost",
    ],
    "waiting_customer_reply": [
        "active_text_communication",
        "active_voice_communication",
        "follow_up_scheduled",
        "lost",
    ],
    "need_clarification": [
        "active_text_communication",
        "active_voice_communication",
        "need_confirmed",
        "transferred_to_support",
        "lost",
    ],
    "need_confirmed": [
        "offer_selection",
        "offer_preparing",
        "transferred_to_support",
        "lost",
    ],
    "offer_selection": [
        "offer_preparing",
        "conditions_negotiation",
        "lost",
    ],
    "offer_preparing": [
        "offer_sent",
        "conditions_negotiation",
        "lost",
    ],
    "offer_sent": [
        "conditions_negotiation",
        "counterparty_data_requested",
        "follow_up_scheduled",
        "lost",
    ],
    "conditions_negotiation": [
        "counterparty_data_requested",
        "counterparty_data_received",
        "document_preparing",
        "invoice_preparing",
        "transferred_to_support",
        "lost",
    ],
    "counterparty_data_requested": [
        "counterparty_data_received",
        "follow_up_scheduled",
        "lost",
    ],
    "counterparty_data_received": [
        "document_preparing",
        "invoice_preparing",
        "lost",
    ],
    "document_preparing": [
        "document_sent",
        "lost",
    ],
    "document_sent": [
        "document_under_review",
        "document_confirmed",
        "follow_up_scheduled",
        "lost",
    ],
    "document_under_review": [
        "document_confirmed",
        "transferred_to_support",
        "lost",
    ],
    "document_confirmed": [
        "invoice_preparing",
        "invoice_sent",
        "lost",
    ],
    "invoice_preparing": [
        "invoice_sent",
        "lost",
    ],
    "invoice_sent": [
        "payment_expected",
        "partially_paid",
        "paid",
        "payment_overdue",
        "lost",
    ],
    "payment_expected": [
        "partially_paid",
        "paid",
        "payment_overdue",
        "lost",
    ],
    "partially_paid": [
        "paid",
        "payment_overdue",
        "lost",
    ],
    "payment_overdue": [
        "payment_expected",
        "partially_paid",
        "paid",
        "lost",
    ],
    "paid": [
        "won",
        "transferred_to_execution",
        "transferred_to_support",
    ],
    "follow_up_scheduled": [
        "active_text_communication",
        "active_voice_communication",
        "waiting_customer_reply",
        "lost",
    ],
    "postponed": [
        "follow_up_scheduled",
        "active_text_communication",
        "active_voice_communication",
        "lost",
    ],
    "transferred_to_support": [
        "active_text_communication",
        "active_voice_communication",
        "conditions_negotiation",
        "lost",
        "won",
    ],
    "won": [],
    "lost": [],
    "transferred_to_execution": [],
}

LEGACY_STAGE_ALIASES = {
    "new": "new_qualified_lead",
    "active_text": "active_text_communication",
    "invoice": "invoice_sent",
    "paid": "paid",
    "won": "won",
}
TERMINAL_STAGE_CODES = {"won", "lost", "transferred_to_execution"}
SCENARIOS_REQUIRING_OFFER = {"quotation_based_sale", "booking_based_sale", "subscription_sale"}


class DealStateMachineError(Exception):
    def __init__(
        self,
        *,
        status_code: int,
        error: str,
        message: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.error = error
        self.message = message
        self.details = details or {}

    def response(self) -> dict[str, Any]:
        return {"error": self.error, "message": self.message, "details": self.details}


def _raise(status_code: int, error: str, message: str, details: dict[str, Any] | None = None) -> None:
    raise DealStateMachineError(status_code=status_code, error=error, message=message, details=details)


def _normalize_stage_code(value: str | None) -> str:
    code = str(value or "").strip()
    return LEGACY_STAGE_ALIASES.get(code, code)


def _get_deal(session, *, tenant_id: str, deal_id: str) -> SalesDealRow:
    row = session.execute(
        select(SalesDealRow).where(
            SalesDealRow.tenant_id == tenant_id,
            SalesDealRow.deal_id == deal_id,
        )
    ).scalar_one_or_none()
    if row is None:
        _raise(404, "deal_not_found", "Deal not found", {"deal_id": deal_id})
    return row


def _get_current_stage(session, deal: SalesDealRow) -> SalesPipelineStageRow:
    row = session.execute(
        select(SalesPipelineStageRow).where(
            SalesPipelineStageRow.tenant_id == deal.tenant_id,
            SalesPipelineStageRow.stage_id == deal.stage_id,
        )
    ).scalar_one_or_none()
    if row is None:
        _raise(
            404,
            "stage_not_found",
            "Current deal stage not found",
            {"deal_id": deal.deal_id, "stage_id": deal.stage_id},
        )
    if row.pipeline_id != deal.pipeline_id:
        _raise(
            400,
            "invalid_stage_pipeline",
            "Current deal stage does not belong to deal pipeline",
            {
                "deal_id": deal.deal_id,
                "stage_id": row.stage_id,
                "stage_pipeline_id": row.pipeline_id,
                "deal_pipeline_id": deal.pipeline_id,
            },
        )
    return row


def _get_target_stage(session, deal: SalesDealRow, target_stage_code: str) -> SalesPipelineStageRow:
    code = _normalize_stage_code(target_stage_code)
    row = session.execute(
        select(SalesPipelineStageRow).where(
            SalesPipelineStageRow.tenant_id == deal.tenant_id,
            SalesPipelineStageRow.pipeline_id == deal.pipeline_id,
            SalesPipelineStageRow.code == code,
        )
    ).scalar_one_or_none()
    if row is None:
        _raise(
            404,
            "stage_not_found",
            "Target stage not found",
            {"deal_id": deal.deal_id, "target_stage_code": code},
        )
    if not row.is_active:
        _raise(
            400,
            "stage_inactive",
            "Target stage is inactive",
            {"stage_id": row.stage_id, "stage_code": row.code},
        )
    return row


def _first_offer(session, deal: SalesDealRow, statuses: set[str] | None = None) -> SalesOfferRow | None:
    stmt = select(SalesOfferRow).where(
        SalesOfferRow.tenant_id == deal.tenant_id,
        SalesOfferRow.deal_id == deal.deal_id,
    )
    if statuses is not None:
        stmt = stmt.where(SalesOfferRow.status.in_(statuses))
    return session.execute(stmt.order_by(SalesOfferRow.updated_at.desc(), SalesOfferRow.id.desc())).scalars().first()


def _counterparty(session, deal: SalesDealRow) -> SalesCounterpartyRow | None:
    return session.execute(
        select(SalesCounterpartyRow).where(
            SalesCounterpartyRow.tenant_id == deal.tenant_id,
            SalesCounterpartyRow.deal_id == deal.deal_id,
        )
    ).scalar_one_or_none()


def _first_document(session, deal: SalesDealRow, statuses: set[str] | None = None) -> SalesDocumentRow | None:
    stmt = select(SalesDocumentRow).where(
        SalesDocumentRow.tenant_id == deal.tenant_id,
        SalesDocumentRow.deal_id == deal.deal_id,
    )
    if statuses is not None:
        stmt = stmt.where(SalesDocumentRow.status.in_(statuses))
    return session.execute(stmt.order_by(SalesDocumentRow.updated_at.desc(), SalesDocumentRow.id.desc())).scalars().first()


def _first_invoice(session, deal: SalesDealRow, statuses: set[str] | None = None) -> SalesInvoiceRow | None:
    stmt = select(SalesInvoiceRow).where(
        SalesInvoiceRow.tenant_id == deal.tenant_id,
        SalesInvoiceRow.deal_id == deal.deal_id,
    )
    if statuses is not None:
        stmt = stmt.where(SalesInvoiceRow.status.in_(statuses))
    return session.execute(stmt.order_by(SalesInvoiceRow.updated_at.desc(), SalesInvoiceRow.id.desc())).scalars().first()


def _successful_payment(session, deal: SalesDealRow) -> SalesPaymentRow | None:
    return session.execute(
        select(SalesPaymentRow)
        .where(
            SalesPaymentRow.tenant_id == deal.tenant_id,
            SalesPaymentRow.deal_id == deal.deal_id,
            SalesPaymentRow.status == "success",
        )
        .order_by(SalesPaymentRow.updated_at.desc(), SalesPaymentRow.id.desc())
    ).scalars().first()


def _confirmed_conditions(session, deal: SalesDealRow) -> SalesConditionRow | None:
    return session.execute(
        select(SalesConditionRow)
        .where(
            SalesConditionRow.tenant_id == deal.tenant_id,
            SalesConditionRow.deal_id == deal.deal_id,
            SalesConditionRow.confirmed_at.is_not(None),
        )
        .order_by(SalesConditionRow.updated_at.desc(), SalesConditionRow.id.desc())
    ).scalars().first()


def _has_paid_invoice_or_payment(session, deal: SalesDealRow) -> bool:
    return _first_invoice(session, deal, {"paid"}) is not None or _successful_payment(session, deal) is not None


def _scenario_allows_transition(deal: SalesDealRow, from_code: str, to_code: str) -> bool:
    if deal.scenario_type == "quick_sale" and from_code == "need_confirmed":
        return to_code in {"invoice_preparing", "invoice_sent"}
    if deal.scenario_type == "custom_human_escalation" and to_code == "transferred_to_support":
        return True
    return False


def _validate_allowed_transition(
    deal: SalesDealRow,
    from_stage: SalesPipelineStageRow,
    to_stage: SalesPipelineStageRow,
) -> None:
    if from_stage.stage_id == to_stage.stage_id:
        return
    allowed = ALLOWED_TRANSITIONS.get(from_stage.code, [])
    if to_stage.code in allowed or _scenario_allows_transition(deal, from_stage.code, to_stage.code):
        return
    _raise(
        400,
        "invalid_stage_transition",
        f"Cannot transition deal from {from_stage.code} to {to_stage.code}",
        {
            "deal_id": deal.deal_id,
            "from_stage_code": from_stage.code,
            "to_stage_code": to_stage.code,
        },
    )


def _validate_preconditions(
    session,
    *,
    deal: SalesDealRow,
    to_stage: SalesPipelineStageRow,
    reason: str | None,
    metadata: dict[str, Any],
) -> None:
    target = to_stage.code
    if target == "need_confirmed" and not str(deal.need_summary or "").strip():
        _raise(
            400,
            "missing_required_need_summary",
            "need_summary is required before moving deal to need_confirmed",
            {"deal_id": deal.deal_id, "to_stage_code": target},
        )

    if target == "invoice_preparing" and deal.scenario_type in SCENARIOS_REQUIRING_OFFER:
        if _first_offer(session, deal) is None:
            _raise(
                400,
                "offer_required_before_invoice",
                "Offer is required before invoice preparation for this scenario",
                {"deal_id": deal.deal_id, "scenario_type": deal.scenario_type},
            )

    if target == "offer_sent" and _first_offer(session, deal, {"draft", "ready", "sent"}) is None:
        _raise(
            400,
            "offer_required_before_offer_sent",
            "Offer is required before moving deal to offer_sent",
            {"deal_id": deal.deal_id, "to_stage_code": target},
        )

    if target == "counterparty_data_received":
        counterparty = _counterparty(session, deal)
        status = str(counterparty.completeness_status if counterparty else "").strip().lower()
        if status not in {"complete", "completed", "partial_acceptable"}:
            _raise(
                400,
                "counterparty_required_before_document",
                "Complete or acceptable counterparty data is required",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )

    if target == "document_sent" and _first_document(session, deal, {"rendered", "sent"}) is None:
        _raise(
            400,
            "document_required_before_document_sent",
            "Rendered or sent document is required before moving deal to document_sent",
            {"deal_id": deal.deal_id, "to_stage_code": target},
        )

    if target == "invoice_sent":
        if _first_invoice(session, deal, {"issued", "sent"}) is None:
            _raise(
                400,
                "invoice_required_before_invoice_sent",
                "Issued or sent invoice is required before moving deal to invoice_sent",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )
        if deal.document_required and _first_document(session, deal, {"confirmed", "signed"}) is None:
            _raise(
                400,
                "document_confirmed_required_before_invoice_sent",
                "Confirmed document is required before sending invoice",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )

    if target == "paid" and not _has_paid_invoice_or_payment(session, deal):
        _raise(
            400,
            "payment_required_before_paid",
            "Successful payment or paid invoice is required before moving deal to paid",
            {"deal_id": deal.deal_id, "to_stage_code": target},
        )

    if target == "won":
        if deal.payment_required and not _has_paid_invoice_or_payment(session, deal):
            _raise(
                400,
                "payment_required_before_won",
                "Successful payment or paid invoice is required before moving deal to won",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )
        if not deal.payment_required and _confirmed_conditions(session, deal) is None:
            _raise(
                400,
                "conditions_required_before_won",
                "Confirmed conditions are required before moving deal to won without payment",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )

    if target == "lost":
        close_reason = (
            str(metadata.get("lost_reason") or "").strip()
            or str(metadata.get("close_reason") or "").strip()
            or str(reason or "").strip()
            or str(deal.lost_reason or "").strip()
            or str(deal.close_reason or "").strip()
        )
        if not close_reason:
            _raise(
                400,
                "lost_reason_required",
                "lost_reason or close_reason is required before moving deal to lost",
                {"deal_id": deal.deal_id, "to_stage_code": target},
            )


def _record_stage_change(
    session,
    *,
    deal: SalesDealRow,
    from_stage_id: str | None,
    to_stage_id: str,
    actor_type: str,
    actor_id: str | None,
    reason: str | None,
    changed_at: str,
) -> None:
    session.add(
        SalesStageHistoryRow(
            history_id=new_id("sth"),
            tenant_id=deal.tenant_id,
            deal_id=deal.deal_id,
            from_stage_id=from_stage_id,
            to_stage_id=to_stage_id,
            changed_by_type=actor_type,
            changed_by_id=actor_id,
            reason=reason,
            changed_at=changed_at,
        )
    )


def _apply_terminal_state(deal: SalesDealRow, *, to_stage: SalesPipelineStageRow, reason: str | None, metadata: dict[str, Any], now: str) -> None:
    if to_stage.code == "won":
        deal.status = "won"
        deal.closed_at = deal.closed_at or now
        deal.won_reason = str(metadata.get("won_reason") or reason or deal.won_reason or "deal.won").strip()
        return

    if to_stage.code == "lost":
        close_reason = (
            str(metadata.get("lost_reason") or "").strip()
            or str(metadata.get("close_reason") or "").strip()
            or str(reason or "").strip()
            or str(deal.lost_reason or "").strip()
            or str(deal.close_reason or "").strip()
        )
        deal.status = "lost"
        deal.closed_at = deal.closed_at or now
        deal.lost_reason = close_reason
        deal.close_reason = deal.close_reason or close_reason
        return

    if to_stage.code == "transferred_to_execution":
        deal.status = "closed"
        deal.closed_at = deal.closed_at or now
        deal.close_reason = str(metadata.get("close_reason") or reason or deal.close_reason or "transferred_to_execution").strip()
        return

    if deal.status in {"won", "lost", "closed"}:
        deal.status = "active"
        deal.closed_at = None


def transition_deal_stage(
    session,
    *,
    tenant_id: str,
    deal_id: str,
    target_stage_code: str,
    actor_type: str,
    actor_id: str | None = None,
    reason: str | None = None,
    metadata: dict | None = None,
    force: bool = False,
) -> SalesPipelineStageRow:
    reason_text = str(reason or "").strip() or None
    event_metadata = dict(metadata or {})
    if force:
        if actor_type not in {"system", "admin"}:
            _raise(
                403,
                "force_transition_forbidden",
                "force transition is allowed only for system or admin actors",
                {"actor_type": actor_type},
            )
        if not reason_text:
            _raise(
                400,
                "force_transition_reason_required",
                "reason is required for force transition",
                {"deal_id": deal_id, "target_stage_code": target_stage_code},
            )
        event_metadata["force"] = True

    deal = _get_deal(session, tenant_id=tenant_id, deal_id=deal_id)
    current_stage = _get_current_stage(session, deal)
    target_stage = _get_target_stage(session, deal, target_stage_code)

    if not force:
        _validate_allowed_transition(deal, current_stage, target_stage)
        _validate_preconditions(session, deal=deal, to_stage=target_stage, reason=reason_text, metadata=event_metadata)

    now = utc_now_iso()
    if current_stage.stage_id == target_stage.stage_id:
        deal.updated_at = now
        _apply_terminal_state(deal, to_stage=target_stage, reason=reason_text, metadata=event_metadata, now=now)
        return target_stage

    previous_stage_id = deal.stage_id
    deal.stage_id = target_stage.stage_id
    _apply_terminal_state(deal, to_stage=target_stage, reason=reason_text, metadata=event_metadata, now=now)
    deal.updated_at = now

    _record_stage_change(
        session,
        deal=deal,
        from_stage_id=previous_stage_id,
        to_stage_id=target_stage.stage_id,
        actor_type=actor_type,
        actor_id=actor_id,
        reason=reason_text,
        changed_at=now,
    )
    SalesEventPublisher.publish_sales_event(
        session,
        tenant_id=deal.tenant_id,
        event_type=sales_event_types.DEAL_STAGE_CHANGED,
        aggregate_type="deal",
        aggregate_id=deal.deal_id,
        actor_type=actor_type,
        actor_id=actor_id,
        payload={
            "deal_id": deal.deal_id,
            "pipeline_id": deal.pipeline_id,
            "from_stage_id": previous_stage_id,
            "from_stage_code": current_stage.code,
            "to_stage_id": target_stage.stage_id,
            "to_stage_code": target_stage.code,
            "reason": reason_text,
            "metadata": event_metadata,
        },
    )
    return target_stage
