"""SQLite schema creation and database access."""

from __future__ import annotations

import logging
import sqlite3
from pathlib import Path

from .. import config

logger = logging.getLogger(__name__)

# Singleton connection — avoids opening multiple connections per process
# which causes "database is locked" errors with WAL mode.
_conn: sqlite3.Connection | None = None


_SCHEMA_SQL = """
-- Core settings (voice signature, preferences, etc.)
CREATE TABLE IF NOT EXISTS settings (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- Campaigns
CREATE TABLE IF NOT EXISTS campaigns (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    icp_json    TEXT,
    status      TEXT NOT NULL DEFAULT 'draft',
    mode        TEXT NOT NULL DEFAULT 'copilot',
    config_json TEXT,
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    updated_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- Contacts (prospects)
CREATE TABLE IF NOT EXISTS contacts (
    id            TEXT PRIMARY KEY,
    campaign_id   TEXT,
    name          TEXT,
    title         TEXT,
    company       TEXT,
    linkedin_url  TEXT,
    linkedin_id   TEXT,
    profile_json  TEXT,
    analysis_json TEXT,
    fit_score     REAL DEFAULT 0.0,
    status        TEXT NOT NULL DEFAULT 'pending',
    created_at    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);

-- Outreaches (one per contact per campaign)
CREATE TABLE IF NOT EXISTS outreaches (
    id              TEXT PRIMARY KEY,
    campaign_id     TEXT NOT NULL,
    contact_id      TEXT NOT NULL,
    status          TEXT NOT NULL DEFAULT 'pending',
    next_action     TEXT,
    scheduled_at    INTEGER,
    followup_count  INTEGER NOT NULL DEFAULT 0,
    outcome_json    TEXT,
    invited_at      INTEGER,
    accepted_at     INTEGER,
    first_reply_at  INTEGER,
    created_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    updated_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
    FOREIGN KEY (contact_id) REFERENCES contacts(id)
);

-- Messages (conversation thread)
CREATE TABLE IF NOT EXISTS messages (
    id          TEXT PRIMARY KEY,
    outreach_id TEXT NOT NULL,
    role        TEXT NOT NULL,  -- 'sdr' or 'prospect'
    text        TEXT NOT NULL,
    sentiment   TEXT,
    read_at     INTEGER,  -- Unix timestamp when prospect read our message (NULL = unread/unknown)
    timestamp   INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
);

-- Actions log (audit trail)
CREATE TABLE IF NOT EXISTS actions_log (
    id           TEXT PRIMARY KEY,
    outreach_id  TEXT,
    action_type  TEXT NOT NULL,
    result       TEXT,
    details_json TEXT,
    timestamp    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
);

-- Rate limits (per day)
CREATE TABLE IF NOT EXISTS rate_limits (
    id          TEXT PRIMARY KEY,
    date        TEXT NOT NULL UNIQUE,
    sent        INTEGER NOT NULL DEFAULT 0,
    accepted    INTEGER NOT NULL DEFAULT 0,
    daily_limit INTEGER NOT NULL DEFAULT 15,
    blocked     INTEGER NOT NULL DEFAULT 0,
    updated_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- Usage tracking (free tier monthly counters)
CREATE TABLE IF NOT EXISTS usage (
    month              TEXT PRIMARY KEY,  -- 'YYYY-MM'
    invitations_sent   INTEGER NOT NULL DEFAULT 0,
    messages_sent      INTEGER NOT NULL DEFAULT 0,
    campaigns_created  INTEGER NOT NULL DEFAULT 0,
    icps_generated     INTEGER NOT NULL DEFAULT 0,
    engagements_sent   INTEGER NOT NULL DEFAULT 0,
    updated_at         INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- ICPs (persisted Ideal Customer Profiles)
CREATE TABLE IF NOT EXISTS icps (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    icp_json    TEXT NOT NULL,      -- full IcpResult serialized as JSON
    target_desc TEXT,               -- original target description
    source_url  TEXT,               -- company URL if provided
    status      TEXT NOT NULL DEFAULT 'active',
    confidence  REAL DEFAULT 0.5,   -- best ICP confidence score
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    updated_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- ICP data sources (websites, text, KB)
CREATE TABLE IF NOT EXISTS icp_sources (
    id          TEXT PRIMARY KEY,
    icp_id      TEXT,
    source_type TEXT NOT NULL,      -- 'website', 'text', 'kb'
    uri         TEXT,
    title       TEXT,
    content_hash TEXT,
    chunk_count INTEGER DEFAULT 0,
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (icp_id) REFERENCES icps(id)
);

-- ICP chunks (for RAG, Phase 2+)
CREATE TABLE IF NOT EXISTS icp_chunks (
    id          TEXT PRIMARY KEY,
    source_id   TEXT NOT NULL,
    text        TEXT NOT NULL,
    ctx_text    TEXT,               -- chunk + surrounding context
    header_path TEXT DEFAULT '',
    position    INTEGER DEFAULT 0,
    embedding   BLOB,              -- numpy float32 array (Phase 3+)
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (source_id) REFERENCES icp_sources(id)
);

-- Engagements (post comments and reactions)
CREATE TABLE IF NOT EXISTS engagements (
    id            TEXT PRIMARY KEY,
    outreach_id   TEXT,
    action_type   TEXT NOT NULL,       -- 'comment' or 'react'
    post_id       TEXT NOT NULL,       -- LinkedIn post ID/URN
    post_text     TEXT,                -- Snapshot of the post text (for context)
    text          TEXT,                -- Comment text (null for reactions)
    reaction_type TEXT,                -- 'LIKE', 'CELEBRATE', etc. (null for comments)
    status        TEXT NOT NULL DEFAULT 'sent',  -- 'sent', 'pending_review', 'failed'
    reasoning     TEXT,                -- JSON: LLM reasoning for comment choice
    created_at    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
);

-- Scheduler jobs (Sprint 17: autonomous scheduling)
CREATE TABLE IF NOT EXISTS scheduler_jobs (
    id           TEXT PRIMARY KEY,
    campaign_id  TEXT NOT NULL,
    outreach_id  TEXT,
    job_type     TEXT NOT NULL,     -- 'invite', 'followup', 'engage', 'check_replies'
    status       TEXT NOT NULL DEFAULT 'pending',  -- 'pending', 'running', 'completed', 'failed'
    scheduled_at INTEGER NOT NULL,
    started_at   INTEGER,
    completed_at INTEGER,
    retry_count  INTEGER NOT NULL DEFAULT 0,
    error        TEXT,
    created_at   INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
    FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
);

-- Experiments (PM hypothesis tracking)
CREATE TABLE IF NOT EXISTS experiments (
    id          TEXT PRIMARY KEY,
    snapshot    TEXT NOT NULL,
    result_json TEXT NOT NULL,
    campaign_ids TEXT,
    status      TEXT NOT NULL DEFAULT 'pending',
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- A/B Tests (controlled experiments on messaging variants)
CREATE TABLE IF NOT EXISTS ab_tests (
    id           TEXT PRIMARY KEY,
    campaign_id  TEXT NOT NULL,
    name         TEXT NOT NULL,          -- e.g., "Pain-first vs Question-first hook"
    hypothesis   TEXT,                   -- what we expect to learn
    variant_a    TEXT NOT NULL,          -- description of variant A (control)
    variant_b    TEXT NOT NULL,          -- description of variant B (treatment)
    status       TEXT NOT NULL DEFAULT 'running',  -- running, completed, cancelled
    winner       TEXT,                   -- 'A', 'B', or 'inconclusive'
    result_json  TEXT,                   -- per-variant stats at completion
    created_at   INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    completed_at INTEGER,
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);

-- Inbound signals (connection requests, unsolicited DMs, post comments)
CREATE TABLE IF NOT EXISTS inbound_signals (
    id                 TEXT PRIMARY KEY,
    signal_type        TEXT NOT NULL,       -- 'invitation', 'message', 'comment'
    sender_name        TEXT,
    sender_id          TEXT,                -- LinkedIn provider_id
    sender_headline    TEXT,
    sender_company     TEXT,
    sender_url         TEXT,                -- LinkedIn profile URL
    content            TEXT,                -- invite message, DM text, or comment text
    post_id            TEXT,                -- only for comments (our post)
    profile_json       TEXT,                -- full profile if fetched
    intent             TEXT,                -- 'buying_signal', 'networking', 'job_seeking', 'spam', 'partnership', 'unknown'
    matched_icp_id     TEXT,
    confidence         REAL DEFAULT 0.0,
    recommended_action TEXT,                -- 'engage_immediately', 'ask_purpose', 'accept_and_monitor', 'ignore'
    reasoning          TEXT,
    status             TEXT DEFAULT 'new',  -- 'new', 'qualified', 'engaged', 'converted', 'dismissed'
    campaign_id        TEXT,
    created_at         INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    qualified_at       INTEGER,
    FOREIGN KEY (matched_icp_id) REFERENCES icps(id)
);

-- Published posts (for comment monitoring)
CREATE TABLE IF NOT EXISTS published_posts (
    id           TEXT PRIMARY KEY,
    post_id      TEXT NOT NULL,              -- LinkedIn post ID/URN
    text         TEXT,                       -- post content snapshot
    topic        TEXT,
    published_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    last_checked INTEGER,                    -- last time we checked for comments
    comment_count INTEGER DEFAULT 0
);

-- Proactive signals (buying intent from prospect behavior)
CREATE TABLE IF NOT EXISTS signals (
    id             TEXT PRIMARY KEY,
    signal_type    TEXT NOT NULL,             -- 'keyword_mention', 'prospect_post', 'job_change',
                                             -- 'competitor_mention', 'hiring_surge', 'funding_event',
                                             -- 'news_event', 'profile_view', 'post_engagement',
                                             -- 'commenter_match'
    source         TEXT NOT NULL,             -- 'keyword_search', 'prospect_scan', 'profile_scan',
                                             -- 'job_search', 'news_search', 'comment_xref'
    prospect_id    TEXT,                      -- FK to contacts.id (NULL if new prospect)
    prospect_name  TEXT,
    prospect_title TEXT,
    linkedin_id    TEXT,                      -- LinkedIn provider_id (for dedup + linking)
    campaign_id    TEXT,                      -- FK to campaigns.id (if linked to campaign)

    -- Signal content
    content        TEXT,                      -- Post text, job description, news snippet
    post_id        TEXT,                      -- LinkedIn post ID (if post-related)
    metadata_json  TEXT,                      -- Flexible: {keywords_matched, company, old_title, new_title, etc.}

    -- Classification
    intent         TEXT,                      -- 'buying_signal', 'pain_point', 'competitor_eval',
                                             -- 'job_seeking', 'thought_leadership', 'unknown'
    confidence     REAL DEFAULT 0.0,          -- 0.0-1.0 (from classifier)
    signal_score   REAL DEFAULT 0.0,          -- 0.0-1.0 (composite weighted score)
    reasoning      TEXT,

    -- State
    status         TEXT DEFAULT 'new',        -- 'new', 'classified', 'actioned', 'dismissed', 'expired'
    action_taken   TEXT,                      -- 'campaign_created', 'outreach_triggered', 'engaged', NULL
    actioned_at    INTEGER,
    expires_at     INTEGER,                   -- Signal freshness expiry (auto-decay)

    detected_at    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    classified_at  INTEGER,

    FOREIGN KEY (prospect_id) REFERENCES contacts(id),
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);

-- Signal watchlists (keyword monitoring configuration)
CREATE TABLE IF NOT EXISTS signal_watchlists (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,                -- e.g., "Competitor mentions", "Pain point keywords"
    watch_type  TEXT NOT NULL,                -- 'keyword', 'competitor', 'company', 'person'
    keywords    TEXT NOT NULL,                -- JSON array: ["cold outreach", "SDR automation"]
    campaign_id TEXT,                         -- Optional: link signals to specific campaign
    is_active   INTEGER DEFAULT 1,
    last_polled_at INTEGER,                   -- Last time this watchlist was polled
    created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);

-- Intent events (compound intent from stacked signals)
CREATE TABLE IF NOT EXISTS intent_events (
    id              TEXT PRIMARY KEY,
    linkedin_id     TEXT NOT NULL,
    company         TEXT,
    event_type      TEXT NOT NULL,             -- 'new_leader_building', 'active_evaluation',
                                               -- 'growth_mode', 'engaged_thought_leader',
                                               -- 'warm_inbound', 'multi_signal_hot'
    signal_ids      TEXT NOT NULL,             -- JSON array of contributing signal IDs
    signal_types    TEXT NOT NULL,             -- JSON array of contributing signal types
    composite_score REAL NOT NULL,
    action_taken    TEXT,                       -- 'outreach_created', 'priority_boosted', NULL
    detected_at     INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    expires_at      INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_intent_events_linkedin ON intent_events(linkedin_id);
CREATE INDEX IF NOT EXISTS idx_intent_events_type ON intent_events(event_type);
CREATE INDEX IF NOT EXISTS idx_intent_events_score ON intent_events(composite_score DESC);

-- Signal accounts (aggregated signal scores per prospect)
CREATE TABLE IF NOT EXISTS signal_accounts (
    linkedin_id     TEXT PRIMARY KEY,
    prospect_name   TEXT,
    company         TEXT,
    total_signals   INTEGER DEFAULT 0,
    composite_score REAL DEFAULT 0.0,
    top_signal_type TEXT,                     -- Most impactful signal type
    last_signal_at  INTEGER,
    updated_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- Strategy patterns (cross-campaign learned patterns)
CREATE TABLE IF NOT EXISTS strategy_patterns (
    id                      TEXT PRIMARY KEY,
    pattern_type            TEXT NOT NULL,
    pattern_key             TEXT NOT NULL,
    description             TEXT,
    evidence_json           TEXT NOT NULL,
    confidence              REAL DEFAULT 0.0,
    estimated_revenue_impact REAL DEFAULT 0.0,
    acceptance_rate         REAL,
    reply_rate              REAL,
    conversion_rate         REAL,
    sample_size             INTEGER DEFAULT 0,
    status                  TEXT DEFAULT 'active',
    discovered_at           INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    last_validated          INTEGER,
    created_at              INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);

-- Strategy actions (audit trail of autonomous actions)
CREATE TABLE IF NOT EXISTS strategy_actions (
    id              TEXT PRIMARY KEY,
    action_type     TEXT NOT NULL,
    campaign_id     TEXT,
    pattern_id      TEXT,
    details_json    TEXT NOT NULL,
    outcome_json    TEXT,
    status          TEXT DEFAULT 'applied',
    created_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    measured_at     INTEGER,
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
    FOREIGN KEY (pattern_id) REFERENCES strategy_patterns(id)
);

-- Indexes
CREATE INDEX IF NOT EXISTS idx_experiments_created ON experiments(created_at);
CREATE INDEX IF NOT EXISTS idx_ab_tests_campaign ON ab_tests(campaign_id);
CREATE INDEX IF NOT EXISTS idx_ab_tests_status ON ab_tests(status);
CREATE INDEX IF NOT EXISTS idx_scheduler_jobs_status ON scheduler_jobs(status, scheduled_at);
CREATE INDEX IF NOT EXISTS idx_scheduler_jobs_campaign ON scheduler_jobs(campaign_id);
CREATE INDEX IF NOT EXISTS idx_contacts_campaign ON contacts(campaign_id);
CREATE INDEX IF NOT EXISTS idx_outreaches_campaign ON outreaches(campaign_id);
CREATE INDEX IF NOT EXISTS idx_outreaches_campaign_status ON outreaches(campaign_id, status);
CREATE INDEX IF NOT EXISTS idx_outreaches_contact ON outreaches(contact_id);
CREATE INDEX IF NOT EXISTS idx_outreaches_status ON outreaches(status);
CREATE INDEX IF NOT EXISTS idx_messages_outreach ON messages(outreach_id);
CREATE INDEX IF NOT EXISTS idx_actions_log_outreach ON actions_log(outreach_id);
CREATE INDEX IF NOT EXISTS idx_rate_limits_date ON rate_limits(date);
CREATE INDEX IF NOT EXISTS idx_icps_status ON icps(status);
CREATE INDEX IF NOT EXISTS idx_icp_sources_icp ON icp_sources(icp_id);
CREATE INDEX IF NOT EXISTS idx_icp_chunks_source ON icp_chunks(source_id);
CREATE INDEX IF NOT EXISTS idx_engagements_outreach ON engagements(outreach_id);
CREATE INDEX IF NOT EXISTS idx_engagements_created ON engagements(created_at);
CREATE INDEX IF NOT EXISTS idx_campaigns_status ON campaigns(status);
CREATE INDEX IF NOT EXISTS idx_inbound_signals_status ON inbound_signals(status);
CREATE INDEX IF NOT EXISTS idx_inbound_signals_type ON inbound_signals(signal_type);
CREATE INDEX IF NOT EXISTS idx_inbound_signals_sender ON inbound_signals(sender_id);
CREATE INDEX IF NOT EXISTS idx_published_posts_post ON published_posts(post_id);
CREATE INDEX IF NOT EXISTS idx_signals_status ON signals(status);
CREATE INDEX IF NOT EXISTS idx_signals_type ON signals(signal_type);
CREATE INDEX IF NOT EXISTS idx_signals_prospect ON signals(linkedin_id);
CREATE INDEX IF NOT EXISTS idx_signals_detected ON signals(detected_at);
CREATE INDEX IF NOT EXISTS idx_signals_score ON signals(signal_score DESC);
CREATE INDEX IF NOT EXISTS idx_signal_watchlists_active ON signal_watchlists(is_active);
CREATE INDEX IF NOT EXISTS idx_signal_watchlists_campaign ON signal_watchlists(campaign_id);

CREATE INDEX IF NOT EXISTS idx_strategy_patterns_type ON strategy_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_strategy_patterns_status ON strategy_patterns(status);
CREATE INDEX IF NOT EXISTS idx_strategy_patterns_revenue ON strategy_patterns(estimated_revenue_impact);
CREATE INDEX IF NOT EXISTS idx_strategy_actions_campaign ON strategy_actions(campaign_id);
CREATE INDEX IF NOT EXISTS idx_strategy_actions_type ON strategy_actions(action_type);
CREATE INDEX IF NOT EXISTS idx_strategy_actions_status ON strategy_actions(status);
"""


def get_db() -> sqlite3.Connection:
    """Return a singleton SQLite connection with WAL mode enabled.

    Re-uses the same connection across the process lifetime to avoid
    "database is locked" errors from multiple open connections competing
    for the write lock.
    """
    global _conn
    if _conn is not None:
        return _conn

    config.ensure_dirs()
    path = config.db_path()
    is_new = not path.exists()

    conn = sqlite3.connect(str(path), timeout=30)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=10000")
    conn.execute("PRAGMA foreign_keys=ON")

    if is_new:
        logger.info(f"Creating new database at {path}")
        conn.executescript(_SCHEMA_SQL)
        conn.commit()
        _run_migrations(conn)
    else:
        _run_migrations(conn)

    _conn = conn
    return conn


def _run_migrations(conn: sqlite3.Connection) -> None:
    """Apply schema migrations to existing databases."""
    # Sprint 2: Add followup_count to outreaches
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN followup_count INTEGER NOT NULL DEFAULT 0")
        conn.commit()
        logger.info("Migration: added followup_count to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 3: Add engagements table (for existing DBs created before Sprint 3)
    try:
        conn.execute("SELECT 1 FROM engagements LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS engagements (
                id            TEXT PRIMARY KEY,
                outreach_id   TEXT,
                action_type   TEXT NOT NULL,
                post_id       TEXT NOT NULL,
                post_text     TEXT,
                text          TEXT,
                reaction_type TEXT,
                status        TEXT NOT NULL DEFAULT 'sent',
                reasoning     TEXT,
                created_at    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
            );
            CREATE INDEX IF NOT EXISTS idx_engagements_outreach ON engagements(outreach_id);
            CREATE INDEX IF NOT EXISTS idx_engagements_created ON engagements(created_at);
        """)
        conn.commit()
        logger.info("Migration: added engagements table")

    # Sprint 3: Add engagements_sent to usage
    try:
        conn.execute("ALTER TABLE usage ADD COLUMN engagements_sent INTEGER NOT NULL DEFAULT 0")
        conn.commit()
        logger.info("Migration: added engagements_sent to usage")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 12: Add outcome_json to outreaches (close reason, notes, booking)
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN outcome_json TEXT")
        conn.commit()
        logger.info("Migration: added outcome_json to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 14: Add context_json to campaigns (offerings, case_studies, social_proofs, preferences)
    try:
        conn.execute("ALTER TABLE campaigns ADD COLUMN context_json TEXT")
        conn.commit()
        logger.info("Migration: added context_json to campaigns")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 16: Add memory_json to outreaches (structured follow-up decisions)
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN memory_json TEXT")
        conn.commit()
        logger.info("Migration: added memory_json to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 21: Add event timestamps to outreaches
    for col in ("invited_at", "accepted_at", "first_reply_at"):
        try:
            conn.execute(f"ALTER TABLE outreaches ADD COLUMN {col} INTEGER")
            conn.commit()
            logger.info(f"Migration: added {col} to outreaches")
        except sqlite3.OperationalError:
            pass  # Column already exists

    # Sprint 17: Add scheduler_jobs table (for existing DBs created before Sprint 17)
    try:
        conn.execute("SELECT 1 FROM scheduler_jobs LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS scheduler_jobs (
                id           TEXT PRIMARY KEY,
                campaign_id  TEXT NOT NULL,
                outreach_id  TEXT,
                job_type     TEXT NOT NULL,
                status       TEXT NOT NULL DEFAULT 'pending',
                scheduled_at INTEGER NOT NULL,
                started_at   INTEGER,
                completed_at INTEGER,
                retry_count  INTEGER NOT NULL DEFAULT 0,
                error        TEXT,
                created_at   INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
                FOREIGN KEY (outreach_id) REFERENCES outreaches(id)
            );
            CREATE INDEX IF NOT EXISTS idx_scheduler_jobs_status ON scheduler_jobs(status, scheduled_at);
            CREATE INDEX IF NOT EXISTS idx_scheduler_jobs_campaign ON scheduler_jobs(campaign_id);
        """)
        conn.commit()
        logger.info("Migration: added scheduler_jobs table")

    # Feature 1.4: Add read_at to messages (read receipt tracking)
    try:
        conn.execute("ALTER TABLE messages ADD COLUMN read_at INTEGER")
        conn.commit()
        logger.info("Migration: added read_at to messages")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Sprint 22: Add experiments table
    try:
        conn.execute("SELECT 1 FROM experiments LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS experiments (
                id TEXT PRIMARY KEY, snapshot TEXT NOT NULL, result_json TEXT NOT NULL,
                campaign_ids TEXT, status TEXT NOT NULL DEFAULT 'pending',
                created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
            );
            CREATE INDEX IF NOT EXISTS idx_experiments_created ON experiments(created_at);
        """)
        conn.commit()
        logger.info("Migration: added experiments table")


    # Feature 3.2: Add variant column to outreaches (A/B testing)
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN variant TEXT")
        conn.commit()
        logger.info("Migration: added variant to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Feature 3.2: Add ab_tests table
    try:
        conn.execute("SELECT 1 FROM ab_tests LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS ab_tests (
                id           TEXT PRIMARY KEY,
                campaign_id  TEXT NOT NULL,
                name         TEXT NOT NULL,
                hypothesis   TEXT,
                variant_a    TEXT NOT NULL,
                variant_b    TEXT NOT NULL,
                status       TEXT NOT NULL DEFAULT 'running',
                winner       TEXT,
                result_json  TEXT,
                created_at   INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                completed_at INTEGER,
                FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
            );
            CREATE INDEX IF NOT EXISTS idx_ab_tests_campaign ON ab_tests(campaign_id);
            CREATE INDEX IF NOT EXISTS idx_ab_tests_status ON ab_tests(status);
        """)
        conn.commit()
        logger.info("Migration: added ab_tests table")


    # Feature 3.5: Add channel column to outreaches (linkedin/email)
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN channel TEXT DEFAULT 'linkedin'")
        conn.commit()
        logger.info("Migration: added channel to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Feature 3.5: Add channel column to messages
    try:
        conn.execute("ALTER TABLE messages ADD COLUMN channel TEXT DEFAULT 'linkedin'")
        conn.commit()
        logger.info("Migration: added channel to messages")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Feature 4.1: Add source column to contacts (search/import/discovery)
    try:
        conn.execute("ALTER TABLE contacts ADD COLUMN source TEXT DEFAULT 'search'")
        conn.commit()
        logger.info("Migration: added source to contacts")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Feature 4.4: CRM mappings table for HubSpot integration
    conn.execute("""
        CREATE TABLE IF NOT EXISTS crm_mappings (
            id              TEXT PRIMARY KEY,
            contact_id      TEXT NOT NULL,
            crm_type        TEXT NOT NULL DEFAULT 'hubspot',
            crm_contact_id  TEXT,
            crm_deal_id     TEXT,
            synced_at       INTEGER,
            created_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
            FOREIGN KEY (contact_id) REFERENCES contacts(id)
        )
    """)
    conn.execute("CREATE INDEX IF NOT EXISTS idx_crm_mappings_contact ON crm_mappings(contact_id)")
    conn.commit()

    # Inbound pipeline: inbound_signals table
    try:
        conn.execute("SELECT 1 FROM inbound_signals LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS inbound_signals (
                id                 TEXT PRIMARY KEY,
                signal_type        TEXT NOT NULL,
                sender_name        TEXT,
                sender_id          TEXT,
                sender_headline    TEXT,
                sender_company     TEXT,
                sender_url         TEXT,
                content            TEXT,
                post_id            TEXT,
                profile_json       TEXT,
                intent             TEXT,
                matched_icp_id     TEXT,
                confidence         REAL DEFAULT 0.0,
                recommended_action TEXT,
                reasoning          TEXT,
                status             TEXT DEFAULT 'new',
                campaign_id        TEXT,
                created_at         INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                qualified_at       INTEGER,
                FOREIGN KEY (matched_icp_id) REFERENCES icps(id)
            );
            CREATE INDEX IF NOT EXISTS idx_inbound_signals_status ON inbound_signals(status);
            CREATE INDEX IF NOT EXISTS idx_inbound_signals_type ON inbound_signals(signal_type);
            CREATE INDEX IF NOT EXISTS idx_inbound_signals_sender ON inbound_signals(sender_id);
        """)
        conn.commit()
        logger.info("Migration: added inbound_signals table")

    # Inbound pipeline: published_posts table
    try:
        conn.execute("SELECT 1 FROM published_posts LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS published_posts (
                id           TEXT PRIMARY KEY,
                post_id      TEXT NOT NULL,
                text         TEXT,
                topic        TEXT,
                published_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                last_checked INTEGER,
                comment_count INTEGER DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_published_posts_post ON published_posts(post_id);
        """)
        conn.commit()
        logger.info("Migration: added published_posts table")

    # Signal-based selling: signals table (v1.0)
    try:
        conn.execute("SELECT 1 FROM signals LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS signals (
                id             TEXT PRIMARY KEY,
                signal_type    TEXT NOT NULL,
                source         TEXT NOT NULL,
                prospect_id    TEXT,
                prospect_name  TEXT,
                prospect_title TEXT,
                linkedin_id    TEXT,
                campaign_id    TEXT,
                content        TEXT,
                post_id        TEXT,
                metadata_json  TEXT,
                intent         TEXT,
                confidence     REAL DEFAULT 0.0,
                signal_score   REAL DEFAULT 0.0,
                reasoning      TEXT,
                status         TEXT DEFAULT 'new',
                action_taken   TEXT,
                actioned_at    INTEGER,
                expires_at     INTEGER,
                detected_at    INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                classified_at  INTEGER,
                FOREIGN KEY (prospect_id) REFERENCES contacts(id),
                FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
            );
            CREATE INDEX IF NOT EXISTS idx_signals_status ON signals(status);
            CREATE INDEX IF NOT EXISTS idx_signals_type ON signals(signal_type);
            CREATE INDEX IF NOT EXISTS idx_signals_prospect ON signals(linkedin_id);
            CREATE INDEX IF NOT EXISTS idx_signals_detected ON signals(detected_at);
            CREATE INDEX IF NOT EXISTS idx_signals_score ON signals(signal_score DESC);
        """)
        conn.commit()
        logger.info("Migration: added signals table")

    # Signal-based selling: signal_watchlists table (v1.0)
    try:
        conn.execute("SELECT 1 FROM signal_watchlists LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS signal_watchlists (
                id          TEXT PRIMARY KEY,
                name        TEXT NOT NULL,
                watch_type  TEXT NOT NULL,
                keywords    TEXT NOT NULL,
                campaign_id TEXT,
                is_active   INTEGER DEFAULT 1,
                last_polled_at INTEGER,
                created_at  INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
            );
            CREATE INDEX IF NOT EXISTS idx_signal_watchlists_active ON signal_watchlists(is_active);
            CREATE INDEX IF NOT EXISTS idx_signal_watchlists_campaign ON signal_watchlists(campaign_id);
        """)
        conn.commit()
        logger.info("Migration: added signal_watchlists table")

    # Signal-based selling: signal_accounts table (v1.0)
    try:
        conn.execute("SELECT 1 FROM signal_accounts LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS signal_accounts (
                linkedin_id     TEXT PRIMARY KEY,
                prospect_name   TEXT,
                company         TEXT,
                total_signals   INTEGER DEFAULT 0,
                composite_score REAL DEFAULT 0.0,
                top_signal_type TEXT,
                last_signal_at  INTEGER,
                updated_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
            );
        """)
        conn.commit()
        logger.info("Migration: added signal_accounts table")

    # Signal-based selling: last_scanned_at on contacts (v1.0)
    try:
        conn.execute("ALTER TABLE contacts ADD COLUMN last_scanned_at INTEGER")
        conn.commit()
        logger.info("Migration: added last_scanned_at to contacts")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Signal activation: signal_id on outreaches (v1.1)
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN signal_id TEXT")
        conn.commit()
        logger.info("Migration: added signal_id to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Strategy engine: estimated_revenue on contacts
    try:
        conn.execute("ALTER TABLE contacts ADD COLUMN estimated_revenue REAL DEFAULT 0.0")
        conn.commit()
        logger.info("Migration: added estimated_revenue to contacts")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Strategy engine: strategy_json on outreaches
    try:
        conn.execute("ALTER TABLE outreaches ADD COLUMN strategy_json TEXT")
        conn.commit()
        logger.info("Migration: added strategy_json to outreaches")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Strategy engine: spawned_by on campaigns
    try:
        conn.execute("ALTER TABLE campaigns ADD COLUMN spawned_by TEXT")
        conn.commit()
        logger.info("Migration: added spawned_by to campaigns")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Strategy engine: strategy_patterns table
    try:
        conn.execute("SELECT 1 FROM strategy_patterns LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS strategy_patterns (
                id                      TEXT PRIMARY KEY,
                pattern_type            TEXT NOT NULL,
                pattern_key             TEXT NOT NULL,
                description             TEXT,
                evidence_json           TEXT NOT NULL,
                confidence              REAL DEFAULT 0.0,
                estimated_revenue_impact REAL DEFAULT 0.0,
                acceptance_rate         REAL,
                reply_rate              REAL,
                conversion_rate         REAL,
                sample_size             INTEGER DEFAULT 0,
                status                  TEXT DEFAULT 'active',
                discovered_at           INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                last_validated          INTEGER,
                created_at              INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
            );
            CREATE INDEX IF NOT EXISTS idx_strategy_patterns_type ON strategy_patterns(pattern_type);
            CREATE INDEX IF NOT EXISTS idx_strategy_patterns_status ON strategy_patterns(status);
            CREATE INDEX IF NOT EXISTS idx_strategy_patterns_revenue ON strategy_patterns(estimated_revenue_impact);
        """)
        conn.commit()
        logger.info("Migration: added strategy_patterns table")

    # Strategy engine: strategy_actions table
    try:
        conn.execute("SELECT 1 FROM strategy_actions LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS strategy_actions (
                id              TEXT PRIMARY KEY,
                action_type     TEXT NOT NULL,
                campaign_id     TEXT,
                pattern_id      TEXT,
                details_json    TEXT NOT NULL,
                outcome_json    TEXT,
                status          TEXT DEFAULT 'applied',
                created_at      INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                measured_at     INTEGER,
                FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
                FOREIGN KEY (pattern_id) REFERENCES strategy_patterns(id)
            );
            CREATE INDEX IF NOT EXISTS idx_strategy_actions_campaign ON strategy_actions(campaign_id);
            CREATE INDEX IF NOT EXISTS idx_strategy_actions_type ON strategy_actions(action_type);
            CREATE INDEX IF NOT EXISTS idx_strategy_actions_status ON strategy_actions(status);
        """)
        conn.commit()
        logger.info("Migration: added strategy_actions table")


    # Tracking fix: add outreach_id to inbound_signals for message tracking
    try:
        conn.execute("ALTER TABLE inbound_signals ADD COLUMN outreach_id TEXT")
        conn.commit()
        logger.info("Migration: added outreach_id to inbound_signals")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Tracking fix: invite attempt tracking on outreaches
    for col, ctype in [("invite_attempts", "INTEGER NOT NULL DEFAULT 0"),
                        ("last_attempt_error", "TEXT")]:
        try:
            conn.execute(f"ALTER TABLE outreaches ADD COLUMN {col} {ctype}")
            conn.commit()
            logger.info("Migration: added %s to outreaches", col)
        except sqlite3.OperationalError:
            pass  # Column already exists

    # Tracking fix: campaign_id on engagements for manual engagement linking
    try:
        conn.execute("ALTER TABLE engagements ADD COLUMN campaign_id TEXT")
        conn.commit()
        logger.info("Migration: added campaign_id to engagements")
    except sqlite3.OperationalError:
        pass  # Column already exists

    # Compound intent events table (Week 10)
    try:
        conn.execute("SELECT 1 FROM intent_events LIMIT 0")
    except sqlite3.OperationalError:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS intent_events (
                id              TEXT PRIMARY KEY,
                linkedin_id     TEXT NOT NULL,
                company         TEXT,
                event_type      TEXT NOT NULL,
                signal_ids      TEXT NOT NULL,
                signal_types    TEXT NOT NULL,
                composite_score REAL NOT NULL,
                action_taken    TEXT,
                detected_at     INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
                expires_at      INTEGER NOT NULL
            );
            CREATE INDEX IF NOT EXISTS idx_intent_events_linkedin ON intent_events(linkedin_id);
            CREATE INDEX IF NOT EXISTS idx_intent_events_type ON intent_events(event_type);
            CREATE INDEX IF NOT EXISTS idx_intent_events_score ON intent_events(composite_score DESC);
        """)
        conn.commit()
        logger.info("Migration: added intent_events table")


    # Voice memo support: format column on messages (voice memo sprint)
    try:
        conn.execute("ALTER TABLE messages ADD COLUMN format TEXT DEFAULT 'text'")
        conn.commit()
        logger.info("Migration: added format column to messages")
    except sqlite3.OperationalError:
        pass  # Column already exists


def reset_db() -> None:
    """Delete the database file entirely."""
    path = config.db_path()
    if path.exists():
        path.unlink()
        logger.info("Database deleted")
