import asyncio
import json
import os
import re
import secrets
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import date, datetime
from datetime import time as dt_time
from io import BytesIO
from threading import Lock

import cv2
import face_recognition
import numpy as np
import pytz
from fastapi.responses import (
    HTMLResponse,
    RedirectResponse,
    Response,
    StreamingResponse,
)
from PIL import Image
from supabase import create_client

from fastapi import Cookie, FastAPI, Request, WebSocket, WebSocketDisconnect

TIMEZONE = pytz.timezone("Asia/Jakarta")  # Sesuaikan timezone Anda
OPERATION_START = dt_time(6, 0)  # 06:00
OPERATION_END = dt_time(8, 0)  # 08:00


def is_operation_hours() -> bool:
    """Cek apakah sekarang dalam jam operasional"""
    now = datetime.now(TIMEZONE).time()
    return OPERATION_START <= now <= OPERATION_END


# ============================================================
# CONFIG
# ============================================================
SUPABASE_URL = "https://vtuamuvlfnouzekjhtsk.supabase.co"
SUPABASE_KEY = "sb_publishable_0LJO9qBDgV29zXMPaS44Iw_3d4GP9uw"
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)

ADMIN_EMAIL = "admin@example.com"
ADMIN_PASSWORD = "admin123"
active_sessions: set[str] = set()

ENROLL_TARGET = 10
FACES_DIR = "faces"
THRESHOLD = 0.55

os.makedirs(FACES_DIR, exist_ok=True)

face_cascade = cv2.CascadeClassifier(
    cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)

# Thread pool untuk jalankan blocking calls (face_recognition, supabase)
executor = ThreadPoolExecutor(max_workers=2)

app = FastAPI()


# ============================================================
# FRAME BUFFER — simpan frame terbaru di memory
# ============================================================
class FrameBuffer:
    """
    Simpan frame terbaru di memory.
    Thread-safe pakai Lock.
    """

    def __init__(self):
        self.frame: np.ndarray | None = None  # raw frame dari ESP32
        self.display_frame: np.ndarray | None = None  # frame + bounding box
        self.faces = []  # list (x,y,w,h)
        self.face_crops: list[np.ndarray] = []  # cropped wajah
        self.lock = Lock()
        self.jpeg_bytes: bytes | None = None  # encoded JPEG untuk stream

    def update(self, frame, display_frame, faces, face_crops, jpeg_bytes):
        with self.lock:
            self.frame = frame
            self.display_frame = display_frame
            self.faces = faces
            self.face_crops = face_crops
            self.jpeg_bytes = jpeg_bytes

    def get_latest_crops(self) -> tuple[list, list]:
        """Ambil snapshot face_crops + faces saat ini"""
        with self.lock:
            return list(self.faces), [c.copy() for c in self.face_crops]

    def get_latest_frame(self) -> np.ndarray | None:
        """Ambil snapshot raw frame saat ini"""
        with self.lock:
            return self.frame.copy() if self.frame is not None else None

    def get_jpeg(self) -> bytes | None:
        with self.lock:
            return self.jpeg_bytes


buffer = FrameBuffer()


# ============================================================
# KNOWN FACES — cache di memory
# ============================================================
KNOWN_ENCODINGS: list[np.ndarray] = []
KNOWN_USERS: list[dict] = []  # [{"user_id": ..., "user_name": ...}]
faces_lock = Lock()  # lock untuk update KNOWN


def load_all_faces():
    """Load semua enrolled faces dari DB + file .npy"""
    global KNOWN_ENCODINGS, KNOWN_USERS

    try:
        users = (
            supabase.table("users")
            .select("id, user_name, face_file")
            .eq("is_enrolled", True)
            .execute()
        )

        new_encodings = []
        new_users = []

        for u in users.data:
            if not u["face_file"]:
                continue
            path = os.path.join(FACES_DIR, u["face_file"])
            if not os.path.exists(path):
                print(f"⚠️  File tidak ditemukan: {path}")
                continue

            encs = np.load(path)
            if encs.ndim == 1:
                encs = encs.reshape(1, -1)

            for e in encs:
                new_encodings.append(e)
                new_users.append({"user_id": u["id"], "user_name": u["user_name"]})

        with faces_lock:
            KNOWN_ENCODINGS = new_encodings
            KNOWN_USERS = new_users

        print(
            f"✅ Loaded {len(new_encodings)} encodings, {len(set(u['user_name'] for u in new_users))} user"
        )

    except Exception as e:
        print(f"❌ load_all_faces error: {e}")


# Load saat startup
load_all_faces()


# ============================================================
# UTILS
# ============================================================
def normalize_name(name: str) -> str:
    return re.sub(r"[^a-z0-9_]", "_", name.strip().lower())


def is_valid_image(image_bytes: bytes) -> bool:
    try:
        Image.open(BytesIO(image_bytes)).verify()
        return True
    except Exception:
        return False


def encode_to_jpeg(frame: np.ndarray, quality: int = 85) -> bytes | None:
    success, encoded = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
    return encoded.tobytes() if success else None


# ============================================================
# FACE DETECTION — cepat, pakai opencv
# ============================================================
def detect_faces(frame: np.ndarray):
    """
    Deteksi wajah + crop.
    Ini CEPAT (~5-15ms), aman di main loop.
    """
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=4)

    face_crops = []
    for x, y, w, h in faces:
        pad = int(0.2 * w)
        x1 = max(0, x - pad)
        y1 = max(0, y - pad)
        x2 = min(frame.shape[1], x + w + pad)
        y2 = min(frame.shape[0], y + h + pad)
        face_crops.append(frame[y1:y2, x1:x2])

    return faces, face_crops


def draw_boxes(
    frame: np.ndarray, faces, labels: list[tuple[str, tuple]] | None = None
) -> np.ndarray:
    """
    Draw bounding box. Kalau labels None, box kuning polos.
    labels: list of (text, color_bgr)
    """
    display = frame.copy()
    for i, (x, y, w, h) in enumerate(faces):
        if labels and i < len(labels):
            text, color = labels[i]
        else:
            text, color = "", (0, 255, 255)  # kuning default

        cv2.rectangle(display, (x, y), (x + w, y + h), color, 2)
        if text:
            cv2.putText(
                display,
                text,
                (x, max(y - 10, 15)),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.6,
                color,
                2,
            )
    return display


# ============================================================
# FACE RECOGNITION — SLOW, jalankan di thread
# ============================================================
def identify_face_sync(face_bgr: np.ndarray) -> dict:
    """
    Identify 1 wajah. BLOCKING — panggil via asyncio.to_thread().
    """
    with faces_lock:
        if len(KNOWN_ENCODINGS) == 0:
            return {"status": "unknown"}
        encodings_snapshot = list(KNOWN_ENCODINGS)
        users_snapshot = list(KNOWN_USERS)

    face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB)
    encodings = face_recognition.face_encodings(face_rgb)

    if len(encodings) == 0:
        return {"status": "unknown"}

    unknown = encodings[0]
    distances = face_recognition.face_distance(np.array(encodings_snapshot), unknown)

    best_idx = int(np.argmin(distances))
    best_distance = float(distances[best_idx])

    if best_distance <= THRESHOLD:
        user = users_snapshot[best_idx]
        return {
            "status": "recognized",
            "user_id": user["user_id"],
            "name": user["user_name"],
            "distance": round(best_distance, 4),
        }

    return {"status": "unknown"}


def run_recognition_sync(face_crops: list[np.ndarray]) -> list[dict]:
    """
    Recognize semua wajah. BLOCKING — jalankan di thread.
    """
    results = []
    for crop in face_crops:
        result = identify_face_sync(crop)
        results.append(result)
    return results


def record_attendance_sync(user_id: str, user_name: str) -> dict:
    """
    Record attendance ke DB. BLOCKING — jalankan di thread.
    Cek duplikat di DB langsung.
    """
    today = date.today()
    now = datetime.now().time()

    try:
        existing = (
            supabase.table("attendance")
            .select("id")
            .eq("user_id", user_id)
            .eq("checkin_date", today.isoformat())
            .execute()
        )

        if existing.data:
            return {"status": "already_checked_in", "date": today.isoformat()}

        supabase.table("attendance").insert(
            {
                "user_id": user_id,
                "user_name": user_name,
                "checkin_date": today.isoformat(),
                "checkin_time": now.strftime("%H:%M:%S"),
            }
        ).execute()

        return {
            "status": "checked_in",
            "date": today.isoformat(),
            "time": now.strftime("%H:%M:%S"),
        }
    except Exception as e:
        print(f"❌ Attendance error: {e}")
        return {"status": "error", "message": str(e)}


def do_recognition_and_attendance(face_crops: list[np.ndarray]) -> list[dict]:
    """
    Gabung: recognition + attendance dalam 1 blocking function.
    Panggil via asyncio.to_thread().
    """
    results = run_recognition_sync(face_crops)

    # Record attendance untuk yang berhasil dikenali
    for r in results:
        if r["status"] == "recognized":
            r["attendance"] = record_attendance_sync(r["user_id"], r["name"])

    return results


def do_enroll_sync(face_crop: np.ndarray, user_id: str, user_name: str) -> bool:
    """
    Enroll 1 wajah + update DB. BLOCKING — panggil via asyncio.to_thread().
    """
    face_rgb = cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB)

    # Deteksi ulang pakai face_recognition untuk akurasi
    face_locations = face_recognition.face_locations(face_rgb)
    if len(face_locations) != 1:
        print(f"⚠️  Enroll gagal: ditemukan {len(face_locations)} wajah")
        return False

    encodings = face_recognition.face_encodings(face_rgb, face_locations)
    if not encodings:
        print("⚠️  Enroll gagal: tidak bisa generate encoding")
        return False

    new_encoding = encodings[0]

    # Save ke .npy
    safe_name = normalize_name(user_name)
    filename = f"{user_id}_{safe_name}.npy"
    path = os.path.join(FACES_DIR, filename)

    if os.path.exists(path):
        existing = np.load(path)
        if existing.ndim == 1:
            existing = existing.reshape(1, -1)
        all_enc = np.vstack([existing, new_encoding])
    else:
        all_enc = np.array([new_encoding])

    np.save(path, all_enc)
    print(f"✅ Enroll #{len(all_enc)} untuk '{user_name}'")

    # Update DB
    supabase.table("users").update({"face_file": filename, "is_enrolled": True}).eq(
        "id", user_id
    ).execute()

    return True


# ============================================================
# CONNECTION MANAGER
# ============================================================
class ConnectionManager:
    def __init__(self):
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)
        print(f"✅ WS connected (total: {len(self.active_connections)})")

    def disconnect(self, websocket: WebSocket):
        if websocket in self.active_connections:
            self.active_connections.remove(websocket)
            print(f"❌ WS disconnected (total: {len(self.active_connections)})")

    async def broadcast_text(self, message: str):
        dead = []
        for ws in self.active_connections:
            try:
                await ws.send_text(message)
            except Exception:
                dead.append(ws)
        for ws in dead:
            self.disconnect(ws)


manager = ConnectionManager()


# ============================================================
# SESSION — state untuk enroll
# ============================================================
SESSION = {
    "mode": None,  # "ENROLL" | None
    "user_id": None,
    "user_name": None,
    "enroll_count": 0,
    "capture_requested": False,
    "processing": False,  # anti double-process
}


def reset_session():
    SESSION["mode"] = None
    SESSION["user_id"] = None
    SESSION["user_name"] = None
    SESSION["enroll_count"] = 0
    SESSION["capture_requested"] = False
    SESSION["processing"] = False


async def schedule_broadcaster():
    """
    Background task yang jalan terus untuk broadcast schedule changes.
    Cek waktu tiap 30 detik, broadcast saat transisi.
    """
    last_state = is_operation_hours()

    while True:
        await asyncio.sleep(30)  # Cek tiap 30 detik

        current_state = is_operation_hours()

        # Kalau ada perubahan state, broadcast
        if current_state != last_state:
            message = "itsWorkingTime" if current_state else "notWorkingTime"
            await manager.broadcast_text(message)
            print(f"📢 Schedule broadcast: {message}")
            last_state = current_state


# ============================================================
# WEBSOCKET — 2 endpoint terpisah
# ============================================================

# --- 1. STREAM WEBSOCKET (ESP32 kirim frame) ---
# ESP32 connect ke /ws/stream, kirim bytes terus-terusan
# Server: decode → detect → draw box → save ke buffer
# TIDAK ada blocking call di sini


@app.websocket("/ws/stream")
async def ws_stream(websocket: WebSocket):
    await manager.connect(websocket)

    try:
        while True:
            message = await websocket.receive()

            if message["type"] == "websocket.disconnect":
                break

            # ===== TERIMA BYTES (frame dari ESP32) =====
            if message["type"] == "websocket.receive" and "bytes" in message:
                raw = message["bytes"]
                if not raw:
                    continue

                # Decode
                img_array = np.frombuffer(raw, dtype=np.uint8)
                frame = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
                if frame is None:
                    continue

                # Deteksi wajah (cepat, opencv)
                faces, face_crops = detect_faces(frame)

                # Draw box kuning di semua wajah (default, tanpa label)
                display = draw_boxes(frame, faces)

                # Encode ke JPEG
                jpeg = encode_to_jpeg(display)

                # Update buffer
                buffer.update(frame, display, faces, face_crops, jpeg)

                # Kalau sedang enroll dan capture_requested, trigger proses
                if (
                    SESSION["mode"] == "ENROLL"
                    and SESSION["capture_requested"]
                    and not SESSION["processing"]
                ):
                    asyncio.create_task(process_enroll_capture())

            # ===== TERIMA TEXT (commands dari ESP32) =====
            if message["type"] == "websocket.receive" and "text" in message:
                text = message["text"].strip()

                # Query: isWorkingTime?
                if text == "isWorkingTime?":
                    response = (
                        "itsWorkingTime" if is_operation_hours() else "notWorkingTime"
                    )
                    await websocket.send_text(response)
                    print(f"📞 ESP32 query working time → {response}")
                    continue

                # Command: stop
                if text == "stop":
                    reset_session()
                    await manager.broadcast_text(json.dumps({"type": "stop"}))
                    continue

                # Command: capture (untuk enroll)
                if text == "capture" and SESSION["mode"] == "ENROLL":
                    SESSION["capture_requested"] = True
                    print("📸 Capture requested dari ESP32")
                    continue

    except WebSocketDisconnect:
        pass
    finally:
        manager.disconnect(websocket)


# --- 2. COMMAND WEBSOCKET (Dashboard / browser) ---
# Dashboard connect ke /ws/cmd, kirim command text
# Server: proses command, broadcast hasil


@app.websocket("/ws/cmd")
async def ws_cmd(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            message = await websocket.receive()

            if message["type"] == "websocket.disconnect":
                break

            if message["type"] == "websocket.receive" and "text" in message:
                text = message["text"].strip()
                print(f"📨 CMD: '{text}'")

                # ---- STOP ----
                if text == "stop":
                    reset_session()
                    await manager.broadcast_text(json.dumps({"type": "stop"}))
                    continue
                if text == "isWorkingTime?":
                    response = (
                        "itsWorkingTime" if is_operation_hours() else "notWorkingTime"
                    )
                    await websocket.send_text(response)
                    print(f"📞 ESP32 query working time → {response}")
                    continue
                if text == "ForceStart":
                    await manager.broadcast_text(
                        json.dumps(
                            {
                                "type": "command",
                                "target": "esp32",
                                "action": "START",
                            }
                        )
                    )
                    print(f"📞 ESP32 ForceStart command received")
                    continue

                if text == "ForceStop":
                    await manager.broadcast_text(
                        json.dumps(
                            {
                                "type": "command",
                                "target": "esp32",
                                "action": "STOP",
                            }
                        )
                    )
                    print(f"📞 ESP32 ForceStop command received")
                    continue
                # ---- RECOGNIZE ----
                if text == "recognize":
                    if not is_operation_hours():
                        await manager.broadcast_text(
                            json.dumps(
                                {
                                    "type": "command",
                                    "target": "esp32",
                                    "action": "TEMP_START",
                                    "timeout": 10,  # FIX: Tambahkan ini!
                                }
                            )
                        )

                        print("⏳ Waiting for stream to start...")
                        await asyncio.sleep(2.5)
                    # Ambil crops dari buffer terkini
                    faces, face_crops = buffer.get_latest_crops()

                    if len(face_crops) == 0:
                        await websocket.send_text(
                            json.dumps(
                                {
                                    "type": "recognize_result",
                                    "status": "no_face",
                                    "message": "Tidak ada wajah terdeteksi",
                                }
                            )
                        )
                        if not is_operation_hours():
                            print("🛑 Stopping temporary stream (no face detected)")
                            await manager.broadcast_text(
                                json.dumps(
                                    {
                                        "type": "command",
                                        "target": "esp32",
                                        "action": "STOP",
                                    }
                                )
                            )
                        continue

                    # Jalankan di background thread
                    await websocket.send_text(
                        json.dumps(
                            {"type": "recognize_status", "message": "Memproses..."}
                        )
                    )

                    asyncio.create_task(run_recognize_task(faces, face_crops))
                    continue

                # ---- JSON COMMAND ----
                try:
                    data = json.loads(text)
                except json.JSONDecodeError:
                    continue

                cmd = data.get("command")

                # ---- START ENROLL ----
                if cmd == "start_enroll":
                    nama = (data.get("nama") or "").strip()
                    if not nama:
                        await websocket.send_text(
                            json.dumps(
                                {"type": "error", "message": "Nama tidak boleh kosong"}
                            )
                        )
                        continue

                    if not re.match(r"^[a-zA-Z0-9_ ]+$", nama):
                        await websocket.send_text(
                            json.dumps({"type": "error", "message": "Nama tidak valid"})
                        )
                        continue

                    # Get or create user di DB
                    try:
                        user, created = await asyncio.to_thread(
                            get_or_create_user_sync, nama
                        )
                    except Exception as e:
                        await websocket.send_text(
                            json.dumps({"type": "error", "message": str(e)})
                        )
                        continue

                    # Cek apakah sudah fully enrolled
                    face_file = user.get("face_file")
                    if face_file:
                        npy_path = os.path.join(FACES_DIR, face_file)
                        if os.path.isfile(npy_path):
                            existing = np.load(npy_path)
                            if existing.ndim == 1:
                                existing = existing.reshape(1, -1)
                            if len(existing) >= ENROLL_TARGET:
                                await websocket.send_text(
                                    json.dumps(
                                        {
                                            "type": "error",
                                            "message": f"'{nama}' sudah ter-enroll ({len(existing)} encodings)",
                                        }
                                    )
                                )
                                continue
                    # npy_path = os.path.join(FACES_DIR, user.get("face_file") or "")
                    # if os.path.exists(npy_path):
                    #     existing = np.load(npy_path)
                    #     if existing.ndim == 1:
                    #         existing = existing.reshape(1, -1)
                    #     if len(existing) >= ENROLL_TARGET:
                    #         await websocket.send_text(json.dumps({
                    #             "type": "error",
                    #             "message": f"'{nama}' sudah ter-enroll ({len(existing)} encodings)"
                    #         }))
                    #         continue

                    reset_session()
                    SESSION["mode"] = "ENROLL"
                    SESSION["user_id"] = user["id"]
                    SESSION["user_name"] = nama
                    SESSION["enroll_count"] = 0

                    await manager.broadcast_text(
                        json.dumps(
                            {
                                "type": "enroll_started",
                                "name": nama,
                                "count": 0,
                                "total": ENROLL_TARGET,
                            }
                        )
                    )
                    print(f"🔵 Enroll started: '{nama}'")

                # ---- CAPTURE (dari dashboard) ----
                elif cmd == "capture":
                    if SESSION["mode"] == "ENROLL":
                        SESSION["capture_requested"] = True
                        print("📸 Capture requested dari dashboard")

                # ---- STOP ENROLL ----
                elif cmd == "stop_enroll":
                    reset_session()
                    await manager.broadcast_text(json.dumps({"type": "stop"}))

    except WebSocketDisconnect:
        pass
    finally:
        manager.disconnect(websocket)


# ============================================================
# BACKGROUND TASKS
# ============================================================


async def run_recognize_task(faces, face_crops: list[np.ndarray]):
    """Recognition + attendance di background thread, broadcast hasilnya"""
    try:
        # Jalankan blocking calls di thread pool
        results = await asyncio.to_thread(do_recognition_and_attendance, face_crops)

        # Build labels untuk draw
        labels = []
        for r in results:
            if r["status"] == "recognized":
                labels.append((r["name"], (0, 255, 0)))  # hijau
            else:
                labels.append(("Unknown", (0, 0, 255)))  # merah

        # Draw di frame terbaru dan update buffer display
        frame = buffer.get_latest_frame()
        if frame is not None:
            labeled = draw_boxes(frame, faces, labels)
            jpeg = encode_to_jpeg(labeled)
            # Update buffer display sementara (akan ditimpa frame berikutnya)
            with buffer.lock:
                buffer.display_frame = labeled
                buffer.jpeg_bytes = jpeg

        # Broadcast hasil
        await manager.broadcast_text(
            json.dumps({"type": "recognize_result", "faces": results})
        )
        print(f"🎯 Recognize done: {[r.get('name', 'unknown') for r in results]}")
        if not is_operation_hours():
            await manager.broadcast_text(
                json.dumps(
                    {
                        "type": "command",
                        "target": "esp32",
                        "action": "STOP",
                    }
                )
            )

    except Exception as e:
        print(f"❌ Recognition error: {e}")
        await manager.broadcast_text(
            json.dumps({"type": "error", "message": f"Recognition error: {str(e)}"})
        )
        if not is_operation_hours():
            await manager.broadcast_text(
                json.dumps(
                    {
                        "type": "command",
                        "target": "esp32",
                        "action": "STOP",
                    }
                )
            )


async def process_enroll_capture():
    """Proses 1 enroll capture di background"""
    SESSION["processing"] = True
    SESSION["capture_requested"] = False

    try:
        _, face_crops = buffer.get_latest_crops()

        if len(face_crops) != 1:
            await manager.broadcast_text(
                json.dumps(
                    {
                        "type": "enroll_warning",
                        "message": f"Ditemukan {len(face_crops)} wajah. Harus tepat 1.",
                    }
                )
            )
            return

        # Jalankan enroll di thread (blocking: face_recognition + DB)
        success = await asyncio.to_thread(
            do_enroll_sync, face_crops[0], SESSION["user_id"], SESSION["user_name"]
        )

        if not success:
            await manager.broadcast_text(
                json.dumps(
                    {"type": "enroll_warning", "message": "Gagal enroll. Coba lagi."}
                )
            )
            return

        SESSION["enroll_count"] += 1
        count = SESSION["enroll_count"]
        print(f"📸 Enroll progress: {count}/{ENROLL_TARGET}")

        # Kirim progress
        await manager.broadcast_text(
            json.dumps(
                {
                    "type": "enroll_progress",
                    "count": count,
                    "total": ENROLL_TARGET,
                    "name": SESSION["user_name"],
                }
            )
        )

        # Cek selesai
        if count >= ENROLL_TARGET:
            nama = SESSION["user_name"]
            reset_session()

            # Reload faces di memory
            await asyncio.to_thread(load_all_faces)

            await manager.broadcast_text(
                json.dumps(
                    {
                        "type": "enroll_done",
                        "name": nama,
                        "message": f"Enroll '{nama}' selesai!",
                    }
                )
            )
            await manager.broadcast_text(json.dumps({"type": "stop"}))
            print(f"✅ Enroll selesai: '{nama}'")

    except Exception as e:
        print(f"❌ Enroll error: {e}")
        await manager.broadcast_text(
            json.dumps({"type": "error", "message": f"Enroll error: {str(e)}"})
        )
    finally:
        SESSION["processing"] = False


# ============================================================
# SUPABASE HELPERS (sync, untuk di-thread)
# ============================================================
def get_or_create_user_sync(user_name: str) -> tuple[dict, bool]:
    existing = supabase.table("users").select("*").eq("user_name", user_name).execute()
    if existing.data:
        return existing.data[0], False

    res = (
        supabase.table("users")
        .insert({"user_name": user_name, "is_enrolled": False, "face_file": None})
        .execute()
    )

    if not res.data:
        raise RuntimeError("Gagal membuat user")
    return res.data[0], True


# ============================================================
# MJPEG STREAM — browser/TFT baca dari buffer
# ============================================================
@app.get("/stream")
def stream_video():
    def generator():
        while True:
            jpeg = buffer.get_jpeg()
            if jpeg:
                yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n")
            time.sleep(0.05)  # ~20 FPS max

    return StreamingResponse(
        generator(), media_type="multipart/x-mixed-replace; boundary=frame"
    )


@app.get("/image")
def get_image():
    jpeg = buffer.get_jpeg()
    if jpeg:
        return Response(content=jpeg, media_type="image/jpeg")
    # Fallback blank
    blank = Image.new("RGB", (320, 240), color=(20, 20, 30))
    buf = BytesIO()
    blank.save(buf, "JPEG")
    return Response(content=buf.getvalue(), media_type="image/jpeg")


# ============================================================
# AUTH
# ============================================================
@app.post("/auth/login")
async def login(request: Request):
    body = await request.json()
    if body.get("email") == ADMIN_EMAIL and body.get("password") == ADMIN_PASSWORD:
        token = secrets.token_hex(32)
        active_sessions.add(token)
        resp = Response(
            content=json.dumps({"status": "success"}), media_type="application/json"
        )
        resp.set_cookie(
            "session_token", token, httponly=True, samesite="lax", max_age=86400
        )
        return resp
    return Response(
        content=json.dumps({"status": "error", "message": "Invalid credentials"}),
        status_code=401,
        media_type="application/json",
    )


@app.post("/auth/logout")
async def logout(session_token: str = Cookie(default=None)):
    if session_token:
        active_sessions.discard(session_token)
    resp = RedirectResponse(url="/login", status_code=303)
    resp.delete_cookie("session_token")
    return resp


@app.get("/auth/check")
async def check_auth(session_token: str = Cookie(default=None)):
    if session_token and session_token in active_sessions:
        return {"authenticated": True}
    return Response(
        content=json.dumps({"authenticated": False}),
        status_code=401,
        media_type="application/json",
    )


# app = FastAPI()


html_rekap = """
    <!DOCTYPE html>
    <html lang="id">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Rekap Absensi Bulanan</title>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Syne:wght@700&display=swap');

            * { margin: 0; padding: 0; box-sizing: border-box; }

            body {
                font-family: 'DM Sans', sans-serif;
                background: #0a0a0f;
                color: #e2e2e5;
                min-height: 100vh;
                padding: 30px 20px;
            }

            .topbar {
                max-width: 900px;
                margin: 0 auto 30px;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }

            .topbar h1 {
                font-family: 'Syne', sans-serif;
                font-size: 22px;
                letter-spacing: -0.5px;
            }

            .admin-link {
                font-size: 13px;
                color: #555;
                text-decoration: none;
                padding: 8px 14px;
                border: 1px solid #222;
                border-radius: 6px;
                transition: all 0.2s;
            }

            .admin-link:hover {
                border-color: #444;
                color: #999;
            }

            .controls {
                max-width: 900px;
                margin: 0 auto 20px;
                display: flex;
                align-items: center;
                gap: 12px;
            }

            .controls label {
                font-size: 14px;
                color: #666;
            }

            select {
                padding: 8px 12px;
                background: #13131a;
                color: #e2e2e5;
                border: 1px solid #222;
                border-radius: 6px;
                cursor: pointer;
                font-size: 14px;
                font-family: inherit;
                min-width: 180px;
                outline: none;
                transition: border-color 0.2s;
            }

            select:focus { border-color: #444; }

            .table-wrapper {
                max-width: 900px;
                margin: 0 auto;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 10px;
                overflow: hidden;
            }

            table { width: 100%; border-collapse: collapse; }

            th {
                background: #0f0f14;
                padding: 12px 16px;
                text-align: left;
                font-size: 11px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: 1px;
                color: #555;
                border-bottom: 1px solid #1e1e28;
            }

            td {
                padding: 14px 16px;
                border-bottom: 1px solid #1a1a22;
                font-size: 14px;
            }

            tr:last-child td { border-bottom: none; }

            tbody tr {
                cursor: pointer;
                transition: background 0.15s;
            }

            tbody tr:hover { background: #1a1a24; }

            .name-cell {
                color: #7eaaff;
                font-weight: 500;
            }

            .total-cell {
                font-weight: 600;
                color: #4ade80;
                text-align: center;
            }

            .summary {
                max-width: 900px;
                margin: 14px auto 0;
                font-size: 13px;
                color: #444;
                text-align: right;
            }

            .hint {
                max-width: 900px;
                margin: 10px auto 0;
                font-size: 12px;
                color: #333;
            }

            .empty-cell, .loading-cell {
                text-align: center;
                color: #444;
                padding: 40px;
                font-style: italic;
            }
        </style>
    </head>
    <body>

    <div class="topbar">
        <h1>📊 Rekap Absensi Bulanan</h1>
        <a href="/dashboard" class="admin-link">⚙️ Admin</a>
    </div>

    <div class="controls">
        <label for="month">Bulan:</label>
        <select id="month"></select>
    </div>

    <div class="table-wrapper">
        <table>
            <thead>
                <tr>
                    <th>Nama</th>
                    <th>Bulan</th>
                    <th style="text-align:center;">Total Absen</th>
                </tr>
            </thead>
            <tbody id="result-body">
                <tr><td colspan="3" class="loading-cell">Memuat data...</td></tr>
            </tbody>
        </table>
    </div>

    <div id="summary" class="summary" style="display:none;"></div>
    <div class="hint">💡 Klik pada nama untuk melihat detail absensi</div>

    <script>
        const SUPABASE_URL = "https://vtuamuvlfnouzekjhtsk.supabase.co";
        const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ0dWFtdXZsZm5vdXpla2podHNrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY3NTkyMzksImV4cCI6MjA4MjMzNTIzOX0.wjWTw3kQVxpcj95LGsV9Ti3NrjGjFaqdmsqhROjLBWY";
        const TABLE = "attendance";
        const monthSelect = document.getElementById("month");

        function initMonthOptions() {
            const now = new Date();
            for (let i = 0; i < 12; i++) {
                const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
                const year = d.getFullYear();
                const month = String(d.getMonth() + 1).padStart(2, '0');
                const value = `${year}-${month}`;
                const label = d.toLocaleString("id-ID", { month: "long", year: "numeric" });
                const opt = document.createElement("option");
                opt.value = value;
                opt.textContent = label;
                monthSelect.appendChild(opt);
            }
        }

        async function loadMonthlyAttendance(month) {
            try {
                document.getElementById("result-body").innerHTML =
                    "<tr><td colspan='3' class='loading-cell'>Memuat data...</td></tr>";

                const [year, monthNum] = month.split("-");
                const start = `${year}-${monthNum}-01`;
                let nextMonthInt = parseInt(monthNum) + 1;
                let nextYear = year;
                let nextMonth = String(nextMonthInt).padStart(2, '0');
                if (nextMonthInt > 12) { nextMonth = '01'; nextYear = parseInt(year) + 1; }

                const url =
                    `${SUPABASE_URL}/rest/v1/${TABLE}` +
                    `?checkin_date=gte.${start}` +
                    `&checkin_date=lt.${nextYear}-${nextMonth}-01` +
                    `&select=user_name,checkin_date`;

                const res = await fetch(url, {
                    headers: {
                        apikey: SUPABASE_ANON_KEY,
                        Authorization: `Bearer ${SUPABASE_ANON_KEY}`
                    }
                });

                if (!res.ok) throw new Error(`HTTP ${res.status}`);
                const data = await res.json();

                const counts = {};
                data.forEach(row => {
                    if (row.user_name) counts[row.user_name] = (counts[row.user_name] || 0) + 1;
                });

                renderTable(counts, month);
            } catch (error) {
                document.getElementById("result-body").innerHTML =
                    `<tr><td colspan='3' class='empty-cell' style='color:#f44336;'>Gagal memuat data</td></tr>`;
            }
        }

        function renderTable(counts, month) {
            const tbody = document.getElementById("result-body");
            const summaryDiv = document.getElementById("summary");
            tbody.innerHTML = "";

            if (Object.keys(counts).length === 0) {
                tbody.innerHTML = "<tr><td colspan='3' class='empty-cell'>Tidak ada data untuk bulan ini</td></tr>";
                summaryDiv.style.display = "none";
                return;
            }

            const sorted = Object.entries(counts).sort((a, b) => a[0].localeCompare(b[0], 'id'));
            const [year, monthNum] = month.split("-");
            const monthName = new Date(year, parseInt(monthNum) - 1).toLocaleString("id-ID", { month: "long", year: "numeric" });

            let total = 0;
            sorted.forEach(([name, count]) => {
                total += count;
                const tr = document.createElement("tr");
                tr.innerHTML = `<td class="name-cell">${name}</td><td>${monthName}</td><td class="total-cell">${count}</td>`;
                tr.onclick = () => window.location.href = `/detail?name=${encodeURIComponent(name)}&month=${month}`;
                tbody.appendChild(tr);
            });

            summaryDiv.innerHTML = `${sorted.length} karyawan &middot; ${total} total absensi`;
            summaryDiv.style.display = "block";
        }

        monthSelect.addEventListener("change", () => loadMonthlyAttendance(monthSelect.value));
        initMonthOptions();
        loadMonthlyAttendance(monthSelect.value);
    </script>
    </body>
    </html>
    """

# ================= HALAMAN 2: DETAIL ABSENSI =================
html_detail = """
    <!DOCTYPE html>
    <html lang="id">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Detail Absensi</title>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Syne:wght@700&display=swap');

            * { margin: 0; padding: 0; box-sizing: border-box; }

            body {
                font-family: 'DM Sans', sans-serif;
                background: #0a0a0f;
                color: #e2e2e5;
                min-height: 100vh;
                padding: 30px 20px;
            }

            .topbar {
                max-width: 900px;
                margin: 0 auto 24px;
                display: flex;
                align-items: center;
                gap: 14px;
            }

            .back-btn {
                font-size: 13px;
                color: #666;
                text-decoration: none;
                padding: 7px 12px;
                border: 1px solid #222;
                border-radius: 6px;
                transition: all 0.2s;
                white-space: nowrap;
            }

            .back-btn:hover { border-color: #444; color: #999; }

            .topbar h1 {
                font-family: 'Syne', sans-serif;
                font-size: 20px;
            }

            .info-card {
                max-width: 900px;
                margin: 0 auto 20px;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 10px;
                padding: 18px 20px;
                display: flex;
                gap: 40px;
            }

            .info-item label {
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: 1px;
                color: #444;
                display: block;
                margin-bottom: 4px;
            }

            .info-item span {
                font-size: 16px;
                font-weight: 600;
            }

            .info-item .name-val { color: #7eaaff; }
            .info-item .total-val { color: #4ade80; }

            .table-wrapper {
                max-width: 900px;
                margin: 0 auto;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 10px;
                overflow: hidden;
            }

            table { width: 100%; border-collapse: collapse; }

            th {
                background: #0f0f14;
                padding: 12px 16px;
                text-align: left;
                font-size: 11px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: 1px;
                color: #555;
                border-bottom: 1px solid #1e1e28;
            }

            td {
                padding: 13px 16px;
                border-bottom: 1px solid #1a1a22;
                font-size: 14px;
            }

            tr:last-child td { border-bottom: none; }

            .num-cell { color: #444; text-align: center; width: 50px; }
            .date-cell { color: #7eaaff; font-weight: 500; }
            .day-cell { color: #555; }
            .time-cell { color: #4ade80; }

            .weekend td { background: #111118; }
            .weekend .day-cell { color: #f06262; }

            .empty-cell {
                text-align: center;
                color: #444;
                padding: 40px;
                font-style: italic;
            }
        </style>
    </head>
    <body>

    <div class="topbar">
        <a href="/" class="back-btn">← Kembali</a>
        <h1>📋 Detail Absensi</h1>
    </div>

    <div class="info-card">
        <div class="info-item">
            <label>Nama</label>
            <span class="name-val" id="infoName">-</span>
        </div>
        <div class="info-item">
            <label>Bulan</label>
            <span id="infoMonth">-</span>
        </div>
        <div class="info-item">
            <label>Total Absensi</label>
            <span class="total-val" id="infoTotal">-</span>
        </div>
    </div>

    <div class="table-wrapper">
        <table>
            <thead>
                <tr>
                    <th style="text-align:center;">#</th>
                    <th>Tanggal</th>
                    <th>Hari</th>
                    <th>Check-in</th>
                </tr>
            </thead>
            <tbody id="result-body">
                <tr><td colspan="4" class="empty-cell">Memuat data...</td></tr>
            </tbody>
        </table>
    </div>

    <script>
        const SUPABASE_URL = "https://vtuamuvlfnouzekjhtsk.supabase.co";
        const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ0dWFtdXZsZm5vdXpla2podHNrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY3NTkyMzksImV4cCI6MjA4MjMzNTIzOX0.wjWTw3kQVxpcj95LGsV9Ti3NrjGjFaqdmsqhROjLBWY";
        const TABLE = "attendance";

        function getParams() {
            const p = new URLSearchParams(window.location.search);
            return { name: p.get('name'), month: p.get('month') };
        }

        function isWeekend(dateStr) {
            const d = new Date(dateStr + 'T00:00:00');
            return d.getDay() === 0 || d.getDay() === 6;
        }

        async function loadDetail() {
            const { name, month } = getParams();
            if (!name || !month) {
                document.getElementById("result-body").innerHTML =
                    "<tr><td colspan='4' class='empty-cell' style='color:#f44336;'>Parameter tidak valid</td></tr>";
                return;
            }

            document.getElementById("infoName").textContent = name;
            const [year, monthNum] = month.split("-");
            document.getElementById("infoMonth").textContent =
                new Date(year, parseInt(monthNum) - 1).toLocaleString("id-ID", { month: "long", year: "numeric" });

            try {
                let nextMonthInt = parseInt(monthNum) + 1;
                let nextYear = year;
                let nextMonth = String(nextMonthInt).padStart(2, '0');
                if (nextMonthInt > 12) { nextMonth = '01'; nextYear = parseInt(year) + 1; }

                const url =
                    `${SUPABASE_URL}/rest/v1/${TABLE}` +
                    `?user_name=eq.${encodeURIComponent(name)}` +
                    `&checkin_date=gte.${year}-${monthNum}-01` +
                    `&checkin_date=lt.${nextYear}-${nextMonth}-01` +
                    `&select=checkin_date,checkin_time` +
                    `&order=checkin_date.asc`;

                const res = await fetch(url, {
                    headers: {
                        apikey: SUPABASE_ANON_KEY,
                        Authorization: `Bearer ${SUPABASE_ANON_KEY}`
                    }
                });

                if (!res.ok) throw new Error(`HTTP ${res.status}`);
                const data = await res.json();

                renderDetail(data);
            } catch (e) {
                document.getElementById("result-body").innerHTML =
                    "<tr><td colspan='4' class='empty-cell' style='color:#f44336;'>Gagal memuat data</td></tr>";
            }
        }

        function renderDetail(data) {
            const tbody = document.getElementById("result-body");
            tbody.innerHTML = "";
            document.getElementById("infoTotal").textContent = data.length;

            if (data.length === 0) {
                tbody.innerHTML = "<tr><td colspan='4' class='empty-cell'>Tidak ada data</td></tr>";
                return;
            }

            data.forEach((row, i) => {
                const d = new Date(row.checkin_date + 'T00:00:00');
                const formattedDate = d.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric' });
                const dayName = d.toLocaleDateString('id-ID', { weekday: 'long' });
                const weekend = isWeekend(row.checkin_date);

                const tr = document.createElement("tr");
                if (weekend) tr.classList.add("weekend");
                tr.innerHTML = `
                    <td class="num-cell">${i + 1}</td>
                    <td class="date-cell">${formattedDate}</td>
                    <td class="day-cell">${dayName}</td>
                    <td class="time-cell">${row.checkin_time || '-'}</td>
                `;
                tbody.appendChild(tr);
            });
        }

        loadDetail();
    </script>
    </body>
    </html>
    """

# ================= HALAMAN LOGIN =================
html_login = """
    <!DOCTYPE html>
    <html lang="id">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Admin Login</title>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Syne:wght@700&display=swap');

            * { margin: 0; padding: 0; box-sizing: border-box; }

            body {
                font-family: 'DM Sans', sans-serif;
                background: #0a0a0f;
                color: #e2e2e5;
                min-height: 100vh;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 20px;
            }

            .login-box {
                width: 100%;
                max-width: 380px;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 14px;
                padding: 40px 32px;
            }

            .login-box h1 {
                font-family: 'Syne', sans-serif;
                font-size: 24px;
                margin-bottom: 6px;
            }

            .login-box p {
                font-size: 13px;
                color: #555;
                margin-bottom: 30px;
            }

            .form-group {
                margin-bottom: 18px;
            }

            .form-group label {
                display: block;
                font-size: 12px;
                color: #555;
                margin-bottom: 6px;
                text-transform: uppercase;
                letter-spacing: 0.8px;
            }

            .form-group input {
                width: 100%;
                padding: 10px 14px;
                background: #0a0a0f;
                border: 1px solid #222;
                border-radius: 8px;
                color: #e2e2e5;
                font-size: 14px;
                font-family: inherit;
                outline: none;
                transition: border-color 0.2s;
            }

            .form-group input:focus { border-color: #7eaaff; }

            .error-msg {
                background: rgba(244, 67, 54, 0.1);
                border: 1px solid #333;
                color: #f06262;
                font-size: 13px;
                padding: 10px 14px;
                border-radius: 8px;
                margin-bottom: 18px;
                display: none;
            }

            .btn-login {
                width: 100%;
                padding: 11px;
                background: #7eaaff;
                color: #0a0a0f;
                border: none;
                border-radius: 8px;
                font-size: 15px;
                font-weight: 600;
                font-family: inherit;
                cursor: pointer;
                transition: background 0.2s;
            }

            .btn-login:hover { background: #6e9aef; }
            .btn-login:disabled { background: #333; color: #555; cursor: not-allowed; }

            .back-link {
                display: block;
                text-align: center;
                margin-top: 24px;
                font-size: 13px;
                color: #444;
                text-decoration: none;
                transition: color 0.2s;
            }

            .back-link:hover { color: #7eaaff; }
        </style>
    </head>
    <body>
        <div class="login-box">
            <h1>⚙️ Admin</h1>
            <p>Silakan login untuk mengakses dashboard</p>

            <div class="error-msg" id="errorMsg">Email atau password salah</div>

            <div class="form-group">
                <label>Email</label>
                <input type="email" id="email" placeholder="admin@example.com" autocomplete="off">
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" id="password" placeholder="••••••••">
            </div>

            <button class="btn-login" id="loginBtn" onclick="login()">Login</button>

            <a href="/" class="back-link">← Kembali ke Rekap Absensi</a>
        </div>

        <script>
            // Jika sudah login, redirect langsung ke dashboard
            fetch('/auth/check').then(r => {
                if (r.ok) window.location.href = '/dashboard';
            });

            // Enter key → login
            document.getElementById('password').addEventListener('keydown', (e) => {
                if (e.key === 'Enter') login();
            });

            async function login() {
                const email = document.getElementById('email').value.trim();
                const password = document.getElementById('password').value;
                const btn = document.getElementById('loginBtn');
                const errorMsg = document.getElementById('errorMsg');

                if (!email || !password) return;

                btn.disabled = true;
                btn.textContent = 'Logging in...';
                errorMsg.style.display = 'none';

                try {
                    const res = await fetch('/auth/login', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ email, password })
                    });

                    if (res.ok) {
                        window.location.href = '/dashboard';
                    } else {
                        errorMsg.style.display = 'block';
                        btn.disabled = false;
                        btn.textContent = 'Login';
                    }
                } catch (e) {
                    errorMsg.style.display = 'block';
                    btn.disabled = false;
                    btn.textContent = 'Login';
                }
            }
        </script>
    </body>
    </html>
    """

# ================= DASHBOARD ADMIN =================
html_dashboard = """
  <!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Dashboard</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Syne:wght@700&display=swap');

        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            font-family: 'DM Sans', sans-serif;
            background: #0a0a0f;
            color: #e2e2e5;
            min-height: 100vh;
        }

        /* ===== SIDEBAR ===== */
        .sidebar {
            position: fixed;
            top: 0; left: 0;
            width: 220px;
            height: 100vh;
            background: #10101a;
            border-right: 1px solid #1e1e28;
            padding: 24px 16px;
            display: flex;
            flex-direction: column;
            z-index: 10;
        }

        .sidebar-logo {
            font-family: 'Syne', sans-serif;
            font-size: 18px;
            padding: 0 8px;
            margin-bottom: 30px;
        }

        .nav-item {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 12px;
            border-radius: 8px;
            color: #555;
            text-decoration: none;
            font-size: 14px;
            transition: all 0.15s;
            margin-bottom: 2px;
        }

        .nav-item:hover { background: #1a1a24; color: #e2e2e5; }
        .nav-item.active { background: #1a1a24; color: #7eaaff; }
        .nav-item .icon { font-size: 16px; width: 20px; text-align: center; }

        .sidebar-bottom {
            margin-top: auto;
            border-top: 1px solid #1e1e28;
            padding-top: 16px;
        }

        .logout-btn {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 12px;
            border-radius: 8px;
            color: #f06262;
            background: none;
            border: none;
            font-size: 14px;
            font-family: inherit;
            cursor: pointer;
            width: 100%;
            transition: background 0.15s;
        }

        .logout-btn:hover { background: rgba(240, 98, 98, 0.1); }

        /* ===== MAIN ===== */
        .main {
            margin-left: 220px;
            padding: 30px;
            min-height: 100vh;
        }

        .main-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 28px;
        }

        .main-header h1 {
            font-family: 'Syne', sans-serif;
            font-size: 22px;
        }

        .badge {
            font-size: 12px;
            background: rgba(126, 170, 255, 0.1);
            color: #7eaaff;
            padding: 4px 10px;
            border-radius: 20px;
            border: 1px solid rgba(126, 170, 255, 0.2);
        }

        /* ===== CARDS ===== */
        .cards {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
            gap: 14px;
            margin-bottom: 28px;
        }

        .card {
            background: #13131a;
            border: 1px solid #1e1e28;
            border-radius: 10px;
            padding: 20px;
        }

        .card-label {
            font-size: 11px;
            color: #555;
            text-transform: uppercase;
            letter-spacing: 1px;
            margin-bottom: 8px;
        }

        .card-value {
            font-size: 26px;
            font-weight: 600;
        }

        .card-value.blue { color: #7eaaff; }
        .card-value.green { color: #4ade80; }
        .card-value.yellow { color: #fbbf24; }

        /* ===== STREAM SECTION ===== */
        .stream-section {
            display: grid;
            grid-template-columns: 1fr 300px;
            gap: 20px;
        }

        .stream-box {
            background: #13131a;
            border: 1px solid #1e1e28;
            border-radius: 10px;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .stream-placeholder {
            width: 100%;
            aspect-ratio: 4/3;
            background: #0a0a0f;
            border-radius: 8px;
            border: 1px dashed #222;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #333;
            font-size: 14px;
        }

        #streamImg {
            width: 100%;
            border-radius: 8px;
            display: block;
        }

        .stream-controls {
            display: flex;
            gap: 10px;
            margin-top: 16px;
            flex-wrap: wrap;
            justify-content: center;
        }

        .btn {
            padding: 9px 18px;
            border-radius: 8px;
            border: none;
            font-size: 13px;
            font-weight: 600;
            font-family: inherit;
            cursor: pointer;
            transition: all 0.2s;
        }

        .btn:disabled {
            opacity: 0.35;
            cursor: not-allowed;
        }

        .btn-blue { background: #7eaaff; color: #0a0a0f; }
        .btn-blue:hover:not(:disabled) { background: #6e9aef; }

        .btn-green { background: #4ade80; color: #0a0a0f; }
        .btn-green:hover:not(:disabled) { background: #3bcc70; }

        .btn-outline { background: transparent; color: #f06262; border: 1px solid #333; }
        .btn-outline:hover:not(:disabled) { border-color: #f06262; }

        /* ===== PANEL KANAN ===== */
        .panel {
            background: #13131a;
            border: 1px solid #1e1e28;
            border-radius: 10px;
            padding: 20px;
            display: flex;
            flex-direction: column;
            gap: 16px;
        }

        .panel-title {
            font-size: 13px;
            color: #555;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        .status-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 9px 0;
            border-bottom: 1px solid #1a1a22;
            font-size: 13px;
        }

        .status-row:last-child { border-bottom: none; }
        .status-row .label { color: #555; }

        .dot {
            display: inline-block;
            width: 8px; height: 8px;
            border-radius: 50%;
            margin-right: 6px;
        }

        .dot-green { background: #4ade80; }
        .dot-yellow { background: #fbbf24; }
        .dot-red { background: #f06262; }

        /* ===== ENROLL PROGRESS ===== */
        .enroll-panel {
            display: none;
            flex-direction: column;
            gap: 10px;
            padding: 14px;
            background: #0a0a0f;
            border-radius: 8px;
            border: 1px solid #1e1e28;
        }

        .enroll-panel.show { display: flex; }

        .enroll-name {
            font-size: 14px;
            font-weight: 600;
            color: #7eaaff;
        }

        .enroll-progress-text {
            font-size: 12px;
            color: #555;
            display: flex;
            justify-content: space-between;
        }

        /* progress bar track */
        .progress-track {
            width: 100%;
            height: 6px;
            background: #1e1e28;
            border-radius: 3px;
            overflow: hidden;
        }

        /* progress bar fill */
        .progress-fill {
            height: 100%;
            width: 0%;
            background: #4ade80;
            border-radius: 3px;
            transition: width 0.35s ease;
        }

        /* ===== RESULT BOX ===== */
        .result-box {
            display: none;
            flex-direction: column;
            gap: 8px;
            padding: 14px;
            background: #0a0a0f;
            border-radius: 8px;
            border: 1px solid #1e1e28;
        }

        .result-box.show { display: flex; }

        .result-label {
            font-size: 11px;
            color: #555;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        .result-face {
            padding: 8px 0;
            border-bottom: 1px solid #1a1a22;
        }

        .result-face:last-child { border-bottom: none; }

        .result-face .result-name {
            color: #7eaaff;
            font-weight: 600;
            font-size: 15px;
        }

        .result-face .result-name.unknown { color: #f06262; }

        .result-face .result-detail {
            color: #555;
            font-size: 12px;
            margin-top: 3px;
        }

        .result-face .result-detail .att-ok { color: #4ade80; }
        .result-face .result-detail .att-dup { color: #fbbf24; }

        /* ===== TOAST ===== */
        .toast {
            position: fixed;
            bottom: 24px;
            right: 24px;
            padding: 14px 20px;
            border-radius: 10px;
            font-size: 14px;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 10px;
            z-index: 200;
            transform: translateY(100px);
            opacity: 0;
            transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
            max-width: 320px;
            pointer-events: none;
        }

        .toast.show { transform: translateY(0); opacity: 1; }

        .toast.success {
            background: #13131a;
            border: 1px solid rgba(74, 222, 128, 0.3);
            color: #4ade80;
        }

        .toast.error {
            background: #13131a;
            border: 1px solid rgba(240, 98, 98, 0.3);
            color: #f06262;
        }

        .toast.warn {
            background: #13131a;
            border: 1px solid rgba(251, 191, 36, 0.3);
            color: #fbbf24;
        }
    </style>
</head>
<body>

<!-- ===== SIDEBAR ===== -->
<div class="sidebar">
    <div class="sidebar-logo">⚙️ Admin Panel</div>

    <a href="/" class="nav-item"><span class="icon">📊</span> Rekap Absensi</a>
    <a href="/dashboard" class="nav-item active"><span class="icon">🎯</span> Face Recognition</a>
    <a href="/users" class="nav-item"><span class="icon">👥</span> User Management</a>

    <div class="sidebar-bottom">
        <button class="logout-btn" onclick="logout()">
            <span class="icon">🚪</span> Logout
        </button>
    </div>
</div>

<!-- ===== MAIN ===== -->
<div class="main">
    <div class="main-header">
        <h1>Face Recognition</h1>
        <span class="badge">🔒 Admin Only</span>
    </div>

    <!-- Cards -->
    <div class="cards">
        <div class="card">
            <div class="card-label">Status</div>
            <div class="card-value blue" id="statusCard">Idle</div>
        </div>
        <div class="card">
            <div class="card-label">WebSocket</div>
            <div class="card-value yellow" id="wsStatus">Disconnected</div>
        </div>
        <div class="card">
            <div class="card-label">Enrolled</div>
            <div class="card-value green" id="enrolledCount">0</div>
        </div>
    </div>

    <!-- Stream + Panel -->
    <div class="stream-section">

        <!-- Stream -->
        <div class="stream-box">
            <div class="stream-placeholder" id="placeholder">📹 Menunggu stream dari ESP32...</div>
            <img id="streamImg" src="/stream" style="display:none;">

            <div class="stream-controls">
                <button class="btn btn-blue" id="btnForceStart" onclick="sendForceStart()">▶️ ForceStart</button>
                <button class="btn btn-blue" style="display:none;" id="btnForceStop" onclick="sendForceStop()">⏸️ ForceStop</button>
                <button class="btn btn-blue" id="btnRecognize" onclick="sendRecognize()">🎯 Recognize</button>
                <button class="btn btn-green" id="btnEnroll" onclick="openEnroll()">📸 Enroll</button>
                <!-- Capture: hanya keliatan saat mode ENROLL -->
                <button class="btn btn-outline" id="btnCapture" onclick="sendCapture()" style="display:none;">📷 Capture</button>
                <button class="btn btn-outline" id="btnStop" onclick="sendStop()" style="display:none;">⏹ Stop</button>
            </div>
        </div>

        <!-- Panel Kanan -->
        <div class="panel">
            <div class="panel-title">Status</div>

            <div>
                <div class="status-row">
                    <span class="label">Connection</span>
                    <span><span class="dot dot-yellow" id="dotWs"></span><span id="connText">Disconnected</span></span>
                </div>
                <div class="status-row">
                    <span class="label">Mode</span>
                    <span id="modeText">—</span>
                </div>
                <div class="status-row">
                    <span class="label">Stream</span>
                    <span><span class="dot dot-yellow" id="dotStream"></span><span id="streamText">Menunggu</span></span>
                </div>
            </div>

            <!-- Enroll Progress (tersembunyi default) -->
            <div class="enroll-panel" id="enrollPanel">
                <div class="enroll-name" id="enrollName">—</div>
                <div class="enroll-progress-text">
                    <span id="enrollProgressLabel">0 / 10</span>
                    <span id="enrollPercentLabel">0%</span>
                </div>
                <div class="progress-track">
                    <div class="progress-fill" id="progressFill"></div>
                </div>
            </div>

            <!-- Result Box (tersembunyi default) -->
            <div class="result-box" id="resultBox">
                <div class="result-label">Hasil Recognition</div>
                <div id="resultList">—</div>
            </div>
        </div>
    </div>
</div>

<!-- ===== TOAST ===== -->
<div class="toast" id="toast"></div>

<!-- ===== ENROLL MODAL (simple prompt pengganti) ===== -->
<div id="enrollModal" style="
    display:none; position:fixed; inset:0;
    background:rgba(0,0,0,0.6); backdrop-filter:blur(4px);
    z-index:100; align-items:center; justify-content:center;
">
    <div style="
        background:#13131a; border:1px solid #1e1e28; border-radius:14px;
        padding:32px; width:100%; max-width:380px;
    ">
        <h2 style="font-family:'Syne',sans-serif; font-size:18px; margin-bottom:6px;">📸 Enroll Baru</h2>
        <p style="font-size:13px; color:#555; margin-bottom:22px;">Masukkan nama untuk enrollment wajah</p>

        <label style="display:block; font-size:12px; color:#555; text-transform:uppercase; letter-spacing:0.8px; margin-bottom:6px;">Nama</label>
        <input id="enrollInput" type="text" placeholder="Contoh: John_Doe" style="
            width:100%; padding:10px 14px; background:#0a0a0f;
            border:1px solid #222; border-radius:8px; color:#e2e2e5;
            font-size:14px; font-family:inherit; outline:none;
        " oninput="this.style.borderColor = this.value.trim() ? '#7eaaff' : '#222'">

        <div style="display:flex; justify-content:flex-end; gap:10px; margin-top:24px;">
            <button onclick="closeEnrollModal()" style="
                padding:9px 20px; border-radius:8px; background:transparent;
                color:#666; border:1px solid #1e1e28; font-size:14px;
                font-family:inherit; cursor:pointer;
            ">Cancel</button>
            <button onclick="submitEnroll()" style="
                padding:9px 20px; border-radius:8px; background:#4ade80;
                color:#0a0a0f; border:none; font-size:14px; font-weight:600;
                font-family:inherit; cursor:pointer;
            ">Mulai Enroll</button>
        </div>
    </div>
</div>

<script>
// ============================================================
// STATE
// ============================================================
let ws = null;
let currentMode = null;   // null | "ENROLL"
let streamStarted = false;

const ENROLL_TARGET = 10; // harus sama dengan server

// ============================================================
// WEBSOCKET
// ============================================================
function connectWebSocket() {
    ws = new WebSocket(`ws://${location.host}/ws/cmd`);

    ws.onopen = () => {
        setWsStatus(true);
    };

    ws.onmessage = (e) => {
        let msg;
        try {
            msg = JSON.parse(e.data);                // BUG FIX 1: "e.data" bukan "event.data"
        } catch (_) {
            console.log("Raw WS:", e.data);
            return;
        }

        console.log("📨 WS msg:", msg);

        switch (msg.type) {
            // ---- STOP ----
            case "stop":
                handleStop();
                break;

            // ---- RECOGNIZE ----
            case "recognize_status":
                setStatus("Memproses…", "yellow");
                break;

            case "recognize_result":
                handleRecognizeResult(msg);
                break;

            // ---- ENROLL ----
            case "enroll_started":
                handleEnrollStarted(msg);
                break;

            case "enroll_progress":
                handleEnrollProgress(msg);
                break;

            case "enroll_done":
                handleEnrollDone(msg);
                break;

            case "enroll_warning":
                showToast(msg.message, "warn");
                break;

            // ---- ERROR ----
            case "error":
                showToast(msg.message, "error");
                break;
        }
    };                                                // BUG FIX 2: closing brace onmessage

    ws.onclose = () => {
        setWsStatus(false);
        setTimeout(connectWebSocket, 3000);           // auto reconnect
    };

    ws.onerror = () => ws.close();
}

// ============================================================
// STREAM — nyala dari awal, placeholder kalau belum ada data
// ============================================================
function initStream() {
    const img = document.getElementById('streamImg');
    img.src = '/stream?' + Date.now();               // cache buster

    img.onload = () => {
        // Kalau gambar berhasil load, tampilkan stream, sembunyikan placeholder
        if (!streamStarted) {
            streamStarted = true;
            document.getElementById('placeholder').style.display = 'none';
            img.style.display = 'block';
            document.getElementById('dotStream').className = 'dot dot-green';
            document.getElementById('streamText').textContent = 'On';
        }
    };

    img.onerror = () => {
        // Kalau error (ESP32 belum kirim), retry setelah 2 detik
        setTimeout(initStream, 2000);
    };
}

// ============================================================
// SEND COMMANDS
// ============================================================
function sendRecognize() {
    if (!ws || ws.readyState !== 1) {
        showToast("WebSocket tidak terhubung", "error");
        return;
    }
    ws.send("recognize");
    setStatus("Recognizing…", "blue");
    hideResultAndEnroll();
}

function sendForceStart() {
    document.getElementById('btnForceStart').style.display = 'none';
    document.getElementById('btnForceStop').style.display = 'block';
    if (!ws || ws.readyState !== 1) return;
    ws.send(JSON.stringify({ command: "ForceStart" }));
}
function sendForceStop() {
    document.getElementById('btnForceStart').style.display = 'block';
    document.getElementById('btnForceStop').style.display = 'none';
    if (!ws || ws.readyState !== 1) return;
    ws.send(JSON.stringify({ command: "ForceStop" }));
}
function sendCapture() {
    if (!ws || ws.readyState !== 1) return;
    ws.send(JSON.stringify({ command: "capture" }));
}

function sendStop() {
    if (!ws || ws.readyState !== 1) return;
    ws.send("stop");
    handleStop();
}

// ============================================================
// ENROLL MODAL
// ============================================================
function openEnroll() {
    document.getElementById('enrollModal').style.display = 'flex';
    document.getElementById('enrollInput').value = '';
    document.getElementById('enrollInput').focus();
}

function closeEnrollModal() {
    document.getElementById('enrollModal').style.display = 'none';
}

function submitEnroll() {
    const nama = document.getElementById('enrollInput').value.trim();
    if (!nama) {
        showToast("Nama tidak boleh kosong", "error");
        return;
    }

    // Validasi: huruf, angka, underscore, spasi
    if (!/^[a-zA-Z0-9_ ]+$/.test(nama)) {
        showToast("Nama hanya boleh huruf, angka, underscore, atau spasi", "error");
        return;
    }

    closeEnrollModal();

    if (!ws || ws.readyState !== 1) {
        showToast("WebSocket tidak terhubung", "error");
        return;
    }

    ws.send(JSON.stringify({ command: "start_enroll", nama: nama }));  // BUG FIX 3: "nama" bukan "name"
}

// ============================================================
// HANDLERS — terima hasil dari server
// ============================================================
function handleStop() {
    currentMode = null;
    setStatus("Idle", "blue");
    document.getElementById('modeText').textContent = '—';
    document.getElementById('btnCapture').style.display = 'none';
    document.getElementById('btnStop').style.display = 'none';
    document.getElementById('enrollPanel').classList.remove('show');
}

function handleRecognizeResult(msg) {
    const faces = msg.faces || [];

    // Update result box
    const resultBox = document.getElementById('resultBox');
    const resultList = document.getElementById('resultList');

    if (faces.length === 0) {
        setStatus("Tidak ada wajah", "yellow");
        showToast("Tidak ada wajah terdeteksi", "warn");
        return;
    }

    let html = '';
    faces.forEach((face, i) => {
        if (face.status === "recognized") {
            const att = face.attendance || {};
            const attClass = att.status === "checked_in" ? "att-ok" : "att-dup";
            const attLabel = att.status === "checked_in"
                ? `✅ Check-in berhasil (${att.time})`
                : `⚠️ Sudah check-in hari ini`;

            html += `
                <div class="result-face">
                    <div class="result-name">${face.name}</div>
                    <div class="result-detail">
                        Distance: ${face.distance} &nbsp;|&nbsp;
                        <span class="${attClass}">${attLabel}</span>
                    </div>
                </div>`;
        } else {
            html += `
                <div class="result-face">
                    <div class="result-name unknown">Unknown</div>
                    <div class="result-detail">Wajah tidak dikenali</div>
                </div>`;
        }
    });

    resultList.innerHTML = html;
    resultBox.classList.add('show');

    // Status
    const recognized = faces.filter(f => f.status === "recognized");
    setStatus(recognized.length > 0 ? "Done ✓" : "Unknown", recognized.length > 0 ? "green" : "yellow");

    // Toast ringkas
    if (recognized.length > 0) {
        showToast(`Dikenali: ${recognized.map(f => f.name).join(', ')}`, "success");
    } else {
        showToast("Wajah tidak dikenali", "warn");
    }
}

function handleEnrollStarted(msg) {
    currentMode = "ENROLL";
    setStatus("Enrolling…", "green");
    document.getElementById('modeText').textContent = 'Enrollment';

    // Tampilkan Capture + Stop, sembunyikan Enroll
    document.getElementById('btnCapture').style.display = 'inline-flex';
    document.getElementById('btnStop').style.display = 'inline-flex';

    // Enroll panel
    document.getElementById('enrollPanel').classList.add('show');
    document.getElementById('enrollName').textContent = msg.name;
    updateProgress(0, ENROLL_TARGET);

    hideResultBox();
    showToast(`Enroll dimulai untuk "${msg.name}". Tekan Capture untuk foto.`, "success");
}

function handleEnrollProgress(msg) {
    updateProgress(msg.count, msg.total);
    showToast(`Foto ${msg.count}/${msg.total} berhasil`, "success");
}

function handleEnrollDone(msg) {
    updateProgress(ENROLL_TARGET, ENROLL_TARGET);
    showToast(msg.message, "success");

    // Fetch ulang jumlah enrolled
    fetchEnrolledCount();

    // Setelah 1.5s, auto stop
    setTimeout(() => handleStop(), 1500);
}

// ============================================================
// UI HELPERS
// ============================================================
function setStatus(text, color) {
    const el = document.getElementById('statusCard');
    el.textContent = text;
    el.className = `card-value ${color}`;
}

function setWsStatus(connected) {
    document.getElementById('wsStatus').textContent = connected ? 'Connected' : 'Disconnected';
    document.getElementById('wsStatus').style.color = connected ? '#4ade80' : '#fbbf24';
    document.getElementById('dotWs').className = connected ? 'dot dot-green' : 'dot dot-yellow';
    document.getElementById('connText').textContent = connected ? 'Connected' : 'Disconnected';
}

function updateProgress(current, total) {
    const pct = Math.round((current / total) * 100);
    document.getElementById('enrollProgressLabel').textContent = `${current} / ${total}`;
    document.getElementById('enrollPercentLabel').textContent = `${pct}%`;
    document.getElementById('progressFill').style.width = pct + '%';
}

function hideResultAndEnroll() {
    document.getElementById('resultBox').classList.remove('show');
    document.getElementById('enrollPanel').classList.remove('show');
}

function hideResultBox() {
    document.getElementById('resultBox').classList.remove('show');
}

// ============================================================
// FETCH enrolled count dari Supabase
// ============================================================
async function fetchEnrolledCount() {
    try {
        const SUPABASE_URL = "https://vtuamuvlfnouzekjhtsk.supabase.co";
        const SUPABASE_KEY = "sb_publishable_0LJO9qBDgV29zXMPaS44Iw_3d4GP9uw";

        const res = await fetch(
            `${SUPABASE_URL}/rest/v1/users?is_enrolled=eq.true&select=count`,
            {
                headers: {
                    apikey: SUPABASE_KEY,
                    Authorization: `Bearer ${SUPABASE_KEY}`,
                    Prefer: "count=exact"
                }
            }
        );

        const count = res.headers.get("content-range")?.split("/").pop() || "0";
        document.getElementById('enrolledCount').textContent = count;
    } catch (_) {
        // silent
    }
}

// ============================================================
// TOAST
// ============================================================
let toastTimeout = null;

function showToast(message, type = "success") {
    const toast = document.getElementById('toast');
    const icon = type === "success" ? "✅" : type === "error" ? "❌" : "⚠️";
    toast.className = `toast ${type} show`;
    toast.innerHTML = `<span>${icon}</span> ${message}`;

    if (toastTimeout) clearTimeout(toastTimeout);
    toastTimeout = setTimeout(() => toast.classList.remove('show'), 3500);
}

// ============================================================
// LOGOUT
// ============================================================
function logout() {
    fetch('/auth/logout', { method: 'POST', redirect: 'follow' })
        .then(() => { window.location.href = '/login'; });
}

// ============================================================
// INIT
// ============================================================
connectWebSocket();
initStream();
fetchEnrolledCount();

// Enter key di modal → submit
document.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && document.getElementById('enrollModal').style.display === 'flex') {
        submitEnroll();
    }
    if (e.key === 'Escape' && document.getElementById('enrollModal').style.display === 'flex') {
        closeEnrollModal();
    }
});
</script>

</body>
</html>
"""


# ================= ROUTES =================
@app.get("/", response_class=HTMLResponse)
async def get_rekap():
    """Halaman rekap bulanan"""
    return html_rekap


@app.get("/detail", response_class=HTMLResponse)
async def get_detail():
    """Halaman detail absensi per nama"""
    return html_detail


@app.get("/login", response_class=HTMLResponse)
def login_page():
    """Halaman login - PUBLIC"""
    return html_login


@app.get("/dashboard", response_class=HTMLResponse)
def dashboard(session_token: str = Cookie(default=None)):
    """Admin Dashboard - PROTECTED"""

    # Cek autentikasi — jika belum login, redirect ke /login
    if not session_token or session_token not in active_sessions:
        return RedirectResponse(url="/login", status_code=303)

    # Sudah login → tampilkan dashboard
    return html_dashboard


# ============= USER MANAGEMENT PAGE =============
# Tambahkan route ini di file main.py Anda


@app.get("/users")
def users_page(session_token: str = Cookie(default=None)):
    """User Management - PROTECTED"""

    if not session_token or session_token not in active_sessions:
        return RedirectResponse(url="/login", status_code=303)

    return HTMLResponse("""
    <!DOCTYPE html>
    <html lang="id">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>User Management</title>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=Syne:wght@700&display=swap');

            * { margin: 0; padding: 0; box-sizing: border-box; }

            body {
                font-family: 'DM Sans', sans-serif;
                background: #0a0a0f;
                color: #e2e2e5;
                min-height: 100vh;
            }

            /* ===== SIDEBAR ===== */
            .sidebar {
                position: fixed;
                top: 0; left: 0;
                width: 220px;
                height: 100vh;
                background: #10101a;
                border-right: 1px solid #1e1e28;
                padding: 24px 16px;
                display: flex;
                flex-direction: column;
                z-index: 10;
            }

            .sidebar-logo {
                font-family: 'Syne', sans-serif;
                font-size: 18px;
                padding: 0 8px;
                margin-bottom: 30px;
            }

            .nav-item {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 10px 12px;
                border-radius: 8px;
                color: #555;
                text-decoration: none;
                font-size: 14px;
                transition: all 0.15s;
                margin-bottom: 2px;
            }

            .nav-item:hover { background: #1a1a24; color: #e2e2e5; }
            .nav-item.active { background: #1a1a24; color: #7eaaff; }
            .nav-item .icon { font-size: 16px; width: 20px; text-align: center; }

            .sidebar-bottom {
                margin-top: auto;
                border-top: 1px solid #1e1e28;
                padding-top: 16px;
            }

            .logout-btn {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 10px 12px;
                border-radius: 8px;
                color: #f06262;
                background: none;
                border: none;
                font-size: 14px;
                font-family: inherit;
                cursor: pointer;
                width: 100%;
                transition: background 0.15s;
            }

            .logout-btn:hover { background: rgba(240, 98, 98, 0.1); }

            /* ===== MAIN ===== */
            .main {
                margin-left: 220px;
                padding: 30px;
                min-height: 100vh;
            }

            .main-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 24px;
            }

            .main-header h1 {
                font-family: 'Syne', sans-serif;
                font-size: 22px;
            }

            .badge {
                font-size: 12px;
                background: rgba(126, 170, 255, 0.1);
                color: #7eaaff;
                padding: 4px 10px;
                border-radius: 20px;
                border: 1px solid rgba(126, 170, 255, 0.2);
            }

            /* ===== TOOLBAR ===== */
            .toolbar {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 16px;
                gap: 12px;
            }

            .toolbar-left {
                display: flex;
                align-items: center;
                gap: 12px;
            }

            .search-box {
                position: relative;
            }

            .search-box input {
                padding: 9px 14px 9px 36px;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 8px;
                color: #e2e2e5;
                font-size: 14px;
                font-family: inherit;
                width: 240px;
                outline: none;
                transition: border-color 0.2s;
            }

            .search-box input:focus { border-color: #7eaaff; }

            .search-box .search-icon {
                position: absolute;
                left: 12px;
                top: 50%;
                transform: translateY(-50%);
                color: #444;
                font-size: 14px;
            }

            .filter-select {
                padding: 9px 14px;
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 8px;
                color: #e2e2e5;
                font-size: 14px;
                font-family: inherit;
                outline: none;
                cursor: pointer;
                transition: border-color 0.2s;
            }

            .filter-select:focus { border-color: #7eaaff; }

            .count-label {
                font-size: 13px;
                color: #444;
                margin-left: 8px;
            }

            /* ===== TABLE ===== */
            .table-wrapper {
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 10px;
                overflow: hidden;
            }

            table {
                width: 100%;
                border-collapse: collapse;
            }

            th {
                background: #0f0f14;
                padding: 12px 16px;
                text-align: left;
                font-size: 11px;
                font-weight: 600;
                text-transform: uppercase;
                letter-spacing: 1px;
                color: #444;
                border-bottom: 1px solid #1e1e28;
                white-space: nowrap;
                user-select: none;
            }

            th.sortable { cursor: pointer; }
            th.sortable:hover { color: #7eaaff; }
            th .sort-arrow { margin-left: 4px; opacity: 0.4; }
            th.sorted .sort-arrow { opacity: 1; color: #7eaaff; }

            td {
                padding: 14px 16px;
                border-bottom: 1px solid #1a1a22;
                font-size: 14px;
                vertical-align: middle;
            }

            tr:last-child td { border-bottom: none; }

            tbody tr {
                transition: background 0.15s;
            }

            tbody tr:hover { background: #161620; }

            /* ===== CELLS ===== */
            .user-cell {
                display: flex;
                align-items: center;
                gap: 12px;
            }

            .avatar {
                width: 36px;
                height: 36px;
                border-radius: 8px;
                background: linear-gradient(135deg, #1e1e28, #2a2a36);
                border: 1px solid #222;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 15px;
                font-weight: 600;
                color: #7eaaff;
                flex-shrink: 0;
            }

            .user-name { font-weight: 500; }
            .user-id { font-size: 11px; color: #333; font-family: monospace; margin-top: 2px; }

            .face-file-cell {
                font-size: 12px;
                color: #555;
                font-family: monospace;
                max-width: 200px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            .face-file-cell.empty { color: #333; font-style: italic; font-family: inherit; }

            .date-cell { color: #666; font-size: 13px; white-space: nowrap; }

            /* ===== BADGES ===== */
            .badge-enrolled {
                display: inline-flex;
                align-items: center;
                gap: 5px;
                font-size: 12px;
                font-weight: 600;
                padding: 4px 10px;
                border-radius: 20px;
            }

            .badge-enrolled.yes {
                background: rgba(74, 222, 128, 0.1);
                color: #4ade80;
                border: 1px solid rgba(74, 222, 128, 0.2);
            }

            .badge-enrolled.no {
                background: rgba(100, 100, 120, 0.1);
                color: #555;
                border: 1px solid rgba(100, 100, 120, 0.2);
            }

            .badge-dot {
                width: 6px; height: 6px;
                border-radius: 50%;
            }

            .badge-enrolled.yes .badge-dot { background: #4ade80; }
            .badge-enrolled.no .badge-dot { background: #555; }

            /* ===== ACTION BUTTONS ===== */
            .actions {
                display: flex;
                align-items: center;
                gap: 6px;
            }

            .btn-icon {
                width: 34px;
                height: 34px;
                border-radius: 6px;
                border: 1px solid #1e1e28;
                background: transparent;
                color: #666;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 15px;
                transition: all 0.15s;
            }

            .btn-icon:hover { background: #1a1a24; border-color: #2a2a36; }

            .btn-icon.edit:hover { color: #7eaaff; border-color: rgba(126, 170, 255, 0.3); }
            .btn-icon.delete:hover { color: #f06262; border-color: rgba(240, 98, 98, 0.3); background: rgba(240, 98, 98, 0.05); }

            /* ===== EMPTY STATE ===== */
            .empty-state {
                text-align: center;
                padding: 60px 20px;
                color: #333;
            }

            .empty-state .empty-icon { font-size: 36px; margin-bottom: 12px; }
            .empty-state p { font-size: 14px; }

            /* ===== MODAL ===== */
            .modal-overlay {
                position: fixed;
                inset: 0;
                background: rgba(0, 0, 0, 0.6);
                backdrop-filter: blur(4px);
                display: none;
                align-items: center;
                justify-content: center;
                z-index: 100;
            }

            .modal-overlay.show { display: flex; }

            .modal {
                background: #13131a;
                border: 1px solid #1e1e28;
                border-radius: 14px;
                width: 100%;
                max-width: 440px;
                padding: 28px;
                position: relative;
            }

            .modal h2 {
                font-family: 'Syne', sans-serif;
                font-size: 18px;
                margin-bottom: 6px;
            }

            .modal .modal-subtitle {
                font-size: 13px;
                color: #555;
                margin-bottom: 24px;
            }

            .modal-close {
                position: absolute;
                top: 20px; right: 20px;
                background: none;
                border: none;
                color: #555;
                font-size: 18px;
                cursor: pointer;
                width: 28px; height: 28px;
                display: flex;
                align-items: center;
                justify-content: center;
                border-radius: 6px;
                transition: all 0.15s;
            }

            .modal-close:hover { background: #1a1a24; color: #e2e2e5; }

            .form-group {
                margin-bottom: 16px;
            }

            .form-group label {
                display: block;
                font-size: 12px;
                color: #555;
                margin-bottom: 6px;
                text-transform: uppercase;
                letter-spacing: 0.8px;
            }

            .form-group input,
            .form-group select {
                width: 100%;
                padding: 10px 14px;
                background: #0a0a0f;
                border: 1px solid #222;
                border-radius: 8px;
                color: #e2e2e5;
                font-size: 14px;
                font-family: inherit;
                outline: none;
                transition: border-color 0.2s;
            }

            .form-group input:focus,
            .form-group select:focus { border-color: #7eaaff; }

            .form-group input:disabled {
                color: #444;
                cursor: not-allowed;
            }

            .form-group select option { background: #13131a; }

            .modal-actions {
                display: flex;
                justify-content: flex-end;
                gap: 10px;
                margin-top: 24px;
            }

            .btn {
                padding: 9px 20px;
                border-radius: 8px;
                border: none;
                font-size: 14px;
                font-weight: 600;
                font-family: inherit;
                cursor: pointer;
                transition: all 0.2s;
            }

            .btn-ghost {
                background: transparent;
                color: #666;
                border: 1px solid #1e1e28;
            }

            .btn-ghost:hover { background: #1a1a24; color: #e2e2e5; }

            .btn-blue { background: #7eaaff; color: #0a0a0f; }
            .btn-blue:hover { background: #6e9aef; }

            .btn-red { background: #f06262; color: #fff; }
            .btn-red:hover { background: #e05050; }

            /* ===== CONFIRM MODAL ===== */
            .confirm-icon {
                width: 48px; height: 48px;
                border-radius: 12px;
                background: rgba(240, 98, 98, 0.1);
                border: 1px solid rgba(240, 98, 98, 0.2);
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 22px;
                margin-bottom: 16px;
            }

            .confirm-modal h2 { color: #f06262; }

            .confirm-modal .confirm-name {
                font-weight: 600;
                color: #e2e2e5;
                margin-top: 4px;
            }

            /* ===== TOAST ===== */
            .toast {
                position: fixed;
                bottom: 24px;
                right: 24px;
                padding: 14px 20px;
                border-radius: 10px;
                font-size: 14px;
                font-weight: 500;
                display: flex;
                align-items: center;
                gap: 10px;
                z-index: 200;
                transform: translateY(100px);
                opacity: 0;
                transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
                max-width: 320px;
            }

            .toast.show { transform: translateY(0); opacity: 1; }

            .toast.success {
                background: #13131a;
                border: 1px solid rgba(74, 222, 128, 0.3);
                color: #4ade80;
            }

            .toast.error {
                background: #13131a;
                border: 1px solid rgba(240, 98, 98, 0.3);
                color: #f06262;
            }
        </style>
    </head>
    <body>

    <!-- ===== SIDEBAR ===== -->
    <div class="sidebar">
        <div class="sidebar-logo">⚙️ Admin Panel</div>

        <a href="/" class="nav-item"><span class="icon">📊</span> Rekap Absensi</a>
        <a href="/dashboard" class="nav-item"><span class="icon">🎯</span> Face Recognition</a>
        <a href="/users" class="nav-item active"><span class="icon">👥</span> User Management</a>

        <div class="sidebar-bottom">
            <button class="logout-btn" onclick="logout()">
                <span class="icon">🚪</span> Logout
            </button>
        </div>
    </div>

    <!-- ===== MAIN ===== -->
    <div class="main">

        <div class="main-header">
            <h1>User Management</h1>
            <span class="badge">🔒 Admin Only</span>
        </div>

        <!-- Toolbar -->
        <div class="toolbar">
            <div class="toolbar-left">
                <div class="search-box">
                    <span class="search-icon">🔍</span>
                    <input type="text" id="searchInput" placeholder="Cari nama user..." oninput="filterUsers()">
                </div>
                <select class="filter-select" id="filterEnrolled" onchange="filterUsers()">
                    <option value="all">Semua Status</option>
                    <option value="true">Enrolled</option>
                    <option value="false">Belum Enrolled</option>
                </select>
                <span class="count-label" id="countLabel">0 user</span>
            </div>
        </div>

        <!-- Table -->
        <div class="table-wrapper">
            <table>
                <thead>
                    <tr>
                        <th class="sortable" onclick="sortTable('user_name')">
                            Nama <span class="sort-arrow" id="sort-user_name">↕</span>
                        </th>
                        <th>Face File</th>
                        <th class="sortable" onclick="sortTable('is_enrolled')">
                            Status <span class="sort-arrow" id="sort-is_enrolled">↕</span>
                        </th>
                        <th class="sortable" onclick="sortTable('created_at')">
                            Dibuat <span class="sort-arrow" id="sort-created_at">↕</span>
                        </th>
                        <th style="width: 100px;">Aksi</th>
                    </tr>
                </thead>
                <tbody id="userTableBody">
                    <tr><td colspan="5" class="empty-state"><div class="empty-icon">⏳</div><p>Memuat data...</p></td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- ===== EDIT MODAL ===== -->
    <div class="modal-overlay" id="editModal">
        <div class="modal">
            <button class="modal-close" onclick="closeModal('editModal')">✕</button>
            <h2>Edit User</h2>
            <p class="modal-subtitle">Ubah informasi user di bawah</p>

            <input type="hidden" id="editUserId">

            <div class="form-group">
                <label>ID</label>
                <input type="text" id="editId" disabled>
            </div>
            <div class="form-group">
                <label>Nama</label>
                <input type="text" id="editName" placeholder="Nama user">
            </div>
            <div class="form-group">
                <label>Face File</label>
                <input type="text" id="editFaceFile" readonly placeholder="Path file face">
            </div>
            <div class="form-group">
                <label>Status Enrolled</label>
                <select id="editEnrolled">
                    <option value="true">✅ Enrolled</option>
                    <option value="false">❌ Belum Enrolled</option>
                </select>
            </div>

            <div class="modal-actions">
                <button class="btn btn-ghost" onclick="closeModal('editModal')">Cancel</button>
                <button class="btn btn-blue" onclick="saveEdit()">Simpan</button>
            </div>
        </div>
    </div>

    <!-- ===== DELETE CONFIRM MODAL ===== -->
    <div class="modal-overlay" id="deleteModal">
        <div class="modal confirm-modal">
            <button class="modal-close" onclick="closeModal('deleteModal')">✕</button>
            <div class="confirm-icon">🗑️</div>
            <h2>Hapus User</h2>
            <p class="modal-subtitle">
                Aksi ini tidak bisa dibatalkan.<br>
                <span class="confirm-name" id="deleteUserName">—</span> akan dihapus permanen.
            </p>

            <input type="hidden" id="deleteUserId">

            <div class="modal-actions">
                <button class="btn btn-ghost" onclick="closeModal('deleteModal')">Cancel</button>
                <button class="btn btn-red" onclick="confirmDelete()">Hapus</button>
            </div>
        </div>
    </div>

    <!-- ===== TOAST ===== -->
    <div class="toast" id="toast"></div>

    <script>
        // ===== CONFIG =====
        const SUPABASE_URL = "https://vtuamuvlfnouzekjhtsk.supabase.co";
        const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ0dWFtdXZsZm5vdXpla2podHNrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY3NTkyMzksImV4cCI6MjA4MjMzNTIzOX0.wjWTw3kQVxpcj95LGsV9Ti3NrjGjFaqdmsqhROjLBWY";

        const headers = {
            apikey: SUPABASE_ANON_KEY,
            Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
            "Content-Type": "application/json",
            Prefer: "return=representation"
        };

        // ===== STATE =====
        let allUsers = [];
        let sortField = 'created_at';
        let sortAsc = false;

        // ===== FETCH =====
        async function fetchUsers() {
            try {
                const res = await fetch(
                    `${SUPABASE_URL}/rest/v1/users?select=*&order=created_at.desc`,
                    { headers }
                );

                if (!res.ok) throw new Error(`HTTP ${res.status}`);

                allUsers = await res.json();
                renderTable(allUsers);

            } catch (e) {
                console.error(e);
                showToast("Gagal memuat data user", "error");
            }
        }

        // ===== RENDER =====
        function renderTable(users) {
            const tbody = document.getElementById("userTableBody");
            document.getElementById("countLabel").textContent = `${users.length} user`;

            if (users.length === 0) {
                tbody.innerHTML = `
                    <tr><td colspan="5">
                        <div class="empty-state">
                            <div class="empty-icon">👤</div>
                            <p>Tidak ada user ditemukan</p>
                        </div>
                    </td></tr>`;
                return;
            }

            tbody.innerHTML = users.map(user => `
                <tr>
                    <td>
                        <div class="user-cell">
                            <div class="avatar">${user.user_name ? user.user_name[0].toUpperCase() : '?'}</div>
                            <div>
                                <div class="user-name">${user.user_name || '—'}</div>
                                <div class="user-id">${user.id.slice(0, 18)}...</div>
                            </div>
                        </div>
                    </td>
                    <td>
                        <span class="face-file-cell ${!user.face_file ? 'empty' : ''}">
                            ${user.face_file || 'Tidak ada file'}
                        </span>
                    </td>
                    <td>
                        <span class="badge-enrolled ${user.is_enrolled ? 'yes' : 'no'}">
                            <span class="badge-dot"></span>
                            ${user.is_enrolled ? 'Enrolled' : 'Belum Enrolled'}
                        </span>
                    </td>
                    <td>
                        <span class="date-cell">${formatDate(user.created_at)}</span>
                    </td>
                    <td>
                        <div class="actions">
                            <button class="btn-icon edit" onclick="openEdit('${user.id}')" title="Edit">✏️</button>
                            <button class="btn-icon delete" onclick="openDelete('${user.id}', '${user.user_name || 'User'}')" title="Hapus">🗑️</button>
                        </div>
                    </td>
                </tr>
            `).join('');
        }

        // ===== FILTER + SEARCH =====
        function filterUsers() {
            const search = document.getElementById("searchInput").value.toLowerCase();
            const status = document.getElementById("filterEnrolled").value;

            let filtered = allUsers;

            if (search) {
                filtered = filtered.filter(u =>
                    (u.user_name || '').toLowerCase().includes(search)
                );
            }

            if (status !== 'all') {
                filtered = filtered.filter(u => String(u.is_enrolled) === status);
            }

            renderTable(filtered);
        }

        // ===== SORT =====
        function sortTable(field) {
            if (sortField === field) {
                sortAsc = !sortAsc;
            } else {
                sortField = field;
                sortAsc = true;
            }

            // Reset arrows
            document.querySelectorAll('.sort-arrow').forEach(el => {
                el.textContent = '↕';
                el.parentElement.classList.remove('sorted');
            });

            // Set active arrow
            const activeArrow = document.getElementById(`sort-${field}`);
            activeArrow.textContent = sortAsc ? '↑' : '↓';
            activeArrow.parentElement.classList.add('sorted');

            // Sort
            allUsers.sort((a, b) => {
                let valA = a[field];
                let valB = b[field];

                if (typeof valA === 'boolean') {
                    valA = valA ? 1 : 0;
                    valB = valB ? 1 : 0;
                }

                if (valA < valB) return sortAsc ? -1 : 1;
                if (valA > valB) return sortAsc ? 1 : -1;
                return 0;
            });

            filterUsers();
        }

        // ===== EDIT MODAL =====
        function openEdit(id) {
            const user = allUsers.find(u => u.id === id);
            if (!user) return;

            document.getElementById("editUserId").value = user.id;
            document.getElementById("editId").value = user.id;
            document.getElementById("editName").value = user.user_name || '';
            document.getElementById("editFaceFile").value = user.face_file || '';
            document.getElementById("editEnrolled").value = String(user.is_enrolled);

            openModal('editModal');
        }

        async function saveEdit() {
            const id = document.getElementById("editUserId").value;
            const name = document.getElementById("editName").value.trim();
            const faceFile = document.getElementById("editFaceFile").value.trim();
            const enrolled = document.getElementById("editEnrolled").value === 'true';

            if (!name) {
                showToast("Nama tidak boleh kosong", "error");
                return;
            }

            try {
                const res = await fetch(
                    `${SUPABASE_URL}/rest/v1/users?id=eq.${id}`,
                    {
                        method: "PATCH",
                        headers,
                        body: JSON.stringify({
                            user_name: name,
                            face_file: faceFile || null,
                            is_enrolled: enrolled
                        })
                    }
                );

                if (!res.ok) throw new Error(`HTTP ${res.status}`);

                closeModal('editModal');
                showToast("User berhasil diupdate", "success");
                await fetchUsers();

            } catch (e) {
                console.error(e);
                showToast("Gagal mengupdate user", "error");
            }
        }

        // ===== DELETE MODAL =====
        function openDelete(id, name) {
            document.getElementById("deleteUserId").value = id;
            document.getElementById("deleteUserName").textContent = name;
            openModal('deleteModal');
        }

        async function confirmDelete() {
            const id = document.getElementById("deleteUserId").value;

            try {
                const res = await fetch(
                    `${SUPABASE_URL}/rest/v1/users?id=eq.${id}`,
                    {
                        method: "DELETE",
                        headers
                    }
                );

                if (!res.ok) throw new Error(`HTTP ${res.status}`);

                closeModal('deleteModal');
                showToast("User berhasil dihapus", "success");
                await fetchUsers();

            } catch (e) {
                console.error(e);
                showToast("Gagal menghapus user", "error");
            }
        }

        // ===== MODAL HELPERS =====
        function openModal(id) {
            document.getElementById(id).classList.add('show');
        }

        function closeModal(id) {
            document.getElementById(id).classList.remove('show');
        }

        // Tutup modal kalau klik di luar
        document.querySelectorAll('.modal-overlay').forEach(overlay => {
            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) overlay.classList.remove('show');
            });
        });

        // ===== TOAST =====
        let toastTimeout = null;

        function showToast(message, type = "success") {
            const toast = document.getElementById("toast");
            toast.className = `toast ${type} show`;
            toast.innerHTML = `<span>${type === 'success' ? '✅' : '❌'}</span> ${message}`;

            if (toastTimeout) clearTimeout(toastTimeout);
            toastTimeout = setTimeout(() => toast.classList.remove('show'), 3000);
        }

        // ===== HELPERS =====
        function formatDate(isoString) {
            if (!isoString) return '—';
            const d = new Date(isoString);
            return d.toLocaleDateString('id-ID', {
                day: '2-digit',
                month: 'short',
                year: 'numeric',
                hour: '2-digit',
                minute: '2-digit'
            });
        }

        function logout() {
            fetch('/auth/logout', { method: 'POST', redirect: 'follow' })
                .then(() => window.location.href = '/login');
        }

        // ===== INIT =====
        fetchUsers();
    </script>
    </body>
    </html>
    """)


def generate_token():
    """Generate unique session token"""
    return secrets.token_hex(32)


def get_admin_from_cookie(session_token: str = Cookie(default=None)):
    """Check apakah session token valid"""
    if session_token and session_token in active_sessions:
        return True
    return False


def require_admin(session_token: str = Cookie(default=None)):
    """Dependency: redirect ke login jika belum login"""
    if not session_token or session_token not in active_sessions:
        return None  # Belum login
    return True  # Sudah login


# ============= AUTH ROUTES =============
@app.post("/auth/login")
async def login(request: Request):
    """Login admin"""
    body = await request.json()
    email = body.get("email", "")
    password = body.get("password", "")

    if email == ADMIN_EMAIL and password == ADMIN_PASSWORD:
        token = generate_token()
        active_sessions.add(token)

        response = Response(
            content=json.dumps({"status": "success", "message": "Login successful"}),
            media_type="application/json",
        )
        # Set HttpOnly cookie - tidak bisa diakses JavaScript
        response.set_cookie(
            key="session_token",
            value=token,
            httponly=True,
            samesite="lax",
            max_age=86400,  # 24 jam
        )
        return response
    else:
        return Response(
            content=json.dumps({"status": "error", "message": "Invalid credentials"}),
            status_code=401,
            media_type="application/json",
        )


@app.post("/auth/logout")
async def logout(session_token: str = Cookie(default=None)):
    """Logout admin"""
    if session_token and session_token in active_sessions:
        active_sessions.discard(session_token)

    response = RedirectResponse(url="/login", status_code=303)
    response.delete_cookie(key="session_token")
    return response


@app.get("/auth/check")
async def check_auth(session_token: str = Cookie(default=None)):
    """Check status login (dipakai oleh JS)"""
    if session_token and session_token in active_sessions:
        return {"authenticated": True}
    return Response(
        content=json.dumps({"authenticated": False}),
        status_code=401,
        media_type="application/json",
    )


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)
