"""Database migrations for local storage.

This module handles schema migrations for the Gobby database.

For new databases (version == 0):
    BASELINE_SCHEMA_V2 is applied, jumping directly to version 75.

For existing databases (version >= 75):
    Any migrations in MIGRATIONS (v76+) are applied incrementally.

To add a new migration:
    1. Add it to the MIGRATIONS list below with version = 76, 77, etc.
    2. Use SQL strings for schema changes, callables for data migrations.
    3. Also add the migration to BASELINE_SCHEMA_V2 for future fresh installs.
"""

import logging
from collections.abc import Callable

from gobby.storage.database import LocalDatabase

logger = logging.getLogger(__name__)


class MigrationUnsupportedError(Exception):
    """Raised when database version is too old to migrate."""

    pass


# Migration can be SQL string or a callable that takes LocalDatabase
MigrationAction = str | Callable[[LocalDatabase], None]

# Baseline version - the schema state at v81 (flattened)
# This is applied for new databases directly
# Note: Migrations >= BASELINE_VERSION still run for existing databases
BASELINE_VERSION = 81

# Baseline schema - flattened from v81 production state, includes all migrations
# This is applied for new databases directly
# Generated by: sqlite3 ~/.gobby/gobby-hub.db .schema
BASELINE_SCHEMA = """
CREATE TABLE schema_version (
    version INTEGER PRIMARY KEY,
    applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    repo_path TEXT,
    github_url TEXT,
    github_repo TEXT,
    linear_team_id TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_projects_name ON projects(name);

-- Placeholder projects for orphaned/migrated data
INSERT INTO projects (id, name, repo_path, created_at, updated_at)
VALUES ('00000000-0000-0000-0000-000000000000', '_orphaned', NULL, datetime('now'), datetime('now'));
INSERT INTO projects (id, name, repo_path, created_at, updated_at)
VALUES ('00000000-0000-0000-0000-000000000001', '_migrated', NULL, datetime('now'), datetime('now'));

CREATE TABLE mcp_servers (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    transport TEXT NOT NULL,
    url TEXT,
    command TEXT,
    args TEXT,
    env TEXT,
    headers TEXT,
    enabled INTEGER DEFAULT 1,
    description TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_mcp_servers_name ON mcp_servers(name);
CREATE INDEX idx_mcp_servers_project_id ON mcp_servers(project_id);
CREATE INDEX idx_mcp_servers_enabled ON mcp_servers(enabled);
CREATE UNIQUE INDEX idx_mcp_servers_name_project ON mcp_servers(name, project_id);

CREATE TABLE tools (
    id TEXT PRIMARY KEY,
    mcp_server_id TEXT NOT NULL REFERENCES mcp_servers(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    input_schema TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(mcp_server_id, name)
);
CREATE INDEX idx_tools_server_id ON tools(mcp_server_id);
CREATE INDEX idx_tools_name ON tools(name);

CREATE TABLE tool_embeddings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tool_id TEXT NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
    server_name TEXT NOT NULL,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    embedding BLOB NOT NULL,
    embedding_model TEXT NOT NULL,
    embedding_dim INTEGER NOT NULL,
    text_hash TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(tool_id)
);
CREATE INDEX idx_tool_embeddings_tool ON tool_embeddings(tool_id);
CREATE INDEX idx_tool_embeddings_server ON tool_embeddings(server_name);
CREATE INDEX idx_tool_embeddings_project ON tool_embeddings(project_id);
CREATE INDEX idx_tool_embeddings_hash ON tool_embeddings(text_hash);

CREATE TABLE tool_schema_hashes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    server_name TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    project_id TEXT NOT NULL,
    schema_hash TEXT NOT NULL,
    last_verified_at TEXT NOT NULL DEFAULT (datetime('now')),
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(project_id, server_name, tool_name)
);
CREATE INDEX idx_schema_hashes_server ON tool_schema_hashes(server_name);
CREATE INDEX idx_schema_hashes_project ON tool_schema_hashes(project_id);
CREATE INDEX idx_schema_hashes_verified ON tool_schema_hashes(last_verified_at);

CREATE TABLE tool_metrics (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    server_name TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    call_count INTEGER NOT NULL DEFAULT 0,
    success_count INTEGER NOT NULL DEFAULT 0,
    failure_count INTEGER NOT NULL DEFAULT 0,
    total_latency_ms REAL NOT NULL DEFAULT 0,
    avg_latency_ms REAL,
    last_called_at TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(project_id, server_name, tool_name)
);
CREATE INDEX idx_tool_metrics_project ON tool_metrics(project_id);
CREATE INDEX idx_tool_metrics_server ON tool_metrics(server_name);
CREATE INDEX idx_tool_metrics_tool ON tool_metrics(tool_name);
CREATE INDEX idx_tool_metrics_call_count ON tool_metrics(call_count DESC);
CREATE INDEX idx_tool_metrics_last_called ON tool_metrics(last_called_at);

CREATE TABLE tool_metrics_daily (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    server_name TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    date TEXT NOT NULL,
    call_count INTEGER NOT NULL DEFAULT 0,
    success_count INTEGER NOT NULL DEFAULT 0,
    failure_count INTEGER NOT NULL DEFAULT 0,
    total_latency_ms REAL NOT NULL DEFAULT 0,
    avg_latency_ms REAL,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(project_id, server_name, tool_name, date)
);
CREATE INDEX idx_tool_metrics_daily_project ON tool_metrics_daily(project_id);
CREATE INDEX idx_tool_metrics_daily_date ON tool_metrics_daily(date);
CREATE INDEX idx_tool_metrics_daily_server ON tool_metrics_daily(server_name);

CREATE TABLE agent_runs (
    id TEXT PRIMARY KEY,
    parent_session_id TEXT NOT NULL REFERENCES sessions(id),
    child_session_id TEXT REFERENCES sessions(id),
    workflow_name TEXT,
    provider TEXT NOT NULL,
    model TEXT,
    status TEXT NOT NULL DEFAULT 'pending',
    prompt TEXT NOT NULL,
    result TEXT,
    error TEXT,
    tool_calls_count INTEGER DEFAULT 0,
    turns_used INTEGER DEFAULT 0,
    started_at TEXT,
    completed_at TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_agent_runs_parent_session ON agent_runs(parent_session_id);
CREATE INDEX idx_agent_runs_child_session ON agent_runs(child_session_id);
CREATE INDEX idx_agent_runs_status ON agent_runs(status);
CREATE INDEX idx_agent_runs_provider ON agent_runs(provider);

CREATE TABLE sessions (
    id TEXT PRIMARY KEY,
    external_id TEXT NOT NULL,
    machine_id TEXT NOT NULL,
    source TEXT NOT NULL,
    project_id TEXT NOT NULL REFERENCES projects(id),
    title TEXT,
    status TEXT DEFAULT 'active',
    jsonl_path TEXT,
    summary_path TEXT,
    summary_markdown TEXT,
    compact_markdown TEXT,
    git_branch TEXT,
    parent_session_id TEXT REFERENCES sessions(id),
    transcript_processed BOOLEAN DEFAULT FALSE,
    agent_depth INTEGER DEFAULT 0,
    spawned_by_agent_id TEXT,
    workflow_name TEXT,
    step_variables TEXT,
    agent_run_id TEXT REFERENCES agent_runs(id) ON DELETE SET NULL,
    context_injected INTEGER DEFAULT 0,
    original_prompt TEXT,
    usage_input_tokens INTEGER DEFAULT 0,
    usage_output_tokens INTEGER DEFAULT 0,
    usage_cache_creation_tokens INTEGER DEFAULT 0,
    usage_cache_read_tokens INTEGER DEFAULT 0,
    usage_total_cost_usd REAL DEFAULT 0.0,
    terminal_context TEXT,
    seq_num INTEGER,
    model TEXT,
    had_edits BOOLEAN DEFAULT 0,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_sessions_external_id ON sessions(external_id);
CREATE INDEX idx_sessions_machine_id ON sessions(machine_id);
CREATE INDEX idx_sessions_source ON sessions(source);
CREATE INDEX idx_sessions_status ON sessions(status);
CREATE INDEX idx_sessions_project_id ON sessions(project_id);
CREATE INDEX idx_sessions_pending_transcript ON sessions(status, transcript_processed)
    WHERE status = 'expired' AND transcript_processed = FALSE;
CREATE INDEX idx_sessions_agent_depth ON sessions(agent_depth);
CREATE INDEX idx_sessions_spawned_by ON sessions(spawned_by_agent_id);
CREATE INDEX idx_sessions_workflow ON sessions(workflow_name);
CREATE INDEX idx_sessions_agent_run ON sessions(agent_run_id);
CREATE UNIQUE INDEX idx_sessions_seq_num ON sessions(project_id, seq_num);
CREATE UNIQUE INDEX idx_sessions_unique ON sessions(external_id, machine_id, source, project_id);

CREATE TABLE session_messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    message_index INTEGER NOT NULL,
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    content_type TEXT DEFAULT 'text',
    tool_name TEXT,
    tool_input TEXT,
    tool_result TEXT,
    timestamp TEXT NOT NULL,
    raw_json TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(session_id, message_index)
);
CREATE INDEX idx_session_messages_session ON session_messages(session_id);
CREATE INDEX idx_session_messages_role ON session_messages(role);
CREATE INDEX idx_session_messages_timestamp ON session_messages(timestamp);
CREATE INDEX idx_session_messages_tool ON session_messages(tool_name);

CREATE TABLE session_message_state (
    session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
    last_byte_offset INTEGER DEFAULT 0,
    last_message_index INTEGER DEFAULT 0,
    last_processed_at TEXT,
    processing_errors INTEGER DEFAULT 0,
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE session_artifacts (
    id TEXT PRIMARY KEY,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    artifact_type TEXT NOT NULL,
    content TEXT NOT NULL,
    metadata_json TEXT,
    source_file TEXT,
    line_start INTEGER,
    line_end INTEGER,
    title TEXT,
    task_id TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_session_artifacts_session ON session_artifacts(session_id);
CREATE INDEX idx_session_artifacts_type ON session_artifacts(artifact_type);
CREATE INDEX idx_session_artifacts_created ON session_artifacts(created_at);
CREATE INDEX idx_session_artifacts_task ON session_artifacts(task_id);
CREATE VIRTUAL TABLE session_artifacts_fts USING fts5(id UNINDEXED, content);

CREATE TABLE artifact_tags (
    artifact_id TEXT NOT NULL REFERENCES session_artifacts(id) ON DELETE CASCADE,
    tag TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (artifact_id, tag)
);
CREATE INDEX idx_artifact_tags_tag ON artifact_tags(tag);

CREATE TABLE session_stop_signals (
    session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
    source TEXT NOT NULL,
    reason TEXT,
    requested_at TEXT NOT NULL,
    acknowledged_at TEXT
);
CREATE INDEX idx_stop_signals_pending ON session_stop_signals(acknowledged_at)
    WHERE acknowledged_at IS NULL;

CREATE TABLE loop_progress (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    progress_type TEXT NOT NULL,
    tool_name TEXT,
    details TEXT,
    recorded_at TEXT NOT NULL,
    is_high_value INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_loop_progress_session ON loop_progress(session_id, recorded_at DESC);
CREATE INDEX idx_loop_progress_high_value ON loop_progress(session_id, is_high_value, recorded_at DESC)
    WHERE is_high_value = 1;

CREATE TABLE tasks (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id),
    parent_task_id TEXT REFERENCES tasks(id),
    created_in_session_id TEXT REFERENCES sessions(id),
    closed_in_session_id TEXT REFERENCES sessions(id),
    closed_commit_sha TEXT,
    closed_at TEXT,
    title TEXT NOT NULL,
    description TEXT,
    status TEXT DEFAULT 'open',
    priority INTEGER DEFAULT 2,
    task_type TEXT DEFAULT 'task',
    assignee TEXT,
    labels TEXT,
    closed_reason TEXT,
    compacted_at TEXT,
    summary TEXT,
    validation_status TEXT CHECK(validation_status IN ('pending', 'valid', 'invalid')),
    validation_feedback TEXT,
    validation_override_reason TEXT,
    original_instruction TEXT,
    details TEXT,
    category TEXT,
    complexity_score INTEGER,
    estimated_subtasks INTEGER,
    expansion_context TEXT,
    validation_criteria TEXT,
    use_external_validator INTEGER DEFAULT 0,
    validation_fail_count INTEGER DEFAULT 0,
    workflow_name TEXT,
    verification TEXT,
    sequence_order INTEGER,
    commits TEXT,
    escalated_at TEXT,
    escalation_reason TEXT,
    github_issue_number INTEGER,
    github_pr_number INTEGER,
    github_repo TEXT,
    linear_issue_id TEXT,
    linear_team_id TEXT,
    seq_num INTEGER,
    path_cache TEXT,
    agent_name TEXT,
    reference_doc TEXT,
    is_expanded INTEGER DEFAULT 0,
    expansion_status TEXT DEFAULT 'none',
    requires_user_review INTEGER DEFAULT 0,
    accepted_by_user INTEGER DEFAULT 0,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX idx_tasks_project ON tasks(project_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_parent ON tasks(parent_task_id);
CREATE INDEX idx_tasks_workflow ON tasks(workflow_name);
CREATE INDEX idx_tasks_sequence ON tasks(workflow_name, sequence_order);
CREATE INDEX idx_tasks_created_session ON tasks(created_in_session_id);
CREATE INDEX idx_tasks_closed_session ON tasks(closed_in_session_id);
CREATE UNIQUE INDEX idx_tasks_seq_num ON tasks(project_id, seq_num);
CREATE INDEX idx_tasks_path_cache ON tasks(path_cache);

CREATE TABLE task_dependencies (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    dep_type TEXT NOT NULL,
    created_at TEXT NOT NULL,
    UNIQUE(task_id, depends_on, dep_type)
);
CREATE INDEX idx_deps_task ON task_dependencies(task_id);
CREATE INDEX idx_deps_depends_on ON task_dependencies(depends_on);

CREATE TABLE session_tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    action TEXT NOT NULL,
    created_at TEXT NOT NULL,
    UNIQUE(session_id, task_id, action)
);
CREATE INDEX idx_session_tasks_session ON session_tasks(session_id);
CREATE INDEX idx_session_tasks_task ON session_tasks(task_id);

CREATE TABLE task_validation_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    iteration INTEGER NOT NULL,
    status TEXT NOT NULL,
    feedback TEXT,
    issues TEXT,
    context_type TEXT,
    context_summary TEXT,
    validator_type TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_validation_history_task ON task_validation_history(task_id);

CREATE TABLE task_selection_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    task_id TEXT NOT NULL,
    selected_at TEXT NOT NULL,
    context TEXT
);
CREATE INDEX idx_task_selection_session ON task_selection_history(session_id, selected_at DESC);
CREATE INDEX idx_task_selection_task ON task_selection_history(session_id, task_id, selected_at DESC);

CREATE TABLE workflow_states (
    session_id TEXT PRIMARY KEY,
    workflow_name TEXT NOT NULL,
    step TEXT NOT NULL,
    step_entered_at TEXT,
    step_action_count INTEGER DEFAULT 0,
    total_action_count INTEGER DEFAULT 0,
    artifacts TEXT,
    observations TEXT,
    reflection_pending INTEGER DEFAULT 0,
    context_injected INTEGER DEFAULT 0,
    variables TEXT,
    task_list TEXT,
    current_task_index INTEGER DEFAULT 0,
    files_modified_this_task INTEGER DEFAULT 0,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);

CREATE TABLE workflow_audit_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    timestamp TEXT NOT NULL DEFAULT (datetime('now')),
    step TEXT NOT NULL,
    event_type TEXT NOT NULL,
    tool_name TEXT,
    rule_id TEXT,
    condition TEXT,
    result TEXT NOT NULL,
    reason TEXT,
    context TEXT,
    FOREIGN KEY (session_id) REFERENCES sessions(id)
);
CREATE INDEX idx_audit_session ON workflow_audit_log(session_id);
CREATE INDEX idx_audit_timestamp ON workflow_audit_log(timestamp);
CREATE INDEX idx_audit_event_type ON workflow_audit_log(event_type);
CREATE INDEX idx_audit_result ON workflow_audit_log(result);

CREATE TABLE memories (
    id TEXT PRIMARY KEY,
    project_id TEXT REFERENCES projects(id),
    memory_type TEXT NOT NULL,
    content TEXT NOT NULL,
    source_type TEXT,
    source_session_id TEXT REFERENCES sessions(id),
    importance REAL DEFAULT 0.5,
    access_count INTEGER DEFAULT 0,
    last_accessed_at TEXT,
    tags TEXT,
    media TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX idx_memories_project ON memories(project_id);
CREATE INDEX idx_memories_type ON memories(memory_type);
CREATE INDEX idx_memories_importance ON memories(importance DESC);

CREATE TABLE memory_embeddings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
    project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
    embedding BLOB NOT NULL,
    embedding_model TEXT NOT NULL,
    embedding_dim INTEGER NOT NULL,
    text_hash TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    UNIQUE(memory_id)
);
CREATE INDEX idx_memory_embeddings_memory ON memory_embeddings(memory_id);
CREATE INDEX idx_memory_embeddings_hash ON memory_embeddings(text_hash);
CREATE INDEX idx_memory_embeddings_project ON memory_embeddings(project_id);

CREATE TABLE session_memories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
    action TEXT NOT NULL,
    created_at TEXT NOT NULL,
    UNIQUE(session_id, memory_id, action)
);
CREATE INDEX idx_session_memories_session ON session_memories(session_id);
CREATE INDEX idx_session_memories_memory ON session_memories(memory_id);

CREATE TABLE memory_crossrefs (
    source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
    target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
    similarity REAL NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    PRIMARY KEY (source_id, target_id)
);
CREATE INDEX idx_crossrefs_source ON memory_crossrefs(source_id);
CREATE INDEX idx_crossrefs_target ON memory_crossrefs(target_id);
CREATE INDEX idx_crossrefs_similarity ON memory_crossrefs(similarity DESC);

CREATE TABLE worktrees (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
    branch_name TEXT NOT NULL,
    worktree_path TEXT NOT NULL,
    base_branch TEXT DEFAULT 'main',
    agent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
    status TEXT DEFAULT 'active',
    merge_state TEXT,
    merged_at TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_worktrees_project ON worktrees(project_id);
CREATE INDEX idx_worktrees_status ON worktrees(status);
CREATE INDEX idx_worktrees_task ON worktrees(task_id);
CREATE INDEX idx_worktrees_session ON worktrees(agent_session_id);
CREATE UNIQUE INDEX idx_worktrees_branch ON worktrees(project_id, branch_name);
CREATE UNIQUE INDEX idx_worktrees_path ON worktrees(worktree_path);

CREATE TABLE merge_resolutions (
    id TEXT PRIMARY KEY,
    worktree_id TEXT NOT NULL REFERENCES worktrees(id) ON DELETE CASCADE,
    source_branch TEXT NOT NULL,
    target_branch TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    tier_used TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_merge_resolutions_worktree ON merge_resolutions(worktree_id);
CREATE INDEX idx_merge_resolutions_status ON merge_resolutions(status);
CREATE INDEX idx_merge_resolutions_source_branch ON merge_resolutions(source_branch);
CREATE INDEX idx_merge_resolutions_target_branch ON merge_resolutions(target_branch);

CREATE TABLE merge_conflicts (
    id TEXT PRIMARY KEY,
    resolution_id TEXT NOT NULL REFERENCES merge_resolutions(id) ON DELETE CASCADE,
    file_path TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    ours_content TEXT,
    theirs_content TEXT,
    resolved_content TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_merge_conflicts_resolution ON merge_conflicts(resolution_id);
CREATE INDEX idx_merge_conflicts_file_path ON merge_conflicts(file_path);
CREATE INDEX idx_merge_conflicts_status ON merge_conflicts(status);

CREATE TABLE inter_session_messages (
    id TEXT PRIMARY KEY,
    from_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    to_session TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
    content TEXT NOT NULL,
    priority TEXT NOT NULL DEFAULT 'normal',
    sent_at TEXT NOT NULL,
    read_at TEXT
);
CREATE INDEX idx_inter_session_messages_from_session ON inter_session_messages(from_session);
CREATE INDEX idx_inter_session_messages_to_session ON inter_session_messages(to_session);
CREATE INDEX idx_inter_session_messages_unread ON inter_session_messages(to_session, read_at)
    WHERE read_at IS NULL;

CREATE TABLE skills (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT NOT NULL,
    content TEXT NOT NULL,
    version TEXT,
    license TEXT,
    compatibility TEXT,
    allowed_tools TEXT,
    metadata TEXT,
    source_path TEXT,
    source_type TEXT,
    source_ref TEXT,
    hub_name TEXT,
    hub_slug TEXT,
    hub_version TEXT,
    enabled INTEGER DEFAULT 1,
    always_apply INTEGER DEFAULT 0,
    injection_format TEXT DEFAULT 'summary',
    project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX idx_skills_name ON skills(name);
CREATE INDEX idx_skills_project_id ON skills(project_id);
CREATE INDEX idx_skills_enabled ON skills(enabled);
CREATE INDEX idx_skills_always_apply ON skills(always_apply);
CREATE UNIQUE INDEX idx_skills_name_project ON skills(name, project_id);
CREATE UNIQUE INDEX idx_skills_name_global ON skills(name) WHERE project_id IS NULL;

CREATE TABLE clones (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    branch_name TEXT NOT NULL,
    clone_path TEXT NOT NULL,
    base_branch TEXT DEFAULT 'main',
    task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
    agent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
    status TEXT DEFAULT 'active',
    remote_url TEXT,
    last_sync_at TEXT,
    cleanup_after TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_clones_project ON clones(project_id);
CREATE INDEX idx_clones_status ON clones(status);
CREATE INDEX idx_clones_task ON clones(task_id);
CREATE INDEX idx_clones_session ON clones(agent_session_id);
CREATE UNIQUE INDEX idx_clones_path ON clones(clone_path);

CREATE TABLE cron_jobs (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    schedule_type TEXT NOT NULL,
    cron_expr TEXT,
    interval_seconds INTEGER,
    run_at TEXT,
    timezone TEXT DEFAULT 'UTC',
    action_type TEXT NOT NULL,
    action_config TEXT NOT NULL,
    enabled INTEGER DEFAULT 1,
    next_run_at TEXT,
    last_run_at TEXT,
    last_status TEXT,
    consecutive_failures INTEGER DEFAULT 0,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX idx_cron_jobs_project ON cron_jobs(project_id);
CREATE INDEX idx_cron_jobs_enabled ON cron_jobs(enabled);
CREATE INDEX idx_cron_jobs_next_run ON cron_jobs(next_run_at);
CREATE INDEX idx_cron_jobs_due ON cron_jobs(project_id, enabled, next_run_at);

CREATE TABLE cron_runs (
    id TEXT PRIMARY KEY,
    cron_job_id TEXT NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
    triggered_at TEXT NOT NULL,
    started_at TEXT,
    completed_at TEXT,
    status TEXT DEFAULT 'pending',
    output TEXT,
    error TEXT,
    agent_run_id TEXT,
    pipeline_execution_id TEXT,
    created_at TEXT NOT NULL
);
CREATE INDEX idx_cron_runs_job ON cron_runs(cron_job_id);
CREATE INDEX idx_cron_runs_triggered ON cron_runs(triggered_at);
CREATE INDEX idx_cron_runs_status ON cron_runs(status);

CREATE TABLE pipeline_executions (
    id TEXT PRIMARY KEY,
    pipeline_name TEXT NOT NULL,
    project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    status TEXT NOT NULL DEFAULT 'pending',
    inputs_json TEXT,
    outputs_json TEXT,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now')),
    completed_at TEXT,
    resume_token TEXT UNIQUE,
    session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
    parent_execution_id TEXT REFERENCES pipeline_executions(id) ON DELETE CASCADE
);
CREATE INDEX idx_pipeline_executions_project ON pipeline_executions(project_id);
CREATE INDEX idx_pipeline_executions_status ON pipeline_executions(status);
CREATE INDEX idx_pipeline_executions_resume_token ON pipeline_executions(resume_token);

CREATE TABLE step_executions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    execution_id TEXT NOT NULL REFERENCES pipeline_executions(id) ON DELETE CASCADE,
    step_id TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    started_at TEXT,
    completed_at TEXT,
    input_json TEXT,
    output_json TEXT,
    error TEXT,
    approval_token TEXT UNIQUE,
    approved_by TEXT,
    approved_at TEXT,
    UNIQUE(execution_id, step_id)
);
CREATE INDEX idx_step_executions_execution ON step_executions(execution_id);
CREATE INDEX idx_step_executions_approval_token ON step_executions(approval_token);

CREATE TABLE agent_definitions (
    id TEXT PRIMARY KEY,
    project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    role TEXT,
    goal TEXT,
    personality TEXT,
    instructions TEXT,
    provider TEXT NOT NULL DEFAULT 'claude',
    model TEXT,
    mode TEXT NOT NULL DEFAULT 'headless',
    terminal TEXT DEFAULT 'auto',
    isolation TEXT,
    base_branch TEXT DEFAULT 'main',
    timeout REAL DEFAULT 120.0,
    max_turns INTEGER DEFAULT 10,
    default_workflow TEXT,
    sandbox_config TEXT,
    skill_profile TEXT,
    workflows TEXT,
    lifecycle_variables TEXT,
    default_variables TEXT,
    enabled INTEGER NOT NULL DEFAULT 1,
    created_at TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX idx_agent_defs_project_name
    ON agent_definitions(project_id, name) WHERE project_id IS NOT NULL;
CREATE UNIQUE INDEX idx_agent_defs_global_name
    ON agent_definitions(name) WHERE project_id IS NULL;
CREATE INDEX idx_agent_defs_project ON agent_definitions(project_id);
CREATE INDEX idx_agent_defs_provider ON agent_definitions(provider);

CREATE TABLE secrets (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    encrypted_value TEXT NOT NULL,
    category TEXT DEFAULT 'general',
    description TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE INDEX idx_secrets_category ON secrets(category);
"""

# Future migrations (v61+)
# Add new migrations here. Do not modify the baseline schema above.


def _migrate_session_seq_num_project_scoped(db: LocalDatabase) -> None:
    """Change sessions.seq_num index from global to project-scoped.

    This allows different projects to have independent session numbering (#1, #2, etc.)
    matching how tasks already work.
    """
    # Check if the old global index exists
    row = db.fetchone(
        "SELECT sql FROM sqlite_master WHERE type='index' AND name='idx_sessions_seq_num'"
    )
    if not row:
        logger.debug("idx_sessions_seq_num index does not exist, skipping")
        return

    # Check if it's already project-scoped (contains 'project_id')
    if "project_id" in (row["sql"] or "").lower():
        logger.debug("idx_sessions_seq_num is already project-scoped, skipping")
        return

    # Drop the old global index and create new project-scoped index atomically
    with db.transaction() as conn:
        conn.execute("DROP INDEX IF EXISTS idx_sessions_seq_num")
        conn.execute("CREATE UNIQUE INDEX idx_sessions_seq_num ON sessions(project_id, seq_num)")

    logger.info("Changed sessions.seq_num index from global to project-scoped")


def _migrate_backfill_session_seq_num_per_project(db: LocalDatabase) -> None:
    """Re-backfill session seq_num values to be per-project.

    This migration re-numbers sessions so each project has independent numbering
    starting from 1. Required after changing the index to be project-scoped.
    """
    # Get all sessions grouped by project, ordered by created_at
    sessions = db.fetchall(
        """
        SELECT id, project_id FROM sessions
        ORDER BY project_id, created_at ASC, id ASC
        """
    )

    if not sessions:
        logger.debug("No sessions to re-number")
        return

    # Wrap the entire re-numbering in a transaction for atomicity
    with db.transaction() as conn:
        # First, clear all seq_num values to avoid unique constraint violations
        # when the existing seq_num order doesn't match created_at order
        conn.execute("UPDATE sessions SET seq_num = NULL")

        # Assign seq_num per project
        current_project: str | None = None
        seq_num = 0
        updated = 0

        for session in sessions:
            if session["project_id"] != current_project:
                current_project = session["project_id"]
                seq_num = 1
            else:
                seq_num += 1

            conn.execute(
                "UPDATE sessions SET seq_num = ? WHERE id = ?",
                (seq_num, session["id"]),
            )
            updated += 1

    logger.info(f"Re-numbered {updated} sessions with per-project seq_num")


def _migrate_add_hub_tracking_to_skills(db: LocalDatabase) -> None:
    """Add hub tracking fields to skills table.

    Adds hub_name, hub_slug, and hub_version columns to track which hub
    a skill was installed from.
    """
    with db.transaction() as conn:
        conn.execute("ALTER TABLE skills ADD COLUMN hub_name TEXT")
        conn.execute("ALTER TABLE skills ADD COLUMN hub_slug TEXT")
        conn.execute("ALTER TABLE skills ADD COLUMN hub_version TEXT")

    logger.info("Added hub tracking fields to skills table")


def _migrate_add_skill_injection_columns(db: LocalDatabase) -> None:
    """Add always_apply and injection_format columns to skills table.

    These columns enable per-skill control over:
    - always_apply: Whether skill should always be injected at session start
    - injection_format: How to inject the skill (summary, full, content)

    The values are extracted from SKILL.md frontmatter during sync and stored
    as columns for efficient querying.
    """
    with db.transaction() as conn:
        conn.execute("ALTER TABLE skills ADD COLUMN always_apply INTEGER DEFAULT 0")
        conn.execute("ALTER TABLE skills ADD COLUMN injection_format TEXT DEFAULT 'summary'")
        conn.execute("CREATE INDEX idx_skills_always_apply ON skills(always_apply)")

    logger.info("Added always_apply and injection_format columns to skills table")


def _migrate_add_deleted_at_to_projects(db: LocalDatabase) -> None:
    """Add deleted_at column to projects table (idempotent).

    SQLite doesn't support ADD COLUMN IF NOT EXISTS, so we check
    the column list first.
    """
    columns = db.fetchall("PRAGMA table_info(projects)")
    if any(col["name"] == "deleted_at" for col in columns):
        logger.debug("projects.deleted_at column already exists, skipping")
        return
    with db.transaction() as conn:
        conn.execute("ALTER TABLE projects ADD COLUMN deleted_at TEXT")
    logger.info("Added deleted_at column to projects table")


def _migrate_add_title_task_id_to_artifacts(db: LocalDatabase) -> None:
    """Add title and task_id columns to session_artifacts (idempotent)."""
    # Check existing columns to avoid duplicate column errors
    columns = {row["name"] for row in db.fetchall("PRAGMA table_info(session_artifacts)")}
    with db.transaction() as conn:
        if "title" not in columns:
            conn.execute("ALTER TABLE session_artifacts ADD COLUMN title TEXT")
        if "task_id" not in columns:
            conn.execute("ALTER TABLE session_artifacts ADD COLUMN task_id TEXT")
        conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_session_artifacts_task ON session_artifacts(task_id)"
        )
    logger.info("Added title and task_id columns to session_artifacts")


def _migrate_agent_definition_prompt_fields(db: LocalDatabase) -> None:
    """Add role, goal, personality, instructions columns to agent_definitions (idempotent)."""
    columns = {row["name"] for row in db.fetchall("PRAGMA table_info(agent_definitions)")}
    with db.transaction() as conn:
        if "role" not in columns:
            conn.execute("ALTER TABLE agent_definitions ADD COLUMN role TEXT")
        if "goal" not in columns:
            conn.execute("ALTER TABLE agent_definitions ADD COLUMN goal TEXT")
        if "personality" not in columns:
            conn.execute("ALTER TABLE agent_definitions ADD COLUMN personality TEXT")
        if "instructions" not in columns:
            conn.execute("ALTER TABLE agent_definitions ADD COLUMN instructions TEXT")
    logger.info("Added role, goal, personality, instructions columns to agent_definitions")


MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
    # Project-scoped session refs: Change seq_num index from global to project-scoped
    (76, "Make sessions.seq_num project-scoped", _migrate_session_seq_num_project_scoped),
    # Project-scoped session refs: Re-backfill seq_num per project
    (77, "Backfill sessions.seq_num per project", _migrate_backfill_session_seq_num_per_project),
    # Hub tracking: Add hub_name, hub_slug, hub_version to skills table
    (78, "Add hub tracking fields to skills", _migrate_add_hub_tracking_to_skills),
    # Skill injection: Add always_apply and injection_format columns
    (79, "Add skill injection columns", _migrate_add_skill_injection_columns),
    # Pipeline system: Add pipeline_executions and step_executions tables
    (
        80,
        "Add pipeline execution tables",
        """
        CREATE TABLE IF NOT EXISTS pipeline_executions (
            id TEXT PRIMARY KEY,
            pipeline_name TEXT NOT NULL,
            project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
            status TEXT NOT NULL DEFAULT 'pending',
            inputs_json TEXT,
            outputs_json TEXT,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now')),
            completed_at TEXT,
            resume_token TEXT UNIQUE,
            session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
            parent_execution_id TEXT REFERENCES pipeline_executions(id) ON DELETE CASCADE
        );
        CREATE INDEX IF NOT EXISTS idx_pipeline_executions_project ON pipeline_executions(project_id);
        CREATE INDEX IF NOT EXISTS idx_pipeline_executions_status ON pipeline_executions(status);
        CREATE INDEX IF NOT EXISTS idx_pipeline_executions_resume_token ON pipeline_executions(resume_token);

        CREATE TABLE IF NOT EXISTS step_executions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            execution_id TEXT NOT NULL REFERENCES pipeline_executions(id) ON DELETE CASCADE,
            step_id TEXT NOT NULL,
            status TEXT NOT NULL DEFAULT 'pending',
            started_at TEXT,
            completed_at TEXT,
            input_json TEXT,
            output_json TEXT,
            error TEXT,
            approval_token TEXT UNIQUE,
            approved_by TEXT,
            approved_at TEXT,
            UNIQUE(execution_id, step_id)
        );
        CREATE INDEX IF NOT EXISTS idx_step_executions_execution ON step_executions(execution_id);
        CREATE INDEX IF NOT EXISTS idx_step_executions_approval_token ON step_executions(approval_token);
        """,
    ),
    # Add step_variables JSON column to sessions for spawn-time variable passing
    (
        81,
        "Add step_variables to sessions",
        "ALTER TABLE sessions ADD COLUMN step_variables TEXT",
    ),
    # Rename task status 'review' to 'needs_review' for clarity
    (
        82,
        "Rename task status 'review' to 'needs_review'",
        "UPDATE tasks SET status = 'needs_review' WHERE status = 'review'",
    ),
    # Soft-delete support: Add deleted_at column to projects
    (
        83,
        "Add deleted_at column to projects",
        _migrate_add_deleted_at_to_projects,
    ),
    # Add _personal system project
    (
        84,
        "Add _personal system project",
        """INSERT OR IGNORE INTO projects (id, name, repo_path, created_at, updated_at)
        VALUES ('00000000-0000-0000-0000-000000060887', '_personal', NULL, datetime('now'), datetime('now'))""",
    ),
    # Memory V4: Add memory_embeddings table for semantic search
    (
        85,
        "Add memory_embeddings table",
        """
        CREATE TABLE IF NOT EXISTS memory_embeddings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
            project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
            embedding BLOB NOT NULL,
            embedding_model TEXT NOT NULL,
            embedding_dim INTEGER NOT NULL,
            text_hash TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now')),
            UNIQUE(memory_id)
        );
        CREATE INDEX IF NOT EXISTS idx_memory_embeddings_memory ON memory_embeddings(memory_id);
        CREATE INDEX IF NOT EXISTS idx_memory_embeddings_hash ON memory_embeddings(text_hash);
        CREATE INDEX IF NOT EXISTS idx_memory_embeddings_project ON memory_embeddings(project_id);
        """,
    ),
    # Memory V4: Add mem0_id column for Mem0 dual-mode sync
    (
        86,
        "Add mem0_id to memories",
        """
        ALTER TABLE memories ADD COLUMN mem0_id TEXT;
        CREATE INDEX IF NOT EXISTS idx_memories_mem0_id ON memories(mem0_id);
        """,
    ),
    # Artifacts V2: Add title and task_id columns to session_artifacts
    (
        87,
        "Add title and task_id to session_artifacts",
        _migrate_add_title_task_id_to_artifacts,
    ),
    # Artifacts V2: Add artifact_tags junction table
    (
        88,
        "Add artifact_tags table",
        """
        CREATE TABLE IF NOT EXISTS artifact_tags (
            artifact_id TEXT NOT NULL REFERENCES session_artifacts(id) ON DELETE CASCADE,
            tag TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (artifact_id, tag)
        );
        CREATE INDEX IF NOT EXISTS idx_artifact_tags_tag ON artifact_tags(tag);
        """,
    ),
    # Cron scheduler: Add cron_jobs and cron_runs tables
    (
        89,
        "Add cron scheduler tables",
        """
        CREATE TABLE IF NOT EXISTS cron_jobs (
            id TEXT PRIMARY KEY,
            project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
            name TEXT NOT NULL,
            description TEXT,
            schedule_type TEXT NOT NULL,
            cron_expr TEXT,
            interval_seconds INTEGER,
            run_at TEXT,
            timezone TEXT DEFAULT 'UTC',
            action_type TEXT NOT NULL,
            action_config TEXT NOT NULL,
            enabled INTEGER DEFAULT 1,
            next_run_at TEXT,
            last_run_at TEXT,
            last_status TEXT,
            consecutive_failures INTEGER DEFAULT 0,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_cron_jobs_project ON cron_jobs(project_id);
        CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled);
        CREATE INDEX IF NOT EXISTS idx_cron_jobs_next_run ON cron_jobs(next_run_at);
        CREATE INDEX IF NOT EXISTS idx_cron_jobs_due ON cron_jobs(project_id, enabled, next_run_at);

        CREATE TABLE IF NOT EXISTS cron_runs (
            id TEXT PRIMARY KEY,
            cron_job_id TEXT NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
            triggered_at TEXT NOT NULL,
            started_at TEXT,
            completed_at TEXT,
            status TEXT DEFAULT 'pending',
            output TEXT,
            error TEXT,
            agent_run_id TEXT,
            pipeline_execution_id TEXT,
            created_at TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_cron_runs_job ON cron_runs(cron_job_id);
        CREATE INDEX IF NOT EXISTS idx_cron_runs_triggered ON cron_runs(triggered_at);
        CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
        """,
    ),
    # Task comments: Add task_comments table for threaded comments
    (
        90,
        "Add task_comments table",
        """
        CREATE TABLE IF NOT EXISTS task_comments (
            id TEXT PRIMARY KEY,
            task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
            parent_comment_id TEXT REFERENCES task_comments(id) ON DELETE CASCADE,
            author TEXT NOT NULL,
            author_type TEXT NOT NULL DEFAULT 'session',
            body TEXT NOT NULL,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
        CREATE INDEX IF NOT EXISTS idx_task_comments_parent ON task_comments(parent_comment_id);
        CREATE INDEX IF NOT EXISTS idx_task_comments_created ON task_comments(task_id, created_at);
        """,
    ),
    # Task status cleanup: Remove 'failed' and 'needs_decomposition' statuses
    (
        91,
        "Migrate failed → escalated and needs_decomposition → open",
        """
        UPDATE tasks SET status = 'escalated',
            escalated_at = COALESCE(escalated_at, updated_at),
            escalation_reason = COALESCE(escalation_reason, 'migrated_from_failed')
        WHERE status = 'failed';

        UPDATE tasks SET status = 'open' WHERE status = 'needs_decomposition';
        """,
    ),
    # Agent definitions: Store agent configuration in database
    (
        92,
        "Add agent_definitions table",
        """
        CREATE TABLE IF NOT EXISTS agent_definitions (
            id TEXT PRIMARY KEY,
            project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
            name TEXT NOT NULL,
            description TEXT,
            provider TEXT NOT NULL DEFAULT 'claude',
            model TEXT,
            mode TEXT NOT NULL DEFAULT 'headless',
            terminal TEXT DEFAULT 'auto',
            isolation TEXT,
            base_branch TEXT DEFAULT 'main',
            timeout REAL DEFAULT 120.0,
            max_turns INTEGER DEFAULT 10,
            default_workflow TEXT,
            sandbox_config TEXT,
            skill_profile TEXT,
            workflows TEXT,
            lifecycle_variables TEXT,
            default_variables TEXT,
            enabled INTEGER NOT NULL DEFAULT 1,
            created_at TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at TEXT NOT NULL DEFAULT (datetime('now'))
        );
        CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_defs_project_name
            ON agent_definitions(project_id, name) WHERE project_id IS NOT NULL;
        CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_defs_global_name
            ON agent_definitions(name) WHERE project_id IS NULL;
        CREATE INDEX IF NOT EXISTS idx_agent_defs_project ON agent_definitions(project_id);
        CREATE INDEX IF NOT EXISTS idx_agent_defs_provider ON agent_definitions(provider);
        """,
    ),
    # Secrets store: Encrypted API keys and sensitive values
    (
        93,
        "Add secrets table",
        """
        CREATE TABLE IF NOT EXISTS secrets (
            id TEXT PRIMARY KEY,
            name TEXT NOT NULL UNIQUE,
            encrypted_value TEXT NOT NULL,
            category TEXT DEFAULT 'general',
            description TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_secrets_category ON secrets(category);
        """,
    ),
    # Agent definition prompt fields: role, goal, personality, instructions
    (
        94,
        "Add role, goal, personality, instructions columns to agent_definitions",
        _migrate_agent_definition_prompt_fields,
    ),
]


def get_current_version(db: LocalDatabase) -> int:
    """Get current schema version from database."""
    try:
        row = db.fetchone("SELECT MAX(version) as version FROM schema_version")
        return row["version"] if row and row["version"] else 0
    except Exception:
        return 0


def _apply_baseline(db: LocalDatabase) -> None:
    """Apply baseline schema for new databases (flattened at v75)."""
    logger.info("Applying baseline schema (v75)")

    # Execute baseline schema
    for statement in BASELINE_SCHEMA.strip().split(";"):
        statement = statement.strip()
        if statement:
            db.execute(statement)

    # Record baseline version
    db.execute(
        "INSERT INTO schema_version (version) VALUES (?)",
        (BASELINE_VERSION,),
    )

    logger.info(f"Baseline schema applied, now at version {BASELINE_VERSION}")


def _run_migration_list(
    db: LocalDatabase,
    current_version: int,
    migrations: list[tuple[int, str, MigrationAction]],
) -> int:
    """
    Run migrations from a list.

    Args:
        db: LocalDatabase instance
        current_version: Current schema version
        migrations: List of (version, description, action) tuples

    Returns:
        Number of migrations applied
    """
    applied = 0
    last_version = current_version

    for version, description, action in migrations:
        if version > current_version:
            logger.debug(f"Applying migration {version}: {description}")
            try:
                if callable(action):
                    # Python data migration
                    action(db)
                else:
                    # SQL migration (may contain multiple statements)
                    for statement in action.strip().split(";"):
                        statement = statement.strip()
                        if statement:
                            db.execute(statement)

                # Record migration
                db.execute(
                    "INSERT INTO schema_version (version) VALUES (?)",
                    (version,),
                )
                applied += 1
                last_version = version
            except Exception as e:
                logger.error(f"Migration {version} failed: {e}")
                raise

    if applied > 0:
        logger.debug(f"Applied {applied} migration(s), now at version {last_version}")

    return applied


def run_migrations(db: LocalDatabase) -> int:
    """
    Run pending migrations.

    For new databases (version == 0):
        - Applies baseline schema (v75) directly.

    For existing databases:
        - Runs any new migrations from v76 onwards.

    Args:
        db: LocalDatabase instance

    Returns:
        Number of migrations applied
    """
    current_version = get_current_version(db)
    total_applied = 0

    if current_version == 0:
        # New database with flattened baseline: apply schema directly
        logger.info("Using flattened baseline for new database")
        _apply_baseline(db)
        total_applied = 1
        current_version = BASELINE_VERSION
    elif current_version < BASELINE_VERSION:
        # Unsupported: Pre-v75 database without local migrations
        # Since we removed legacy migrations, we can't upgrade.
        msg = (
            f"Database version {current_version} is older than baseline "
            f"{BASELINE_VERSION}. Upgrade not supported without legacy migrations."
        )
        logger.error(msg)
        raise MigrationUnsupportedError(msg)

    # Run any new migrations (v76+)
    if MIGRATIONS:
        applied = _run_migration_list(db, current_version, MIGRATIONS)
        total_applied += applied

    return total_applied
