"""Pattern update event emission and lifecycle context helpers for Deal Desk."""

from __future__ import annotations

import hashlib
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable

from g_gremlin import sfdc as sfdc_helpers

from .config import DealDeskConfig
from .context import ApprovalDecision, ArtifactRef, DealAssumption, DealContextV1, QuoteRef
from .forensics import (
    build_deal_pattern_profile,
    extract_amount,
    extract_discount_pct,
    extract_payment_terms,
    extract_product_family,
    extract_segment,
    extract_stage_durations,
    extract_template_key,
    extract_term_months,
    fetch_historical_opportunities,
    infer_deal_motion,
)
from .profiles import PatternExtractedSignals, PatternUpdateEventV1
from .store import DealDeskStore


logger = logging.getLogger(__name__)

_CLOSE_WORKFLOW_ORDER = {
    "deal_active": 0,
    "submitted_for_close": 1,
    "certifying": 2,
    "exception_pending": 3,
    "certified": 4,
    "closed_won_progressed": 5,
    "post_close_executed": 6,
}


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def resolve_crm_org_id(
    *,
    org_alias: str | None,
    crm_org_id: str | None = None,
) -> str:
    if crm_org_id not in (None, ""):
        return str(crm_org_id)
    try:
        org_identity = sfdc_helpers.get_org_identity(org_alias=org_alias)
    except Exception:
        org_identity = {}
    resolved = org_identity.get("org_id") if isinstance(org_identity, dict) else None
    return str(resolved or org_alias or "unknown-org")


def build_pattern_update_event(
    *,
    source_type: str,
    crm_org_id: str,
    opportunity_id: str | None,
    source_ref: str,
    observed_at: str | None = None,
    source_receipt_id: str | None = None,
    raw_record: dict[str, Any] | None = None,
    extracted_motion_id: str | None = None,
    confidence: float | None = None,
    discount_pct: float | None = None,
    term_months: int | None = None,
    payment_terms: str | None = None,
    template_key: str | None = None,
    approver_roles: Iterable[str] | None = None,
    approval_latency_hours: float | None = None,
    stage_durations: dict[str, float] | None = None,
    exception_flags: Iterable[str] | None = None,
    bundle_signature: str | None = None,
    outcome: str | None = None,
    custom: dict[str, Any] | None = None,
    evidence_ids: Iterable[str] | None = None,
) -> PatternUpdateEventV1:
    record = raw_record if isinstance(raw_record, dict) else {}
    inferred_motion_id, inferred_confidence, _ = infer_deal_motion(record) if record else ("unknown", 0.35, "")
    motion_id = str(extracted_motion_id or inferred_motion_id or "unknown").strip() or "unknown"
    event_confidence = float(confidence if confidence is not None else inferred_confidence)

    custom_payload = dict(custom or {})
    if record:
        if "segment" not in custom_payload:
            segment = extract_segment(record)
            if segment:
                custom_payload["segment"] = segment
        if "product_family" not in custom_payload:
            product_family = extract_product_family(record)
            if product_family:
                custom_payload["product_family"] = product_family
        if "amount" not in custom_payload:
            amount = extract_amount(record)
            if amount is not None:
                custom_payload["amount"] = amount

    signals = PatternExtractedSignals(
        discount_pct=discount_pct if discount_pct is not None else extract_discount_pct(record),
        term_months=term_months if term_months is not None else extract_term_months(record),
        payment_terms=payment_terms if payment_terms is not None else extract_payment_terms(record),
        template_key=template_key if template_key is not None else extract_template_key(record),
        approver_roles=_dedupe_texts(approver_roles),
        approval_latency_hours=approval_latency_hours,
        stage_durations=dict(stage_durations or extract_stage_durations(record)),
        exception_flags=_dedupe_texts(exception_flags),
        bundle_signature=bundle_signature,
        outcome=outcome,
        custom=custom_payload,
    )

    return PatternUpdateEventV1(
        event_id=_event_id(
            source_type=source_type,
            crm_org_id=crm_org_id,
            opportunity_id=opportunity_id,
            source_ref=source_ref,
        ),
        crm_org_id=crm_org_id,
        opportunity_id=str(opportunity_id or "").strip() or None,
        observed_at=str(observed_at or utc_now_iso()),
        source_type=source_type,
        source_ref=source_ref,
        source_receipt_id=str(source_receipt_id or "").strip() or None,
        extracted_motion_id=motion_id,
        extracted_signals=signals,
        confidence=max(0.0, min(1.0, event_confidence)),
        evidence_ids=_dedupe_texts(evidence_ids),
    )


def save_pattern_update_events(
    *,
    store: DealDeskStore,
    events: Iterable[PatternUpdateEventV1],
    org_alias: str | None = None,
    config: DealDeskConfig | None = None,
    refresh_profile: bool = False,
) -> list[Path]:
    saved_paths: list[Path] = []
    event_list = list(events)
    for event in event_list:
        saved_paths.append(store.save_pattern_update_event(event))

    if refresh_profile and event_list:
        first = event_list[0]
        try:
            refresh_deal_pattern_profile(
                store=store,
                crm_org_id=first.crm_org_id,
                org_alias=org_alias,
                config=config,
            )
        except Exception as exc:
            logger.warning("Failed to refresh DealPatternProfile after event emission: %s", exc)
    return saved_paths


def refresh_deal_pattern_profile(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    config: DealDeskConfig | None = None,
) -> Path:
    historical_rows = fetch_historical_opportunities(org_alias=org_alias)
    update_events = store.list_pattern_update_events(crm_org_id=crm_org_id, org_alias=org_alias)
    effective_config = config or store.load_generated_config(crm_org_id=crm_org_id, org_alias=org_alias)
    profile = build_deal_pattern_profile(
        crm_org_id=crm_org_id,
        generated_at=utc_now_iso(),
        records=historical_rows,
        org_alias=org_alias,
        config=effective_config,
        update_events=update_events,
    )
    return store.save_deal_pattern_profile(profile)


def update_context_for_approval_stamp(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    observed_at: str,
    actor: str | None,
    actor_role: str | None = None,
    channel: str | None = None,
    rationale: str | None = None,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="dealdesk_approved",
        account_id=account_id,
    )
    decision = ApprovalDecision(
        actor=str(actor or "unknown"),
        actor_role=str(actor_role or "").strip() or None,
        decision="approved",
        channel=str(channel or "").strip() or None,
        decided_at=observed_at,
        rationale=str(rationale or "").strip() or None,
        receipt_id=str(receipt_id or "").strip() or None,
    )
    if not any(_same_approval_decision(existing, decision) for existing in context.approval_chain):
        context.approval_chain.append(decision)
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
    context.updated_at = observed_at
    context.lifecycle_state = "dealdesk_approved"
    _advance_close_workflow_state(context, "certified")
    return store.save_context(context)


def update_context_for_renewal_shell(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    renewal_opportunity_id: str,
    observed_at: str,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="renewal_shell_created",
        account_id=account_id,
    )
    context.renewal_opportunity_id = renewal_opportunity_id
    context.updated_at = observed_at
    context.lifecycle_state = "renewal_shell_created"
    _advance_close_workflow_state(context, "post_close_executed")
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
        receipt_path = str((store.state_dir / "receipts" / receipt_id / "receipt.json").resolve())
        _append_artifact_ref(
            context,
            artifact_type="post_close_trigger_receipt",
            path=receipt_path,
            created_at=observed_at,
        )
    return store.save_context(context)


def update_context_for_close_progression(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    observed_at: str,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="closed_won_progressed",
        account_id=account_id,
    )
    context.updated_at = observed_at
    context.lifecycle_state = "closed_won_progressed"
    _advance_close_workflow_state(context, "closed_won_progressed")
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
    return store.save_context(context)


def update_context_for_close_packet(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    observed_at: str,
    close_packet_json_path: str,
    close_packet_markdown_path: str | None = None,
    overall_status: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="close_packet_generated",
        account_id=account_id,
    )
    _append_path(context.close_packet_paths, close_packet_json_path)
    _append_artifact_ref(
        context,
        artifact_type="close_packet",
        path=close_packet_json_path,
        created_at=observed_at,
    )
    if close_packet_markdown_path:
        _append_artifact_ref(
            context,
            artifact_type="close_packet",
            path=close_packet_markdown_path,
            created_at=observed_at,
        )
    if overall_status and overall_status not in context.close_findings:
        context.close_findings.append(overall_status)
    context.updated_at = observed_at
    if context.lifecycle_state not in ("dealdesk_approved", "renewal_shell_created"):
        context.lifecycle_state = "close_packet_generated"
    if str(overall_status or "").strip().lower() == "fail":
        _advance_close_workflow_state(context, "exception_pending")
    else:
        _advance_close_workflow_state(context, "certifying")
    return store.save_context(context)


def update_context_for_quote_create(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    quote_id: str,
    engine: str,
    status: str,
    observed_at: str,
    assumptions: Iterable[DealAssumption] | None = None,
    approved_by: str | None = None,
    approval_note: str | None = None,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="quote_created",
        account_id=account_id,
    )
    if assumptions is not None:
        context.assumptions = [assumption.model_copy(deep=True) for assumption in assumptions]
    if not any(existing.quote_id == quote_id for existing in context.quote_refs):
        context.quote_refs.append(
            QuoteRef(
                quote_id=quote_id,
                engine=str(engine),
                status=str(status),
                created_at=observed_at,
            )
        )
    if approved_by:
        decision = ApprovalDecision(
            actor=str(approved_by),
            actor_role="dealdesk_quote_create",
            decision="approved",
            channel="manual_quote_create",
            decided_at=observed_at,
            rationale=str(approval_note or "").strip() or None,
            receipt_id=str(receipt_id or "").strip() or None,
        )
        if not any(_same_approval_decision(existing, decision) for existing in context.approval_chain):
            context.approval_chain.append(decision)
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
    context.updated_at = observed_at
    context.lifecycle_state = "quote_created"
    return store.save_context(context)


def update_context_for_line_items(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    quote_id: str,
    observed_at: str,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="quote_line_items_created",
        account_id=account_id,
    )
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
    context.updated_at = observed_at
    context.lifecycle_state = "quote_line_items_created"
    return store.save_context(context)


def update_context_for_quote_rollback(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    quote_id: str,
    observed_at: str,
    receipt_id: str | None = None,
    account_id: str | None = None,
) -> Path:
    context = _load_or_create_context(
        store=store,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
        opportunity_id=opportunity_id,
        observed_at=observed_at,
        lifecycle_state="quote_rolled_back",
        account_id=account_id,
    )
    for quote_ref in context.quote_refs:
        if quote_ref.quote_id == quote_id:
            quote_ref.status = "rolled_back"
    if receipt_id:
        _append_receipt_artifact(context, store.state_dir, receipt_id, observed_at=observed_at)
    context.updated_at = observed_at
    context.lifecycle_state = "quote_rolled_back"
    return store.save_context(context)


def _load_or_create_context(
    *,
    store: DealDeskStore,
    crm_org_id: str,
    org_alias: str | None,
    opportunity_id: str,
    observed_at: str,
    lifecycle_state: str,
    account_id: str | None = None,
) -> DealContextV1:
    context = store.load_context(
        opportunity_id=opportunity_id,
        crm_org_id=crm_org_id,
        org_alias=org_alias,
    )
    if context is None:
        context = DealContextV1(
            context_id=f"ctx-{opportunity_id}",
            crm_org_id=crm_org_id,
            org_alias=org_alias,
            opportunity_id=opportunity_id,
            account_id=account_id,
            created_at=observed_at,
            updated_at=observed_at,
            lifecycle_state=lifecycle_state,
        )
    else:
        if account_id and not context.account_id:
            context.account_id = account_id
        context.updated_at = observed_at
    return context


def _append_receipt_artifact(
    context: DealContextV1,
    state_dir: Path,
    receipt_id: str,
    *,
    observed_at: str,
) -> None:
    if receipt_id not in context.receipt_ids:
        context.receipt_ids.append(receipt_id)
    receipt_path = str((state_dir / "receipts" / receipt_id / "receipt.json").resolve())
    _append_artifact_ref(context, artifact_type="receipt", path=receipt_path, created_at=observed_at)


def _append_artifact_ref(
    context: DealContextV1,
    *,
    artifact_type: str,
    path: str,
    created_at: str,
) -> None:
    if any(existing.path == path and existing.type == artifact_type for existing in context.artifact_refs):
        return
    context.artifact_refs.append(
        ArtifactRef(type=artifact_type, path=path, created_at=created_at)
    )


def _append_path(paths: list[str], value: str | None) -> None:
    if value and value not in paths:
        paths.append(value)


def _same_approval_decision(left: ApprovalDecision, right: ApprovalDecision) -> bool:
    if left.receipt_id and right.receipt_id:
        return left.receipt_id == right.receipt_id
    return (
        left.actor == right.actor
        and left.decision == right.decision
        and left.decided_at == right.decided_at
    )


def _dedupe_texts(values: Iterable[Any] | None) -> list[str]:
    seen: set[str] = set()
    results: list[str] = []
    for value in values or []:
        text = str(value or "").strip()
        if not text or text in seen:
            continue
        seen.add(text)
        results.append(text)
    return results


def _event_id(
    *,
    source_type: str,
    crm_org_id: str,
    opportunity_id: str | None,
    source_ref: str,
) -> str:
    payload = "|".join(
        [
            str(source_type),
            str(crm_org_id),
            str(opportunity_id or ""),
            str(source_ref),
        ]
    )
    digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
    return f"evt-{digest}"


def _advance_close_workflow_state(context: DealContextV1, new_state: str) -> None:
    current_rank = _CLOSE_WORKFLOW_ORDER.get(str(context.close_workflow_state or "deal_active"), 0)
    new_rank = _CLOSE_WORKFLOW_ORDER.get(str(new_state or "deal_active"), current_rank)
    if new_rank >= current_rank:
        context.close_workflow_state = new_state  # type: ignore[assignment]


__all__ = [
    "build_pattern_update_event",
    "refresh_deal_pattern_profile",
    "resolve_crm_org_id",
    "save_pattern_update_events",
    "update_context_for_approval_stamp",
    "update_context_for_close_progression",
    "update_context_for_close_packet",
    "update_context_for_line_items",
    "update_context_for_quote_create",
    "update_context_for_quote_rollback",
    "update_context_for_renewal_shell",
    "utc_now_iso",
]
