"""Process engine — evaluates tenant-scoped automation rules.

Unlike the FSM-based ``WorkflowInstance`` runtime, the process engine handles
*event-triggered rules* that apply to arbitrary subjects (e.g. tickets).  Each
``ProcessDefinition`` row stores a ``trigger_event`` name, a list of conditions
in the workflow evaluator DSL, and a list of named action handlers from the
workflow action registry.

Public entry point::

    from simorgh.apps.workflow.process_engine import ProcessEngine
    ProcessEngine.run(
        "helpdesk.ticket.created",
        subject=ticket,
        tenant_id=tenant.pk,
        payload={"ticket_id": ticket.pk, ...},
    )
"""

from __future__ import annotations

from typing import Any

import structlog

from simorgh.apps.workflow.registry import ActionContext, ConditionSpec, WorkflowError

_log = structlog.get_logger("simorgh.workflow.process_engine")


# ---------------------------------------------------------------------------
# Internal proxy — exposes a Django model instance as `ctx.instance.subject`
# so the workflow evaluator can resolve `subject.*` paths.
# ---------------------------------------------------------------------------

class _InstanceProxy:
    """Minimal object satisfying the evaluator's field resolver contract."""

    def __init__(self, subject: Any, data: dict | None = None) -> None:
        self.subject = subject
        self.data: dict = data or {}


# ---------------------------------------------------------------------------
# Engine
# ---------------------------------------------------------------------------

class ProcessEngine:
    """Evaluate and fire all matching ``ProcessDefinition`` rules for an event."""

    @staticmethod
    def run(
        trigger_event: str,
        *,
        subject: Any,
        tenant_id: int,
        actor: Any | None = None,
        payload: dict[str, Any] | None = None,
    ) -> int:
        """Run every active ``ProcessDefinition`` matching *trigger_event* and *tenant_id*.

        Returns the number of rules that fired (conditions matched).
        """
        from simorgh.apps.workflow.models import ProcessDefinition

        definitions = ProcessDefinition.objects.filter(
            tenant_id=tenant_id,
            trigger_event=trigger_event,
            is_active=True,
        ).order_by("sort_order", "pk")

        fired = 0
        for process in definitions:
            try:
                did_fire = ProcessEngine._run_one(
                    process,
                    subject=subject,
                    actor=actor,
                    payload=payload or {},
                )
                if did_fire:
                    fired += 1
            except Exception:
                _log.exception(
                    "process_engine.run_failed",
                    process_id=process.pk,
                    trigger=trigger_event,
                )
        return fired

    # ------------------------------------------------------------------
    # Single-process evaluation
    # ------------------------------------------------------------------

    @staticmethod
    def _run_one(
        process: Any,
        *,
        subject: Any,
        actor: Any | None,
        payload: dict[str, Any],
    ) -> bool:
        """Evaluate one process definition. Returns True if actions were fired."""
        from django.utils import timezone
        from simorgh.apps.workflow.evaluator import evaluate_conditions
        from simorgh.apps.workflow.registry import get_action_handler
        from simorgh.apps.workflow.models import ProcessExecutionLog

        conditions = tuple(ConditionSpec(expression=c) for c in (process.conditions or []))
        instance_proxy = _InstanceProxy(subject)

        eval_ctx = ActionContext(
            instance=instance_proxy,
            definition=None,  # type: ignore[arg-type]
            transition=None,
            actor=actor,
            payload=dict(payload),
        )

        if conditions and not evaluate_conditions(conditions, eval_ctx):
            return False

        # Fire actions
        actions_fired: list[str] = []
        success = True
        error_message = ""

        for action_spec in (process.actions or []):
            name = action_spec.get("name", "")
            params = action_spec.get("params", {})
            if not name:
                continue
            try:
                handler = get_action_handler(name)
                action_ctx = ActionContext(
                    instance=instance_proxy,
                    definition=None,  # type: ignore[arg-type]
                    transition=None,
                    actor=actor,
                    payload={**payload, **params},
                )
                handler(action_ctx)
                actions_fired.append(name)
            except WorkflowError as exc:
                _log.warning(
                    "process_engine.action_not_found",
                    action=name,
                    process_id=process.pk,
                    error=str(exc),
                )
            except Exception as exc:
                success = False
                error_message = f"{name}: {exc}"
                _log.warning(
                    "process_engine.action_failed",
                    action=name,
                    process_id=process.pk,
                    error=str(exc),
                )
                break  # stop on first action failure

        # Persist execution log
        try:
            _write_log(process, subject=subject, payload=payload, actions_fired=actions_fired, success=success, error_message=error_message)
        except Exception:
            _log.exception("process_engine.log_write_failed", process_id=process.pk)

        # Update run stats
        try:
            process.run_count = process.run_count + 1
            process.last_run_at = timezone.now()
            process.save(update_fields=["run_count", "last_run_at"])
        except Exception:
            _log.exception("process_engine.stats_update_failed", process_id=process.pk)

        return True


def _write_log(
    process: Any,
    *,
    subject: Any,
    payload: dict[str, Any],
    actions_fired: list[str],
    success: bool,
    error_message: str,
) -> None:
    from django.contrib.contenttypes.models import ContentType
    from simorgh.apps.workflow.models import ProcessExecutionLog

    ct = None
    object_id = ""
    if subject is not None:
        try:
            ct = ContentType.objects.get_for_model(subject)
            object_id = str(subject.pk)
        except Exception:
            pass

    # Use the process's org node for the log row (required by TenantScopedModel)
    ProcessExecutionLog.objects.create(
        tenant_id=process.tenant_id,
        organization_node_id=process.organization_node_id,
        process=process,
        trigger_event=process.trigger_event,
        subject_ct=ct,
        object_id=object_id,
        actions_fired=actions_fired,
        success=success,
        error_message=error_message,
    )
