"""Object-centric process mining for financial risk events."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any

from .models import RiskGraphClaimSnapshot

APPROVAL_EVENTS = {"approval_approved", "finance_approved", "claim_approved"}
PAYMENT_EVENTS = {"payment_requested", "payment_completed"}
RETURN_EVENTS = {"claim_returned", "approval_returned", "supplement_required"}
SUBMIT_EVENTS = {"claim_submitted", "application_submitted"}


@dataclass(slots=True)
class ObjectCentricEvent:
    event_id: str
    event_type: str
    occurred_at: datetime
    object_refs: dict[str, list[str]]
    actor: str = ""
    source: str = ""
    metadata: dict[str, Any] = field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        return {
            "event_id": self.event_id,
            "event_type": self.event_type,
            "occurred_at": self.occurred_at.isoformat(),
            "object_refs": {key: list(value) for key, value in self.object_refs.items()},
            "actor": self.actor,
            "source": self.source,
            "metadata": dict(self.metadata),
        }


@dataclass(slots=True)
class ConformanceRisk:
    risk_code: str
    title: str
    detail: str
    severity: str
    related_event_ids: list[str] = field(default_factory=list)
    object_refs: dict[str, list[str]] = field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        return {
            "risk_code": self.risk_code,
            "title": self.title,
            "detail": self.detail,
            "severity": self.severity,
            "related_event_ids": list(self.related_event_ids),
            "object_refs": {key: list(value) for key, value in self.object_refs.items()},
        }


class ObjectCentricProcessMiner:
    def build_from_claims(
        self,
        claims: list[RiskGraphClaimSnapshot],
    ) -> list[ObjectCentricEvent]:
        events: list[ObjectCentricEvent] = []
        for claim in claims:
            events.extend(self._claim_events(claim))
        return sorted(events, key=lambda item: (item.occurred_at, item.event_id))

    def build_from_dicts(self, rows: list[dict[str, Any]]) -> list[ObjectCentricEvent]:
        events: list[ObjectCentricEvent] = []
        for index, row in enumerate(rows):
            occurred_at = _datetime_from_value(row.get("occurred_at"))
            if occurred_at is None:
                continue
            event_type = str(row.get("event_type") or "").strip()
            if not event_type:
                continue
            events.append(
                ObjectCentricEvent(
                    event_id=str(row.get("event_id") or f"event:{index}:{event_type}"),
                    event_type=event_type,
                    occurred_at=occurred_at,
                    object_refs=_normalize_object_refs(row.get("object_refs")),
                    actor=str(row.get("actor") or "").strip(),
                    source=str(row.get("source") or "").strip(),
                    metadata=dict(row.get("metadata") or {}),
                )
            )
        return sorted(events, key=lambda item: (item.occurred_at, item.event_id))

    def _claim_events(self, claim: RiskGraphClaimSnapshot) -> list[ObjectCentricEvent]:
        object_refs = _claim_object_refs(claim)
        events: list[ObjectCentricEvent] = []
        occurred_at = claim.occurred_at or claim.submitted_at
        if occurred_at:
            events.append(
                ObjectCentricEvent(
                    event_id=f"{claim.claim_id}:expense_occurred",
                    event_type="expense_occurred",
                    occurred_at=occurred_at,
                    object_refs=object_refs,
                    actor=claim.employee_id or claim.employee_name,
                    source="expense_claim",
                    metadata={"amount": str(claim.amount), "expense_type": claim.expense_type},
                )
            )
        if claim.submitted_at:
            events.append(
                ObjectCentricEvent(
                    event_id=f"{claim.claim_id}:claim_submitted",
                    event_type="claim_submitted",
                    occurred_at=claim.submitted_at,
                    object_refs=object_refs,
                    actor=claim.employee_id or claim.employee_name,
                    source="expense_claim",
                    metadata={"status": claim.status},
                )
            )
        for item in claim.items:
            item_time = _datetime_from_value(item.item_date) or occurred_at or datetime.now(UTC)
            item_refs = _merge_object_refs(
                object_refs,
                {
                    "claim_item": [item.item_id] if item.item_id else [],
                    "invoice": [item.invoice_id] if item.invoice_id else [],
                },
            )
            events.append(
                ObjectCentricEvent(
                    event_id=f"{claim.claim_id}:item:{item.item_id or len(events)}",
                    event_type="expense_item_recorded",
                    occurred_at=item_time,
                    object_refs=item_refs,
                    actor=claim.employee_id or claim.employee_name,
                    source="expense_item",
                    metadata={
                        "amount": str(item.item_amount),
                        "item_type": item.item_type,
                        "item_location": item.item_location,
                    },
                )
            )
            if item.invoice_id:
                events.append(
                    ObjectCentricEvent(
                        event_id=f"{claim.claim_id}:invoice:{item.invoice_id}",
                        event_type="invoice_attached",
                        occurred_at=item_time,
                        object_refs=item_refs,
                        actor=claim.employee_id or claim.employee_name,
                        source="invoice",
                    )
                )
        for index, flag in enumerate(claim.risk_flags):
            signal = _risk_signal_from_flag(flag)
            if not signal:
                continue
            events.append(
                ObjectCentricEvent(
                    event_id=f"{claim.claim_id}:risk_flag:{index}:{signal}",
                    event_type="risk_flagged",
                    occurred_at=claim.submitted_at or occurred_at or datetime.now(UTC),
                    object_refs=object_refs,
                    source="risk_rule",
                    metadata={"risk_signal": signal, "raw": flag},
                )
            )
        return events


class ConformanceRiskDetector:
    def detect(self, events: list[ObjectCentricEvent]) -> list[ConformanceRisk]:
        risks: list[ConformanceRisk] = []
        for claim_key, claim_events in _events_by_object(events, "claim").items():
            ordered = sorted(claim_events, key=lambda item: (item.occurred_at, item.event_id))
            risks.extend(self._detect_claim_risks(claim_key, ordered))
        return risks

    def _detect_claim_risks(
        self,
        claim_key: str,
        events: list[ObjectCentricEvent],
    ) -> list[ConformanceRisk]:
        risks: list[ConformanceRisk] = []
        event_types = [event.event_type for event in events]
        first_submit = _first_event(events, SUBMIT_EVENTS)
        first_approval = _first_event(events, APPROVAL_EVENTS)
        first_payment = _first_event(events, PAYMENT_EVENTS)

        if first_payment and (not first_approval or first_payment.occurred_at < first_approval.occurred_at):
            related = [first_payment.event_id]
            if first_approval:
                related.append(first_approval.event_id)
            risks.append(
                ConformanceRisk(
                    risk_code="payment_before_approval",
                    title="Payment before approval",
                    detail="Payment event appears before an approval event.",
                    severity="critical",
                    related_event_ids=related,
                    object_refs={"claim": [claim_key]},
                )
            )
        if first_approval and (not first_submit or first_approval.occurred_at < first_submit.occurred_at):
            related = [first_approval.event_id]
            if first_submit:
                related.append(first_submit.event_id)
            risks.append(
                ConformanceRisk(
                    risk_code="approval_bypass",
                    title="Approval bypass",
                    detail="Approval appears before submission or without submission.",
                    severity="high",
                    related_event_ids=related,
                    object_refs={"claim": [claim_key]},
                )
            )
        return_count = sum(1 for event_type in event_types if event_type in RETURN_EVENTS)
        submit_count = sum(1 for event_type in event_types if event_type in SUBMIT_EVENTS)
        if return_count >= 2 or (return_count >= 1 and submit_count >= 2):
            risks.append(
                ConformanceRisk(
                    risk_code="rework_loop",
                    title="Rework loop",
                    detail="Claim has repeated return and resubmission events.",
                    severity="medium",
                    related_event_ids=[
                        event.event_id
                        for event in events
                        if event.event_type in RETURN_EVENTS | SUBMIT_EVENTS
                    ],
                    object_refs={"claim": [claim_key]},
                )
            )
        if "invoice_attached" in event_types and not first_submit:
            risks.append(
                ConformanceRisk(
                    risk_code="process_bypass",
                    title="Process bypass",
                    detail="Invoice exists without a claim submission event.",
                    severity="medium",
                    related_event_ids=[
                        event.event_id for event in events if event.event_type == "invoice_attached"
                    ],
                    object_refs={"claim": [claim_key]},
                )
            )
        return risks


def _claim_object_refs(claim: RiskGraphClaimSnapshot) -> dict[str, list[str]]:
    return {
        "claim": [claim.claim_id] if claim.claim_id else [],
        "employee": [claim.employee_id or claim.employee_name]
        if claim.employee_id or claim.employee_name
        else [],
        "department": [claim.department_id or claim.department_name]
        if claim.department_id or claim.department_name
        else [],
        "expense_type": [claim.expense_type] if claim.expense_type else [],
    }


def _normalize_object_refs(value: Any) -> dict[str, list[str]]:
    if not isinstance(value, dict):
        return {}
    normalized: dict[str, list[str]] = {}
    for key, raw_items in value.items():
        if isinstance(raw_items, list):
            items = [str(item).strip() for item in raw_items if str(item).strip()]
        else:
            items = [str(raw_items).strip()] if str(raw_items or "").strip() else []
        normalized[str(key).strip()] = list(dict.fromkeys(items))
    return normalized


def _merge_object_refs(*refs: dict[str, list[str]]) -> dict[str, list[str]]:
    merged: dict[str, list[str]] = {}
    for ref in refs:
        for key, values in ref.items():
            bucket = merged.setdefault(key, [])
            bucket.extend(str(value).strip() for value in values if str(value).strip())
    return {key: list(dict.fromkeys(values)) for key, values in merged.items()}


def _events_by_object(
    events: list[ObjectCentricEvent],
    object_type: str,
) -> dict[str, list[ObjectCentricEvent]]:
    grouped: dict[str, list[ObjectCentricEvent]] = {}
    for event in events:
        for object_key in event.object_refs.get(object_type, []):
            grouped.setdefault(object_key, []).append(event)
    return grouped


def _first_event(
    events: list[ObjectCentricEvent],
    event_types: set[str],
) -> ObjectCentricEvent | None:
    for event in events:
        if event.event_type in event_types:
            return event
    return None


def _risk_signal_from_flag(flag: Any) -> str:
    if isinstance(flag, dict):
        raw = flag.get("risk_signal") or flag.get("signal") or flag.get("rule_code") or flag.get("code")
    else:
        raw = flag
    return "_".join(str(raw or "").strip().lower().split())


def _datetime_from_value(value: Any) -> datetime | None:
    if isinstance(value, datetime):
        return value
    if hasattr(value, "year") and hasattr(value, "month") and hasattr(value, "day"):
        return datetime(value.year, value.month, value.day, tzinfo=UTC)
    if isinstance(value, str) and value.strip():
        try:
            return datetime.fromisoformat(value)
        except ValueError:
            return None
    return None
