"""FlowPilot migration Phase 1 — schema for the unified session surface.

Revision ID: f07010f17b01
Revises: 074
Create Date: 2026-04-17

Creates the backing store for the FlowPilot unified session surface:

- `session_facts` — "What we know" facts, keyed to a session, with a polymorphic
  `source_ref` pointing at a task-lane item inside `ai_sessions.pending_task_lane`
  (no DB-level FK; integrity enforced at the service layer per the design doc).
- `session_suggested_fixes` — AI-proposed resolution paths. Only one active
  (`superseded_at IS NULL`) per session at a time.
- `draft_templates` — scripts pending post-resolve templatization
  (Option 2 in the three-option dialog).
- `account_settings` — new per-account key/value settings table with a JSONB
  `preferences` grab-bag. Rows are created lazily on first write.
- Column additions to `ai_sessions` — resolution/escalation markdown + external IDs,
  plus `state_version` (incremented by any write that invalidates the resolution
  note preview cache).
- Column additions to `script_templates` — provenance fields for templates
  promoted from draft_templates.

All four new tenant-scoped tables have RLS enabled + forced with a
`tenant_isolation` policy matching the repo pattern (USING + WITH CHECK on
`account_id = app.current_account_id`). Downgrade is reversible: drops in the
inverse order of creation.

Chained from `074` (add_network_diagrams_table) per the single-head state of
production; the other local heads on feat/flowpilot-migration are branch
artifacts not present in production.
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID, JSONB


revision = "f07010f17b01"
down_revision = "074"
branch_labels = None
depends_on = None


_CURRENT_ACCOUNT = (
    "COALESCE("
    "NULLIF(current_setting('app.current_account_id', TRUE), ''), "
    "'00000000-0000-0000-0000-000000000000'"
    ")::uuid"
)


def upgrade() -> None:
    # ── ai_sessions: resolution / escalation columns + state_version ───────
    op.add_column(
        "ai_sessions",
        sa.Column("resolution_note_markdown", sa.Text(), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column("resolution_note_posted_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column("resolution_note_external_id", sa.String(128), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column("escalation_package_markdown", sa.Text(), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column("escalation_package_posted_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column("escalation_package_external_id", sa.String(128), nullable=True),
    )
    op.add_column(
        "ai_sessions",
        sa.Column(
            "state_version",
            sa.Integer(),
            nullable=False,
            server_default=sa.text("0"),
        ),
    )

    # ── script_templates: provenance for post-resolve promotion ────────────
    op.add_column(
        "script_templates",
        sa.Column(
            "source_session_id",
            UUID(as_uuid=True),
            sa.ForeignKey("ai_sessions.id"),
            nullable=True,
        ),
    )
    op.add_column(
        "script_templates",
        sa.Column(
            "source_user_id",
            UUID(as_uuid=True),
            sa.ForeignKey("users.id"),
            nullable=True,
        ),
    )
    op.add_column(
        "script_templates",
        sa.Column("source_ticket_ref", sa.String(64), nullable=True),
    )

    # ── session_facts ──────────────────────────────────────────────────────
    op.create_table(
        "session_facts",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "session_id",
            UUID(as_uuid=True),
            sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column(
            "account_id",
            UUID(as_uuid=True),
            sa.ForeignKey("accounts.id"),
            nullable=False,
        ),
        sa.Column("text", sa.Text(), nullable=False),
        sa.Column("source_type", sa.String(32), nullable=False),
        # `source_ref` is a polymorphic pointer to a task-lane item inside
        # ai_sessions.pending_task_lane JSON, NOT a FK to any table.
        # Integrity enforced at the service layer per Section 4.2 of the
        # migration design doc.
        sa.Column("source_ref", UUID(as_uuid=True), nullable=True),
        sa.Column("source_summary", sa.Text(), nullable=True),
        sa.Column(
            "created_by",
            UUID(as_uuid=True),
            sa.ForeignKey("users.id"),
            nullable=False,
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column(
            "updated_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
        sa.CheckConstraint(
            "source_type IN ('question', 'diagnostic_check', 'user_note', 'ai_synthesis')",
            name="ck_session_facts_source_type",
        ),
    )
    # Active-facts-per-session; partial index excludes soft-deleted rows.
    op.create_index(
        "idx_session_facts_session",
        "session_facts",
        ["session_id"],
        postgresql_where=sa.text("deleted_at IS NULL"),
    )
    op.create_index(
        "idx_session_facts_account",
        "session_facts",
        ["account_id"],
    )
    op.execute("ALTER TABLE session_facts ENABLE ROW LEVEL SECURITY")
    op.execute("ALTER TABLE session_facts FORCE ROW LEVEL SECURITY")
    op.execute(f"""
        CREATE POLICY tenant_isolation ON session_facts
        USING (account_id = {_CURRENT_ACCOUNT})
        WITH CHECK (account_id = {_CURRENT_ACCOUNT})
    """)

    # ── session_suggested_fixes ────────────────────────────────────────────
    op.create_table(
        "session_suggested_fixes",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "session_id",
            UUID(as_uuid=True),
            sa.ForeignKey("ai_sessions.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column(
            "account_id",
            UUID(as_uuid=True),
            sa.ForeignKey("accounts.id"),
            nullable=False,
        ),
        sa.Column("title", sa.String(200), nullable=False),
        sa.Column("description", sa.Text(), nullable=False),
        sa.Column("confidence_pct", sa.Integer(), nullable=False),
        sa.Column(
            "script_template_id",
            UUID(as_uuid=True),
            sa.ForeignKey("script_templates.id"),
            nullable=True,
        ),
        sa.Column("ai_drafted_script", sa.Text(), nullable=True),
        sa.Column("ai_drafted_parameters", JSONB(), nullable=True),
        sa.Column("user_decision", sa.String(32), nullable=True),
        sa.Column("superseded_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.CheckConstraint(
            "confidence_pct BETWEEN 0 AND 100",
            name="ck_session_suggested_fixes_confidence_pct",
        ),
        sa.CheckConstraint(
            "user_decision IS NULL OR user_decision IN ("
            "'one_off', 'draft_template', 'build_template', 'dismissed')",
            name="ck_session_suggested_fixes_user_decision",
        ),
    )
    # Only-one-active-per-session is enforced by service-layer supersession;
    # this partial index serves the "find active fix" query.
    op.create_index(
        "idx_session_suggested_fixes_session_active",
        "session_suggested_fixes",
        ["session_id"],
        postgresql_where=sa.text("superseded_at IS NULL"),
    )
    op.execute("ALTER TABLE session_suggested_fixes ENABLE ROW LEVEL SECURITY")
    op.execute("ALTER TABLE session_suggested_fixes FORCE ROW LEVEL SECURITY")
    op.execute(f"""
        CREATE POLICY tenant_isolation ON session_suggested_fixes
        USING (account_id = {_CURRENT_ACCOUNT})
        WITH CHECK (account_id = {_CURRENT_ACCOUNT})
    """)

    # ── draft_templates ────────────────────────────────────────────────────
    op.create_table(
        "draft_templates",
        sa.Column(
            "id",
            UUID(as_uuid=True),
            primary_key=True,
            server_default=sa.text("gen_random_uuid()"),
        ),
        sa.Column(
            "account_id",
            UUID(as_uuid=True),
            sa.ForeignKey("accounts.id"),
            nullable=False,
        ),
        sa.Column(
            "source_session_id",
            UUID(as_uuid=True),
            sa.ForeignKey("ai_sessions.id"),
            nullable=False,
        ),
        sa.Column(
            "source_user_id",
            UUID(as_uuid=True),
            sa.ForeignKey("users.id"),
            nullable=False,
        ),
        sa.Column("script_body", sa.Text(), nullable=False),
        sa.Column("proposed_parameters", JSONB(), nullable=False),
        sa.Column("proposed_name", sa.String(200), nullable=True),
        sa.Column(
            "proposed_category_id",
            UUID(as_uuid=True),
            sa.ForeignKey("script_categories.id"),
            nullable=True,
        ),
        sa.Column(
            "status",
            sa.String(32),
            nullable=False,
            server_default=sa.text("'pending'"),
        ),
        sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column(
            "promoted_template_id",
            UUID(as_uuid=True),
            sa.ForeignKey("script_templates.id"),
            nullable=True,
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.CheckConstraint(
            "status IN ('pending', 'accepted', 'rejected')",
            name="ck_draft_templates_status",
        ),
    )
    # Supports the Script Library "N scripts ready to review" badge.
    op.create_index(
        "idx_draft_templates_account_pending",
        "draft_templates",
        ["account_id"],
        postgresql_where=sa.text("status = 'pending'"),
    )
    op.execute("ALTER TABLE draft_templates ENABLE ROW LEVEL SECURITY")
    op.execute("ALTER TABLE draft_templates FORCE ROW LEVEL SECURITY")
    op.execute(f"""
        CREATE POLICY tenant_isolation ON draft_templates
        USING (account_id = {_CURRENT_ACCOUNT})
        WITH CHECK (account_id = {_CURRENT_ACCOUNT})
    """)

    # ── account_settings ───────────────────────────────────────────────────
    # One row per account, created lazily on first write. The `preferences`
    # JSONB is a grab-bag for simple settings (e.g. templatize_prompt_enabled).
    # Settings graduate to typed columns via future migrations when they meet
    # the promotion criteria in Section 4.6 of the design doc (hot path /
    # validation / joins).
    op.create_table(
        "account_settings",
        sa.Column(
            "account_id",
            UUID(as_uuid=True),
            sa.ForeignKey("accounts.id", ondelete="CASCADE"),
            primary_key=True,
        ),
        sa.Column(
            "preferences",
            JSONB(),
            nullable=False,
            server_default=sa.text("'{}'::jsonb"),
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
        sa.Column(
            "updated_at",
            sa.DateTime(timezone=True),
            nullable=False,
            server_default=sa.text("now()"),
        ),
    )
    op.execute("ALTER TABLE account_settings ENABLE ROW LEVEL SECURITY")
    op.execute("ALTER TABLE account_settings FORCE ROW LEVEL SECURITY")
    op.execute(f"""
        CREATE POLICY tenant_isolation ON account_settings
        USING (account_id = {_CURRENT_ACCOUNT})
        WITH CHECK (account_id = {_CURRENT_ACCOUNT})
    """)


def downgrade() -> None:
    # Drop in reverse order so FK dependencies unwind cleanly.
    op.execute("DROP POLICY IF EXISTS tenant_isolation ON account_settings")
    op.execute("ALTER TABLE account_settings DISABLE ROW LEVEL SECURITY")
    op.drop_table("account_settings")

    op.execute("DROP POLICY IF EXISTS tenant_isolation ON draft_templates")
    op.execute("ALTER TABLE draft_templates DISABLE ROW LEVEL SECURITY")
    op.drop_index("idx_draft_templates_account_pending", table_name="draft_templates")
    op.drop_table("draft_templates")

    op.execute("DROP POLICY IF EXISTS tenant_isolation ON session_suggested_fixes")
    op.execute("ALTER TABLE session_suggested_fixes DISABLE ROW LEVEL SECURITY")
    op.drop_index(
        "idx_session_suggested_fixes_session_active",
        table_name="session_suggested_fixes",
    )
    op.drop_table("session_suggested_fixes")

    op.execute("DROP POLICY IF EXISTS tenant_isolation ON session_facts")
    op.execute("ALTER TABLE session_facts DISABLE ROW LEVEL SECURITY")
    op.drop_index("idx_session_facts_account", table_name="session_facts")
    op.drop_index("idx_session_facts_session", table_name="session_facts")
    op.drop_table("session_facts")

    op.drop_column("script_templates", "source_ticket_ref")
    op.drop_column("script_templates", "source_user_id")
    op.drop_column("script_templates", "source_session_id")

    op.drop_column("ai_sessions", "state_version")
    op.drop_column("ai_sessions", "escalation_package_external_id")
    op.drop_column("ai_sessions", "escalation_package_posted_at")
    op.drop_column("ai_sessions", "escalation_package_markdown")
    op.drop_column("ai_sessions", "resolution_note_external_id")
    op.drop_column("ai_sessions", "resolution_note_posted_at")
    op.drop_column("ai_sessions", "resolution_note_markdown")
