#!/usr/bin/env python3
"""
VPS Monitor Backend — serveur central.
Agrège les données de tous les agents et expose une API REST pour le frontend.
"""

import asyncio
import os
import secrets
import sqlite3
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Annotated

import bcrypt as _bcrypt
import aiohttp
from fastapi import Depends, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from pydantic import BaseModel

# ─── Config ───────────────────────────────────────────────────────────────────

DB_FILE        = Path(os.getenv("DB_FILE",     "data/monitor.db"))
SECRET_FILE    = Path(os.getenv("SECRET_FILE", "data/.jwt_secret"))
AGENT_TIMEOUT  = int(os.getenv("AGENT_TIMEOUT", "5"))
JWT_ALGORITHM  = "HS256"
JWT_EXPIRE_MIN = int(os.getenv("JWT_EXPIRE_MINUTES", "1440"))  # 24 h

DB_FILE.parent.mkdir(parents=True, exist_ok=True)

# Clé JWT : env var > fichier persisté > génération + sauvegarde
def _load_jwt_secret() -> str:
    env = os.getenv("JWT_SECRET")
    if env:
        return env
    if SECRET_FILE.exists():
        return SECRET_FILE.read_text().strip()
    secret = secrets.token_hex(32)
    SECRET_FILE.write_text(secret)
    SECRET_FILE.chmod(0o600)
    return secret

JWT_SECRET = _load_jwt_secret()

bearer_scheme = HTTPBearer()

# ─── Modèles ──────────────────────────────────────────────────────────────────

class VpsConfig(BaseModel):
    id:          str
    name:        str
    host:        str
    port:        int = 8001
    api_key:     str
    description: str = ""


class ActionRequest(BaseModel):
    action: str   # start | stop | restart


class RegisterRequest(BaseModel):
    username: str
    password: str


class LoginRequest(BaseModel):
    username: str
    password: str


# ─── SQLite ───────────────────────────────────────────────────────────────────

@contextmanager
def get_db():
    conn = sqlite3.connect(DB_FILE, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()


def init_db() -> None:
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                username TEXT PRIMARY KEY,
                password TEXT NOT NULL,
                role     TEXT NOT NULL DEFAULT 'admin'
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS vps (
                id          TEXT PRIMARY KEY,
                name        TEXT NOT NULL,
                host        TEXT NOT NULL,
                port        INTEGER NOT NULL DEFAULT 8001,
                api_key     TEXT NOT NULL,
                description TEXT NOT NULL DEFAULT ''
            )
        """)


init_db()


# ─── Persistance ──────────────────────────────────────────────────────────────

def load_users() -> list[dict]:
    with get_db() as conn:
        return [dict(r) for r in conn.execute("SELECT * FROM users").fetchall()]


def add_user(user: dict) -> None:
    with get_db() as conn:
        conn.execute(
            "INSERT INTO users (username, password, role) VALUES (?, ?, ?)",
            (user["username"], user["password"], user["role"]),
        )


def load_vps() -> list[dict]:
    with get_db() as conn:
        return [dict(r) for r in conn.execute("SELECT * FROM vps").fetchall()]


def insert_vps(vps: dict) -> None:
    with get_db() as conn:
        conn.execute(
            "INSERT INTO vps (id, name, host, port, api_key, description) VALUES (?, ?, ?, ?, ?, ?)",
            (vps["id"], vps["name"], vps["host"], vps["port"], vps["api_key"], vps.get("description", "")),
        )


def remove_vps(vps_id: str) -> bool:
    with get_db() as conn:
        cur = conn.execute("DELETE FROM vps WHERE id = ?", (vps_id,))
        return cur.rowcount > 0


# ─── Auth helpers ─────────────────────────────────────────────────────────────

def create_token(username: str, role: str) -> str:
    payload = {
        "sub": username,
        "role": role,
        "exp": datetime.now(timezone.utc) + timedelta(minutes=JWT_EXPIRE_MIN),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def get_current_user(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)]
) -> dict:
    try:
        payload  = jwt.decode(credentials.credentials, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        username = payload.get("sub")
        if not username:
            raise HTTPException(status_code=401, detail="Token invalide")
    except JWTError:
        raise HTTPException(status_code=401, detail="Token invalide ou expiré")
    user = next((u for u in load_users() if u["username"] == username), None)
    if not user:
        raise HTTPException(status_code=401, detail="Utilisateur introuvable")
    return user


# ─── App ──────────────────────────────────────────────────────────────────────

app = FastAPI(title="VPS Monitor Backend", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=os.getenv("CORS_ORIGINS", "*").split(","),
    allow_methods=["*"],
    allow_headers=["*"],
)

# ─── Helpers HTTP ─────────────────────────────────────────────────────────────

async def agent_get(vps: dict, path: str):
    url     = f"http://{vps['host']}:{vps['port']}{path}"
    headers = {"X-API-Key": vps["api_key"]}
    timeout = aiohttp.ClientTimeout(total=AGENT_TIMEOUT)
    async with aiohttp.ClientSession() as session:
        async with session.get(url, headers=headers, timeout=timeout) as r:
            r.raise_for_status()
            return await r.json()


async def agent_post(vps: dict, path: str, payload: dict | None = None):
    url     = f"http://{vps['host']}:{vps['port']}{path}"
    headers = {"X-API-Key": vps["api_key"]}
    timeout = aiohttp.ClientTimeout(total=AGENT_TIMEOUT)
    async with aiohttp.ClientSession() as session:
        async with session.post(url, headers=headers, json=payload, timeout=timeout) as r:
            r.raise_for_status()
            return await r.json()


async def fetch_vps_status(vps: dict) -> dict:
    """Interroge un agent et retourne son état complet."""
    try:
        containers = await agent_get(vps, "/containers")
        return {
            "id":          vps["id"],
            "name":        vps["name"],
            "host":        vps["host"],
            "description": vps.get("description", ""),
            "online":      True,
            "containers":  containers,
        }
    except Exception as e:
        return {
            "id":          vps["id"],
            "name":        vps["name"],
            "host":        vps["host"],
            "description": vps.get("description", ""),
            "online":      False,
            "error":       str(e),
            "containers":  [],
        }


# ─── Routes Auth ──────────────────────────────────────────────────────────────

@app.get("/api/auth/status")
def auth_status():
    """Indique si des utilisateurs existent déjà (pour le frontend)."""
    return {"has_users": len(load_users()) > 0}


@app.post("/api/auth/register", status_code=201)
def register(body: RegisterRequest):
    """Enregistre le premier utilisateur (admin). Fermé ensuite."""
    if len(load_users()) > 0:
        raise HTTPException(
            status_code=403,
            detail="L'enregistrement public est désactivé. Seul l'admin peut créer des comptes."
        )
    if not body.username.strip() or len(body.password) < 6:
        raise HTTPException(status_code=422, detail="Mot de passe trop court (6 caractères min.)")
    user = {
        "username": body.username.strip(),
        "password": _bcrypt.hashpw(body.password.encode(), _bcrypt.gensalt()).decode(),
        "role":     "admin",
    }
    add_user(user)
    token = create_token(user["username"], user["role"])
    return {"access_token": token, "token_type": "bearer", "role": user["role"]}


@app.post("/api/auth/login")
def login(body: LoginRequest):
    """Authentifie un utilisateur et retourne un JWT."""
    users = load_users()
    user  = next((u for u in users if u["username"] == body.username), None)
    if not user or not _bcrypt.checkpw(body.password.encode(), user["password"].encode()):
        raise HTTPException(status_code=401, detail="Identifiants incorrects")
    token = create_token(user["username"], user["role"])
    return {"access_token": token, "token_type": "bearer", "role": user["role"]}


@app.get("/api/auth/me")
def me(current_user: Annotated[dict, Depends(get_current_user)]):
    return {"username": current_user["username"], "role": current_user["role"]}


# ─── Routes VPS ───────────────────────────────────────────────────────────────

@app.get("/api/vps")
def list_vps(_: Annotated[dict, Depends(get_current_user)]):
    """Liste les VPS configurés (sans les clés API)."""
    return [
        {"id": v["id"], "name": v["name"], "host": v["host"], "description": v.get("description", "")}
        for v in load_vps()
    ]


@app.post("/api/vps", status_code=201)
def add_vps(vps: VpsConfig, _: Annotated[dict, Depends(get_current_user)]):
    """Ajoute un nouveau VPS."""
    if any(v["id"] == vps.id for v in load_vps()):
        raise HTTPException(status_code=409, detail="Un VPS avec cet ID existe déjà")
    insert_vps(vps.model_dump())
    return {"status": "ok", "id": vps.id}


@app.delete("/api/vps/{vps_id}")
def delete_vps(vps_id: str, _: Annotated[dict, Depends(get_current_user)]):
    """Supprime un VPS de la configuration."""
    if not remove_vps(vps_id):
        raise HTTPException(status_code=404, detail="VPS introuvable")
    return {"status": "ok"}


@app.get("/api/status")
async def all_status(_: Annotated[dict, Depends(get_current_user)]):
    """Retourne l'état de tous les VPS en parallèle."""
    vps_list = load_vps()
    results  = await asyncio.gather(*[fetch_vps_status(v) for v in vps_list])
    return list(results)


@app.get("/api/vps/{vps_id}/status")
async def vps_status(vps_id: str, _: Annotated[dict, Depends(get_current_user)]):
    """Retourne l'état d'un VPS spécifique."""
    vps = next((v for v in load_vps() if v["id"] == vps_id), None)
    if not vps:
        raise HTTPException(status_code=404, detail="VPS introuvable")
    return await fetch_vps_status(vps)


@app.get("/api/vps/{vps_id}/containers/{container_id}/logs")
async def container_logs(
    vps_id: str, container_id: str, lines: int = 100,
    _: Annotated[dict, Depends(get_current_user)] = None
):
    """Récupère les logs d'un conteneur via l'agent."""
    vps = next((v for v in load_vps() if v["id"] == vps_id), None)
    if not vps:
        raise HTTPException(status_code=404, detail="VPS introuvable")
    try:
        return await agent_get(vps, f"/containers/{container_id}/logs?lines={lines}")
    except Exception as e:
        raise HTTPException(status_code=502, detail=str(e))


@app.post("/api/vps/{vps_id}/containers/{container_id}/action")
async def container_action(
    vps_id: str, container_id: str, body: ActionRequest,
    _: Annotated[dict, Depends(get_current_user)] = None
):
    """Effectue une action sur un conteneur via l'agent."""
    vps = next((v for v in load_vps() if v["id"] == vps_id), None)
    if not vps:
        raise HTTPException(status_code=404, detail="VPS introuvable")
    try:
        return await agent_post(vps, f"/containers/{container_id}/action?action={body.action}")
    except Exception as e:
        raise HTTPException(status_code=502, detail=str(e))
