-- mc-webui v2 SQLite Schema
-- WAL mode and foreign keys are enabled programmatically in Database.__init__

-- Schema versioning for future migrations
CREATE TABLE IF NOT EXISTS schema_version (
    version     INTEGER PRIMARY KEY,
    applied_at  TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO schema_version (version) VALUES (1);

-- Device identity and settings
CREATE TABLE IF NOT EXISTS device (
    id          INTEGER PRIMARY KEY CHECK (id = 1),  -- singleton row
    public_key  TEXT NOT NULL DEFAULT '',
    name        TEXT NOT NULL DEFAULT '',
    created_at  TEXT NOT NULL DEFAULT (datetime('now')),
    self_info   TEXT  -- JSON blob with full device info
);

-- All known contacts (replaces contacts_cache.jsonl)
CREATE TABLE IF NOT EXISTS contacts (
    public_key      TEXT PRIMARY KEY,       -- hex, lowercase
    name            TEXT NOT NULL DEFAULT '',
    type            INTEGER DEFAULT 0,      -- node type from device
    flags           INTEGER DEFAULT 0,
    out_path        TEXT DEFAULT '',         -- outgoing path string
    out_path_len    INTEGER DEFAULT 0,
    last_advert     TEXT,                   -- ISO 8601 timestamp
    adv_lat         REAL,                   -- GPS latitude from advert
    adv_lon         REAL,                   -- GPS longitude from advert
    first_seen      TEXT NOT NULL DEFAULT (datetime('now')),
    last_seen       TEXT NOT NULL DEFAULT (datetime('now')),
    source          TEXT DEFAULT 'advert',  -- 'advert', 'device', 'manual'
    is_protected    INTEGER DEFAULT 0,      -- 1 = protected from cleanup
    lastmod         TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Channel configuration
CREATE TABLE IF NOT EXISTS channels (
    idx         INTEGER PRIMARY KEY,        -- channel index (0-7)
    name        TEXT NOT NULL DEFAULT '',
    secret      TEXT,                       -- channel secret/key (hex)
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Region registry (user-curated MeshCore flood scopes)
CREATE TABLE IF NOT EXISTS regions (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT NOT NULL UNIQUE,           -- firmware-safe name, e.g. 'pl-ma'
    key_hex     TEXT NOT NULL,                  -- 32 hex chars = 16-byte scope key
    is_default  INTEGER NOT NULL DEFAULT 0,     -- mirrors firmware CMD_GET_DEFAULT_FLOOD_SCOPE
    created_at  TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- User-configured MeshCore Analyzer services
CREATE TABLE IF NOT EXISTS analyzers (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    name          TEXT NOT NULL UNIQUE,
    url_template  TEXT NOT NULL,                  -- must contain '{packetHash}'
    is_default    INTEGER NOT NULL DEFAULT 0,
    is_disabled   INTEGER NOT NULL DEFAULT 0,
    created_at    TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_analyzers_one_default
    ON analyzers(is_default) WHERE is_default = 1;

-- Per-channel region mapping (absent row = no override; firmware default applies)
CREATE TABLE IF NOT EXISTS channel_scopes (
    channel_idx INTEGER PRIMARY KEY,
    region_id   INTEGER NOT NULL REFERENCES regions(id) ON DELETE CASCADE,
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Channel messages (replaces CHAN/SENT_CHAN from .msgs)
CREATE TABLE IF NOT EXISTS channel_messages (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_idx         INTEGER NOT NULL DEFAULT 0,
    sender              TEXT NOT NULL DEFAULT '',
    content             TEXT NOT NULL DEFAULT '',
    timestamp           INTEGER NOT NULL DEFAULT 0,         -- unix epoch
    sender_timestamp    INTEGER,                            -- sender's clock
    is_own              INTEGER NOT NULL DEFAULT 0,         -- 1 = sent by us
    txt_type            INTEGER DEFAULT 0,
    snr                 REAL,
    path_len            INTEGER,
    pkt_payload         TEXT,                               -- for echo matching
    raw_json            TEXT,                               -- original JSON line
    created_at          TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Direct messages (replaces PRIV/SENT_MSG from .msgs)
CREATE TABLE IF NOT EXISTS direct_messages (
    id                  INTEGER PRIMARY KEY AUTOINCREMENT,
    contact_pubkey      TEXT,                               -- FK to contacts (nullable for unknown)
    direction           TEXT NOT NULL CHECK (direction IN ('in', 'out')),
    content             TEXT NOT NULL DEFAULT '',
    timestamp           INTEGER NOT NULL DEFAULT 0,         -- unix epoch
    sender_timestamp    INTEGER,
    txt_type            INTEGER DEFAULT 0,
    snr                 REAL,
    path_len            INTEGER,
    expected_ack        TEXT,                               -- ACK code for delivery tracking
    pkt_payload         TEXT,                               -- raw packet payload for hash/analyzer
    signature           TEXT,                               -- dedup signature
    raw_json            TEXT,
    created_at          TEXT NOT NULL DEFAULT (datetime('now')),
    FOREIGN KEY (contact_pubkey) REFERENCES contacts(public_key) ON DELETE SET NULL
);

-- ACK tracking (replaces .acks.jsonl)
CREATE TABLE IF NOT EXISTS acks (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    expected_ack    TEXT NOT NULL,           -- ACK code to match
    received_at     TEXT NOT NULL DEFAULT (datetime('now')),
    snr             REAL,
    rssi            REAL,
    route_type      TEXT,                   -- 'direct', 'flood', etc.
    is_retry        INTEGER DEFAULT 0,
    dm_id           INTEGER,                -- FK to direct_messages (nullable)
    FOREIGN KEY (dm_id) REFERENCES direct_messages(id) ON DELETE SET NULL
);

-- Echo tracking (replaces .echoes.jsonl)
CREATE TABLE IF NOT EXISTS echoes (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    pkt_payload     TEXT NOT NULL,           -- matches channel_messages.pkt_payload
    path            TEXT,                   -- relay path string
    snr             REAL,
    received_at     TEXT NOT NULL DEFAULT (datetime('now')),
    direction       TEXT DEFAULT 'incoming', -- 'sent' or 'incoming'
    cm_id           INTEGER,                -- FK to channel_messages (nullable)
    hash_size       INTEGER NOT NULL DEFAULT 1,  -- bytes per hop hash: 1, 2, or 3
    FOREIGN KEY (cm_id) REFERENCES channel_messages(id) ON DELETE SET NULL
);

-- Path tracking (replaces .path.jsonl)
CREATE TABLE IF NOT EXISTS paths (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    contact_pubkey  TEXT,
    pkt_payload     TEXT,
    path            TEXT,
    snr             REAL,
    path_len        INTEGER,
    received_at     TEXT NOT NULL DEFAULT (datetime('now'))
);

-- User-configured paths for DM retry rotation
CREATE TABLE IF NOT EXISTS contact_paths (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    contact_pubkey  TEXT NOT NULL REFERENCES contacts(public_key) ON DELETE CASCADE,
    path_hex        TEXT NOT NULL DEFAULT '',   -- raw hex path bytes (e.g. "5e34e761")
    hash_size       INTEGER NOT NULL DEFAULT 1, -- bytes per hop: 1, 2, or 3
    label           TEXT NOT NULL DEFAULT '',    -- friendly label (e.g. "via Zalesie")
    is_primary      INTEGER NOT NULL DEFAULT 0, -- 1 = priority/default path
    sort_order      INTEGER NOT NULL DEFAULT 0, -- lower = tried first during rotation
    created_at      TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Advertisements (replaces .adverts.jsonl)
CREATE TABLE IF NOT EXISTS advertisements (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    public_key      TEXT NOT NULL,
    name            TEXT NOT NULL DEFAULT '',
    type            INTEGER DEFAULT 0,
    lat             REAL,
    lon             REAL,
    timestamp       INTEGER NOT NULL DEFAULT 0,
    snr             REAL,
    raw_payload     TEXT,
    created_at      TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Read status tracking (replaces .read_status.json)
CREATE TABLE IF NOT EXISTS read_status (
    key             TEXT PRIMARY KEY,       -- 'chan_0', 'dm_<pubkey>', etc.
    last_seen_ts    INTEGER DEFAULT 0,      -- unix timestamp
    is_muted        INTEGER DEFAULT 0,      -- 1 = muted (channels only)
    is_favorite     INTEGER DEFAULT 0,      -- 1 = favorite (channels only)
    updated_at      TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Ignored contacts (adverts cached but not pending/auto-added)
CREATE TABLE IF NOT EXISTS ignored_contacts (
    public_key  TEXT PRIMARY KEY REFERENCES contacts(public_key) ON DELETE CASCADE,
    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Blocked contacts (ignored + messages hidden from display)
CREATE TABLE IF NOT EXISTS blocked_contacts (
    public_key  TEXT PRIMARY KEY REFERENCES contacts(public_key) ON DELETE CASCADE,
    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Blocked names (for bots/contacts without known public_key)
CREATE TABLE IF NOT EXISTS blocked_names (
    name        TEXT PRIMARY KEY,
    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- Application settings (key-value store, replaces .webui_settings.json)
CREATE TABLE IF NOT EXISTS app_settings (
    key         TEXT PRIMARY KEY,
    value       TEXT NOT NULL DEFAULT '',       -- JSON-encoded value
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- ============================================================
-- Indexes
-- ============================================================

CREATE INDEX IF NOT EXISTS idx_cm_channel_ts ON channel_messages(channel_idx, timestamp);
CREATE INDEX IF NOT EXISTS idx_cm_pkt ON channel_messages(pkt_payload);
CREATE INDEX IF NOT EXISTS idx_dm_contact ON direct_messages(contact_pubkey, timestamp);
CREATE INDEX IF NOT EXISTS idx_dm_ack ON direct_messages(expected_ack);
CREATE INDEX IF NOT EXISTS idx_acks_code ON acks(expected_ack);
CREATE INDEX IF NOT EXISTS idx_echoes_pkt ON echoes(pkt_payload);
CREATE INDEX IF NOT EXISTS idx_adv_pubkey ON advertisements(public_key, timestamp);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE INDEX IF NOT EXISTS idx_cp_contact ON contact_paths(contact_pubkey, sort_order);
CREATE INDEX IF NOT EXISTS idx_regions_default ON regions(is_default) WHERE is_default = 1;

-- ============================================================
-- Full-Text Search (FTS5)
-- ============================================================

CREATE VIRTUAL TABLE IF NOT EXISTS channel_messages_fts USING fts5(
    content,
    content=channel_messages,
    content_rowid=id
);

CREATE VIRTUAL TABLE IF NOT EXISTS direct_messages_fts USING fts5(
    content,
    content=direct_messages,
    content_rowid=id
);

-- FTS triggers: keep FTS index in sync with source tables

CREATE TRIGGER IF NOT EXISTS cm_fts_insert AFTER INSERT ON channel_messages BEGIN
    INSERT INTO channel_messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS cm_fts_delete AFTER DELETE ON channel_messages BEGIN
    INSERT INTO channel_messages_fts(channel_messages_fts, rowid, content)
        VALUES ('delete', old.id, old.content);
END;

CREATE TRIGGER IF NOT EXISTS cm_fts_update AFTER UPDATE OF content ON channel_messages BEGIN
    INSERT INTO channel_messages_fts(channel_messages_fts, rowid, content)
        VALUES ('delete', old.id, old.content);
    INSERT INTO channel_messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS dm_fts_insert AFTER INSERT ON direct_messages BEGIN
    INSERT INTO direct_messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER IF NOT EXISTS dm_fts_delete AFTER DELETE ON direct_messages BEGIN
    INSERT INTO direct_messages_fts(direct_messages_fts, rowid, content)
        VALUES ('delete', old.id, old.content);
END;

CREATE TRIGGER IF NOT EXISTS dm_fts_update AFTER UPDATE OF content ON direct_messages BEGIN
    INSERT INTO direct_messages_fts(direct_messages_fts, rowid, content)
        VALUES ('delete', old.id, old.content);
    INSERT INTO direct_messages_fts(rowid, content) VALUES (new.id, new.content);
END;
