from __future__ import annotations

import asyncio
import logging
from contextlib import asynccontextmanager
from datetime import timedelta
from pathlib import Path

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select, text
from starlette.concurrency import run_in_threadpool

from .api import agent_api, auth, computers, management, policies, requests_api
from .config import settings
from .database import SessionLocal, engine
from .middleware.ratelimit import RateLimitMiddleware
from .models import Base, Computer, utcnow
from .services.notify import notify
from .services.policy import effective_policy
from .websocket import handlers
from .websocket.manager import hub

log = logging.getLogger("cms")


def _sweep_sync() -> None:
    """Tugas berkala: tandai komputer offline yang heartbeat-nya berhenti, kedaluwarsakan permintaan, kirim ulang kebijakan jika jadwal berganti."""
    with SessionLocal() as db:
        limit = utcnow() - timedelta(seconds=settings.offline_after_seconds)
        for c in db.execute(select(Computer).where(Computer.is_online.is_(True), Computer.last_seen_at < limit)).scalars():
            if not hub.is_online(c.id):  # tidak ada koneksi aktif
                c.is_online, c.usage_state = False, "offline"
                db.commit()
                notify(db, "computer_offline", f"Komputer offline: {c.label or c.hostname}", None, severity="warning", computer=c)
        requests_api.expire_old(db)
        for c in db.execute(select(Computer).where(Computer.is_online.is_(True))).scalars():
            pol = effective_policy(db, c)
            if c.policy_hash != pol["version"] and hub.is_online(c.id):
                hub.send_agent_threadsafe(c.id, {"type": "policy_update", "policy": pol})


async def _sweeper():
    while True:
        await asyncio.sleep(15)
        try:
            await run_in_threadpool(_sweep_sync)
        except Exception as e:  # pragma: no cover
            log.warning("sweeper: %s", e)


@asynccontextmanager
async def lifespan(app: FastAPI):
    hub.loop = asyncio.get_running_loop()
    if settings.create_tables_on_start:
        Base.metadata.create_all(engine)
    with SessionLocal() as db:
        db.execute(text("SELECT 1"))
        auth.ensure_setup_code(db)
        for c in db.execute(select(Computer).where(Computer.is_online.is_(True))).scalars():
            c.is_online, c.usage_state = False, "offline"  # agent akan menyambung ulang
        db.commit()
    task = asyncio.create_task(_sweeper())
    yield
    task.cancel()


app = FastAPI(title="Classroom Management System", version="1.0.0", lifespan=lifespan)
app.add_middleware(RateLimitMiddleware)
for r in (auth.router, computers.router, policies.router, requests_api.router, management.router, agent_api.router, handlers.router):
    app.include_router(r)


@app.get("/api/health")
def health():
    with SessionLocal() as db:
        db.execute(text("SELECT 1"))
    return {"status": "ok", "agents_online": len(hub.agents), "dashboards": len(hub.dashboards)}


@app.exception_handler(RequestValidationError)
async def validation_handler(_, exc: RequestValidationError):
    msgs = []
    for e in exc.errors():
        loc = ".".join(str(x) for x in e["loc"] if x not in ("body", "query"))
        msgs.append(f"{loc}: {e['msg']}")
    return JSONResponse({"detail": "; ".join(msgs)}, status_code=422)


# ---- Unduhan paket agent (hanya untuk administrator lewat dashboard; tidak ada rahasia di dalamnya) ---
@app.get("/api/server-info")
def server_info():
    return {"name": "Classroom Management System", "version": "1.0.0", "https": bool(settings.ssl_certfile)}


# ---- Frontend statis (hasil build React) --------------------------------------------------------------
_dist = Path(settings.frontend_dist)
if _dist.exists():
    app.mount("/assets", StaticFiles(directory=_dist / "assets"), name="assets")

    @app.get("/{path:path}", include_in_schema=False)
    def spa(path: str):
        if path.startswith(("api/", "ws/")):
            raise HTTPException(404)
        f = (_dist / path).resolve()
        if path and f.is_file() and _dist.resolve() in f.parents:
            return FileResponse(f)
        return FileResponse(_dist / "index.html")
