"""
Chat models for conversations and messages.
Domain-driven organization following news.py, research.py patterns.

Chat schema:
- ChatMessage.content is NOT NULL — every row stores its rendered text inline.
- No CHECK constraint (content NOT NULL is sufficient).
- Step rows live in ChatProgressStep, NOT in ChatMessage.
- ChatSession.status is a typed Enum (ChatSessionStatus).
"""

import enum

from sqlalchemy import (
    JSON,
    Column,
    Enum,
    ForeignKey,
    Index,
    Integer,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import relationship
from sqlalchemy_utc import UtcDateTime, utcnow

from .base import Base


class ChatRole(str, enum.Enum):
    """Who authored a chat message."""

    USER = "user"
    ASSISTANT = "assistant"


class ChatMessageType(str, enum.Enum):
    """Role a message plays in the conversation flow.

    Step rows (transient research progress) live in ChatProgressStep, not
    in ChatMessage. The chat_message_type Enum no longer includes "step".
    """

    QUERY = "query"
    FOLLOWUP = "followup"
    RESPONSE = "response"


class ChatSessionStatus(str, enum.Enum):
    """Lifecycle state of a chat session."""

    ACTIVE = "active"
    ARCHIVED = "archived"
    DELETED = "deleted"


class ChatSession(Base):
    """
    Chat conversation session.
    A session contains multiple messages and can span multiple research operations.
    """

    __tablename__ = "chat_sessions"

    # UUID as primary key (consistent with ResearchHistory)
    id = Column(String(36), primary_key=True)

    # Session metadata
    title = Column(String(500))
    # No create_constraint=True: the migration intentionally omits the
    # CHECK constraint (matches the project pattern; enforcement is
    # at the ORM/service layer via ChatSessionStatus(value)). Adding it
    # here would create schema drift — fresh installs would have the
    # CHECK, migrated DBs would not.
    status = Column(
        Enum(
            ChatSessionStatus,
            values_callable=lambda obj: [e.value for e in obj],
            name="chat_session_status",
        ),
        nullable=False,
        default=ChatSessionStatus.ACTIVE,
        server_default="active",
    )

    # Accumulated context across messages — used to build the LLM prompt for
    # follow-up turns. Updated under row-level lock by ChatService.
    accumulated_context = Column(JSON)

    created_at = Column(UtcDateTime, nullable=False, default=utcnow())

    # message_count is load-bearing: used with atomic UPDATE...RETURNING for
    # safe sequence_number generation in add_message(). Step rows do NOT
    # increment this counter (they live in chat_progress_steps with their
    # own per-research counter on research_history.step_count).
    message_count = Column(
        Integer, nullable=False, default=0, server_default="0"
    )

    # Relationships
    messages = relationship(
        "ChatMessage",
        back_populates="session",
        cascade="all, delete-orphan",
        order_by="ChatMessage.sequence_number",
    )
    researches = relationship(
        "ResearchHistory",
        back_populates="chat_session",
        # DB-level FK (research_history.chat_session_id ONDELETE SET NULL)
        # handles the cleanup; no need for the ORM to emit a redundant
        # UPDATE before the parent DELETE.
        passive_deletes=True,
    )
    progress_steps = relationship(
        "ChatProgressStep",
        back_populates="session",
        cascade="all, delete-orphan",
        order_by="ChatProgressStep.created_at",
    )

    # Indexes for performance
    __table_args__ = (
        Index("idx_chat_session_status", "status"),
        Index("idx_chat_session_created", "created_at"),
        # Composite for the sidebar list_sessions hot query
        # (WHERE status=? ORDER BY created_at DESC).
        Index("idx_chat_session_status_created", "status", "created_at"),
    )

    def __repr__(self):
        return (
            f"<ChatSession(id='{self.id}', title='{self.title}', "
            f"messages={self.message_count})>"
        )


class ChatMessage(Base):
    """
    Individual durable message within a chat session.
    Content is always inline; research_id is a back-reference,
    not a content source.
    """

    __tablename__ = "chat_messages"

    # UUID as primary key
    id = Column(String(36), primary_key=True)

    # Foreign keys
    # No `index=True` here — see __table_args__ note below: single-column
    # indexes are named explicitly so create_all and the 0010 migration
    # produce identically-named indexes.
    session_id = Column(
        String(36),
        ForeignKey("chat_sessions.id", ondelete="CASCADE"),
        nullable=False,
    )

    # Back-reference to research, if this turn triggered a research run.
    # ON DELETE SET NULL: deleting the underlying research preserves the
    # chat row (content is stored inline; the link just becomes stale).
    # This is the snapshot semantic the schema delivers.
    research_id = Column(
        String(36),
        ForeignKey("research_history.id", ondelete="SET NULL"),
        nullable=True,
    )

    # Message content
    # See ChatSession.status note: no create_constraint=True to match the
    # migration's deliberately-no-CHECK shape.
    role = Column(
        Enum(
            ChatRole,
            values_callable=lambda obj: [e.value for e in obj],
            name="chat_role",
        ),
        nullable=False,
    )
    message_type = Column(
        Enum(
            ChatMessageType,
            values_callable=lambda obj: [e.value for e in obj],
            name="chat_message_type",
        ),
        nullable=False,
    )
    content = Column(Text, nullable=False)

    # Sequence for ordering within a session
    sequence_number = Column(Integer, nullable=False)

    created_at = Column(UtcDateTime, nullable=False, default=utcnow())

    # Relationships
    session = relationship("ChatSession", back_populates="messages")
    research = relationship("ResearchHistory", back_populates="chat_messages")

    # Named Index in __table_args__ (NOT index=True on the column) so both
    # the create_all path (fresh installs, test fixtures) and the migration
    # path (0010 CHAT_INDEXES list) produce identically-named indexes —
    # mirroring the ResearchResource pattern in models/research.py.
    __table_args__ = (
        UniqueConstraint(
            "session_id",
            "sequence_number",
            name="uq_chat_message_session_seq",
        ),
        # Named indexes mirroring 0010 CHAT_INDEXES so create_all and the
        # migration produce identically-named indexes (avoids drift between
        # fresh-install and migrated DBs).
        Index("ix_chat_messages_session_id", "session_id"),
        Index("ix_chat_messages_research_id", "research_id"),
        Index(
            "ix_chat_messages_session_created",
            "session_id",
            "created_at",
        ),
    )

    def __repr__(self):
        return (
            f"<ChatMessage(session='{self.session_id}', "
            f"seq={self.sequence_number}, role='{self.role}')>"
        )


class ChatProgressStep(Base):
    """
    Transient research-progress step. Streamed live during a running research,
    persisted so a session reload during in-flight research can rehydrate the
    progress accordion.

    Lives in its own table so step rows do not pollute conversation
    sequencing or the chat_messages.message_count counter.
    """

    __tablename__ = "chat_progress_steps"

    id = Column(String(36), primary_key=True)

    # Step belongs to a research run. CASCADE: deleting the research drops
    # all its steps (they're transient progress; nothing to preserve).
    # No `index=True` — see __table_args__ below.
    research_id = Column(
        String(36),
        ForeignKey("research_history.id", ondelete="CASCADE"),
        nullable=False,
    )

    # Denormalized session FK for fast session-scoped queries (the resume
    # path on chat reload reads steps by session). CASCADE: deleting the
    # session drops all its steps.
    # No `index=True` — see __table_args__ below.
    session_id = Column(
        String(36),
        ForeignKey("chat_sessions.id", ondelete="CASCADE"),
        nullable=False,
    )

    # Phase from research_service._STEP_PHASES (e.g. "search", "observation").
    # Nullable so legacy rows migrated from chat_messages can be carried
    # forward without phase data.
    phase = Column(String(64), nullable=True)

    content = Column(Text, nullable=False)

    # Per-research sequence — allocated atomically by ChatService.add_progress_step
    # via UPDATE...RETURNING on research_history.step_count.
    sequence_number = Column(Integer, nullable=False)

    created_at = Column(UtcDateTime, nullable=False, default=utcnow())

    # Relationships
    research = relationship("ResearchHistory", back_populates="progress_steps")
    session = relationship("ChatSession", back_populates="progress_steps")

    __table_args__ = (
        UniqueConstraint(
            "research_id",
            "sequence_number",
            name="uq_chat_progress_step_research_seq",
        ),
        # Named indexes mirroring 0010 CHAT_INDEXES so create_all and the
        # migration produce identically-named indexes.
        Index("ix_chat_progress_steps_research_id", "research_id"),
        Index("ix_chat_progress_steps_session_id", "session_id"),
        Index(
            "ix_chat_progress_steps_session_created",
            "session_id",
            "created_at",
        ),
    )

    def __repr__(self):
        return (
            f"<ChatProgressStep(research='{self.research_id}', "
            f"seq={self.sequence_number}, phase='{self.phase}')>"
        )
