"""Align action child table constraints with SQLAlchemy metadata.

Ensures schema parity between metadata-based table creation and Alembic
migrations by enforcing the same uniqueness and check constraints that
exist in the SQLAlchemy model definitions for ``action_invariants`` and
``action_arguments``.

Revision ID: a5_006_action_invariants_unique_constraint
Revises: m9_002_plan_resume_fields
Create Date: 2026-04-07 08:30:00

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

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


def _deduplicate_action_invariants() -> None:
    """Remove duplicate (action_name, position) rows before adding the constraint."""

    op.execute(
        sa.text(
            """
            DELETE FROM action_invariants
            WHERE id NOT IN (
                SELECT MIN(id)
                FROM action_invariants
                GROUP BY action_name, position
            )
            """
        )
    )


def _deduplicate_action_arguments() -> None:
    """Normalize action argument rows prior to adding constraints."""

    # Drop duplicate (action_name, name) rows keeping the lowest id so the
    # unique constraint can be created safely.
    op.execute(
        sa.text(
            """
            DELETE FROM action_arguments
            WHERE id NOT IN (
                SELECT MIN(id)
                FROM action_arguments
                GROUP BY action_name, name
            )
            """
        )
    )

    # Coerce invalid arg_type values to "string" so the CHECK constraint passes.
    op.execute(
        sa.text(
            """
            UPDATE action_arguments
            SET arg_type = 'string'
            WHERE arg_type NOT IN ('string', 'integer', 'float', 'boolean', 'list')
               OR arg_type IS NULL
            """
        )
    )

    # Coerce invalid requirement values to "optional" by default.
    op.execute(
        sa.text(
            """
            UPDATE action_arguments
            SET requirement = 'optional'
            WHERE requirement NOT IN ('required', 'optional')
               OR requirement IS NULL
            """
        )
    )


def upgrade() -> None:
    """Add the uq_action_invariants_position unique constraint."""

    _deduplicate_action_invariants()
    with op.batch_alter_table("action_invariants") as batch:
        batch.create_unique_constraint(
            "uq_action_invariants_position",
            ["action_name", "position"],
        )

    _deduplicate_action_arguments()
    with op.batch_alter_table("action_arguments") as batch:
        batch.create_unique_constraint(
            "uq_action_arguments_name",
            ["action_name", "name"],
        )
        batch.create_check_constraint(
            "ck_action_arguments_type",
            "arg_type IN ('string', 'integer', 'float', 'boolean', 'list')",
        )
        batch.create_check_constraint(
            "ck_action_arguments_requirement",
            "requirement IN ('required', 'optional')",
        )


def downgrade() -> None:
    """Drop the uq_action_invariants_position unique constraint."""
    with op.batch_alter_table("action_invariants") as batch:
        batch.drop_constraint(
            "uq_action_invariants_position",
            type_="unique",
        )

    with op.batch_alter_table("action_arguments") as batch:
        batch.drop_constraint(
            "ck_action_arguments_requirement",
            type_="check",
        )
        batch.drop_constraint(
            "ck_action_arguments_type",
            type_="check",
        )
        batch.drop_constraint(
            "uq_action_arguments_name",
            type_="unique",
        )
