"""Rebaseline plan phase/state enums for Action + Apply terminal states.

This migration updates the ``v3_plans`` table CHECK constraints to align with
the four-phase lifecycle: Action -> Strategize -> Execute -> Apply.

Changes:
- ``ck_v3_plans_phase``: add ``'action'``, remove ``'applied'``
- ``ck_v3_plans_state``: add ``'applied'`` and ``'constrained'``
- Default ``phase`` changes from ``'strategize'`` to ``'action'``

SQLite does not support ``ALTER TABLE ... DROP CONSTRAINT``, so we rebuild
the ``v3_plans`` table using the standard temp-table-copy-rename approach.
Child tables (``plan_projects``, ``plan_arguments``, ``plan_invariants``)
have their FKs re-pointed during the rebuild.

Revision ID: a5_005_rebaseline_plan_phases
Revises: b1_001_resource_registry
Create Date: 2026-02-15 12:00:00

"""

from __future__ import annotations

from collections.abc import Sequence
from typing import Any

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a5_005_rebaseline_plan_phases"
down_revision: str | Sequence[str] | None = "b1_001_resource_registry"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None

# ── Column lists (shared by upgrade/downgrade) ────────────────────────────

_V3_PLANS_COLUMNS = [
    sa.Column("plan_id", sa.String(26), nullable=False),
    sa.Column("parent_plan_id", sa.String(26), nullable=True),
    sa.Column("root_plan_id", sa.String(26), nullable=True),
    sa.Column("action_name", sa.String(255), nullable=False),
    sa.Column("namespaced_name", sa.String(255), nullable=False),
    sa.Column("namespace", sa.String(100), nullable=False),
    # phase and processing_state are added dynamically
    sa.Column("attempt", sa.Integer(), nullable=False, server_default=sa.text("1")),
    sa.Column("description", sa.Text(), nullable=False),
    sa.Column("definition_of_done", sa.Text(), nullable=True),
    sa.Column("strategy_actor", sa.String(255), nullable=True),
    sa.Column("execution_actor", sa.String(255), nullable=True),
    sa.Column("review_actor", sa.String(255), nullable=True),
    sa.Column("apply_actor", sa.String(255), nullable=True),
    sa.Column("estimation_actor", sa.String(255), nullable=True),
    sa.Column("invariant_actor", sa.String(255), nullable=True),
    sa.Column("automation_profile", sa.String(255), nullable=True),
    sa.Column(
        "automation_level", sa.String(30), nullable=False, server_default="manual"
    ),
    sa.Column("reusable", sa.Boolean(), nullable=False, server_default=sa.text("1")),
    sa.Column("read_only", sa.Boolean(), nullable=False, server_default=sa.text("0")),
    sa.Column("inputs_schema_json", sa.Text(), nullable=True),
    sa.Column("changeset_id", sa.String(26), nullable=True),
    sa.Column("sandbox_refs_json", sa.Text(), nullable=True),
    sa.Column("validation_summary_json", sa.Text(), nullable=True),
    sa.Column("decision_root_id", sa.String(26), nullable=True),
    sa.Column("error_message", sa.Text(), nullable=True),
    sa.Column("error_details_json", sa.Text(), nullable=True),
    sa.Column("cost_estimate_usd", sa.Float(), nullable=True),
    sa.Column("cost_actual_usd", sa.Float(), nullable=True),
    sa.Column(
        "token_count_input",
        sa.Integer(),
        nullable=True,
        server_default=sa.text("0"),
    ),
    sa.Column(
        "token_count_output",
        sa.Integer(),
        nullable=True,
        server_default=sa.text("0"),
    ),
    sa.Column("created_by", sa.String(255), nullable=True),
    sa.Column("tags_json", sa.Text(), nullable=False, server_default="[]"),
    sa.Column("created_at", sa.String(30), nullable=False),
    sa.Column("updated_at", sa.String(30), nullable=False),
    sa.Column("completed_at", sa.String(30), nullable=True),
    sa.Column("strategize_started_at", sa.String(30), nullable=True),
    sa.Column("strategize_completed_at", sa.String(30), nullable=True),
    sa.Column("execute_started_at", sa.String(30), nullable=True),
    sa.Column("execute_completed_at", sa.String(30), nullable=True),
    sa.Column("apply_started_at", sa.String(30), nullable=True),
    sa.Column("applied_at", sa.String(30), nullable=True),
]

# Column names (excluding phase/processing_state which differ between versions)
_ALL_DATA_COLUMNS = (
    "plan_id, parent_plan_id, root_plan_id, action_name, "
    "namespaced_name, namespace, phase, processing_state, attempt, "
    "description, definition_of_done, "
    "strategy_actor, execution_actor, review_actor, apply_actor, "
    "estimation_actor, invariant_actor, "
    "automation_profile, automation_level, "
    "reusable, read_only, inputs_schema_json, "
    "changeset_id, sandbox_refs_json, validation_summary_json, decision_root_id, "
    "error_message, error_details_json, "
    "cost_estimate_usd, cost_actual_usd, "
    "token_count_input, token_count_output, "
    "created_by, tags_json, "
    "created_at, updated_at, completed_at, "
    "strategize_started_at, strategize_completed_at, "
    "execute_started_at, execute_completed_at, "
    "apply_started_at, applied_at"
)

# ── New (upgrade) constraints ──────────────────────────────────────────────
_NEW_PHASE_CK = "phase IN ('action', 'strategize', 'execute', 'apply')"
_NEW_STATE_CK = (
    "processing_state IN "
    "('queued', 'processing', 'errored', 'complete', 'cancelled', "
    "'applied', 'constrained')"
)
_NEW_PHASE_DEFAULT = "action"

# ── Old (downgrade) constraints ────────────────────────────────────────────
_OLD_PHASE_CK = "phase IN ('strategize', 'execute', 'apply', 'applied')"
_OLD_STATE_CK = (
    "processing_state IN ('queued', 'processing', 'errored', 'complete', 'cancelled')"
)
_OLD_PHASE_DEFAULT = "strategize"


def _rebuild_v3_plans(
    phase_check: str,
    state_check: str,
    phase_default: str,
) -> None:
    """Rebuild v3_plans with new CHECK constraints (SQLite-safe).

    Steps:
    1. Create ``_v3_plans_new`` with updated constraints.
    2. Copy all data from ``v3_plans``.
    3. Drop child-table FKs (SQLite: drop+recreate child tables).
    4. Drop old ``v3_plans``.
    5. Rename ``_v3_plans_new`` → ``v3_plans``.
    6. Recreate child tables pointing at the renamed table.
    7. Recreate indexes.
    """
    conn = op.get_bind()
    dialect = conn.dialect.name

    if dialect == "sqlite":
        # Turn off FK enforcement during rebuild
        conn.execute(sa.text("PRAGMA foreign_keys = OFF"))

    # ── 1. Create temp table ──────────────────────────────────────────
    columns: list[Any] = []
    for col in _V3_PLANS_COLUMNS:
        columns.append(col.copy())
    # Insert phase and processing_state at the right position (after namespace)
    phase_col: Any = sa.Column(
        "phase", sa.String(20), nullable=False, server_default=phase_default
    )
    state_col: Any = sa.Column(
        "processing_state", sa.String(20), nullable=False, server_default="queued"
    )
    # Insert after namespace (index 5 in the list)
    columns.insert(6, phase_col)
    columns.insert(7, state_col)

    op.create_table(
        "_v3_plans_new",
        *columns,
        sa.PrimaryKeyConstraint("plan_id"),
        sa.ForeignKeyConstraint(
            ["parent_plan_id"],
            ["_v3_plans_new.plan_id"],
            name="fk_v3_plans_parent",
            ondelete="SET NULL",
        ),
        sa.ForeignKeyConstraint(
            ["root_plan_id"],
            ["_v3_plans_new.plan_id"],
            name="fk_v3_plans_root",
            ondelete="SET NULL",
        ),
        sa.ForeignKeyConstraint(
            ["action_name"],
            ["actions.namespaced_name"],
            name="fk_v3_plans_action",
            ondelete="RESTRICT",
        ),
        sa.CheckConstraint(phase_check, name="ck_v3_plans_phase"),
        sa.CheckConstraint(state_check, name="ck_v3_plans_state"),
        sa.CheckConstraint(
            "automation_level IN ('manual', 'review_before_apply', 'full_automation')",
            name="ck_v3_plans_automation",
        ),
    )

    # ── 2. Copy data ─────────────────────────────────────────────────
    conn.execute(
        sa.text(
            f"INSERT INTO _v3_plans_new ({_ALL_DATA_COLUMNS}) "
            f"SELECT {_ALL_DATA_COLUMNS} FROM v3_plans"
        )
    )

    # ── 3. Drop child tables (they have FKs to v3_plans) ─────────────
    # Save child data first
    conn.execute(
        sa.text("CREATE TABLE _plan_projects_bak AS SELECT * FROM plan_projects")
    )
    conn.execute(
        sa.text("CREATE TABLE _plan_arguments_bak AS SELECT * FROM plan_arguments")
    )
    conn.execute(
        sa.text("CREATE TABLE _plan_invariants_bak AS SELECT * FROM plan_invariants")
    )

    op.drop_table("plan_invariants")
    op.drop_table("plan_arguments")
    op.drop_table("plan_projects")

    # ── 4. Drop indexes and old v3_plans ──────────────────────────────
    for idx_name in [
        "ix_v3_plans_phase_state",
        "ix_v3_plans_namespace",
        "ix_v3_plans_action",
        "ix_v3_plans_created",
        "ix_v3_plans_root",
        "ix_v3_plans_parent",
        "ix_v3_plans_state",
        "ix_v3_plans_phase",
    ]:
        op.drop_index(idx_name, table_name="v3_plans")
    op.drop_table("v3_plans")

    # ── 5. Rename temp → v3_plans ─────────────────────────────────────
    op.rename_table("_v3_plans_new", "v3_plans")

    # ── 6. Recreate child tables ──────────────────────────────────────
    op.create_table(
        "plan_projects",
        sa.Column(
            "plan_id",
            sa.String(26),
            sa.ForeignKey(
                "v3_plans.plan_id",
                ondelete="CASCADE",
                name="fk_plan_projects_plan",
            ),
            nullable=False,
        ),
        sa.Column("project_name", sa.String(255), nullable=False),
        sa.Column("alias", sa.String(100), nullable=True),
        sa.Column(
            "read_only", sa.Boolean(), nullable=False, server_default=sa.text("0")
        ),
        sa.Column("created_at", sa.String(30), nullable=False),
        sa.PrimaryKeyConstraint("plan_id", "project_name"),
    )
    op.create_index(
        "ix_plan_projects_project", "plan_projects", ["project_name"], unique=False
    )

    op.create_table(
        "plan_arguments",
        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
        sa.Column(
            "plan_id",
            sa.String(26),
            sa.ForeignKey(
                "v3_plans.plan_id",
                ondelete="CASCADE",
                name="fk_plan_arguments_plan",
            ),
            nullable=False,
        ),
        sa.Column("name", sa.String(100), nullable=False),
        sa.Column("value_json", sa.Text(), nullable=True),
        sa.Column("value_type", sa.String(20), nullable=False, server_default="string"),
        sa.Column("position", sa.Integer(), nullable=False),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint("plan_id", "name", name="uq_plan_arguments_name"),
    )
    op.create_index(
        "ix_plan_arguments_plan", "plan_arguments", ["plan_id"], unique=False
    )
    op.create_index(
        "ix_plan_arguments_position",
        "plan_arguments",
        ["plan_id", "position"],
        unique=False,
    )

    op.create_table(
        "plan_invariants",
        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
        sa.Column(
            "plan_id",
            sa.String(26),
            sa.ForeignKey(
                "v3_plans.plan_id",
                ondelete="CASCADE",
                name="fk_plan_invariants_plan",
            ),
            nullable=False,
        ),
        sa.Column("invariant_text", sa.Text(), nullable=False),
        sa.Column(
            "source_scope",
            sa.String(20),
            nullable=False,
            server_default="plan",
        ),
        sa.Column("source_name", sa.String(255), nullable=True),
        sa.Column("position", sa.Integer(), nullable=False),
        sa.Column("created_at", sa.String(30), nullable=False),
        sa.PrimaryKeyConstraint("id"),
        sa.UniqueConstraint(
            "plan_id", "invariant_text", name="uq_plan_invariants_text"
        ),
        sa.CheckConstraint(
            "source_scope IN ('global', 'project', 'action', 'plan')",
            name="ck_plan_invariants_scope",
        ),
    )
    op.create_index(
        "ix_plan_invariants_plan", "plan_invariants", ["plan_id"], unique=False
    )
    op.create_index(
        "ix_plan_invariants_position",
        "plan_invariants",
        ["plan_id", "position"],
        unique=False,
    )

    # ── 7. Restore child data ─────────────────────────────────────────
    conn.execute(sa.text("INSERT INTO plan_projects SELECT * FROM _plan_projects_bak"))
    conn.execute(
        sa.text("INSERT INTO plan_arguments SELECT * FROM _plan_arguments_bak")
    )
    conn.execute(
        sa.text("INSERT INTO plan_invariants SELECT * FROM _plan_invariants_bak")
    )

    conn.execute(sa.text("DROP TABLE _plan_projects_bak"))
    conn.execute(sa.text("DROP TABLE _plan_arguments_bak"))
    conn.execute(sa.text("DROP TABLE _plan_invariants_bak"))

    # ── 8. Recreate v3_plans indexes ──────────────────────────────────
    op.create_index("ix_v3_plans_phase", "v3_plans", ["phase"], unique=False)
    op.create_index("ix_v3_plans_state", "v3_plans", ["processing_state"], unique=False)
    op.create_index("ix_v3_plans_parent", "v3_plans", ["parent_plan_id"], unique=False)
    op.create_index("ix_v3_plans_root", "v3_plans", ["root_plan_id"], unique=False)
    op.create_index("ix_v3_plans_created", "v3_plans", ["created_at"], unique=False)
    op.create_index("ix_v3_plans_action", "v3_plans", ["action_name"], unique=False)
    op.create_index("ix_v3_plans_namespace", "v3_plans", ["namespace"], unique=False)
    op.create_index(
        "ix_v3_plans_phase_state",
        "v3_plans",
        ["phase", "processing_state"],
        unique=False,
    )

    if dialect == "sqlite":
        conn.execute(sa.text("PRAGMA foreign_keys = ON"))


def upgrade() -> None:
    """Rebaseline phase/state constraints for Action phase + Apply terminals."""
    _rebuild_v3_plans(
        phase_check=_NEW_PHASE_CK,
        state_check=_NEW_STATE_CK,
        phase_default=_NEW_PHASE_DEFAULT,
    )


def downgrade() -> None:
    """Revert to pre-rebaseline phase/state constraints."""
    _rebuild_v3_plans(
        phase_check=_OLD_PHASE_CK,
        state_check=_OLD_STATE_CK,
        phase_default=_OLD_PHASE_DEFAULT,
    )
