"""SQLite database connection and schema management.

VAULT ISOLATION: each vault owns its own database at
root_dir/agntspace/agntspace.db. Conversations, decisions, dispatch logs,
canvas items — everything vault-specific — lives inside the vault folder
and travels with it. Switching vaults naturally switches databases.

The old shared location (~/.agntspace/agntspace.db) is migrated once on
first boot via ``_migrate_shared_db()`` in main.py.
"""

from __future__ import annotations

import logging
from pathlib import Path

import aiosqlite

log = logging.getLogger(__name__)

_SCHEMA = """
CREATE TABLE IF NOT EXISTS conversations (
    id TEXT PRIMARY KEY,
    title TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS messages (
    id TEXT PRIMARY KEY,
    conversation_id TEXT NOT NULL REFERENCES conversations(id),
    role TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT NOT NULL,
    token_count INTEGER DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_messages_conversation
    ON messages(conversation_id, created_at);

CREATE VIRTUAL TABLE IF NOT EXISTS vault_fts USING fts5(
    path,
    title,
    content,
    tokenize='porter unicode61'
);

CREATE TABLE IF NOT EXISTS dispatch_log (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    agent_key TEXT NOT NULL,
    agent_name TEXT NOT NULL,
    task TEXT NOT NULL,
    context TEXT DEFAULT '',
    project TEXT DEFAULT '',
    status TEXT NOT NULL DEFAULT 'running',
    content TEXT DEFAULT '',
    input_tokens INTEGER DEFAULT 0,
    output_tokens INTEGER DEFAULT 0,
    started_at TEXT NOT NULL,
    completed_at TEXT DEFAULT '',
    duration_ms INTEGER DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_dispatch_agent
    ON dispatch_log(agent_key, started_at);

CREATE TABLE IF NOT EXISTS vault_index (
    path TEXT PRIMARY KEY,
    title TEXT,
    content_hash TEXT,
    indexed_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS settings (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS dm_allowlist (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    channel TEXT NOT NULL,
    sender_id TEXT NOT NULL,
    approved_at TEXT NOT NULL,
    UNIQUE(channel, sender_id)
);

CREATE TABLE IF NOT EXISTS dm_pairing (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    channel TEXT NOT NULL,
    sender_id TEXT NOT NULL,
    code TEXT NOT NULL,
    created_at TEXT NOT NULL,
    expires_at TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    UNIQUE(channel, sender_id, code)
);

CREATE INDEX IF NOT EXISTS idx_dm_allowlist_lookup
    ON dm_allowlist(channel, sender_id);

CREATE TABLE IF NOT EXISTS canvas_items (
    id TEXT PRIMARY KEY,
    title TEXT DEFAULT '',
    content TEXT NOT NULL,
    content_type TEXT NOT NULL DEFAULT 'markdown',
    pinned INTEGER NOT NULL DEFAULT 0,
    collapsed INTEGER NOT NULL DEFAULT 0,
    sort_order INTEGER NOT NULL DEFAULT 0,
    user_note TEXT DEFAULT '',
    pushed_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS canvas_snapshots (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    items_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);

-- Regression tests: curated correlation_id chains the user wants to
-- preserve as expected-behavior baselines. Reuses the existing decision
-- log + correlation_id substrate; this table only adds the CURATION
-- layer (naming, description, archive, replay status tracking).
CREATE TABLE IF NOT EXISTS regression_tests (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    correlation_id TEXT NOT NULL,
    captured_at TEXT NOT NULL,
    baseline_snapshot TEXT NOT NULL DEFAULT '{}',
    archived INTEGER NOT NULL DEFAULT 0,
    last_replay_correlation_id TEXT DEFAULT NULL,
    last_replay_status TEXT DEFAULT NULL,
    last_replay_at TEXT DEFAULT NULL,
    last_replay_diff TEXT DEFAULT NULL
);

CREATE INDEX IF NOT EXISTS idx_regression_correlation
    ON regression_tests(correlation_id);
CREATE INDEX IF NOT EXISTS idx_regression_archived
    ON regression_tests(archived, captured_at DESC);

-- Scope file writes: every VaultWriter.write/append inside a correlation
-- scope leaves a breadcrumb here so the user can REVERT the scope's file
-- effects later. Complements the per-file .history/ versioning by giving
-- us a per-correlation_id manifest of what to restore.
CREATE TABLE IF NOT EXISTS scope_file_writes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    correlation_id TEXT NOT NULL,
    path TEXT NOT NULL,
    action TEXT NOT NULL,  -- 'create' | 'modify'
    pre_hash TEXT DEFAULT NULL,
    history_version TEXT DEFAULT NULL,
    post_hash TEXT DEFAULT NULL,
    written_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_scope_writes_correlation
    ON scope_file_writes(correlation_id, written_at);
CREATE INDEX IF NOT EXISTS idx_scope_writes_path
    ON scope_file_writes(path);
"""


async def get_db(db_path: Path) -> aiosqlite.Connection:
    """Open an async SQLite connection. Creates the file if needed."""
    db_path.parent.mkdir(parents=True, exist_ok=True)
    db = await aiosqlite.connect(str(db_path))
    db.row_factory = aiosqlite.Row
    await db.execute("PRAGMA journal_mode=WAL")
    await db.execute("PRAGMA foreign_keys=ON")
    # busy_timeout: wait up to 5 s for a write lock instead of failing
    # instantly with "database is locked". Critical because the vault
    # writes through a SEPARATE sqlite3.Connection in vault/scope_writes.py
    # (also WAL) during pipeline operations — WAL allows concurrent readers
    # but only ONE writer at a time, so without this pragma our async writes
    # (orchestrator dispatch log, work queue enqueue, decision log, cost
    # tracker) race the sync scope-writes connection and lose. 5000 ms
    # matches the 5.0 s timeout the raw sqlite3 connection already uses.
    await db.execute("PRAGMA busy_timeout=5000")
    # synchronous=NORMAL is the recommended pairing with WAL — still crash-
    # safe (no committed data lost), just skips extra fsyncs that FULL adds.
    # Cuts commit latency noticeably on Windows where fsync is expensive.
    await db.execute("PRAGMA synchronous=NORMAL")
    return db


async def init_schema(db: aiosqlite.Connection) -> None:
    """Create tables if they don't exist, and run migrations."""
    await db.executescript(_SCHEMA)
    # Migration: add archived column to conversations
    try:
        await db.execute("ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0")
    except Exception:
        pass  # column already exists
    # Migration: add provenance column to messages (JSON — kind, source_channel, etc.)
    try:
        await db.execute("ALTER TABLE messages ADD COLUMN provenance TEXT DEFAULT NULL")
    except Exception:
        pass  # column already exists
    # Migration: add goal_id to dispatch_log for per-goal traceability
    try:
        await db.execute("ALTER TABLE dispatch_log ADD COLUMN goal_id TEXT DEFAULT ''")
    except Exception:
        pass  # column already exists
    try:
        await db.execute("CREATE INDEX IF NOT EXISTS idx_dispatch_goal ON dispatch_log(goal_id)")
    except Exception:
        pass
    # Migration: add artifact tracking to conversations (feedback loop)
    try:
        await db.execute("ALTER TABLE conversations ADD COLUMN artifact_type TEXT DEFAULT NULL")
    except Exception:
        pass
    try:
        await db.execute("ALTER TABLE conversations ADD COLUMN artifact_path TEXT DEFAULT NULL")
    except Exception:
        pass
    try:
        await db.execute("ALTER TABLE conversations ADD COLUMN artifact_meta TEXT DEFAULT NULL")
    except Exception:
        pass
    await db.commit()
