"""Application database schema definition and initialisation.

BanGUI maintains its own SQLite database that stores configuration, session
state, blocklist source definitions, and import run logs.  This module is
the single source of truth for the schema — all ``CREATE TABLE`` statements
live here and are applied on first run via :func:`init_db`.

The fail2ban database is separate and is accessed read-only by the history
and ban services.
"""

import aiosqlite
import structlog

log: structlog.stdlib.BoundLogger = structlog.get_logger()

# ---------------------------------------------------------------------------
# DDL statements
# ---------------------------------------------------------------------------

_CREATE_SETTINGS: str = """
CREATE TABLE IF NOT EXISTS settings (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    key         TEXT    NOT NULL UNIQUE,
    value       TEXT    NOT NULL,
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    updated_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_SESSIONS: str = """
CREATE TABLE IF NOT EXISTS sessions (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    token       TEXT    NOT NULL UNIQUE,
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    expires_at  TEXT    NOT NULL
);
"""

_CREATE_SESSIONS_TOKEN_INDEX: str = """
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);
"""

_CREATE_BLOCKLIST_SOURCES: str = """
CREATE TABLE IF NOT EXISTS blocklist_sources (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL,
    url         TEXT    NOT NULL UNIQUE,
    enabled     INTEGER NOT NULL DEFAULT 1,
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    updated_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_IMPORT_LOG: str = """
CREATE TABLE IF NOT EXISTS import_log (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    source_id       INTEGER REFERENCES blocklist_sources(id) ON DELETE SET NULL,
    source_url      TEXT    NOT NULL,
    timestamp       TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    ips_imported    INTEGER NOT NULL DEFAULT 0,
    ips_skipped     INTEGER NOT NULL DEFAULT 0,
    errors          TEXT
);
"""

_CREATE_GEO_CACHE: str = """
CREATE TABLE IF NOT EXISTS geo_cache (
    ip              TEXT    PRIMARY KEY,
    country_code    TEXT,
    country_name    TEXT,
    asn             TEXT,
    org             TEXT,
    cached_at       TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_HISTORY_ARCHIVE: str = """
CREATE TABLE IF NOT EXISTS history_archive (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    jail        TEXT    NOT NULL,
    ip          TEXT    NOT NULL,
    timeofban   INTEGER NOT NULL,
    bancount    INTEGER NOT NULL,
    data        TEXT    NOT NULL,
    action      TEXT    NOT NULL CHECK(action IN ('ban', 'unban')),
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    UNIQUE(ip, jail, action, timeofban)
);
"""

# Ordered list of DDL statements to execute on initialisation.
_SCHEMA_STATEMENTS: list[str] = [
    _CREATE_SETTINGS,
    _CREATE_SESSIONS,
    _CREATE_SESSIONS_TOKEN_INDEX,
    _CREATE_BLOCKLIST_SOURCES,
    _CREATE_IMPORT_LOG,
    _CREATE_GEO_CACHE,
    _CREATE_HISTORY_ARCHIVE,
]


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


async def init_db(db: aiosqlite.Connection) -> None:
    """Create all BanGUI application tables if they do not already exist.

    This function is idempotent — calling it on an already-initialised
    database has no effect.  It should be called once during application
    startup inside the FastAPI lifespan handler.

    Args:
        db: An open :class:`aiosqlite.Connection` to the application database.
    """
    log.info("initialising_database_schema")
    async with db.execute("PRAGMA journal_mode=WAL;"):
        pass
    async with db.execute("PRAGMA foreign_keys=ON;"):
        pass
    for statement in _SCHEMA_STATEMENTS:
        await db.executescript(statement)
    await db.commit()
    log.info("database_schema_ready")
