from __future__ import annotations

from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
from pathlib import Path
import secrets
import sqlite3
from threading import Lock
from typing import Any
from uuid import uuid4

from cryptography.fernet import Fernet, InvalidToken

ROOT = Path(__file__).resolve().parent.parent
_DB_LOCK = Lock()
_FERNET_LOCK = Lock()
_FERNET: Fernet | None = None
_FERNET_ERROR: str | None = None


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _db_path() -> Path:
    raw = str(os.getenv("AGENT_APP_DB_PATH", "")).strip() or "data/app.sqlite"
    path = Path(raw)
    if not path.is_absolute():
        path = ROOT / path
    path.parent.mkdir(parents=True, exist_ok=True)
    return path


@contextmanager
def _connect():
    conn = sqlite3.connect(str(_db_path()), check_same_thread=False, timeout=30)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    try:
        yield conn
        conn.commit()
    finally:
        conn.close()


def _safe_user_row(row: sqlite3.Row | None) -> dict[str, Any] | None:
    if row is None:
        return None
    return {
        "user_id": str(row["user_id"]),
        "username": str(row["username"]),
        "role": str(row["role"] or "analyst"),
        "langfuse_user_id": str(row["langfuse_user_id"]) if "langfuse_user_id" in row.keys() else "",
        "is_active": bool(int(row["is_active"])),
        "created_at": str(row["created_at"]),
        "updated_at": str(row["updated_at"]),
    }


def _password_iterations() -> int:
    raw = str(os.getenv("AGENT_PASSWORD_HASH_ITERATIONS", "")).strip()
    try:
        value = int(raw)
    except Exception:
        value = 240_000
    return max(120_000, min(value, 1_200_000))


def _reset_fernet_cache() -> None:
    global _FERNET, _FERNET_ERROR
    with _FERNET_LOCK:
        _FERNET = None
        _FERNET_ERROR = None


def _build_fernet() -> tuple[Fernet | None, str | None]:
    raw = str(os.getenv("AGENT_DATA_ENCRYPTION_KEY", "")).strip()
    if not raw:
        return None, "AGENT_DATA_ENCRYPTION_KEY is required for encrypted Langfuse user settings"
    try:
        fernet = Fernet(raw.encode("utf-8"))
    except Exception as exc:
        return None, f"invalid AGENT_DATA_ENCRYPTION_KEY: {exc}"
    return fernet, None


def _require_fernet() -> Fernet:
    global _FERNET, _FERNET_ERROR
    if _FERNET is not None:
        return _FERNET
    with _FERNET_LOCK:
        if _FERNET is not None:
            return _FERNET
        _FERNET, _FERNET_ERROR = _build_fernet()
        if _FERNET is None:
            raise RuntimeError(_FERNET_ERROR or "failed to initialize fernet")
        return _FERNET


def _encrypt_value(value: str) -> str:
    text = str(value or "")
    if not text:
        return ""
    token = _require_fernet().encrypt(text.encode("utf-8")).decode("utf-8")
    return f"fernet:{token}"


def _decrypt_value(value: str) -> str:
    text = str(value or "")
    if not text:
        return ""
    if not text.startswith("fernet:"):
        return text
    token = text.split(":", 1)[1]
    try:
        plain = _require_fernet().decrypt(token.encode("utf-8")).decode("utf-8")
    except InvalidToken as exc:
        raise RuntimeError("failed to decrypt user langfuse settings (invalid token)") from exc
    except Exception as exc:
        raise RuntimeError(f"failed to decrypt user langfuse settings: {exc}") from exc
    return plain


def hash_password(password: str) -> str:
    plain = str(password or "")
    if len(plain) < 8:
        raise ValueError("password must be at least 8 characters")
    iterations = _password_iterations()
    salt = secrets.token_bytes(16)
    digest = hashlib.pbkdf2_hmac("sha256", plain.encode("utf-8"), salt, iterations)
    return f"pbkdf2_sha256${iterations}${salt.hex()}${digest.hex()}"


def verify_password(password: str, encoded_hash: str) -> bool:
    try:
        algo, raw_iterations, raw_salt, raw_digest = str(encoded_hash).split("$", 3)
        if algo != "pbkdf2_sha256":
            return False
        iterations = int(raw_iterations)
        salt = bytes.fromhex(raw_salt)
        expected = bytes.fromhex(raw_digest)
    except Exception:
        return False
    actual = hashlib.pbkdf2_hmac("sha256", str(password or "").encode("utf-8"), salt, iterations)
    return secrets.compare_digest(actual, expected)


def init_user_store() -> None:
    with _DB_LOCK:
        with _connect() as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS users (
                    user_id TEXT PRIMARY KEY,
                    username TEXT NOT NULL UNIQUE,
                    password_hash TEXT NOT NULL,
                    role TEXT NOT NULL DEFAULT 'analyst',
                    langfuse_user_id TEXT NOT NULL DEFAULT '',
                    is_active INTEGER NOT NULL DEFAULT 1,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                )
                """
            )
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS user_sessions (
                    session_id TEXT PRIMARY KEY,
                    user_id TEXT NOT NULL,
                    token_hash TEXT NOT NULL UNIQUE,
                    expires_at TEXT NOT NULL,
                    revoked INTEGER NOT NULL DEFAULT 0,
                    created_at TEXT NOT NULL,
                    last_used_at TEXT,
                    FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE
                )
                """
            )
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS user_langfuse_settings (
                    user_id TEXT PRIMARY KEY,
                    base_url TEXT NOT NULL DEFAULT '',
                    public_key TEXT NOT NULL DEFAULT '',
                    secret_key TEXT NOT NULL DEFAULT '',
                    environment TEXT NOT NULL DEFAULT '',
                    updated_at TEXT NOT NULL,
                    FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE
                )
                """
            )
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS user_langfuse_projects (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id TEXT NOT NULL,
                    name TEXT NOT NULL DEFAULT '',
                    base_url TEXT NOT NULL DEFAULT '',
                    public_key TEXT NOT NULL DEFAULT '',
                    secret_key TEXT NOT NULL DEFAULT '',
                    environment TEXT NOT NULL DEFAULT '',
                    is_default INTEGER NOT NULL DEFAULT 0,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE
                )
                """
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_ulp_user_id ON user_langfuse_projects(user_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at)"
            )

            # --- Chat messages ---
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS chat_messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    thread_id TEXT NOT NULL,
                    user_id TEXT DEFAULT '',
                    role TEXT NOT NULL DEFAULT 'user',
                    content TEXT NOT NULL,
                    model TEXT DEFAULT '',
                    project_id INTEGER,
                    metrics TEXT DEFAULT '',
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                )
                """
            )
            conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_thread ON chat_messages(thread_id)")
            conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_user ON chat_messages(user_id)")
            conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_created ON chat_messages(created_at)")

            # --- Audit logs ---
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS audit_logs (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    ts TEXT NOT NULL DEFAULT (datetime('now')),
                    actor TEXT DEFAULT '',
                    action TEXT NOT NULL,
                    detail TEXT DEFAULT '',
                    ip TEXT DEFAULT '',
                    extra TEXT DEFAULT ''
                )
                """
            )
            conn.execute("CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit_logs(ts)")
            conn.execute("CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_logs(actor)")

            # --- User reports ---
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS user_reports (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    owner_id TEXT NOT NULL,
                    project_id INTEGER,
                    target_user_id TEXT NOT NULL,
                    period_type TEXT NOT NULL,
                    period_start TEXT NOT NULL,
                    period_end TEXT NOT NULL,
                    summary TEXT NOT NULL DEFAULT '{}',
                    sessions TEXT NOT NULL DEFAULT '[]',
                    model_usage TEXT NOT NULL DEFAULT '{}',
                    timeline TEXT NOT NULL DEFAULT '[]',
                    trace_count INTEGER DEFAULT 0,
                    total_cost REAL DEFAULT 0.0,
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                )
                """
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_report_owner ON user_reports(owner_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_report_target ON user_reports(target_user_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_report_period ON user_reports(period_type, period_start)"
            )
            conn.execute(
                """
                CREATE UNIQUE INDEX IF NOT EXISTS idx_report_unique
                    ON user_reports(owner_id, project_id, target_user_id, period_type, period_start)
                """
            )

            # Migration: add llm_analysis column
            try:
                conn.execute("ALTER TABLE user_reports ADD COLUMN llm_analysis TEXT NOT NULL DEFAULT ''")
            except sqlite3.OperationalError:
                pass  # already exists

            # Migration: add langfuse_user_id column to users
            try:
                conn.execute("ALTER TABLE users ADD COLUMN langfuse_user_id TEXT NOT NULL DEFAULT ''")
            except sqlite3.OperationalError:
                pass  # already exists

            # --- Experiments ---
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS experiments (
                    id           INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id      TEXT NOT NULL,
                    project_id   TEXT,
                    name         TEXT NOT NULL,
                    description  TEXT DEFAULT '',
                    experiment_type TEXT NOT NULL,
                    config_json  TEXT NOT NULL DEFAULT '{}',
                    status       TEXT NOT NULL DEFAULT 'created',
                    result_json  TEXT DEFAULT '{}',
                    created_at   TEXT DEFAULT (datetime('now')),
                    updated_at   TEXT DEFAULT (datetime('now'))
                )
                """
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_exp_user ON experiments(user_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_exp_status ON experiments(status)"
            )

            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS experiment_runs (
                    id             INTEGER PRIMARY KEY AUTOINCREMENT,
                    experiment_id  INTEGER NOT NULL REFERENCES experiments(id),
                    variant_label  TEXT NOT NULL,
                    model_spec     TEXT NOT NULL,
                    prompt_text    TEXT,
                    trace_id       TEXT,
                    input_text     TEXT NOT NULL,
                    output_text    TEXT,
                    scores_json    TEXT DEFAULT '{}',
                    latency_ms     REAL,
                    token_usage    TEXT DEFAULT '{}',
                    status         TEXT NOT NULL DEFAULT 'pending',
                    created_at     TEXT DEFAULT (datetime('now'))
                )
                """
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_exprun_exp ON experiment_runs(experiment_id)"
            )
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_exprun_exp_variant ON experiment_runs(experiment_id, variant_label)"
            )

            # --- Migration: copy user_langfuse_settings → user_langfuse_projects ---
            try:
                rows = conn.execute("SELECT * FROM user_langfuse_settings").fetchall()
                for row in rows:
                    uid = str(row["user_id"])
                    exists = conn.execute(
                        "SELECT 1 FROM user_langfuse_projects WHERE user_id = ?", (uid,)
                    ).fetchone()
                    if not exists:
                        now = _now_iso()
                        conn.execute(
                            """
                            INSERT INTO user_langfuse_projects
                                (user_id, name, base_url, public_key, secret_key, environment, is_default, created_at, updated_at)
                            VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)
                            """,
                            (
                                uid,
                                "Default",
                                str(row["base_url"] or ""),
                                str(row["public_key"] or ""),
                                str(row["secret_key"] or ""),
                                str(row["environment"] or ""),
                                now,
                                now,
                            ),
                        )
            except Exception:
                pass


def count_users() -> int:
    with _connect() as conn:
        row = conn.execute("SELECT COUNT(1) AS cnt FROM users").fetchone()
    return int(row["cnt"]) if row is not None else 0


def list_active_users() -> list[dict[str, Any]]:
    """Return all active users ordered by creation date."""
    with _connect() as conn:
        rows = conn.execute(
            "SELECT * FROM users WHERE is_active = 1 ORDER BY created_at"
        ).fetchall()
    return [r for r in (_safe_user_row(row) for row in rows) if r]


def create_user(*, username: str, password: str, role: str = "analyst", langfuse_user_id: str = "") -> dict[str, Any]:
    normalized_username = str(username or "").strip().lower()
    if len(normalized_username) < 3:
        raise ValueError("username must be at least 3 characters")
    allowed_role = str(role or "analyst").strip().lower() or "analyst"
    if allowed_role not in {"viewer", "analyst", "editor", "owner", "ops", "super_admin"}:
        allowed_role = "analyst"
    safe_langfuse_user_id = str(langfuse_user_id or "").strip()[:200]
    password_hash = hash_password(password)
    user_id = f"user_{uuid4().hex[:12]}"
    now = _now_iso()

    with _DB_LOCK:
        try:
            with _connect() as conn:
                conn.execute(
                    """
                    INSERT INTO users(user_id, username, password_hash, role, langfuse_user_id, is_active, created_at, updated_at)
                    VALUES(?, ?, ?, ?, ?, 1, ?, ?)
                    """,
                    (user_id, normalized_username, password_hash, allowed_role, safe_langfuse_user_id, now, now),
                )
        except sqlite3.IntegrityError as exc:
            raise ValueError("username already exists") from exc

    return {
        "user_id": user_id,
        "username": normalized_username,
        "role": allowed_role,
        "langfuse_user_id": safe_langfuse_user_id,
        "is_active": True,
        "created_at": now,
        "updated_at": now,
    }


def get_user(user_id: str) -> dict[str, Any] | None:
    """Retrieve a user by user_id."""
    uid = str(user_id or "").strip()
    if not uid:
        return None
    with _connect() as conn:
        row = conn.execute("SELECT * FROM users WHERE user_id = ?", (uid,)).fetchone()
    return _safe_user_row(row)


def authenticate_user(*, username: str, password: str) -> dict[str, Any] | None:
    normalized_username = str(username or "").strip().lower()
    if not normalized_username:
        return None
    with _connect() as conn:
        row = conn.execute(
            """
            SELECT user_id, username, password_hash, role, langfuse_user_id, is_active, created_at, updated_at
            FROM users WHERE username = ?
            """,
            (normalized_username,),
        ).fetchone()
    if row is None:
        return None
    if int(row["is_active"]) != 1:
        return None
    if not verify_password(password, str(row["password_hash"])):
        return None
    return _safe_user_row(row)


def issue_session(*, user_id: str, expires_hours: int | None = None) -> dict[str, Any]:
    uid = str(user_id or "").strip()
    if not uid:
        raise ValueError("user_id is required")
    raw_hours = expires_hours
    if raw_hours is None:
        env_hours = str(os.getenv("AGENT_AUTH_SESSION_EXPIRES_HOURS", "")).strip()
        try:
            raw_hours = int(env_hours)
        except Exception:
            raw_hours = 24
    lifetime = max(1, min(int(raw_hours), 24 * 30))
    token = f"lsu_{secrets.token_urlsafe(24)}"
    token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
    session_id = f"sess_{uuid4().hex[:12]}"
    created_at = _now_iso()
    expires_at = (datetime.now(timezone.utc) + timedelta(hours=lifetime)).isoformat().replace("+00:00", "Z")

    with _DB_LOCK:
        with _connect() as conn:
            conn.execute(
                """
                INSERT INTO user_sessions(session_id, user_id, token_hash, expires_at, revoked, created_at, last_used_at)
                VALUES (?, ?, ?, ?, 0, ?, NULL)
                """,
                (session_id, uid, token_hash, expires_at, created_at),
            )
    return {
        "session_id": session_id,
        "token": token,
        "expires_at": expires_at,
        "created_at": created_at,
    }


def _is_expired(expires_at: str) -> bool:
    try:
        dt = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
    except Exception:
        return True
    return datetime.now(timezone.utc) > dt.astimezone(timezone.utc)


def validate_session_token(raw_token: str) -> dict[str, Any] | None:
    token = str(raw_token or "").strip()
    if not token:
        return None
    token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
    with _DB_LOCK:
        with _connect() as conn:
            row = conn.execute(
                """
                SELECT
                    s.session_id, s.user_id, s.expires_at, s.revoked, s.created_at, s.last_used_at,
                    u.username, u.role, u.is_active
                FROM user_sessions s
                JOIN users u ON u.user_id = s.user_id
                WHERE s.token_hash = ?
                """,
                (token_hash,),
            ).fetchone()
            if row is None:
                return None
            if int(row["revoked"]) == 1 or int(row["is_active"]) != 1:
                return None
            if _is_expired(str(row["expires_at"])):
                conn.execute(
                    "UPDATE user_sessions SET revoked = 1 WHERE session_id = ?",
                    (str(row["session_id"]),),
                )
                return None
            conn.execute(
                "UPDATE user_sessions SET last_used_at = ? WHERE session_id = ?",
                (_now_iso(), str(row["session_id"])),
            )
    return {
        "session_id": str(row["session_id"]),
        "user_id": str(row["user_id"]),
        "username": str(row["username"]),
        "role": str(row["role"] or "analyst"),
        "expires_at": str(row["expires_at"]),
        "created_at": str(row["created_at"]),
    }


def revoke_session_token(raw_token: str) -> bool:
    token = str(raw_token or "").strip()
    if not token:
        return False
    token_hash = hashlib.sha256(token.encode("utf-8")).hexdigest()
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                "UPDATE user_sessions SET revoked = 1 WHERE token_hash = ?",
                (token_hash,),
            )
    return int(cur.rowcount or 0) > 0


def get_user_by_id(user_id: str) -> dict[str, Any] | None:
    uid = str(user_id or "").strip()
    if not uid:
        return None
    with _connect() as conn:
        row = conn.execute(
            """
            SELECT user_id, username, role, is_active, created_at, updated_at
            FROM users WHERE user_id = ?
            """,
            (uid,),
        ).fetchone()
    return _safe_user_row(row)


def upsert_user_langfuse_settings(
    *,
    user_id: str,
    base_url: str,
    public_key: str,
    secret_key: str,
    environment: str,
) -> dict[str, Any]:
    uid = str(user_id or "").strip()
    if not uid:
        raise ValueError("user_id is required")
    encrypted_public_key = _encrypt_value(public_key)
    encrypted_secret_key = _encrypt_value(secret_key)
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            conn.execute(
                """
                INSERT INTO user_langfuse_settings(user_id, base_url, public_key, secret_key, environment, updated_at)
                VALUES (?, ?, ?, ?, ?, ?)
                ON CONFLICT(user_id) DO UPDATE SET
                    base_url = excluded.base_url,
                    public_key = excluded.public_key,
                    secret_key = excluded.secret_key,
                    environment = excluded.environment,
                    updated_at = excluded.updated_at
                """,
                (
                    uid,
                    str(base_url or "").strip(),
                    encrypted_public_key,
                    encrypted_secret_key,
                    str(environment or "").strip(),
                    now,
                ),
            )
    return {"user_id": uid, "updated_at": now}


def get_user_langfuse_settings(user_id: str) -> dict[str, Any] | None:
    uid = str(user_id or "").strip()
    if not uid:
        return None
    with _connect() as conn:
        row = conn.execute(
            """
            SELECT user_id, base_url, public_key, secret_key, environment, updated_at
            FROM user_langfuse_settings WHERE user_id = ?
            """,
            (uid,),
        ).fetchone()
    if row is None:
        return None
    return {
        "user_id": str(row["user_id"]),
        "base_url": str(row["base_url"] or ""),
        "public_key": _decrypt_value(str(row["public_key"] or "")),
        "secret_key": _decrypt_value(str(row["secret_key"] or "")),
        "environment": str(row["environment"] or ""),
        "updated_at": str(row["updated_at"] or ""),
    }


def get_langfuse_settings_encryption_status() -> dict[str, Any]:
    with _connect() as conn:
        row = conn.execute(
            """
            SELECT
                COUNT(1) AS total,
                SUM(
                    CASE
                        WHEN (public_key != '' AND public_key NOT LIKE 'fernet:%')
                          OR (secret_key != '' AND secret_key NOT LIKE 'fernet:%')
                        THEN 1 ELSE 0
                    END
                ) AS plaintext_rows
            FROM user_langfuse_settings
            """
        ).fetchone()
    total = int(row["total"]) if row and row["total"] is not None else 0
    plaintext_rows = int(row["plaintext_rows"]) if row and row["plaintext_rows"] is not None else 0
    return {
        "total_rows": total,
        "plaintext_rows": plaintext_rows,
        "encrypted_rows": max(0, total - plaintext_rows),
        "fully_encrypted": plaintext_rows == 0,
    }


def migrate_user_langfuse_settings_encryption(*, dry_run: bool = False) -> dict[str, Any]:
    _require_fernet()
    with _DB_LOCK:
        with _connect() as conn:
            rows = conn.execute(
                """
                SELECT user_id, public_key, secret_key
                FROM user_langfuse_settings
                WHERE
                    (public_key != '' AND public_key NOT LIKE 'fernet:%')
                    OR
                    (secret_key != '' AND secret_key NOT LIKE 'fernet:%')
                """
            ).fetchall()
            affected = 0
            for row in rows:
                user_id = str(row["user_id"])
                current_public = str(row["public_key"] or "")
                current_secret = str(row["secret_key"] or "")
                next_public = current_public if current_public.startswith("fernet:") else _encrypt_value(current_public)
                next_secret = current_secret if current_secret.startswith("fernet:") else _encrypt_value(current_secret)
                if dry_run:
                    affected += 1
                    continue
                conn.execute(
                    """
                    UPDATE user_langfuse_settings
                    SET public_key = ?, secret_key = ?, updated_at = ?
                    WHERE user_id = ?
                    """,
                    (next_public, next_secret, _now_iso(), user_id),
                )
                affected += 1

    status = get_langfuse_settings_encryption_status()
    return {
        "dry_run": bool(dry_run),
        "affected_rows": affected,
        "status": status,
    }


# ---------------------------------------------------------------------------
# Multi-project Langfuse CRUD
# ---------------------------------------------------------------------------

def _safe_project_row(row: sqlite3.Row) -> dict[str, Any]:
    return {
        "id": int(row["id"]),
        "user_id": str(row["user_id"]),
        "name": str(row["name"] or ""),
        "base_url": str(row["base_url"] or ""),
        "public_key": _decrypt_value(str(row["public_key"] or "")),
        "secret_key": _decrypt_value(str(row["secret_key"] or "")),
        "environment": str(row["environment"] or ""),
        "is_default": bool(int(row["is_default"])),
        "created_at": str(row["created_at"]),
        "updated_at": str(row["updated_at"]),
    }


def create_user_langfuse_project(
    user_id: str,
    *,
    name: str,
    base_url: str,
    public_key: str,
    secret_key: str,
    environment: str,
    is_default: bool = False,
) -> dict[str, Any]:
    uid = str(user_id or "").strip()
    if not uid:
        raise ValueError("user_id is required")
    now = _now_iso()
    encrypted_pk = _encrypt_value(public_key)
    encrypted_sk = _encrypt_value(secret_key)
    with _DB_LOCK:
        with _connect() as conn:
            # Auto-default if first project for this user
            count = conn.execute(
                "SELECT COUNT(1) AS cnt FROM user_langfuse_projects WHERE user_id = ?", (uid,)
            ).fetchone()
            first_project = int(count["cnt"]) == 0 if count else True
            default_flag = 1 if (is_default or first_project) else 0
            if default_flag == 1:
                conn.execute(
                    "UPDATE user_langfuse_projects SET is_default = 0 WHERE user_id = ?", (uid,)
                )
            cur = conn.execute(
                """
                INSERT INTO user_langfuse_projects
                    (user_id, name, base_url, public_key, secret_key, environment, is_default, created_at, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    uid,
                    str(name or "").strip(),
                    str(base_url or "").strip(),
                    encrypted_pk,
                    encrypted_sk,
                    str(environment or "").strip(),
                    default_flag,
                    now,
                    now,
                ),
            )
            project_id = cur.lastrowid
            row = conn.execute(
                "SELECT * FROM user_langfuse_projects WHERE id = ?", (project_id,)
            ).fetchone()
    return _safe_project_row(row)


def update_user_langfuse_project(
    project_id: int,
    *,
    user_id: str,
    **fields: Any,
) -> dict[str, Any] | None:
    uid = str(user_id or "").strip()
    if not uid:
        raise ValueError("user_id is required")
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            existing = conn.execute(
                "SELECT * FROM user_langfuse_projects WHERE id = ? AND user_id = ?",
                (project_id, uid),
            ).fetchone()
            if existing is None:
                return None
            sets: list[str] = []
            params: list[Any] = []
            for key in ("name", "base_url", "environment"):
                if key in fields and fields[key] is not None:
                    sets.append(f"{key} = ?")
                    params.append(str(fields[key]).strip())
            for key in ("public_key", "secret_key"):
                if key in fields and fields[key] is not None:
                    sets.append(f"{key} = ?")
                    params.append(_encrypt_value(str(fields[key]).strip()))
            if "is_default" in fields and fields["is_default"] is not None:
                if fields["is_default"]:
                    conn.execute(
                        "UPDATE user_langfuse_projects SET is_default = 0 WHERE user_id = ?",
                        (uid,),
                    )
                sets.append("is_default = ?")
                params.append(1 if fields["is_default"] else 0)
            if not sets:
                return _safe_project_row(existing)
            sets.append("updated_at = ?")
            params.append(now)
            params.extend([project_id, uid])
            conn.execute(
                f"UPDATE user_langfuse_projects SET {', '.join(sets)} WHERE id = ? AND user_id = ?",
                params,
            )
            row = conn.execute(
                "SELECT * FROM user_langfuse_projects WHERE id = ?", (project_id,)
            ).fetchone()
    return _safe_project_row(row) if row else None


def delete_user_langfuse_project(project_id: int, *, user_id: str) -> bool:
    uid = str(user_id or "").strip()
    if not uid:
        return False
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                "DELETE FROM user_langfuse_projects WHERE id = ? AND user_id = ?",
                (project_id, uid),
            )
    return int(cur.rowcount or 0) > 0


def list_user_langfuse_projects(user_id: str) -> list[dict[str, Any]]:
    uid = str(user_id or "").strip()
    if not uid:
        return []
    with _connect() as conn:
        rows = conn.execute(
            "SELECT * FROM user_langfuse_projects WHERE user_id = ? ORDER BY is_default DESC, id ASC",
            (uid,),
        ).fetchall()
    return [_safe_project_row(row) for row in rows]


def get_user_langfuse_project(project_id: int, *, user_id: str) -> dict[str, Any] | None:
    uid = str(user_id or "").strip()
    if not uid:
        return None
    with _connect() as conn:
        row = conn.execute(
            "SELECT * FROM user_langfuse_projects WHERE id = ? AND user_id = ?",
            (project_id, uid),
        ).fetchone()
    return _safe_project_row(row) if row else None


def set_default_langfuse_project(project_id: int, *, user_id: str) -> bool:
    uid = str(user_id or "").strip()
    if not uid:
        return False
    with _DB_LOCK:
        with _connect() as conn:
            existing = conn.execute(
                "SELECT 1 FROM user_langfuse_projects WHERE id = ? AND user_id = ?",
                (project_id, uid),
            ).fetchone()
            if not existing:
                return False
            conn.execute(
                "UPDATE user_langfuse_projects SET is_default = 0 WHERE user_id = ?", (uid,)
            )
            conn.execute(
                "UPDATE user_langfuse_projects SET is_default = 1, updated_at = ? WHERE id = ? AND user_id = ?",
                (_now_iso(), project_id, uid),
            )
    return True


# ---------------------------------------------------------------------------
# Chat Messages CRUD
# ---------------------------------------------------------------------------

def insert_chat_message(
    thread_id: str,
    user_id: str,
    role: str,
    content: str,
    model: str = "",
    project_id: int | None = None,
    metrics: str = "",
) -> int:
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                """
                INSERT INTO chat_messages(thread_id, user_id, role, content, model, project_id, metrics, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    str(thread_id or ""),
                    str(user_id or ""),
                    str(role or "user"),
                    str(content or ""),
                    str(model or ""),
                    project_id,
                    str(metrics or ""),
                    now,
                ),
            )
            return int(cur.lastrowid or 0)


def list_chat_threads(user_id: str | None = None, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
    safe_limit = max(1, min(int(limit), 200))
    safe_offset = max(0, int(offset))
    with _connect() as conn:
        if user_id:
            rows = conn.execute(
                """
                SELECT thread_id,
                       MIN(created_at) AS first_at,
                       MAX(created_at) AS last_at,
                       COUNT(1) AS msg_count
                FROM chat_messages
                WHERE user_id = ?
                GROUP BY thread_id
                ORDER BY last_at DESC
                LIMIT ? OFFSET ?
                """,
                (str(user_id), safe_limit, safe_offset),
            ).fetchall()
        else:
            rows = conn.execute(
                """
                SELECT thread_id,
                       MIN(created_at) AS first_at,
                       MAX(created_at) AS last_at,
                       COUNT(1) AS msg_count
                FROM chat_messages
                GROUP BY thread_id
                ORDER BY last_at DESC
                LIMIT ? OFFSET ?
                """,
                (safe_limit, safe_offset),
            ).fetchall()
    return [
        {
            "thread_id": str(row["thread_id"]),
            "first_at": str(row["first_at"]),
            "last_at": str(row["last_at"]),
            "msg_count": int(row["msg_count"]),
        }
        for row in rows
    ]


def list_chat_messages(thread_id: str, limit: int = 100) -> list[dict[str, Any]]:
    safe_limit = max(1, min(int(limit), 500))
    with _connect() as conn:
        rows = conn.execute(
            """
            SELECT id, thread_id, user_id, role, content, model, project_id, metrics, created_at
            FROM chat_messages
            WHERE thread_id = ?
            ORDER BY id ASC
            LIMIT ?
            """,
            (str(thread_id), safe_limit),
        ).fetchall()
    return [
        {
            "id": int(row["id"]),
            "thread_id": str(row["thread_id"]),
            "user_id": str(row["user_id"] or ""),
            "role": str(row["role"]),
            "content": str(row["content"]),
            "model": str(row["model"] or ""),
            "project_id": row["project_id"],
            "metrics": str(row["metrics"] or ""),
            "created_at": str(row["created_at"]),
        }
        for row in rows
    ]


def count_chat_threads() -> int:
    with _connect() as conn:
        row = conn.execute("SELECT COUNT(DISTINCT thread_id) AS cnt FROM chat_messages").fetchone()
    return int(row["cnt"]) if row is not None else 0


# ---------------------------------------------------------------------------
# Audit Logs CRUD
# ---------------------------------------------------------------------------

def insert_audit_log(
    actor: str,
    action: str,
    detail: str = "",
    ip: str = "",
    extra: str = "",
) -> int:
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                """
                INSERT INTO audit_logs(ts, actor, action, detail, ip, extra)
                VALUES (?, ?, ?, ?, ?, ?)
                """,
                (now, str(actor or ""), str(action or ""), str(detail or ""), str(ip or ""), str(extra or "")),
            )
            return int(cur.lastrowid or 0)


def list_audit_logs(limit: int = 100, offset: int = 0) -> list[dict[str, Any]]:
    safe_limit = max(1, min(int(limit), 500))
    safe_offset = max(0, int(offset))
    with _connect() as conn:
        rows = conn.execute(
            """
            SELECT id, ts, actor, action, detail, ip, extra
            FROM audit_logs
            ORDER BY id DESC
            LIMIT ? OFFSET ?
            """,
            (safe_limit, safe_offset),
        ).fetchall()
    return [
        {
            "id": int(row["id"]),
            "ts": str(row["ts"]),
            "actor": str(row["actor"] or ""),
            "action": str(row["action"]),
            "detail": str(row["detail"] or ""),
            "ip": str(row["ip"] or ""),
            "extra": str(row["extra"] or ""),
        }
        for row in rows
    ]


def count_audit_logs() -> int:
    with _connect() as conn:
        row = conn.execute("SELECT COUNT(1) AS cnt FROM audit_logs").fetchone()
    return int(row["cnt"]) if row is not None else 0


# ---------------------------------------------------------------------------
# User Reports CRUD
# ---------------------------------------------------------------------------

def _safe_report_row(row: sqlite3.Row) -> dict[str, Any]:
    def _safe_json(raw: str, default):
        try:
            return json.loads(raw)
        except Exception:
            return default

    return {
        "id": int(row["id"]),
        "owner_id": str(row["owner_id"]),
        "project_id": row["project_id"],
        "target_user_id": str(row["target_user_id"]),
        "period_type": str(row["period_type"]),
        "period_start": str(row["period_start"]),
        "period_end": str(row["period_end"]),
        "summary": _safe_json(str(row["summary"] or "{}"), {}),
        "sessions": _safe_json(str(row["sessions"] or "[]"), []),
        "model_usage": _safe_json(str(row["model_usage"] or "{}"), {}),
        "timeline": _safe_json(str(row["timeline"] or "[]"), []),
        "trace_count": int(row["trace_count"] or 0),
        "total_cost": float(row["total_cost"] or 0.0),
        "llm_analysis": str(row["llm_analysis"] or ""),
        "created_at": str(row["created_at"]),
    }


def insert_user_report(
    owner_id: str,
    project_id: int | None,
    target_user_id: str,
    period_type: str,
    period_start: str,
    period_end: str,
    summary: str,
    sessions: str,
    model_usage: str,
    timeline: str,
    trace_count: int,
    total_cost: float,
    llm_analysis: str = "",
) -> int:
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                """
                INSERT INTO user_reports(
                    owner_id, project_id, target_user_id, period_type,
                    period_start, period_end, summary, sessions,
                    model_usage, timeline, trace_count, total_cost,
                    llm_analysis, created_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(owner_id, project_id, target_user_id, period_type, period_start)
                DO UPDATE SET
                    period_end = excluded.period_end,
                    summary = excluded.summary,
                    sessions = excluded.sessions,
                    model_usage = excluded.model_usage,
                    timeline = excluded.timeline,
                    trace_count = excluded.trace_count,
                    total_cost = excluded.total_cost,
                    llm_analysis = excluded.llm_analysis,
                    created_at = excluded.created_at
                """,
                (
                    str(owner_id or ""),
                    project_id,
                    str(target_user_id or ""),
                    str(period_type or "daily"),
                    str(period_start or ""),
                    str(period_end or ""),
                    str(summary or "{}"),
                    str(sessions or "[]"),
                    str(model_usage or "{}"),
                    str(timeline or "[]"),
                    int(trace_count or 0),
                    float(total_cost or 0.0),
                    str(llm_analysis or ""),
                    now,
                ),
            )
            return int(cur.lastrowid or 0)


def get_user_report(report_id: int) -> dict[str, Any] | None:
    with _connect() as conn:
        row = conn.execute(
            "SELECT * FROM user_reports WHERE id = ?", (report_id,)
        ).fetchone()
    return _safe_report_row(row) if row else None


def list_user_reports(
    owner_id: str | None = None,
    target_user_id: str | None = None,
    period_type: str | None = None,
    project_id: int | None = None,
    limit: int = 50,
    offset: int = 0,
    period_start_gte: str | None = None,
    period_start_lt: str | None = None,
) -> list[dict[str, Any]]:
    safe_limit = max(1, min(int(limit), 200))
    safe_offset = max(0, int(offset))
    clauses: list[str] = []
    params: list[Any] = []
    if owner_id:
        clauses.append("owner_id = ?")
        params.append(str(owner_id))
    if target_user_id:
        clauses.append("target_user_id = ?")
        params.append(str(target_user_id))
    if period_type:
        clauses.append("period_type = ?")
        params.append(str(period_type))
    if project_id is not None:
        clauses.append("project_id = ?")
        params.append(int(project_id))
    if period_start_gte:
        clauses.append("period_start >= ?")
        params.append(str(period_start_gte))
    if period_start_lt:
        clauses.append("period_start < ?")
        params.append(str(period_start_lt))
    where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
    params.extend([safe_limit, safe_offset])
    with _connect() as conn:
        rows = conn.execute(
            f"SELECT * FROM user_reports{where} ORDER BY id DESC LIMIT ? OFFSET ?",
            params,
        ).fetchall()
    return [_safe_report_row(row) for row in rows]


def count_user_reports(
    owner_id: str | None = None,
    target_user_id: str | None = None,
    period_type: str | None = None,
) -> int:
    clauses: list[str] = []
    params: list[Any] = []
    if owner_id:
        clauses.append("owner_id = ?")
        params.append(str(owner_id))
    if target_user_id:
        clauses.append("target_user_id = ?")
        params.append(str(target_user_id))
    if period_type:
        clauses.append("period_type = ?")
        params.append(str(period_type))
    where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
    with _connect() as conn:
        row = conn.execute(
            f"SELECT COUNT(1) AS cnt FROM user_reports{where}", params
        ).fetchone()
    return int(row["cnt"]) if row is not None else 0


def delete_user_report(report_id: int) -> bool:
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                "DELETE FROM user_reports WHERE id = ?", (report_id,)
            )
    return int(cur.rowcount or 0) > 0


# ---------------------------------------------------------------------------
# Experiments CRUD
# ---------------------------------------------------------------------------

def _safe_experiment_row(row: sqlite3.Row) -> dict[str, Any]:
    def _safe_json(raw: str, default):
        try:
            return json.loads(raw)
        except Exception:
            return default

    return {
        "id": int(row["id"]),
        "user_id": str(row["user_id"]),
        "project_id": row["project_id"],
        "name": str(row["name"]),
        "description": str(row["description"] or ""),
        "experiment_type": str(row["experiment_type"]),
        "config": _safe_json(str(row["config_json"] or "{}"), {}),
        "status": str(row["status"]),
        "result": _safe_json(str(row["result_json"] or "{}"), {}),
        "created_at": str(row["created_at"]),
        "updated_at": str(row["updated_at"]),
    }


def _safe_experiment_run_row(row: sqlite3.Row) -> dict[str, Any]:
    def _safe_json(raw: str, default):
        try:
            return json.loads(raw)
        except Exception:
            return default

    return {
        "id": int(row["id"]),
        "experiment_id": int(row["experiment_id"]),
        "variant_label": str(row["variant_label"]),
        "model_spec": str(row["model_spec"]),
        "prompt_text": row["prompt_text"],
        "trace_id": row["trace_id"],
        "input_text": str(row["input_text"]),
        "output_text": row["output_text"],
        "scores": _safe_json(str(row["scores_json"] or "{}"), {}),
        "latency_ms": row["latency_ms"],
        "token_usage": _safe_json(str(row["token_usage"] or "{}"), {}),
        "status": str(row["status"]),
        "created_at": str(row["created_at"]),
    }


def create_experiment(
    user_id: str,
    name: str,
    experiment_type: str,
    config: dict | None = None,
    description: str = "",
    project_id: str | None = None,
) -> int:
    now = _now_iso()
    config_str = json.dumps(config or {}, ensure_ascii=False)
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                """
                INSERT INTO experiments(user_id, project_id, name, description, experiment_type,
                    config_json, status, result_json, created_at, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, 'created', '{}', ?, ?)
                """,
                (str(user_id), project_id, str(name), str(description), str(experiment_type),
                 config_str, now, now),
            )
            return int(cur.lastrowid or 0)


def get_experiment(exp_id: int, user_id: str | None = None) -> dict[str, Any] | None:
    with _connect() as conn:
        if user_id:
            row = conn.execute(
                "SELECT * FROM experiments WHERE id = ? AND user_id = ?", (exp_id, str(user_id))
            ).fetchone()
        else:
            row = conn.execute(
                "SELECT * FROM experiments WHERE id = ?", (exp_id,)
            ).fetchone()
    return _safe_experiment_row(row) if row else None


def list_experiments(
    user_id: str,
    limit: int = 50,
    offset: int = 0,
    experiment_type: str | None = None,
    status: str | None = None,
    include_all_users: bool = False,
) -> tuple[list[dict[str, Any]], int]:
    safe_limit = max(1, min(int(limit), 200))
    safe_offset = max(0, int(offset))
    clauses: list[str] = []
    params: list[Any] = []
    if not include_all_users:
        clauses.append("user_id = ?")
        params.append(str(user_id))
    if experiment_type:
        clauses.append("experiment_type = ?")
        params.append(str(experiment_type))
    if status:
        clauses.append("status = ?")
        params.append(str(status))
    where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
    with _connect() as conn:
        count_row = conn.execute(
            f"SELECT COUNT(1) AS cnt FROM experiments{where}", params
        ).fetchone()
        total = int(count_row["cnt"]) if count_row else 0
        rows = conn.execute(
            f"SELECT * FROM experiments{where} ORDER BY id DESC LIMIT ? OFFSET ?",
            params + [safe_limit, safe_offset],
        ).fetchall()
    return [_safe_experiment_row(r) for r in rows], total


def update_experiment_status(
    exp_id: int,
    status: str,
    result_json: dict | str | None = None,
) -> None:
    now = _now_iso()
    with _DB_LOCK:
        with _connect() as conn:
            if result_json is not None:
                result_str = json.dumps(result_json) if isinstance(result_json, dict) else str(result_json)
                conn.execute(
                    "UPDATE experiments SET status = ?, result_json = ?, updated_at = ? WHERE id = ?",
                    (str(status), result_str, now, exp_id),
                )
            else:
                conn.execute(
                    "UPDATE experiments SET status = ?, updated_at = ? WHERE id = ?",
                    (str(status), now, exp_id),
                )


def delete_experiment(exp_id: int, user_id: str) -> bool:
    with _DB_LOCK:
        with _connect() as conn:
            # Delete experiment first — if user_id doesn't match, nothing is deleted
            cur = conn.execute(
                "DELETE FROM experiments WHERE id = ? AND user_id = ?",
                (exp_id, str(user_id)),
            )
            if int(cur.rowcount or 0) == 0:
                return False
            # Only delete runs after experiment is successfully deleted
            conn.execute(
                "DELETE FROM experiment_runs WHERE experiment_id = ?", (exp_id,)
            )
    return True


def create_experiment_run(
    experiment_id: int,
    variant_label: str,
    model_spec: str,
    input_text: str,
    prompt_text: str | None = None,
    trace_id: str | None = None,
) -> int:
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                """
                INSERT INTO experiment_runs(experiment_id, variant_label, model_spec,
                    prompt_text, trace_id, input_text, status)
                VALUES (?, ?, ?, ?, ?, ?, 'pending')
                """,
                (experiment_id, str(variant_label), str(model_spec),
                 prompt_text, trace_id, str(input_text)),
            )
            return int(cur.lastrowid or 0)


def update_experiment_run(
    run_id: int,
    output_text: str | None = None,
    scores_json: dict | str | None = None,
    latency_ms: float | None = None,
    token_usage: dict | str | None = None,
    status: str | None = None,
) -> None:
    sets: list[str] = []
    params: list[Any] = []
    if output_text is not None:
        sets.append("output_text = ?")
        params.append(str(output_text))
    if scores_json is not None:
        sets.append("scores_json = ?")
        params.append(json.dumps(scores_json) if isinstance(scores_json, dict) else str(scores_json))
    if latency_ms is not None:
        sets.append("latency_ms = ?")
        params.append(float(latency_ms))
    if token_usage is not None:
        sets.append("token_usage = ?")
        params.append(json.dumps(token_usage) if isinstance(token_usage, dict) else str(token_usage))
    if status is not None:
        sets.append("status = ?")
        params.append(str(status))
    if not sets:
        return
    params.append(run_id)
    with _DB_LOCK:
        with _connect() as conn:
            conn.execute(
                f"UPDATE experiment_runs SET {', '.join(sets)} WHERE id = ?",
                params,
            )


def list_experiment_runs(
    exp_id: int,
    limit: int = 200,
    offset: int = 0,
) -> tuple[list[dict[str, Any]], int]:
    safe_limit = max(1, min(int(limit), 500))
    safe_offset = max(0, int(offset))
    with _connect() as conn:
        count_row = conn.execute(
            "SELECT COUNT(1) AS cnt FROM experiment_runs WHERE experiment_id = ?",
            (exp_id,),
        ).fetchone()
        total = int(count_row["cnt"]) if count_row else 0
        rows = conn.execute(
            "SELECT * FROM experiment_runs WHERE experiment_id = ? ORDER BY id ASC LIMIT ? OFFSET ?",
            (exp_id, safe_limit, safe_offset),
        ).fetchall()
    return [_safe_experiment_run_row(r) for r in rows], total


def update_experiment(
    exp_id: int,
    user_id: str | None = None,
    name: str | None = None,
    description: str | None = None,
    config: dict | None = None,
) -> bool:
    """Update experiment metadata (name, description, config)."""
    sets: list[str] = []
    params: list[Any] = []
    if name is not None:
        sets.append("name = ?")
        params.append(str(name))
    if description is not None:
        sets.append("description = ?")
        params.append(str(description))
    if config is not None:
        sets.append("config_json = ?")
        params.append(json.dumps(config, ensure_ascii=False))
    if not sets:
        return False
    sets.append("updated_at = ?")
    params.append(_now_iso())
    clauses = "id = ?"
    params.append(exp_id)
    if user_id:
        clauses += " AND user_id = ?"
        params.append(str(user_id))
    with _DB_LOCK:
        with _connect() as conn:
            cur = conn.execute(
                f"UPDATE experiments SET {', '.join(sets)} WHERE {clauses}",
                params,
            )
    return int(cur.rowcount or 0) > 0


def ensure_bootstrap_user() -> dict[str, Any] | None:
    username = str(os.getenv("AGENT_AUTH_BOOTSTRAP_USERNAME", "")).strip().lower()
    password = str(os.getenv("AGENT_AUTH_BOOTSTRAP_PASSWORD", "")).strip()
    role = str(os.getenv("AGENT_AUTH_BOOTSTRAP_ROLE", "super_admin")).strip().lower() or "super_admin"
    if not username or not password:
        return None

    with _connect() as conn:
        existing = conn.execute("SELECT user_id FROM users WHERE username = ?", (username,)).fetchone()
    if existing is not None:
        return get_user_by_id(str(existing["user_id"]))
    return create_user(username=username, password=password, role=role)
