"""FSM runtime — start instances and fire transitions safely.

Public entry points:
    * ``start_instance(definition, *, tenant, organization_node, subject=None, data=None, actor=None)``
    * ``fire_transition(instance, transition_name, *, actor=None, payload=None)``
    * ``fire_for_event(instance, event_name, *, actor=None, payload=None)``
    * ``record_approval(instance, transition_name, *, actor, decision, role_code="", note="")``

All side-effects (hooks, audit, event dispatch, approval consumption) happen
inside one DB transaction so the FSM can never end up in a torn state.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, Any

import structlog
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.utils import timezone

from simorgh.apps.events.bus import dispatch as dispatch_event
from simorgh.apps.events.bus import register_event
from simorgh.apps.iam.engine import is_allowed
from simorgh.apps.workflow.evaluator import evaluate_conditions
from simorgh.apps.workflow.models import (
    WorkflowApproval,
    WorkflowApprovalDecision,
    WorkflowInstance,
    WorkflowInstanceStatus,
    WorkflowTransitionLog,
)
from simorgh.apps.workflow.registry import (
    ActionContext,
    StateSpec,
    TransitionSpec,
    WorkflowDefinition,
    WorkflowError,
    get_action_handler,
)
from simorgh.core.audit import record_event
from simorgh.core.context import current_request_context

if TYPE_CHECKING:
    from simorgh.apps.accounts.models import User
    from simorgh.apps.organizations.models import OrganizationNode
    from simorgh.apps.tenants.models import Tenant

_log = structlog.get_logger("simorgh.workflow")


# ---------------------------------------------------------------------------
# Platform events emitted by the runtime
# ---------------------------------------------------------------------------
register_event(
    "workflow.instance_started",
    description="Workflow instance created.",
    payload_keys=("instance_id", "definition", "state"),
)
register_event(
    "workflow.transition_fired",
    description="A workflow transition completed successfully.",
    payload_keys=("instance_id", "transition", "from_state", "to_state"),
)
register_event(
    "workflow.instance_completed",
    description="Workflow instance reached a final state.",
    payload_keys=("instance_id", "definition", "state"),
)


# ---------------------------------------------------------------------------
# Start
# ---------------------------------------------------------------------------
@transaction.atomic
def start_instance(
    definition: WorkflowDefinition,
    *,
    tenant: Tenant,
    organization_node: OrganizationNode,
    subject: Any | None = None,
    data: Mapping[str, Any] | None = None,
    actor: User | None = None,
) -> WorkflowInstance:
    """Create and persist a new ``WorkflowInstance`` in its initial state."""

    initial = definition.initial_state
    instance = WorkflowInstance(
        tenant=tenant,
        organization_node=organization_node,
        definition_name=definition.name,
        definition_version=definition.version,
        subject_type=definition.subject_type,
        current_state=initial.name,
        status=WorkflowInstanceStatus.ACTIVE,
        data=dict(data or {}),
        started_by=actor,
    )
    if subject is not None:
        instance.content_type = ContentType.objects.get_for_model(type(subject))
        instance.object_id = str(subject.pk)
    instance.save()

    _run_actions(
        initial.on_enter,
        ActionContext(
            instance=instance,
            definition=definition,
            transition=None,
            actor=actor,
            payload={},
        ),
    )

    record_event(
        "workflow.instance_started",
        resource_type="workflow_instance",
        resource_id=str(instance.public_id),
        after={
            "definition": definition.name,
            "version": definition.version,
            "state": initial.name,
        },
    )
    dispatch_event(
        "workflow.instance_started",
        {
            "instance_id": str(instance.public_id),
            "definition": definition.name,
            "state": initial.name,
        },
    )

    if initial.is_final:
        _close_instance(instance, definition, initial)

    return instance


# ---------------------------------------------------------------------------
# Fire
# ---------------------------------------------------------------------------
@transaction.atomic
def fire_transition(
    instance: WorkflowInstance,
    transition_name: str,
    *,
    actor: User | None = None,
    payload: Mapping[str, Any] | None = None,
) -> WorkflowInstance:
    """Move ``instance`` along ``transition_name``.

    Validation order: instance active → transition exists & leaves current
    state → trigger == "" (manual) → permission check → conditions →
    approval requirement satisfied. Each failure raises ``WorkflowError``.
    """

    from simorgh.apps.workflow.registry import get_workflow

    if instance.status != WorkflowInstanceStatus.ACTIVE:
        raise WorkflowError(f"instance {instance.pk} is not active (status={instance.status})")

    definition = get_workflow(instance.definition_name, instance.definition_version)
    transition = definition.transition(transition_name)
    if transition.from_state != instance.current_state:
        raise WorkflowError(
            f"transition {transition_name!r} starts in {transition.from_state!r}, "
            f"instance is in {instance.current_state!r}",
        )

    payload_dict = dict(payload or {})
    action_ctx = ActionContext(
        instance=instance,
        definition=definition,
        transition=transition,
        actor=actor,
        payload=payload_dict,
    )

    _check_permission(transition, actor)
    if not evaluate_conditions(transition.conditions, action_ctx):
        raise WorkflowError(f"transition {transition_name!r} blocked by guard condition")

    from_state = definition.state(transition.from_state)
    to_state = definition.state(transition.to_state)
    approval = _consume_approval_if_required(instance, transition, from_state)

    _run_actions(from_state.on_exit, action_ctx)
    _run_actions(transition.actions, action_ctx)

    instance.current_state = to_state.name
    instance.save(update_fields=("current_state", "updated_at"))

    log = WorkflowTransitionLog.objects.create(
        tenant=instance.tenant,
        organization_node=instance.organization_node,
        instance=instance,
        transition_name=transition.name,
        from_state=transition.from_state,
        to_state=transition.to_state,
        actor=actor,
        trigger=transition.trigger,
        payload=payload_dict,
    )

    _run_actions(to_state.on_enter, action_ctx)

    if approval is not None:
        approval.consumed_at = timezone.now()
        approval.save(update_fields=("consumed_at",))

    record_event(
        "workflow.transition_fired",
        resource_type="workflow_instance",
        resource_id=str(instance.public_id),
        before={"state": transition.from_state},
        after={"state": transition.to_state, "transition": transition.name},
    )
    dispatch_event(
        "workflow.transition_fired",
        {
            "instance_id": str(instance.public_id),
            "transition": transition.name,
            "from_state": transition.from_state,
            "to_state": transition.to_state,
        },
    )

    if to_state.is_final:
        _close_instance(instance, definition, to_state)

    _log.info(
        "workflow.transition_fired",
        event_name="workflow.transition_fired",
        instance=str(instance.public_id),
        transition=transition.name,
        log_id=str(log.public_id),
    )

    return instance


def fire_for_event(
    instance: WorkflowInstance,
    event_name: str,
    *,
    actor: User | None = None,
    payload: Mapping[str, Any] | None = None,
) -> list[WorkflowInstance]:
    """Fire every transition wired to ``event_name`` that is currently legal.

    Returns the instance after each successful fire (chained transitions are
    not auto-cascaded — one event = one fire per matching transition).
    """

    from simorgh.apps.workflow.registry import get_workflow

    definition = get_workflow(instance.definition_name, instance.definition_version)
    fired: list[WorkflowInstance] = []
    for t in definition.transitions_for_event(event_name):
        if t.from_state != instance.current_state:
            continue
        try:
            fire_transition(instance, t.name, actor=actor, payload=payload)
        except WorkflowError as exc:
            _log.info(
                "workflow.trigger_skipped",
                event_name=event_name,
                transition=t.name,
                reason=str(exc),
            )
            continue
        instance.refresh_from_db()
        fired.append(instance)
    return fired


# ---------------------------------------------------------------------------
# Approvals
# ---------------------------------------------------------------------------
@transaction.atomic
def record_approval(
    instance: WorkflowInstance,
    transition_name: str,
    *,
    actor: User,
    decision: str,
    role_code: str = "",
    note: str = "",
) -> WorkflowApproval:
    """Record (and validate) one approval/rejection for a transition."""

    from simorgh.apps.workflow.registry import get_workflow

    definition = get_workflow(instance.definition_name, instance.definition_version)
    transition = definition.transition(transition_name)
    from_state = definition.state(transition.from_state)

    if not transition.requires_approval:
        raise WorkflowError(
            f"transition {transition_name!r} does not require approval",
        )

    required_perm = from_state.approval_permission or "workflow.approval.grant"
    ctx = current_request_context()
    if not (
        (actor is not None and getattr(actor, "is_superuser", False))
        or ctx.is_superuser
        or required_perm in ctx.permissions
    ):
        raise WorkflowError(
            f"actor lacks required approval permission {required_perm!r}",
        )

    if decision not in (
        WorkflowApprovalDecision.APPROVED,
        WorkflowApprovalDecision.REJECTED,
    ):
        raise WorkflowError(f"unknown approval decision {decision!r}")

    return WorkflowApproval.objects.create(
        tenant=instance.tenant,
        organization_node=instance.organization_node,
        instance=instance,
        transition_name=transition.name,
        decision=decision,
        actor=actor,
        role_code=role_code or from_state.approval_role_code,
        note=note,
    )


# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------
def _check_permission(transition: TransitionSpec, actor: User | None) -> None:
    if not transition.permission:
        return
    if actor is not None and getattr(actor, "is_superuser", False):
        return
    ctx = current_request_context()
    if ctx.is_superuser:
        return
    if not is_allowed(ctx, transition.permission):
        raise WorkflowError(
            f"actor lacks permission {transition.permission!r} for transition {transition.name!r}",
        )


def _consume_approval_if_required(
    instance: WorkflowInstance,
    transition: TransitionSpec,
    from_state: StateSpec,
) -> WorkflowApproval | None:
    if not transition.requires_approval:
        return None
    _ = from_state  # state-level approval permission is checked at record time
    approval = (
        WorkflowApproval.objects.filter(
            instance=instance,
            transition_name=transition.name,
            decision=WorkflowApprovalDecision.APPROVED,
            consumed_at__isnull=True,
        )
        .order_by("-created_at")
        .first()
    )
    if approval is None:
        raise WorkflowError(
            f"transition {transition.name!r} requires an approval — none recorded",
        )
    return approval


def _run_actions(actions: tuple, ctx: ActionContext) -> None:
    for action in actions:
        handler = get_action_handler(action.name)
        merged = ActionContext(
            instance=ctx.instance,
            definition=ctx.definition,
            transition=ctx.transition,
            actor=ctx.actor,
            payload={**ctx.payload, **dict(action.params)},
        )
        try:
            handler(merged)
        except Exception as exc:
            # Surface as WorkflowError so the surrounding transaction rolls back.
            raise WorkflowError(
                f"action {action.name!r} failed: {exc}",
            ) from exc


def _close_instance(
    instance: WorkflowInstance,
    definition: WorkflowDefinition,
    final_state: StateSpec,
) -> None:
    instance.status = WorkflowInstanceStatus.COMPLETED
    instance.closed_at = timezone.now()
    instance.save(update_fields=("status", "closed_at", "updated_at"))
    record_event(
        "workflow.instance_completed",
        resource_type="workflow_instance",
        resource_id=str(instance.public_id),
        after={"state": final_state.name},
    )
    dispatch_event(
        "workflow.instance_completed",
        {
            "instance_id": str(instance.public_id),
            "definition": definition.name,
            "state": final_state.name,
        },
    )
