-- Initial schema.
--
-- Design notes:
--   * Every searchable entity gets a real relational table. Only genuinely
--     list-shaped configuration (upstream resolvers, ACL networks) lives as a
--     JSON value inside the settings key/value table.
--   * Timestamps are stored as unix epoch seconds (INTEGER) so that they sort
--     and range-scan cheaply and carry no timezone ambiguity.
--   * Booleans are INTEGER 0/1.

CREATE TABLE settings (
    key        TEXT PRIMARY KEY,
    value      TEXT NOT NULL,
    updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);

CREATE TABLE admin_user (
    id                   INTEGER PRIMARY KEY CHECK (id = 1),
    username             TEXT    NOT NULL,
    password_hash        TEXT    NOT NULL,
    must_change_password INTEGER NOT NULL DEFAULT 0,
    created_at           INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at           INTEGER NOT NULL DEFAULT (unixepoch()),
    last_login_at        INTEGER
);

CREATE TABLE api_tokens (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    name         TEXT    NOT NULL UNIQUE,
    description  TEXT    NOT NULL DEFAULT '',
    token_prefix TEXT    NOT NULL,
    token_hash   TEXT    NOT NULL,
    enabled      INTEGER NOT NULL DEFAULT 1,
    created_at   INTEGER NOT NULL DEFAULT (unixepoch()),
    last_used_at INTEGER
);
CREATE INDEX idx_api_tokens_prefix ON api_tokens (token_prefix);

CREATE TABLE zones (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL UNIQUE,          -- normalised FQDN with trailing dot
    kind        TEXT    NOT NULL DEFAULT 'forward'
                CHECK (kind IN ('forward', 'reverse4', 'reverse6')),
    description TEXT    NOT NULL DEFAULT '',
    enabled     INTEGER NOT NULL DEFAULT 1,
    default_ttl INTEGER NOT NULL DEFAULT 3600,
    primary_ns  TEXT    NOT NULL,
    admin_email TEXT    NOT NULL,
    serial      INTEGER NOT NULL DEFAULT 1,
    refresh     INTEGER NOT NULL DEFAULT 7200,
    retry       INTEGER NOT NULL DEFAULT 3600,
    expire      INTEGER NOT NULL DEFAULT 1209600,
    minimum     INTEGER NOT NULL DEFAULT 3600,
    auto_serial INTEGER NOT NULL DEFAULT 1,
    created_at  INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at  INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_zones_enabled ON zones (enabled);
CREATE INDEX idx_zones_kind ON zones (kind);

CREATE TABLE records (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    zone_id    INTEGER NOT NULL REFERENCES zones (id) ON DELETE CASCADE,
    name       TEXT    NOT NULL,   -- relative to the apex; '@' is the apex itself
    type       TEXT    NOT NULL,
    data       TEXT    NOT NULL,   -- rdata in zone-file presentation format
    ttl        INTEGER,            -- NULL inherits zones.default_ttl
    enabled    INTEGER NOT NULL DEFAULT 1,
    comment    TEXT    NOT NULL DEFAULT '',
    created_at INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_records_zone ON records (zone_id);
CREATE INDEX idx_records_zone_name ON records (zone_id, name);
CREATE INDEX idx_records_zone_name_type ON records (zone_id, name, type);
CREATE INDEX idx_records_type ON records (type);

CREATE TABLE networks (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL UNIQUE,
    cidr        TEXT    NOT NULL,
    description TEXT    NOT NULL DEFAULT '',
    enabled     INTEGER NOT NULL DEFAULT 1,
    created_at  INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at  INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_networks_enabled ON networks (enabled);

CREATE TABLE policies (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    name          TEXT    NOT NULL UNIQUE,
    description   TEXT    NOT NULL DEFAULT '',
    enabled       INTEGER NOT NULL DEFAULT 1,
    block_action  TEXT    NOT NULL DEFAULT 'nxdomain'
                  CHECK (block_action IN ('nxdomain', 'refused', 'sinkhole')),
    sinkhole_ipv4 TEXT    NOT NULL DEFAULT '0.0.0.0',
    sinkhole_ipv6 TEXT    NOT NULL DEFAULT '::',
    block_ttl     INTEGER NOT NULL DEFAULT 60,
    created_at    INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at    INTEGER NOT NULL DEFAULT (unixepoch())
);

CREATE TABLE domain_lists (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    kind        TEXT    NOT NULL CHECK (kind IN ('blacklist', 'allowlist')),
    name        TEXT    NOT NULL,
    description TEXT    NOT NULL DEFAULT '',
    enabled     INTEGER NOT NULL DEFAULT 1,
    source_url  TEXT    NOT NULL DEFAULT '',
    created_at  INTEGER NOT NULL DEFAULT (unixepoch()),
    updated_at  INTEGER NOT NULL DEFAULT (unixepoch()),
    UNIQUE (kind, name)
);

CREATE TABLE domain_entries (
    id               INTEGER PRIMARY KEY AUTOINCREMENT,
    list_id          INTEGER NOT NULL REFERENCES domain_lists (id) ON DELETE CASCADE,
    domain           TEXT    NOT NULL,   -- normalised: lowercase, no trailing dot
    match_subdomains INTEGER NOT NULL DEFAULT 1,
    enabled          INTEGER NOT NULL DEFAULT 1,
    comment          TEXT    NOT NULL DEFAULT '',
    created_at       INTEGER NOT NULL DEFAULT (unixepoch()),
    UNIQUE (list_id, domain)
);
CREATE INDEX idx_domain_entries_list ON domain_entries (list_id);
CREATE INDEX idx_domain_entries_domain ON domain_entries (domain);

-- Policy assignments.
CREATE TABLE network_policies (
    network_id INTEGER NOT NULL REFERENCES networks (id) ON DELETE CASCADE,
    policy_id  INTEGER NOT NULL REFERENCES policies (id) ON DELETE CASCADE,
    PRIMARY KEY (network_id, policy_id)
);
CREATE INDEX idx_network_policies_policy ON network_policies (policy_id);

CREATE TABLE policy_lists (
    policy_id INTEGER NOT NULL REFERENCES policies (id) ON DELETE CASCADE,
    list_id   INTEGER NOT NULL REFERENCES domain_lists (id) ON DELETE CASCADE,
    PRIMARY KEY (policy_id, list_id)
);
CREATE INDEX idx_policy_lists_list ON policy_lists (list_id);

CREATE TABLE query_logs (
    id             INTEGER PRIMARY KEY AUTOINCREMENT,
    ts             INTEGER NOT NULL,          -- unix milliseconds
    client_ip      TEXT    NOT NULL,
    network_id     INTEGER,
    network_name   TEXT    NOT NULL DEFAULT '',
    qname          TEXT    NOT NULL,
    qtype          TEXT    NOT NULL,
    rcode          TEXT    NOT NULL,
    source         TEXT    NOT NULL,
    cache_hit      INTEGER NOT NULL DEFAULT 0,
    blocked        INTEGER NOT NULL DEFAULT 0,
    policy_id      INTEGER,
    policy_name    TEXT    NOT NULL DEFAULT '',
    blacklist_id   INTEGER,
    blacklist_name TEXT    NOT NULL DEFAULT '',
    matched_rule   TEXT    NOT NULL DEFAULT '',
    protocol       TEXT    NOT NULL DEFAULT 'udp',
    duration_us    INTEGER NOT NULL DEFAULT 0,
    answer_count   INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_query_logs_ts ON query_logs (ts);
CREATE INDEX idx_query_logs_client ON query_logs (client_ip, ts);
CREATE INDEX idx_query_logs_qname ON query_logs (qname, ts);
CREATE INDEX idx_query_logs_blocked ON query_logs (blocked, ts);

CREATE TABLE audit_logs (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    ts          INTEGER NOT NULL,             -- unix milliseconds
    actor       TEXT    NOT NULL DEFAULT '',
    source      TEXT    NOT NULL DEFAULT 'web',
    client_ip   TEXT    NOT NULL DEFAULT '',
    action      TEXT    NOT NULL,
    object_type TEXT    NOT NULL DEFAULT '',
    object_id   TEXT    NOT NULL DEFAULT '',
    object_name TEXT    NOT NULL DEFAULT '',
    details     TEXT    NOT NULL DEFAULT ''
);
CREATE INDEX idx_audit_logs_ts ON audit_logs (ts);
CREATE INDEX idx_audit_logs_object ON audit_logs (object_type, ts);
