"""Workflow-centric entities and supporting utilities."""

from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Mapping
from datetime import datetime
from enum import Enum
from typing import Any, NotRequired, TypedDict
from uuid import UUID, uuid4
from pydantic import Field, field_validator, model_validator
from orcheo.models.base import TimestampedAuditModel, _utcnow
from orcheo.models.workflow_refs import normalize_workflow_handle


__all__ = [
    "Workflow",
    "WorkflowRun",
    "WorkflowRunStatus",
    "WorkflowVersion",
]


_SLUG_RE = re.compile(r"[^a-z0-9]+")


def _slugify(value: str) -> str:
    """Convert the provided value into a workflow-safe slug."""
    normalized = _SLUG_RE.sub("-", value.strip().lower()).strip("-")
    return normalized or value.strip().lower() or str(uuid4())


class WorkflowPublishMetadata(TypedDict):
    """Audit metadata for workflow publish events."""

    require_login: bool


class WorkflowUnpublishedMetadata(TypedDict):
    """Audit metadata for workflow unpublish events."""

    require_login: bool


class WorkflowRunFailedMetadata(TypedDict):
    """Audit metadata captured when a workflow run fails."""

    error: str


class WorkflowRunCancelledMetadata(TypedDict, total=False):
    """Audit metadata captured when a workflow run is cancelled."""

    reason: NotRequired[str]


class Workflow(TimestampedAuditModel):
    """Represents a workflow container with metadata and audit trail."""

    name: str = Field(min_length=1, max_length=128)
    handle: str | None = Field(default=None, max_length=64)
    slug: str = ""
    description: str | None = Field(default=None, max_length=1024)
    tags: list[str] = Field(default_factory=list)
    is_archived: bool = False
    is_public: bool = False
    published_at: datetime | None = None
    published_by: str | None = None
    require_login: bool = False
    share_url: str | None = None

    @field_validator("name", mode="before")
    @classmethod
    def _normalize_name(cls, value: object) -> str:
        candidate = str(value or "").strip()
        if not candidate:
            msg = "Workflow name must not be empty."
            raise ValueError(msg)
        return candidate

    @field_validator("handle", mode="before")
    @classmethod
    def _normalize_handle(cls, value: object) -> str | None:
        if value is None:
            return None
        return normalize_workflow_handle(str(value))

    @field_validator("description", mode="before")
    @classmethod
    def _normalize_description(cls, value: object) -> str | None:
        if value is None:
            return None
        candidate = str(value).strip()
        return candidate or None

    @field_validator("tags", mode="after")
    @classmethod
    def _dedupe_tags(cls, value: list[str]) -> list[str]:
        seen: set[str] = set()
        deduped: list[str] = []
        for tag in value:
            normalized = tag.strip()
            key = normalized.lower()
            if key and key not in seen:
                seen.add(key)
                deduped.append(key)
        return deduped

    @model_validator(mode="after")
    def _populate_slug(self) -> Workflow:
        slug_source = self.slug or self.name
        if not slug_source:
            msg = "Workflow requires a name or slug to be provided."
            raise ValueError(msg)
        object.__setattr__(self, "slug", _slugify(slug_source))
        return self

    # -- Publish management -------------------------------------------------
    def publish(
        self,
        *,
        require_login: bool,
        actor: str,
    ) -> None:
        """Mark the workflow as publicly accessible."""
        if self.is_public:
            msg = "Workflow is already published."
            raise ValueError(msg)

        now = _utcnow()
        metadata: WorkflowPublishMetadata = {
            "require_login": require_login,
        }
        self.is_public = True
        self.published_at = now
        self.published_by = actor
        self.require_login = require_login
        self.record_event(
            actor=actor,
            action="workflow_published",
            metadata=metadata,
        )

    def revoke_publish(self, *, actor: str) -> None:
        """Revoke public access to the workflow."""
        if not self.is_public:
            msg = "Workflow is not currently published."
            raise ValueError(msg)

        metadata: WorkflowUnpublishedMetadata = {
            "require_login": self.require_login,
        }
        self.is_public = False
        self.published_at = None
        self.published_by = None
        self.require_login = False
        self.record_event(
            actor=actor,
            action="workflow_unpublished",
            metadata=metadata,
        )


class WorkflowVersion(TimestampedAuditModel):
    """Versioned definition of a workflow graph."""

    workflow_id: UUID
    version: int = Field(gt=0)
    graph: dict[str, Any] = Field(default_factory=dict)
    mermaid: str | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)
    runnable_config: dict[str, Any] | None = None
    created_by: str
    notes: str | None = None

    def compute_checksum(self) -> str:
        """Return a deterministic checksum for the graph definition."""
        serialized = json.dumps(self.graph, sort_keys=True, separators=(",", ":"))
        return hashlib.sha256(serialized.encode("utf-8")).hexdigest()


class WorkflowRunStatus(str, Enum):
    """Possible states for an individual workflow execution run."""

    PENDING = "pending"
    RUNNING = "running"
    SUCCEEDED = "succeeded"
    FAILED = "failed"
    CANCELLED = "cancelled"

    @property
    def is_terminal(self) -> bool:
        """Return whether the status represents a terminal state."""
        return self in {
            WorkflowRunStatus.SUCCEEDED,
            WorkflowRunStatus.FAILED,
            WorkflowRunStatus.CANCELLED,
        }


class WorkflowRun(TimestampedAuditModel):
    """Runtime record for a workflow execution."""

    workflow_version_id: UUID
    status: WorkflowRunStatus = Field(default=WorkflowRunStatus.PENDING)
    triggered_by: str
    input_payload: dict[str, Any] = Field(default_factory=dict)
    runnable_config: dict[str, Any] = Field(default_factory=dict)
    tags: list[str] = Field(default_factory=list)
    callbacks: list[Any] = Field(default_factory=list)
    metadata: dict[str, Any] = Field(default_factory=dict)
    run_name: str | None = None
    output_payload: dict[str, Any] | None = None
    started_at: datetime | None = None
    completed_at: datetime | None = None
    error: str | None = None

    def mark_started(self, *, actor: str) -> None:
        """Transition the run into the running state."""
        if self.status is not WorkflowRunStatus.PENDING:
            msg = "Only pending runs can be started."
            raise ValueError(msg)
        self.status = WorkflowRunStatus.RUNNING
        self.started_at = _utcnow()
        self.record_event(actor=actor, action="run_started")

    def mark_succeeded(
        self,
        *,
        actor: str,
        output: Mapping[str, Any] | None = None,
    ) -> None:
        """Mark the run as successfully completed."""
        if self.status is not WorkflowRunStatus.RUNNING:
            msg = "Only running runs can be marked as succeeded."
            raise ValueError(msg)
        self.status = WorkflowRunStatus.SUCCEEDED
        self.completed_at = _utcnow()
        self.output_payload = dict(output or {})
        self.error = None
        self.record_event(actor=actor, action="run_succeeded")

    def mark_failed(self, *, actor: str, error: str) -> None:
        """Mark the run as failed with the provided error message."""
        if self.status not in {WorkflowRunStatus.PENDING, WorkflowRunStatus.RUNNING}:
            msg = "Only pending or running runs can be marked as failed."
            raise ValueError(msg)
        self.status = WorkflowRunStatus.FAILED
        self.completed_at = _utcnow()
        self.error = error
        metadata: WorkflowRunFailedMetadata = {"error": error}
        self.record_event(actor=actor, action="run_failed", metadata=metadata)

    def mark_cancelled(self, *, actor: str, reason: str | None = None) -> None:
        """Cancel the run from a non-terminal state."""
        if self.status.is_terminal:
            msg = "Cannot cancel a run that is already completed."
            raise ValueError(msg)
        self.status = WorkflowRunStatus.CANCELLED
        self.completed_at = _utcnow()
        self.error = reason
        metadata: WorkflowRunCancelledMetadata = {}
        if reason:
            metadata["reason"] = reason
        self.record_event(actor=actor, action="run_cancelled", metadata=metadata)
