"""
api.v1.setup — Bootstrap endpoint for first-time local setup.

POST /api/v1/setup/bootstrap
    No auth required. Creates an initial tenant + admin API key if none exists.
    If a key is already saved in ~/.agentpaas/config.json, returns a masked
    confirmation so the UI can prompt the user to paste their existing key.
    Safe to call multiple times — idempotent once configured.
"""
from __future__ import annotations
import json
import os
import secrets
import sys

from typing import Optional

from fastapi import APIRouter, Request, HTTPException
from pydantic import BaseModel

from fastapi.responses import JSONResponse

from agentpaas.db.session import get_db
from agentpaas.db.models import gen_id, now_utc
from agentpaas.api.middleware.auth import hash_key

router = APIRouter(prefix="/setup", tags=["setup"])

CONFIG_FILE = os.path.expanduser("~/.agentpaas/config.json")

# Loopback-only addresses. /bootstrap is the only no-auth endpoint that can
# hand out an admin API key, so it must only ever be callable from the same
# machine. Audit critical #1 — `POST /api/v1/setup/bootstrap` previously
# allowed any unauthenticated remote caller who reached port 8000 to receive
# a fully-scoped admin key before the operator finished setup.
_LOOPBACK = {"127.0.0.1", "::1", "localhost"}


def _require_loopback(request: Request) -> None:
    """Reject /bootstrap calls that don't come from the same host."""
    client_host = (request.client.host if request.client else "") or ""
    if client_host not in _LOOPBACK:
        # 404 (not 403) hides the endpoint's existence from remote scanners.
        raise HTTPException(status_code=404, detail="Not Found")


def _load_config() -> dict:
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE) as f:
            return json.load(f)
    return {}


def _save_config(cfg: dict) -> None:
    os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
    with open(CONFIG_FILE, "w") as f:
        json.dump(cfg, f, indent=2)


@router.post("/bootstrap")
async def bootstrap(request: Request):
    """
    Create the initial tenant and API key for local use.

    **Loopback-only.** Returns 404 if called from a non-loopback address.
    See audit critical #1 — without this guard the endpoint hands an admin
    API key to anyone who reaches port 8000 first. For production deploys
    behind a reverse proxy, run `agentpaas create-tenant` on the host CLI.

    - If config already has an api_key, return status=already_configured.
    - Otherwise create tenant + key, save to config, return the key once.
    """
    _require_loopback(request)

    cfg = _load_config()
    existing_key = cfg.get("api_key", "")

    if existing_key:
        # Verify the key is still valid in the DB
        db = get_db()
        prefix = existing_key[:8]
        row = db.fetchone(
            "SELECT k.id FROM api_keys k "
            "JOIN tenants t ON k.tenant_id = t.id "
            "WHERE k.key_prefix = ? AND k.status = 'active' AND t.status = 'active'",
            (prefix,),
        )
        if row:
            return {"status": "already_configured", "key_prefix": prefix}
        # Key in config but not in DB (DB was reset) — fall through and recreate

    db = get_db()

    # Check if any active tenant exists
    tenant_row = db.fetchone("SELECT id FROM tenants WHERE status='active' LIMIT 1")
    if tenant_row:
        # Tenant exists but config is missing — we can't recover the raw key;
        # user must supply it manually.
        return JSONResponse(
            status_code=409,
            content={
                "status": "tenant_exists",
                "message": (
                    "A tenant already exists but no API key was found in config. "
                    "Run `agentpaas create-tenant --name YourOrg` in a terminal "
                    "to regenerate a key, then paste it in the setup wizard."
                ),
            },
        )

    # Create brand-new tenant
    tid = gen_id("tn_")
    uid = gen_id("usr_")
    key = f"ap_{secrets.token_hex(16)}"
    now = now_utc()

    db.execute(
        "INSERT INTO tenants (id, name, plan, status, created_at) VALUES (?, ?, ?, 'active', ?)",
        (tid, "local", "free", now),
    )
    db.execute(
        "INSERT INTO users (id, tenant_id, email, role, created_at) VALUES (?, ?, ?, 'admin', ?)",
        (uid, tid, "", now),
    )
    db.execute(
        "INSERT INTO api_keys "
        "(id, tenant_id, user_id, key_hash, key_prefix, name, scopes, rate_limit, status, created_at) "
        "VALUES (?, ?, ?, ?, ?, 'admin', ?, 600, 'active', ?)",
        (
            gen_id("key_"),
            tid,
            uid,
            hash_key(key),
            key[:8],
            json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]),
            now,
        ),
    )
    db.commit()

    cfg["api_key"] = key
    _save_config(cfg)

    return {"status": "created", "api_key": key, "tenant_id": tid}


@router.get("/health")
async def setup_health():
    """Unauthenticated health check used by the setup wizard."""
    return {
        "ok": True,
        "python": sys.version.split()[0],
        "platform": sys.platform,
    }


# ─────────────────────────────────────────────────────────────────────
# FR-001 — desktop one-click login
# ─────────────────────────────────────────────────────────────────────

@router.get("/desktop-key")
async def desktop_key(request: Request):
    """Return the auto-bootstrapped API key for one-click desktop login.

    CR FR-001: a professor double-clicks the app; the server auto-
    bootstraps a tenant + key at startup and writes it to
    ~/.agentpaas/config.json. Without this endpoint the first-run wizard
    would then ask them to paste a key they have no easy way to retrieve
    (it's only in config.json / the terminal). Dogfooding surfaced this
    exact dead-end.

    Security: desktop mode only + loopback only. In desktop mode the
    machine IS the security boundary (single local user); handing the
    key to a same-host browser is no more exposure than the key already
    sitting in the user's home dir. In lab/paas this endpoint 404s — those
    deployments use the SetupWizard's explicit key-paste flow.
    """
    from agentpaas.config import settings

    if settings.deployment_mode != "desktop":
        # Hide existence outside desktop (same posture as Q9 route gating).
        raise HTTPException(status_code=404, detail="Not Found")
    _require_loopback(request)

    cfg = _load_config()
    key = cfg.get("api_key", "")
    if not key:
        # Not bootstrapped yet — caller should fall back to the wizard.
        raise HTTPException(status_code=404, detail="Not bootstrapped")
    return {"api_key": key, "tenant_id": cfg.get("tenant_id", "")}


# ─────────────────────────────────────────────────────────────────────
# FR-002 — data directory (first-run wizard shows + lets desktop users
# choose where their research data lives)
# ─────────────────────────────────────────────────────────────────────

class DataDirRequest(BaseModel):
    data_dir: str


@router.get("/data-dir")
async def get_data_dir(request: Request):
    """Report the current data directory + its sub-dirs (FR-002).

    Loopback-only — the data dir is a local-filesystem concern and there
    is no reason a remote caller needs it. Surfaces enough for the wizard
    to show "your data lives here" with the standard sub-folders.
    """
    _require_loopback(request)
    from agentpaas.config import settings

    return {
        "data_dir": settings.data_dir,
        "is_default": not bool(
            os.getenv("AGENTPAAS_DATA_DIR") or _data_dir_override()
        ),
        "mode": settings.deployment_mode,
        "subdirs": {
            "instances": settings.instances_dir,
            "knowledge_bases": settings.knowledge_bases_dir,
            "logs": settings.logs_dir,
        },
        # Changing data_dir needs a restart because DB/dir handles are
        # bound at process startup. The wizard tells the user this.
        "change_requires_restart": True,
    }


@router.put("/data-dir")
async def set_data_dir(req: DataDirRequest, request: Request):
    """Persist a chosen data directory to ~/.agentpaas/config.json (FR-002).

    Loopback-only. Validates that the path's parent exists and is
    writable, creates the dir if needed, then writes it to config.json so
    `_default_data_dir()` picks it up on the next startup. Returns
    change_requires_restart=true — the running process keeps using the
    old dir until restarted (DB handle is already open).
    """
    _require_loopback(request)

    target = os.path.abspath(os.path.expanduser(req.data_dir.strip()))
    if not target:
        raise HTTPException(status_code=400, detail="data_dir is empty")

    parent = os.path.dirname(target)
    if not os.path.isdir(parent):
        raise HTTPException(
            status_code=400,
            detail=f"Parent directory does not exist: {parent}",
        )
    if not os.access(parent, os.W_OK):
        raise HTTPException(
            status_code=400,
            detail=f"Parent directory is not writable: {parent}",
        )

    try:
        os.makedirs(target, exist_ok=True)
    except OSError as e:
        raise HTTPException(status_code=400, detail=f"Cannot create dir: {e}")

    cfg = _load_config()
    cfg["data_dir"] = target
    _save_config(cfg)

    return {
        "ok": True,
        "data_dir": target,
        "change_requires_restart": True,
        "message": "数据目录已保存，重启服务后生效。",
    }


def _data_dir_override() -> str:
    """Helper mirroring config._data_dir_from_config_json for the is_default
    check above (kept local to avoid importing a private from config)."""
    cfg = _load_config()
    return cfg.get("data_dir", "") or ""


@router.get("/mode")
async def get_deployment_mode():
    """Unauthenticated read of the current deployment_mode + display info.

    CR-20260607-001 rev.2 Q8 decision: the webui top bar surfaces a chip
    showing which mode the user is on (Desktop / Lab / PaaS). The chip
    is rendered BEFORE the user logs in (so the SetupWizard / first-run
    flow can adapt to the mode), so this endpoint must be reachable
    without a Bearer.

    Response shape lets the frontend stay dumb:
      - mode:         "desktop" | "lab" | "paas"
      - tenant_alias: human-readable string for the chip ({hostname}/{user}
                      for desktop, tenant.name for lab/paas, "" if unset)
      - bootstrapped: true once the desktop auto-bootstrap has run; the
                      first-run wizard short-circuits to login once true

    Defense-in-depth: even though /mode itself is no-auth, the
    tenant_alias is the hostname/user pair — same info available from
    OS-level network packet sniffing on the same LAN. We do not
    leak tenant_id (PK that flows into LLM provider logs).
    """
    from agentpaas.config import settings

    mode = settings.deployment_mode
    tenant_alias = ""
    bootstrapped = False
    try:
        cfg = _load_config()
        if cfg.get("api_key"):
            bootstrapped = True
        db = get_db()
        row = db.fetchone(
            "SELECT name FROM tenants WHERE status='active' "
            "ORDER BY created_at ASC LIMIT 1"
        )
        if row:
            tenant_alias = row.get("name", "") or ""
    except Exception:  # pragma: no cover (no DB / fresh install)
        pass

    return {
        "mode": mode,
        "tenant_alias": tenant_alias,
        "bootstrapped": bootstrapped,
    }


# ─────────────────────────────────────────────────────────────────────
# Desktop-mode auto-bootstrap (CR-20260607-001 rev.2, Q11)
# ─────────────────────────────────────────────────────────────────────

def auto_bootstrap_desktop() -> Optional[str]:
    """Idempotent first-boot tenant creation for ``deployment_mode=desktop``.

    Called from the FastAPI startup hook. If we're a fresh desktop install
    with no tenant yet, create:
      - tenant_id = "tn_local_{12-hex-uuid}" (anonymous, per Q11 decision)
      - tenants.name = "{hostname}/{user}" as a human-readable alias
      - admin user
      - admin API key with full scopes
      - write the raw key to ``~/.agentpaas/config.json`` so the local
        webui can pick it up without a SetupWizard step

    If a tenant already exists, no-op (idempotent across restarts).

    Returns the raw API key when bootstrap occurred, else ``None``.
    """
    from typing import Optional  # local import keeps module-level signature clean
    import socket

    cfg = _load_config()
    if cfg.get("api_key"):
        return None  # already configured — Q10 mode-transition path

    db = get_db()
    tenant_row = db.fetchone(
        "SELECT id FROM tenants WHERE status='active' LIMIT 1"
    )
    if tenant_row:
        return None  # tenant exists from prior bootstrap or admin CLI

    # Q11: tenant_id is anonymous UUID; tenants.name carries hostname/user
    # as contextual alias for later Lab upgrade UX.
    tid = "tn_local_" + secrets.token_hex(6)
    uid = gen_id("usr_")
    raw_key = f"ap_{secrets.token_hex(16)}"
    try:
        alias = f"{socket.gethostname()}/{os.environ.get('USER', '?')}"
    except Exception:
        alias = "local"
    now = now_utc()

    db.execute(
        "INSERT INTO tenants (id, name, plan, status, created_at) "
        "VALUES (?, ?, 'free', 'active', ?)",
        (tid, alias, now),
    )
    db.execute(
        "INSERT INTO users (id, tenant_id, email, role, created_at) "
        "VALUES (?, ?, '', 'admin', ?)",
        (uid, tid, now),
    )
    db.execute(
        "INSERT INTO api_keys "
        "(id, tenant_id, user_id, key_hash, key_prefix, name, scopes, "
        " rate_limit, status, created_at) "
        "VALUES (?, ?, ?, ?, ?, 'admin', ?, 600, 'active', ?)",
        (
            gen_id("key_"),
            tid,
            uid,
            hash_key(raw_key),
            raw_key[:8],
            json.dumps(["agents:*", "keys:*", "billing:read", "admin:*"]),
            now,
        ),
    )
    db.commit()

    cfg["api_key"] = raw_key
    cfg["deployment_mode"] = "desktop"
    cfg["tenant_id"] = tid
    _save_config(cfg)
    return raw_key


# ── Directory browser (loopback-only) ─────────────────────────────────────────

@router.get("/browse-dir")
async def browse_dir(request: Request, path: str = ""):
    """List immediate sub-directories of *path* for the directory-picker UI.

    Loopback-only — exposes the server filesystem to the local browser; never
    callable from a remote host (same oracle defense as /data-dir and /install).

    Query params:
        path: absolute path to browse (empty → user home directory)

    Response:
        current:  resolved absolute path being listed
        parent:   parent directory path (null when at filesystem root)
        home:     user home directory (always returned for quick-nav button)
        entries:  list of {name, path, hidden} — immediate subdirs only
    """
    _require_loopback(request)

    from pathlib import Path

    # Default to home if path is empty or "~"
    resolved = Path(os.path.expanduser(path or "~")).resolve()

    if not resolved.exists():
        # Gracefully fall back to home if path doesn't exist yet
        resolved = Path.home()
    if not resolved.is_dir():
        raise HTTPException(status_code=400, detail=f"Not a directory: {resolved}")

    entries = []
    try:
        for entry in sorted(resolved.iterdir(), key=lambda e: e.name.lower()):
            if not entry.is_dir():
                continue
            entries.append({
                "name": entry.name,
                "path": str(entry),
                "hidden": entry.name.startswith("."),
            })
    except PermissionError:
        pass  # return empty entries for unreadable dirs

    parent = resolved.parent
    return {
        "current": str(resolved),
        "parent": str(parent) if parent != resolved else None,
        "home": str(Path.home()),
        "entries": entries,
    }
