import uuid
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional

from sqlalchemy import JSON, BigInteger, Boolean, DateTime, ForeignKey, Index, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateMixin
from letta.orm.sqlalchemy_base import SqlalchemyBase
from letta.schemas.enums import RunStatus
from letta.schemas.job import LettaRequestConfig
from letta.schemas.letta_stop_reason import StopReasonType
from letta.schemas.run import Run as PydanticRun

if TYPE_CHECKING:
    from letta.orm.agent import Agent
    from letta.orm.message import Message
    from letta.orm.organization import Organization
    from letta.orm.step import Step


class Run(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateMixin):
    """Runs are created when agents process messages and represent a conversation or processing session.
    Unlike Jobs, Runs are specifically tied to agent interactions and message processing.
    """

    __tablename__ = "runs"
    __pydantic_model__ = PydanticRun
    __table_args__ = (
        Index("ix_runs_created_at", "created_at", "id"),
        Index("ix_runs_agent_id", "agent_id"),
        Index("ix_runs_organization_id", "organization_id"),
        Index("ix_runs_conversation_id", "conversation_id"),
    )

    # Generate run ID with run- prefix
    id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"run-{uuid.uuid4()}")

    # Core run fields
    status: Mapped[RunStatus] = mapped_column(String, default=RunStatus.created, doc="The current status of the run.")
    completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the run was completed.")
    stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the run was stopped.")
    background: Mapped[Optional[bool]] = mapped_column(
        Boolean, nullable=True, default=False, doc="Whether the run was created in background mode."
    )
    metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the run.")
    request_config: Mapped[Optional[LettaRequestConfig]] = mapped_column(
        JSON, nullable=True, doc="The request configuration for the run, stored as JSON."
    )

    # Agent relationship - A run belongs to one agent
    agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"), nullable=False, doc="The agent that owns this run.")

    # Conversation relationship - Optional, a run may be associated with a conversation
    conversation_id: Mapped[Optional[str]] = mapped_column(
        String, ForeignKey("conversations.id", ondelete="SET NULL"), nullable=True, doc="The conversation this run belongs to."
    )

    # Callback related columns
    callback_url: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="When set, POST to this URL after run completion.")
    callback_sent_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="Timestamp when the callback was last attempted.")
    callback_status_code: Mapped[Optional[int]] = mapped_column(nullable=True, doc="HTTP status code returned by the callback endpoint.")
    callback_error: Mapped[Optional[str]] = mapped_column(
        nullable=True, doc="Optional error message from attempting to POST the callback endpoint."
    )

    # Timing metrics (in nanoseconds for precision)
    ttft_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Time to first token in nanoseconds")
    total_duration_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Total run duration in nanoseconds")

    # Relationships
    agent: Mapped["Agent"] = relationship("Agent", back_populates="runs")
    organization: Mapped[Optional["Organization"]] = relationship("Organization", back_populates="runs")

    # Steps that are part of this run
    steps: Mapped[List["Step"]] = relationship("Step", back_populates="run", cascade="all, delete-orphan")
    messages: Mapped[List["Message"]] = relationship("Message", back_populates="run", cascade="all, delete-orphan")
