"""First-admin bootstrap: create the initial admin from env vars if no users exist.

Called from ``python -m cyclone`` before either ``cli.main()`` or
``uvicorn`` so users exist by the time the API serves requests.

Precedence:

1. ``CYCLONE_AUTH_DISABLED=1`` — dev escape hatch. Flip the
   ``cyclone.auth.deps.AUTH_DISABLED`` flag so the API returns a
   synthetic admin user without checking credentials. Never raises.
2. Users table non-empty — no-op.
3. ``CYCLONE_ADMIN_USERNAME`` + ``CYCLONE_ADMIN_PASSWORD`` env vars set
   (password >= 12 chars) — create the admin and print confirmation.
   Each env var can also be replaced by a ``*_FILE`` companion
   (``CYCLONE_ADMIN_USERNAME_FILE`` / ``CYCLONE_ADMIN_PASSWORD_FILE``)
   that points at a file on disk — the standard Docker-secret pattern,
   used in production to avoid embedding secrets in ``docker-compose.yml``.
   ``_FILE`` takes precedence when set.
4. Otherwise — raise ``RuntimeError`` with a remediation hint that
   points operators at ``python -m cyclone users create``.
"""

from __future__ import annotations

import os
from pathlib import Path

from sqlalchemy import select

from cyclone.auth import users
from cyclone.auth.deps import AUTH_DISABLED
from cyclone.auth.permissions import Role
from cyclone.db import SessionLocal, User


def _read_secret(env_var: str, file_var: str) -> str | None:
    """Read a secret from a ``*_FILE`` env var (Docker-secret pattern) first,
    falling back to the plain env var. Returns None if neither is set.
    """
    file_path = os.environ.get(file_var)
    if file_path:
        try:
            return Path(file_path).read_text().strip()
        except OSError as exc:
            raise RuntimeError(
                f"failed to read {file_var}={file_path}: {exc}"
            ) from exc
    return os.environ.get(env_var)


def run() -> None:
    """Bootstrap the first admin user, or no-op.

    See module docstring for behavior. Idempotent: safe to call on
    every startup — it short-circuits as soon as the users table is
    non-empty.
    """
    if os.environ.get("CYCLONE_AUTH_DISABLED") == "1":
        # Dev escape hatch — skip bootstrap entirely and tell the API
        # to also short-circuit auth checks.
        import cyclone.auth.deps as _deps

        _deps.AUTH_DISABLED = True
        return

    username = _read_secret(
        "CYCLONE_ADMIN_USERNAME", "CYCLONE_ADMIN_USERNAME_FILE"
    )
    password = _read_secret(
        "CYCLONE_ADMIN_PASSWORD", "CYCLONE_ADMIN_PASSWORD_FILE"
    )

    # First-boot fix: ``python -m cyclone`` calls bootstrap before any
    # subcommand or the FastAPI lifespan handler runs, so on a brand-new
    # DB ``SessionLocal()`` raises "init_db() has not been called".
    # Initialize here so ``serve``, ``users create``, and friends can
    # all reach the DB without the operator having to know about
    # migrations. Idempotent — no-op when the schema is already current.
    from cyclone import db as _db

    _db.init_db()

    with SessionLocal()() as db:
        existing = db.execute(select(User)).scalars().first()
        if existing is not None:
            return  # users exist — nothing to bootstrap

        if not username or not password:
            raise RuntimeError(
                "Cyclone has no users yet. Set CYCLONE_ADMIN_USERNAME and "
                "CYCLONE_ADMIN_PASSWORD env vars (min 12 chars), or run "
                "`python -m cyclone users create <username> --role admin`."
            )
        if len(password) < 12:
            raise RuntimeError(
                "CYCLONE_ADMIN_PASSWORD must be at least 12 characters."
            )

        users.create(
            db,
            username=username,
            password=password,
            role=Role.ADMIN.value,
        )
        print(f"[cyclone] bootstrap admin user '{username}' created")
