"""Add resource registry tables (resource_types, resources, resource_edges).

This migration creates the three core resource registry tables needed for
the Unified Resource Abstraction Layer:

- ``resource_types``: Schema-level definitions that constrain categories
  of resources (CLI args, parent/child rules, sandbox strategy, handler).
- ``resources``: Independently registered entities (git repos, filesystems,
  databases, APIs) with ULID primary keys and optional namespaced names.
- ``resource_edges``: DAG edges (parent → child) with type constraints
  and link-type annotation.

Revision ID: b1_001_resource_registry
Revises: a5_004_spec_aligned_plans
Create Date: 2026-02-13 12:00:00

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

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


def upgrade() -> None:
    """Create resource_types, resources, and resource_edges tables."""
    # --- resource_types table ---
    op.create_table(
        "resource_types",
        # PK: namespaced type name (e.g., "builtin/git-checkout")
        sa.Column("name", sa.String(255), nullable=False),
        # Extracted namespace for filtering (e.g., "builtin")
        sa.Column("namespace", sa.String(100), nullable=False),
        # Human-readable description
        sa.Column("description", sa.Text(), nullable=True),
        # Physical or virtual classification
        sa.Column("resource_kind", sa.String(20), nullable=False),
        # Default sandbox strategy for instances of this type
        sa.Column("sandbox_strategy", sa.String(50), nullable=True),
        # Whether users can add instances via CLI
        sa.Column(
            "user_addable",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("0"),
        ),
        # Handler implementation reference (dotted path or plugin name)
        sa.Column("handler_ref", sa.String(500), nullable=True),
        # JSON: CLI argument definitions for `resource add <type>`
        sa.Column("args_schema_json", sa.Text(), nullable=True),
        # JSON: list of allowed parent type names
        sa.Column("allowed_parent_types_json", sa.Text(), nullable=True),
        # JSON: list of allowed child type definitions
        sa.Column("allowed_child_types_json", sa.Text(), nullable=True),
        # JSON: auto-discovery configuration
        sa.Column("auto_discover_json", sa.Text(), nullable=True),
        # JSON: capability flags (readable, writable, sandboxable, checkpointable)
        sa.Column("capabilities_json", sa.Text(), nullable=True),
        # JSON: equivalence criteria for virtual types
        sa.Column("equivalence_json", sa.Text(), nullable=True),
        # Source: "builtin" or path to YAML config file
        sa.Column("source", sa.String(500), nullable=True),
        # Timestamps (ISO-8601 strings)
        sa.Column("created_at", sa.String(30), nullable=False),
        sa.Column("updated_at", sa.String(30), nullable=False),
        # Constraints
        sa.PrimaryKeyConstraint("name"),
        sa.CheckConstraint(
            "resource_kind IN ('physical', 'virtual')",
            name="ck_resource_types_kind",
        ),
    )

    op.create_index(
        "ix_resource_types_namespace",
        "resource_types",
        ["namespace"],
        unique=False,
    )
    op.create_index(
        "ix_resource_types_kind",
        "resource_types",
        ["resource_kind"],
        unique=False,
    )
    op.create_index(
        "ix_resource_types_user_addable",
        "resource_types",
        ["user_addable"],
        unique=False,
    )

    # --- resources table ---
    op.create_table(
        "resources",
        # PK: ULID (26-char string)
        sa.Column("resource_id", sa.String(26), nullable=False),
        # Optional namespaced name (NULL for auto-discovered children)
        sa.Column("namespaced_name", sa.String(255), nullable=True),
        # Extracted namespace for filtering
        sa.Column("namespace", sa.String(100), nullable=True),
        # Type reference
        sa.Column(
            "type_name",
            sa.String(255),
            sa.ForeignKey(
                "resource_types.name",
                ondelete="RESTRICT",
                name="fk_resources_type",
            ),
            nullable=False,
        ),
        # Physical or virtual (derived from type, stored for query efficiency)
        sa.Column("resource_kind", sa.String(20), nullable=False),
        # Location (physical resources only; NULL for virtual)
        sa.Column("location", sa.Text(), nullable=True),
        # Human-readable description
        sa.Column("description", sa.Text(), nullable=True),
        # Read-only flag
        sa.Column(
            "read_only",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("0"),
        ),
        # Per-resource sandbox strategy override (NULL = use type default)
        sa.Column("sandbox_strategy", sa.String(50), nullable=True),
        # Content hash for equivalence tracking (virtual types)
        sa.Column("content_hash", sa.String(128), nullable=True),
        # JSON: type-specific properties (path, branch, url, etc.)
        sa.Column("properties_json", sa.Text(), nullable=True),
        # JSON: type-specific metadata
        sa.Column("metadata_json", sa.Text(), nullable=True),
        # Whether auto-discovered (not user-registered)
        sa.Column(
            "auto_discovered",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("0"),
        ),
        # Timestamps (ISO-8601 strings)
        sa.Column("created_at", sa.String(30), nullable=False),
        sa.Column("updated_at", sa.String(30), nullable=False),
        # Constraints
        sa.PrimaryKeyConstraint("resource_id"),
        sa.CheckConstraint(
            "resource_kind IN ('physical', 'virtual')",
            name="ck_resources_kind",
        ),
    )

    # Unique index on namespaced_name when non-null
    op.create_index(
        "ix_resources_name",
        "resources",
        ["namespaced_name"],
        unique=True,
        postgresql_where=sa.text("namespaced_name IS NOT NULL"),
    )
    # For SQLite: partial unique index using sqlite_where
    # Note: SQLAlchemy handles this via the unique=True + nullable column combo;
    # SQLite treats NULL values as distinct in unique constraints naturally.

    op.create_index(
        "ix_resources_namespace",
        "resources",
        ["namespace"],
        unique=False,
    )
    op.create_index(
        "ix_resources_type",
        "resources",
        ["type_name"],
        unique=False,
    )
    op.create_index(
        "ix_resources_kind",
        "resources",
        ["resource_kind"],
        unique=False,
    )
    op.create_index(
        "ix_resources_content_hash",
        "resources",
        ["content_hash"],
        unique=False,
    )

    # --- resource_edges table (DAG: parent -> child) ---
    op.create_table(
        "resource_edges",
        # Parent resource ULID
        sa.Column(
            "parent_id",
            sa.String(26),
            sa.ForeignKey(
                "resources.resource_id",
                ondelete="CASCADE",
                name="fk_resource_edges_parent",
            ),
            nullable=False,
        ),
        # Child resource ULID
        sa.Column(
            "child_id",
            sa.String(26),
            sa.ForeignKey(
                "resources.resource_id",
                ondelete="CASCADE",
                name="fk_resource_edges_child",
            ),
            nullable=False,
        ),
        # Link type: contains | references | derived_from
        sa.Column(
            "link_type",
            sa.String(30),
            nullable=False,
            server_default="contains",
        ),
        # Whether auto-discovered (vs manually linked)
        sa.Column(
            "auto_discovered",
            sa.Boolean(),
            nullable=False,
            server_default=sa.text("0"),
        ),
        # Timestamp
        sa.Column("created_at", sa.String(30), nullable=False),
        # Constraints
        sa.PrimaryKeyConstraint("parent_id", "child_id"),
        sa.CheckConstraint(
            "parent_id != child_id",
            name="ck_resource_edges_no_self_loop",
        ),
        sa.CheckConstraint(
            "link_type IN ('contains', 'references', 'derived_from')",
            name="ck_resource_edges_link_type",
        ),
    )

    op.create_index(
        "ix_resource_edges_child",
        "resource_edges",
        ["child_id"],
        unique=False,
    )
    op.create_index(
        "ix_resource_edges_link_type",
        "resource_edges",
        ["link_type"],
        unique=False,
    )


def downgrade() -> None:
    """Drop resource registry tables."""
    # Drop in reverse FK order: edges -> resources -> resource_types
    op.drop_index("ix_resource_edges_link_type", table_name="resource_edges")
    op.drop_index("ix_resource_edges_child", table_name="resource_edges")
    op.drop_table("resource_edges")

    op.drop_index("ix_resources_content_hash", table_name="resources")
    op.drop_index("ix_resources_kind", table_name="resources")
    op.drop_index("ix_resources_type", table_name="resources")
    op.drop_index("ix_resources_namespace", table_name="resources")
    op.drop_index("ix_resources_name", table_name="resources")
    op.drop_table("resources")

    op.drop_index("ix_resource_types_user_addable", table_name="resource_types")
    op.drop_index("ix_resource_types_kind", table_name="resource_types")
    op.drop_index("ix_resource_types_namespace", table_name="resource_types")
    op.drop_table("resource_types")
