import os
from pathlib import Path
from typing import Final

import aiosqlite


# Built-in tool policy profiles.
# Each profile defines the set of tools that are permitted by default.
TOOL_POLICY_PROFILES: Final[dict[str, list[str]]] = {
    "minimal": [
        "read",
        "glob",
        "grep",
    ],
    "coding": [
        "read",
        "write",
        "edit",
        "glob",
        "grep",
        "bash",
        "git",
    ],
    "full": [],  # Empty list means all tools are unrestricted
}


def get_db_path() -> str:
    """Return the resolved path to the Peon SQLite database.

    Resolution order:
      1. ``PEON_DB_PATH`` environment variable (always wins).
      2. ``~/.peon/peon_tasks.db``

    Note: The parent directory is NOT created by this function.
    Directory creation is handled by init_db() when the database is opened.
    """
    env = os.environ.get("PEON_DB_PATH")
    if env:
        p = Path(env).expanduser().resolve()
    else:
        p = Path.home() / ".peon" / "peon_tasks.db"

    return str(p)


SCHEMA = """
CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    path TEXT NOT NULL,
    github_url TEXT NOT NULL DEFAULT '',
    context_pruning TEXT NOT NULL DEFAULT '',
    max_subagent_depth INTEGER NOT NULL DEFAULT 2,
    max_subagents_per_task INTEGER NOT NULL DEFAULT 5,
    max_concurrent_subagents INTEGER NOT NULL DEFAULT 3,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS features (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    name TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'planned'
        CHECK(status IN ('planned', 'in_progress', 'done', 'cancelled')),
    branch TEXT NOT NULL DEFAULT '',
    pr_url TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_features_project_branch ON features(project_id, branch);

CREATE TABLE IF NOT EXISTS tool_policies (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    name TEXT NOT NULL,
    profile TEXT NOT NULL DEFAULT 'coding'
        CHECK(profile IN ('minimal', 'coding', 'full')),
    allow TEXT NOT NULL DEFAULT '',
    also_allow TEXT NOT NULL DEFAULT '',
    deny TEXT NOT NULL DEFAULT '',
    fs_workspace_only INTEGER NOT NULL DEFAULT 1,
    exec_security TEXT NOT NULL DEFAULT 'allowlist'
        CHECK(exec_security IN ('deny', 'allowlist', 'full')),
    exec_timeout_seconds INTEGER NOT NULL DEFAULT 300,
    is_default INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_tool_policies_project_id ON tool_policies(project_id);

CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    feature_id INTEGER REFERENCES features(id),
    tool_policy_id INTEGER REFERENCES tool_policies(id),
    title TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    priority TEXT NOT NULL DEFAULT 'medium'
        CHECK(priority IN ('low', 'medium', 'high', 'critical')),
    status TEXT NOT NULL DEFAULT 'todo'
        CHECK(status IN ('grooming', 'todo', 'in_progress', 'done', 'cancelled', 'timeout')),
    task_type TEXT NOT NULL DEFAULT 'task'
        CHECK(task_type IN ('task', 'review', 'fix', 'verify')),
    review_session_id INTEGER REFERENCES review_sessions(id),
    review_perspective TEXT NOT NULL DEFAULT '',
    commit_base TEXT NOT NULL DEFAULT '',
    commit_head TEXT NOT NULL DEFAULT '',
    branch TEXT NOT NULL DEFAULT '',
    worktree_path TEXT NOT NULL DEFAULT '',
    pr_url TEXT NOT NULL DEFAULT '',
    progress REAL NOT NULL DEFAULT 0.0
        CHECK(progress >= 0.0 AND progress <= 1.0),
    lines_added INTEGER NOT NULL DEFAULT 0,
    lines_deleted INTEGER NOT NULL DEFAULT 0,
    tokens_used INTEGER NOT NULL DEFAULT 0,
    scheduled_at TEXT DEFAULT NULL,
    started_at TIMESTAMP,
    completed_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS task_templates (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    title TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    priority TEXT NOT NULL DEFAULT 'medium'
        CHECK(priority IN ('low', 'medium', 'high', 'critical')),
    feature_id INTEGER REFERENCES features(id),
    cron_expression TEXT NOT NULL,
    timezone TEXT NOT NULL DEFAULT 'UTC',
    enabled INTEGER NOT NULL DEFAULT 1,
    last_run_at TEXT,
    next_run_at TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_task_templates_polling ON task_templates(project_id, enabled, next_run_at);

CREATE TABLE IF NOT EXISTS work_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_id INTEGER NOT NULL REFERENCES tasks(id),
    project_id TEXT NOT NULL REFERENCES projects(id),
    summary TEXT NOT NULL,
    files_changed TEXT NOT NULL DEFAULT '',
    test_result TEXT NOT NULL DEFAULT ''
        CHECK(test_result IN ('', 'pass', 'fail', 'skip')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS peon_memories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    category TEXT NOT NULL DEFAULT 'general'
        CHECK(category IN ('env', 'pattern', 'gotcha', 'test', 'dependency', 'general')),
    content TEXT NOT NULL CHECK(length(content) <= 500),
    source_task_id INTEGER REFERENCES tasks(id),
    access_count INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_peon_memories_project_id ON peon_memories(project_id);

CREATE TABLE IF NOT EXISTS loop_heartbeats (
    id TEXT PRIMARY KEY,
    project_id TEXT NOT NULL REFERENCES projects(id),
    pid INTEGER NOT NULL,
    hostname TEXT NOT NULL,
    discord_status TEXT NOT NULL DEFAULT '',
    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_heartbeat TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS test_commands (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    name TEXT NOT NULL,
    command TEXT NOT NULL,
    timeout_seconds INTEGER NOT NULL DEFAULT 300,
    sort_order INTEGER NOT NULL DEFAULT 0,
    enabled INTEGER NOT NULL DEFAULT 1,
    report_path TEXT NOT NULL DEFAULT '',
    max_retries INTEGER NOT NULL DEFAULT 3,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_test_commands_project_id ON test_commands(project_id);
CREATE INDEX IF NOT EXISTS idx_test_commands_sort_order ON test_commands(project_id, sort_order);

CREATE TABLE IF NOT EXISTS test_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_id INTEGER NOT NULL REFERENCES tasks(id),
    project_id TEXT NOT NULL REFERENCES projects(id),
    test_command_id INTEGER NOT NULL REFERENCES test_commands(id),
    command_name TEXT NOT NULL,
    command TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending', 'running', 'pass', 'fail', 'error', 'timeout')),
    exit_code INTEGER,
    duration_seconds REAL,
    output TEXT NOT NULL DEFAULT '',
    tests_passed INTEGER NOT NULL DEFAULT 0,
    tests_failed INTEGER NOT NULL DEFAULT 0,
    tests_skipped INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS test_cases (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    test_run_id INTEGER NOT NULL REFERENCES test_runs(id),
    name TEXT NOT NULL,
    classname TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL CHECK(status IN ('pass', 'fail', 'error', 'skip')),
    duration_seconds REAL,
    message TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS review_sessions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    task_id INTEGER NOT NULL REFERENCES tasks(id),
    pr_url TEXT NOT NULL DEFAULT '',
    pr_title TEXT NOT NULL DEFAULT '',
    pr_diff TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
    pipeline_stage TEXT NOT NULL DEFAULT 'reviewing'
        CHECK(pipeline_stage IN ('reviewing', 'fixing', 'verifying', 'complete', 'failed')),
    fix_policy TEXT NOT NULL DEFAULT 'auto'
        CHECK(fix_policy IN ('auto', 'manual', 'skip')),
    perspectives TEXT NOT NULL DEFAULT '[]',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_review_sessions_project_id ON review_sessions(project_id);

CREATE TABLE IF NOT EXISTS review_findings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id INTEGER NOT NULL REFERENCES review_sessions(id),
    perspective TEXT NOT NULL,
    agent_status TEXT NOT NULL DEFAULT 'pending'
        CHECK(agent_status IN ('pending', 'in_progress', 'completed', 'failed', 'timeout')),
    severity TEXT NOT NULL DEFAULT 'info'
        CHECK(severity IN ('critical', 'high', 'medium', 'low', 'info')),
    category TEXT NOT NULL DEFAULT '',
    file_path TEXT NOT NULL DEFAULT '',
    line_start INTEGER,
    line_end INTEGER,
    title TEXT NOT NULL DEFAULT '',
    description TEXT NOT NULL DEFAULT '',
    suggestion TEXT NOT NULL DEFAULT '',
    confidence REAL NOT NULL DEFAULT 0.0
        CHECK(confidence >= 0.0 AND confidence <= 1.0),
    created_task_id INTEGER REFERENCES tasks(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_review_findings_session_id ON review_findings(session_id);

CREATE TABLE IF NOT EXISTS review_convergences (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id INTEGER NOT NULL REFERENCES review_sessions(id),
    category TEXT NOT NULL,
    file_path TEXT NOT NULL DEFAULT '',
    finding_ids TEXT NOT NULL DEFAULT '[]',
    perspectives TEXT NOT NULL DEFAULT '[]',
    convergence_count INTEGER NOT NULL DEFAULT 0,
    max_severity TEXT NOT NULL DEFAULT 'info',
    summary TEXT NOT NULL DEFAULT '',
    created_task_id INTEGER REFERENCES tasks(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_review_convergences_session_id ON review_convergences(session_id);

CREATE TABLE IF NOT EXISTS review_agents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id INTEGER NOT NULL REFERENCES review_sessions(id),
    perspective TEXT NOT NULL,
    agent_status TEXT NOT NULL DEFAULT 'pending'
        CHECK(agent_status IN ('pending', 'in_progress', 'completed', 'failed', 'timeout')),
    sub_agent_id INTEGER,
    completed_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_review_agents_session_id ON review_agents(session_id);

CREATE TABLE IF NOT EXISTS sub_agents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    parent_session_id INTEGER,
    parent_task_id INTEGER REFERENCES tasks(id),
    project_id TEXT NOT NULL REFERENCES projects(id),
    label TEXT NOT NULL,
    idempotency_key TEXT NOT NULL DEFAULT '',
    model TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending', 'running', 'completed', 'failed', 'timeout', 'cancelled')),
    depth INTEGER NOT NULL DEFAULT 1,
    pid INTEGER,
    result_summary TEXT NOT NULL DEFAULT '',
    tokens_used INTEGER NOT NULL DEFAULT 0,
    timeout_seconds INTEGER NOT NULL DEFAULT 600,
    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    ended_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_sub_agents_task_status ON sub_agents(parent_task_id, status);
CREATE INDEX IF NOT EXISTS idx_sub_agents_idempotency_key ON sub_agents(idempotency_key);

CREATE TABLE IF NOT EXISTS task_dependencies (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    depends_on_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(task_id, depends_on_task_id),
    CHECK(task_id != depends_on_task_id)
);

CREATE INDEX IF NOT EXISTS idx_task_deps_task_id ON task_dependencies(task_id);
CREATE INDEX IF NOT EXISTS idx_task_deps_depends_on ON task_dependencies(depends_on_task_id);

CREATE TABLE IF NOT EXISTS task_templates (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    feature_id INTEGER REFERENCES features(id),
    title TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    priority TEXT NOT NULL DEFAULT 'medium'
        CHECK(priority IN ('low', 'medium', 'high', 'critical')),
    cron_expression TEXT NOT NULL,
    timezone TEXT NOT NULL DEFAULT 'UTC',
    enabled INTEGER NOT NULL DEFAULT 1,
    last_run_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_task_templates_project_id ON task_templates(project_id);

CREATE TABLE IF NOT EXISTS webhook_configs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    name TEXT NOT NULL,
    url TEXT NOT NULL,
    secret TEXT NOT NULL DEFAULT '',
    events TEXT NOT NULL DEFAULT '[]',
    enabled INTEGER NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_webhook_configs_project_id ON webhook_configs(project_id);

CREATE TABLE IF NOT EXISTS webhook_event_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    webhook_config_id INTEGER REFERENCES webhook_configs(id),
    event_type TEXT NOT NULL,
    payload TEXT NOT NULL DEFAULT '{}',
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending', 'success', 'failed')),
    response_code INTEGER,
    error TEXT NOT NULL DEFAULT '',
    delivery_type TEXT NOT NULL DEFAULT 'webhook'
        CHECK(delivery_type IN ('webhook', 'discord')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_webhook_event_logs_project_id ON webhook_event_logs(project_id);
CREATE INDEX IF NOT EXISTS idx_webhook_event_logs_webhook_config_id ON webhook_event_logs(webhook_config_id);

CREATE TABLE IF NOT EXISTS agent_sessions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    task_id INTEGER REFERENCES tasks(id),
    loop_id TEXT NOT NULL DEFAULT '',
    model TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'running'
        CHECK(status IN ('running', 'completed', 'failed', 'timeout', 'cancelled')),
    exit_code INTEGER,
    tokens_input INTEGER NOT NULL DEFAULT 0,
    tokens_output INTEGER NOT NULL DEFAULT 0,
    cost_usd REAL NOT NULL DEFAULT 0.0,
    error_summary TEXT NOT NULL DEFAULT '',
    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    ended_at TIMESTAMP,
    duration_seconds REAL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_agent_sessions_project_task ON agent_sessions(project_id, task_id);

CREATE TABLE IF NOT EXISTS session_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id INTEGER NOT NULL REFERENCES agent_sessions(id),
    event_type TEXT NOT NULL
        CHECK(event_type IN ('tool_call', 'tool_result', 'file_read', 'file_write', 'git_op', 'error', 'info', 'message', 'checkpoint', 'start')),
    tool_name TEXT NOT NULL DEFAULT '',
    detail TEXT NOT NULL DEFAULT '',
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_session_events_session_id ON session_events(session_id);

CREATE TABLE IF NOT EXISTS discord_configs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id TEXT NOT NULL REFERENCES projects(id),
    bot_token TEXT NOT NULL DEFAULT '',
    guild_id TEXT NOT NULL DEFAULT '',
    enabled INTEGER NOT NULL DEFAULT 1,
    dm_policy TEXT NOT NULL DEFAULT 'disabled'
        CHECK(dm_policy IN ('open', 'pairing', 'disabled')),
    guild_policy TEXT NOT NULL DEFAULT 'disabled'
        CHECK(guild_policy IN ('open', 'allowlist', 'disabled')),
    require_mention INTEGER NOT NULL DEFAULT 1,
    history_limit INTEGER NOT NULL DEFAULT 20,
    model TEXT NOT NULL DEFAULT '',
    system_prompt TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_discord_configs_project_id ON discord_configs(project_id);

CREATE TABLE IF NOT EXISTS discord_channel_mappings (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    discord_config_id INTEGER NOT NULL REFERENCES discord_configs(id) ON DELETE CASCADE,
    channel_id TEXT NOT NULL,
    channel_name TEXT NOT NULL DEFAULT '',
    allowed_user_ids TEXT NOT NULL DEFAULT '[]',
    allowed_role_ids TEXT NOT NULL DEFAULT '[]',
    subscribed_events TEXT NOT NULL DEFAULT '[]',
    enabled INTEGER NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_discord_channel_mappings_config_id ON discord_channel_mappings(discord_config_id);

CREATE TABLE IF NOT EXISTS discord_inbound_messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    discord_config_id INTEGER NOT NULL REFERENCES discord_configs(id) ON DELETE CASCADE,
    channel_id TEXT NOT NULL,
    channel_name TEXT NOT NULL DEFAULT '',
    author_id TEXT NOT NULL,
    author_name TEXT NOT NULL DEFAULT '',
    content TEXT NOT NULL DEFAULT '',
    status TEXT NOT NULL DEFAULT 'pending'
        CHECK(status IN ('pending', 'processed', 'ignored')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_discord_inbound_messages_config_id ON discord_inbound_messages(discord_config_id);
CREATE INDEX IF NOT EXISTS idx_discord_inbound_messages_status ON discord_inbound_messages(status);

CREATE TABLE IF NOT EXISTS discord_messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    config_id INTEGER NOT NULL REFERENCES discord_configs(id) ON DELETE CASCADE,
    channel_id TEXT NOT NULL,
    user_id TEXT NOT NULL,
    username TEXT NOT NULL DEFAULT '',
    content TEXT NOT NULL DEFAULT '',
    message_id TEXT NOT NULL DEFAULT '',
    replied_to TEXT NOT NULL DEFAULT '',
    processed INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_discord_messages_config_id ON discord_messages(config_id);
CREATE INDEX IF NOT EXISTS idx_discord_messages_processed ON discord_messages(processed);

CREATE TABLE IF NOT EXISTS discord_paired_users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    discord_config_id INTEGER NOT NULL REFERENCES discord_configs(id) ON DELETE CASCADE,
    user_id TEXT NOT NULL,
    username TEXT NOT NULL DEFAULT '',
    pairing_code TEXT NOT NULL DEFAULT '',
    approved_by TEXT NOT NULL DEFAULT '',
    paired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(discord_config_id, user_id)
);

CREATE INDEX IF NOT EXISTS idx_discord_paired_users_config_id ON discord_paired_users(discord_config_id);

CREATE TABLE IF NOT EXISTS discord_activity_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    config_id INTEGER NOT NULL REFERENCES discord_configs(id) ON DELETE CASCADE,
    event_type TEXT NOT NULL,
    channel_id TEXT NOT NULL DEFAULT '',
    user_id TEXT NOT NULL DEFAULT '',
    detail TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_discord_activity_logs_config_id ON discord_activity_logs(config_id);
CREATE INDEX IF NOT EXISTS idx_discord_activity_logs_event_type ON discord_activity_logs(event_type);
CREATE INDEX IF NOT EXISTS idx_discord_activity_logs_created_at ON discord_activity_logs(created_at);
"""


# Columns that may be missing from databases created with older schemas.
# On startup, any missing columns are added via ALTER TABLE ADD COLUMN.
# When you add a new column to the SCHEMA above, also add it here so that
# existing databases are patched forward automatically.
_COLUMN_MIGRATIONS: dict[str, list[tuple[str, str]]] = {
    "projects": [
        ("github_url", "TEXT NOT NULL DEFAULT ''"),
        ("context_pruning", "TEXT NOT NULL DEFAULT ''"),
        ("max_subagent_depth", "INTEGER NOT NULL DEFAULT 2"),
        ("max_subagents_per_task", "INTEGER NOT NULL DEFAULT 5"),
        ("max_concurrent_subagents", "INTEGER NOT NULL DEFAULT 3"),
    ],
    "features": [
        ("branch", "TEXT NOT NULL DEFAULT ''"),
        ("pr_url", "TEXT NOT NULL DEFAULT ''"),
    ],
    "tasks": [
        ("feature_id", "INTEGER REFERENCES features(id)"),
        ("progress", "REAL NOT NULL DEFAULT 0.0"),
        ("lines_added", "INTEGER NOT NULL DEFAULT 0"),
        ("lines_deleted", "INTEGER NOT NULL DEFAULT 0"),
        ("tokens_used", "INTEGER NOT NULL DEFAULT 0"),
        ("task_type", "TEXT NOT NULL DEFAULT 'task'"),
        ("review_session_id", "INTEGER REFERENCES review_sessions(id)"),
        ("review_perspective", "TEXT NOT NULL DEFAULT ''"),
        ("scheduled_at", "TEXT DEFAULT NULL"),
    ],
    "review_sessions": [
        ("pipeline_stage", "TEXT NOT NULL DEFAULT 'reviewing'"),
        ("fix_policy", "TEXT NOT NULL DEFAULT 'auto'"),
    ],
    "review_agents": [
        ("sub_agent_id", "INTEGER"),
        ("tool_policy_id", "INTEGER REFERENCES tool_policies(id)"),
    ],
    "agent_sessions": [
        ("duration_seconds", "REAL"),
        ("updated_at", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"),
    ],
    "discord_messages": [
        ("denied", "INTEGER NOT NULL DEFAULT 0"),
        ("deny_reason", "TEXT NOT NULL DEFAULT ''"),
        ("member_roles", "TEXT NOT NULL DEFAULT '[]'"),
    ],
    "webhook_event_logs": [
        ("delivery_type", "TEXT NOT NULL DEFAULT 'webhook'"),
    ],
    "discord_configs": [
        ("model", "TEXT NOT NULL DEFAULT ''"),
        ("system_prompt", "TEXT NOT NULL DEFAULT ''"),
    ],
    "discord_channel_mappings": [
        ("subscribed_events", "TEXT NOT NULL DEFAULT '[]'"),
    ],
    "loop_heartbeats": [
        ("discord_status", "TEXT NOT NULL DEFAULT ''"),
    ],
}


async def _ensure_columns(db: aiosqlite.Connection) -> None:
    """Add any missing columns to existing tables.

    Handles the case where a database was created with an older schema.
    New databases get the full schema from CREATE TABLE IF NOT EXISTS;
    this function patches older databases forward.
    """
    for table, columns in _COLUMN_MIGRATIONS.items():
        cursor = await db.execute(f"PRAGMA table_info({table})")
        existing = {row[1] for row in await cursor.fetchall()}
        for col_name, col_def in columns:
            if col_name not in existing:
                await db.execute(
                    f"ALTER TABLE {table} ADD COLUMN {col_name} {col_def}"
                )


async def _migrate_session_events_check(db: aiosqlite.Connection) -> None:
    """Widen the event_type CHECK constraint on session_events.

    SQLite cannot ALTER a CHECK constraint, so we recreate the table
    while preserving existing data. The migration is idempotent — if the
    new event types already pass the constraint, this is a no-op.
    """
    # Probe whether the old constraint is still in place by attempting
    # an insert with one of the new event types.
    try:
        await db.execute(
            "INSERT INTO session_events (session_id, event_type) VALUES (-1, 'start')"
        )
        # If it succeeded, the constraint already allows new types.
        await db.execute("DELETE FROM session_events WHERE session_id = -1")
        return
    except Exception:
        # Old CHECK is blocking — need to recreate the table.
        await db.execute("ROLLBACK")

    await db.execute("ALTER TABLE session_events RENAME TO _session_events_old")
    await db.executescript(
        """
        CREATE TABLE IF NOT EXISTS session_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id INTEGER NOT NULL REFERENCES agent_sessions(id),
            event_type TEXT NOT NULL
                CHECK(event_type IN ('tool_call', 'tool_result', 'file_read', 'file_write', 'git_op', 'error', 'info', 'message', 'checkpoint', 'start')),
            tool_name TEXT NOT NULL DEFAULT '',
            detail TEXT NOT NULL DEFAULT '',
            timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        CREATE INDEX IF NOT EXISTS idx_session_events_session_id ON session_events(session_id);
        INSERT INTO session_events (id, session_id, event_type, tool_name, detail, timestamp)
            SELECT id, session_id, event_type, tool_name, detail, timestamp
            FROM _session_events_old;
        DROP TABLE _session_events_old;
        """
    )


async def init_db(db_path: str | None = None) -> aiosqlite.Connection:
    """Initialize the database connection.

    Args:
        db_path: Optional path to the database file. If not provided,
                 uses get_db_path() to determine the default location.

    Returns:
        Configured aiosqlite.Connection with WAL mode and foreign keys enabled.

    Note: This function creates the parent directory if it doesn't exist.
    """
    resolved_path = db_path or get_db_path()

    # Create parent directory if it doesn't exist (moved from get_db_path)
    Path(resolved_path).parent.mkdir(parents=True, exist_ok=True)

    db = await aiosqlite.connect(resolved_path)
    db.row_factory = aiosqlite.Row
    await db.execute("PRAGMA journal_mode=WAL")
    await db.execute("PRAGMA foreign_keys=ON")
    await db.executescript(SCHEMA)
    await _ensure_columns(db)
    await _migrate_session_events_check(db)
    await db.commit()
    return db


def row_to_dict(row: aiosqlite.Row) -> dict:
    return dict(row)
