# app.py — Face Attendance System (Plain-Text Passwords + RBAC + Multi-Face) — updated
import base64
import csv
import io
import json
import os
import pickle
import threading
from datetime import datetime, date, timedelta
from functools import wraps
from pathlib import Path

import cv2
import numpy as np
import pymysql
from dotenv import load_dotenv
from flask import (
    Flask, jsonify, redirect, render_template, request,
    send_file, session, url_for
)

load_dotenv()

# -------------------- Flask / Paths --------------------
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev-secret-change-me")

APP_ROOT = Path(__file__).parent.resolve()
DATA_DIR = APP_ROOT / "data" / "students"
MODELS_DIR = APP_ROOT / "models"
for p in (DATA_DIR, MODELS_DIR):
    p.mkdir(parents=True, exist_ok=True)

MODEL_PATH  = MODELS_DIR / "lbph_model.xml"
LABELS_PATH = MODELS_DIR / "labels.pickle"
CONFIG_PATH = MODELS_DIR / "config.json"

# -------------------- MySQL --------------------
MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
MYSQL_DB = os.getenv("MYSQL_DB", "face_attendance")

def get_conn():
    return pymysql.connect(
        host=MYSQL_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASSWORD,
        db=MYSQL_DB, charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor, autocommit=True
    )

# -------------------- Users (Plain-Text Passwords) --------------------
def get_user_by_email(email: str):
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT * FROM users WHERE email=%s", (email,))
        return cur.fetchone()

def get_user_by_id(uid: int):
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT * FROM users WHERE id=%s", (uid,))
        return cur.fetchone()

def create_user(email: str, password: str, full_name: str, role="guest", class_name=None):
    # WARNING: Plain-text passwords (local/testing only)
    with get_conn() as c, c.cursor() as cur:
        cur.execute(
            "INSERT INTO users (email, password_hash, full_name, role, class_name) VALUES (%s,%s,%s,%s,%s)",
            (email, password, full_name, role, class_name)
        )
        cur.execute("SELECT * FROM users WHERE email=%s", (email,))
        return cur.fetchone()

def verify_password(stored_pw: str, input_pw: str) -> bool:
    return stored_pw == input_pw  # plain text match

def audit_login(user_id, success: bool):
    ip = request.headers.get("X-Forwarded-For", request.remote_addr)
    ua = (request.headers.get("User-Agent") or "")[:255]
    with get_conn() as c, c.cursor() as cur:
        cur.execute(
            "INSERT INTO login_audit (user_id, ip_address, user_agent, success) VALUES (%s,%s,%s,%s)",
            (user_id, ip, ua, success)
        )

def current_user():
    uid = session.get("user_id")
    return get_user_by_id(uid) if uid else None

# -------------------- Auth Decorators --------------------
def login_required(f):
    @wraps(f)
    def wrapper(*a, **kw):
        if "user_id" not in session:
            return redirect(url_for("login_page", next=request.path))
        return f(*a, **kw)
    return wrapper

def roles_required(*roles):
    def deco(f):
        @wraps(f)
        def wrapper(*a, **kw):
            if "user_id" not in session or session.get("role") not in roles:
                return redirect(url_for("unauthorized"))
            return f(*a, **kw)
        return wrapper
    return deco

# -------------------- Students / Attendance --------------------
def get_student_by_code(code: str):
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT * FROM students WHERE student_code=%s", (code,))
        return cur.fetchone()

def create_student(code: str, name: str, cls: str):
    with get_conn() as c, c.cursor() as cur:
        cur.execute(
            "INSERT INTO students (student_code, full_name, class_name) VALUES (%s,%s,%s)",
            (code, name, cls)
        )
        cur.execute("SELECT * FROM students WHERE student_code=%s", (code,))
        return cur.fetchone()

def mark_attendance(student_id: int, src="auto"):
    today = date.today()
    now = datetime.now()
    with get_conn() as c, c.cursor() as cur:
        cur.execute(
            """INSERT INTO attendance (student_id, attend_date, time_in, source)
               VALUES (%s,%s,%s,%s)
               ON DUPLICATE KEY UPDATE time_in=time_in""",
            (student_id, today, now, src)
        )

# -------------------- Config / OpenCV --------------------
LBPH_DEFAULT = float(os.getenv("LBPH_CONFIDENCE_THRESHOLD", "75"))  # lower is better in LBPH
MIN_BOX_SIZE  = int(os.getenv("MIN_BOX_SIZE", "60"))
cfg = {"lbph_threshold": LBPH_DEFAULT, "mode": "fast"}

def _load_cfg():
    if CONFIG_PATH.exists():
        try:
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                cfg.update(json.load(f))
        except Exception:
            pass

HAAR_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
face_cascade = cv2.CascadeClassifier(HAAR_PATH)

def normalize_face(gray_200: np.ndarray) -> np.ndarray:
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    return cv2.GaussianBlur(clahe.apply(gray_200), (3,3), 0)

def read_b64_image(b64: str) -> np.ndarray:
    if "," in b64:
        b64 = b64.split(",", 1)[1]
    arr = np.frombuffer(base64.b64decode(b64), np.uint8)
    return cv2.imdecode(arr, cv2.IMREAD_COLOR)

def _inner_margin_crop(gray, x, y, w, h, pct=0.08):
    """Crop a slightly smaller inner box to avoid edge artifacts, then resize to 200x200."""
    mx, my = int(w * pct), int(h * pct)
    x2, y2 = max(0, x + mx), max(0, y + my)
    w2, h2 = max(1, w - 2 * mx), max(1, h - 2 * my)
    face = gray[y2:y2 + h2, x2:x2 + w2]
    if face.size == 0:
        face = gray[y:y + h, x:x + w]
    return cv2.resize(face, (200, 200))

def detect_all_faces_200(bgr_img: np.ndarray):
    """Return list of (face_200x200_gray_norm, (x,y,w,h))."""
    gray = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2GRAY)
    # Slightly stricter minNeighbors to reduce jitter; keep scaleFactor 1.1
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=6, minSize=(MIN_BOX_SIZE, MIN_BOX_SIZE))
    out = []
    for (x, y, w, h) in faces:
        f = _inner_margin_crop(gray, x, y, w, h, pct=0.08)
        out.append((normalize_face(f), (int(x), int(y), int(w), int(h))))
    return out

# -------------------- LBPH Model (in-memory) --------------------
recognizer = None
label_to_code = {}  # {label:int -> student_code:str}

def load_model():
    """Load model & labels; keep server alive if files are missing/bad; do a warm-up predict."""
    global recognizer, label_to_code
    recognizer = None
    label_to_code = {}
    _load_cfg()
    if MODEL_PATH.exists() and LABELS_PATH.exists():
        try:
            rec = cv2.face.LBPHFaceRecognizer_create()
            rec.read(str(MODEL_PATH))
            with open(LABELS_PATH, "rb") as f:
                labels = pickle.load(f)
            if isinstance(labels, dict):
                recognizer = rec
                label_to_code = labels
                # Warm-up (avoid first-call overhead)
                try:
                    _ = np.zeros((200, 200), dtype=np.uint8)
                    recognizer.predict(_)
                except Exception:
                    pass
                app.logger.info("Model loaded: %d labels | cfg=%s", len(label_to_code), cfg)
            else:
                raise ValueError("labels.pickle invalid format")
        except Exception as e:
            recognizer = None
            label_to_code = {}
            app.logger.error("Model load failed: %s", e)

load_model()
TRAINING_LOCK = threading.Lock()
LAST_MARK = {}                # {student_id:int -> datetime}
MARK_COOLDOWN = timedelta(seconds=8)

# -------------------- PAGES --------------------
@app.route("/")
def home():
    if "user_id" not in session:
        return redirect(url_for("login_page"))
    role = session.get("role")
    if role == "super_admin":
        return redirect(url_for("super_admin_page"))
    if role == "admin":
        return redirect(url_for("admin_page"))
    if role == "teacher":
        return redirect(url_for("teacher_page"))
    if role == "student":
        return redirect(url_for("attendance_page"))
    return redirect(url_for("login_page"))

@app.route("/login", methods=["GET","POST"])
def login_page():
    if request.method == "GET":
        return render_template("login.html")
    email = (request.form.get("email") or "").strip().lower()
    password = request.form.get("password") or ""
    user = get_user_by_email(email)
    ok = bool(user) and verify_password(user["password_hash"], password)
    audit_login(user["id"] if user else None, ok)
    if not ok:
        return render_template("login.html", error="Invalid email or password"), 401
    session["user_id"] = user["id"]
    session["role"] = user["role"]
    session["full_name"] = user["full_name"]
    return redirect(request.args.get("next") or url_for("home"))

@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login_page"))

@app.route("/unauthorized")
def unauthorized():
    return render_template("unauthorized.html"), 403

@app.route("/register")
@roles_required("super_admin","admin")
def register_page():
    return render_template("register.html")

@app.route("/attendance")
@roles_required("super_admin","admin","teacher")
def attendance_page():
    return render_template("attendance.html")

@app.route("/admin")
@roles_required("super_admin","admin")
def admin_page():
    return render_template("admin.html")

@app.route("/super_admin")
@roles_required("super_admin")
def super_admin_page():
    return render_template("super_admin.html")

@app.route("/teacher")
@roles_required("teacher")
def teacher_page():
    return render_template("teacher.html")

# -------------------- USER MGMT (Super Admin) --------------------
@app.post("/users/create")
@roles_required("super_admin")
def users_create():
    data = request.get_json(force=True) or {}
    email = (data.get("email") or "").strip().lower()
    password = data.get("password") or ""
    full_name = (data.get("full_name") or "").strip()
    role = (data.get("role") or "guest").strip()
    class_name = data.get("class_name")
    if not email or not password or not full_name:
        return jsonify({"ok": False, "error": "email, password, full_name required"}), 400
    if role not in ("super_admin","admin","teacher","student","guest"):
        return jsonify({"ok": False, "error": "invalid role"}), 400
    if get_user_by_email(email):
        return jsonify({"ok": False, "error": "email already exists"}), 400
    u = create_user(email, password, full_name, role, class_name)
    return jsonify({"ok": True, "user": {"id": u["id"], "email": u["email"], "role": u["role"]}})

# -------------------- APIs: Register / Save / Train --------------------
@app.post("/api/register_student")
@roles_required("super_admin","admin")
def api_register_student():
    d = request.get_json(force=True) or {}
    code = (d.get("student_code") or "").strip()
    name = (d.get("full_name") or "").strip()
    cls  = (d.get("class_name") or "").strip()
    if not code or not name or not cls:
        return jsonify({"ok": False, "error": "All fields required"}), 400
    st = get_student_by_code(code)
    if not st:
        st = create_student(code, name, cls)
    (DATA_DIR / code).mkdir(parents=True, exist_ok=True)
    return jsonify({"ok": True, "student": st})

@app.post("/api/save_image")
@roles_required("super_admin","admin")
def api_save_image():
    d = request.get_json(force=True) or {}
    code = d.get("student_code")
    b64  = d.get("image_base64")
    if not code or not b64:
        return jsonify({"ok": False, "error": "Missing data"})
    bgr = read_b64_image(b64)
    faces = detect_all_faces_200(bgr)
    if not faces:
        return jsonify({"ok": False, "error": "No face detected"})
    face200, _ = max(faces, key=lambda t: t[1][2]*t[1][3])
    folder = DATA_DIR / code; folder.mkdir(parents=True, exist_ok=True)
    out = folder / f"{code}_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.jpg"
    cv2.imwrite(str(out), face200)
    return jsonify({"ok": True})

@app.post("/api/train")
@roles_required("super_admin","admin")
def api_train():
    if not TRAINING_LOCK.acquire(blocking=False):
        return jsonify({"ok": False, "error": "Training already in progress"}), 429
    try:
        body = request.get_json(silent=True) or {}
        mode = (body.get("mode") or "fast").lower()
        limit = int(body.get("limit_per_student", 30 if mode != "full" else 0) or 0)
        from train import main as train_main
        result = train_main(mode, limit if limit > 0 else None)
        load_model()  # reload immediately
        return jsonify({
            "ok": True,
            "message": f"Training complete ({result['mode']}).",
            "num_students": result["students"],
            "num_faces": result["images"],
            "calibration": {"lbph_threshold": result["threshold"], "mode": result["mode"]},
            "dataset_stats": result["stats"]
        })
    except Exception as e:
        return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"})
    finally:
        TRAINING_LOCK.release()

# -------------------- APIs: Recognition (Single & Multi) --------------------
@app.post("/api/recognize_frame")
@roles_required("super_admin","admin","teacher")
def api_recognize_frame():
    if recognizer is None or not label_to_code:
        load_model()
        if recognizer is None:
            return jsonify({"ok": False, "error": "Model not trained yet or failed to load"})
    b64 = request.get_json(force=True).get("image_base64")
    if not b64:
        return jsonify({"ok": False, "error": "No image"})
    bgr = read_b64_image(b64)
    faces = detect_all_faces_200(bgr)
    if not faces:
        return jsonify({"ok": True, "matched": False, "reason": "No face"})
    face200, box = max(faces, key=lambda t: t[1][2]*t[1][3])
    pred, conf = recognizer.predict(face200)
    thr = float(cfg.get("lbph_threshold", LBPH_DEFAULT))
    matched = (pred in label_to_code) and (conf <= thr)

    payload = {
        "ok": True,
        "confidence": float(conf),
        "threshold": thr,
        "box": {"x": box[0], "y": box[1], "w": box[2], "h": box[3]} if box else None
    }

    if matched:
        code = label_to_code[pred]
        st = get_student_by_code(code)
        if st:
            lm = LAST_MARK.get(st["id"])
            if not lm or (datetime.now() - lm) >= MARK_COOLDOWN:
                mark_attendance(st["id"], src="auto")
                LAST_MARK[st["id"]] = datetime.now()
            payload.update({
                "matched": True,
                "student_code": code,
                "full_name": st["full_name"],
                "class_name": st["class_name"]
            })
            return jsonify(payload)

    payload.update({"matched": False, "reason": "Unknown"})
    return jsonify(payload)

@app.post("/api/recognize_frame_multi")
@roles_required("super_admin","admin","teacher")
def api_recognize_frame_multi():
    if recognizer is None or not label_to_code:
        load_model()
        if recognizer is None:
            return jsonify({"ok": False, "error": "Model not trained yet or failed to load"})
    b64 = request.get_json(force=True).get("image_base64")
    if not b64:
        return jsonify({"ok": False, "error": "No image"})
    bgr = read_b64_image(b64)
    faces = detect_all_faces_200(bgr)
    thr = float(cfg.get("lbph_threshold", LBPH_DEFAULT))
    results = []

    for (face200, box) in faces:
        pred, conf = recognizer.predict(face200)
        matched = (pred in label_to_code) and (conf <= thr)
        item = {
            "box": {"x": box[0], "y": box[1], "w": box[2], "h": box[3]},
            "confidence": float(conf),
            "threshold": thr,
            "matched": False
        }
        if matched:
            code = label_to_code[pred]
            st = get_student_by_code(code)
            if st:
                lm = LAST_MARK.get(st["id"])
                if not lm or (datetime.now() - lm) >= MARK_COOLDOWN:
                    mark_attendance(st["id"], src="auto")
                    LAST_MARK[st["id"]] = datetime.now()
                item.update({
                    "matched": True,
                    "student_code": code,
                    "full_name": st["full_name"],
                    "class_name": st["class_name"]
                })
        results.append(item)

    return jsonify({"ok": True, "detections": results, "count": len(results)})

@app.post("/api/manual_attendance")
@roles_required("super_admin","admin","teacher")
def api_manual_attendance():
    d = request.get_json(force=True) or {}
    code = (d.get("student_code") or "").strip()
    if not code:
        return jsonify({"ok": False, "error": "student_code required"}), 400
    st = get_student_by_code(code)
    if not st:
        return jsonify({"ok": False, "error": "Student not found"}), 404
    mark_attendance(st["id"], src="manual")
    return jsonify({"ok": True})


# ======== Add below your existing user helpers ========
# List users
@app.get("/api/users")
@roles_required("super_admin")
def api_users_list():
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT id, email, full_name, role, class_name FROM users ORDER BY id DESC")
        rows = cur.fetchall()
    return jsonify({"ok": True, "rows": rows})

# Update user (email/full_name/password/role/class_name)
@app.post("/api/users/update")
@roles_required("super_admin")
def api_users_update():
    d = request.get_json(force=True) or {}
    uid = d.get("id")
    if not uid:
        return jsonify({"ok": False, "error": "id required"}), 400

    fields = []
    params = []

    if "email" in d and d["email"]:
        fields.append("email=%s"); params.append(d["email"].strip().lower())
    if "full_name" in d and d["full_name"]:
        fields.append("full_name=%s"); params.append(d["full_name"].strip())
    if "role" in d and d["role"]:
        role = d["role"].strip()
        if role not in ("super_admin","admin","teacher","student","guest"):
            return jsonify({"ok": False, "error": "invalid role"}), 400
        fields.append("role=%s"); params.append(role)
    if "class_name" in d:
        fields.append("class_name=%s"); params.append(d["class_name"])
    if "password" in d and d["password"]:
        fields.append("password_hash=%s"); params.append(d["password"])  # plaintext per your current setup

    if not fields:
        return jsonify({"ok": False, "error": "nothing to update"}), 400

    params.append(uid)
    sql = "UPDATE users SET " + ", ".join(fields) + " WHERE id=%s"
    with get_conn() as c, c.cursor() as cur:
        cur.execute(sql, tuple(params))
    return jsonify({"ok": True})

# Delete user
@app.post("/api/users/delete")
@roles_required("super_admin")
def api_users_delete():
    d = request.get_json(force=True) or {}
    uid = d.get("id")
    if not uid:
        return jsonify({"ok": False, "error": "id required"}), 400
    with get_conn() as c, c.cursor() as cur:
        cur.execute("DELETE FROM users WHERE id=%s", (uid,))
    return jsonify({"ok": True})

# ===================== STUDENT APIs (list / create / update / delete / enroll) =====================
from werkzeug.utils import secure_filename
import shutil
import re

def _student_face_dir(code: str) -> Path:
    return DATA_DIR / "students" / secure_filename(code)

def _count_faces(code: str) -> int:
    d = _student_face_dir(code)
    if not d.exists():
        return 0
    return sum(1 for p in d.glob("*.jpg"))

@app.get("/api/students")
@roles_required("super_admin", "admin")
def api_students_list():
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT id, student_code, full_name, class_name FROM students ORDER BY id DESC")
        rows = cur.fetchall()
    # augment with face counts
    for r in rows:
        r["face_count"] = _count_faces(r["student_code"])
    return jsonify({"ok": True, "rows": rows})

@app.post("/api/students/create")
@roles_required("super_admin", "admin")
def api_students_create():
    d = request.get_json(force=True) or {}
    code = (d.get("student_code") or "").strip()
    name = (d.get("full_name") or "").strip()
    cls  = d.get("class_name")
    if not code or not name:
        return jsonify({"ok": False, "error": "student_code and full_name required"}), 400
    # prevent duplicates
    if get_student_by_code(code):
        return jsonify({"ok": False, "error": "student_code exists"}), 400
    st = create_student(code, name, cls)
    # ensure folder
    _student_face_dir(code).mkdir(parents=True, exist_ok=True)
    return jsonify({"ok": True, "student": st})

@app.post("/api/students/update")
@roles_required("super_admin", "admin")
def api_students_update():
    d = request.get_json(force=True) or {}
    sid = d.get("id")
    if not sid:
        return jsonify({"ok": False, "error": "id required"}), 400
    fields, params = [], []
    if "student_code" in d and d["student_code"]:
        # if code changes, we should also rename folder
        new_code = d["student_code"].strip()
        fields.append("student_code=%s"); params.append(new_code)
    if "full_name" in d and d["full_name"]:
        fields.append("full_name=%s"); params.append(d["full_name"].strip())
    if "class_name" in d:
        fields.append("class_name=%s"); params.append(d["class_name"])
    if not fields:
        return jsonify({"ok": False, "error": "nothing to update"}), 400
    params.append(sid)
    # fetch old code for folder rename if needed
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT student_code FROM students WHERE id=%s", (sid,))
        row = cur.fetchone()
    if not row:
        return jsonify({"ok": False, "error": "not found"}), 404
    old_code = row["student_code"]
    with get_conn() as c, c.cursor() as cur:
        cur.execute("UPDATE students SET " + ", ".join(fields) + " WHERE id=%s", tuple(params))
    # rename folder if code changed
    if "student_code" in d and d["student_code"] and d["student_code"].strip() != old_code:
        old_dir = _student_face_dir(old_code)
        new_dir = _student_face_dir(d["student_code"].strip())
        if old_dir.exists():
            new_dir.parent.mkdir(parents=True, exist_ok=True)
            if new_dir.exists():
                shutil.rmtree(new_dir)
            old_dir.rename(new_dir)
    return jsonify({"ok": True})

@app.post("/api/students/delete")
@roles_required("super_admin", "admin")
def api_students_delete():
    d = request.get_json(force=True) or {}
    sid = d.get("id")
    remove_photos = bool(d.get("remove_photos", False))
    if not sid:
        return jsonify({"ok": False, "error": "id required"}), 400
    # fetch to know code
    with get_conn() as c, c.cursor() as cur:
        cur.execute("SELECT student_code FROM students WHERE id=%s", (sid,))
        row = cur.fetchone()
    if not row:
        return jsonify({"ok": False, "error": "not found"}), 404
    code = row["student_code"]
    with get_conn() as c, c.cursor() as cur:
        cur.execute("DELETE FROM students WHERE id=%s", (sid,))
    if remove_photos:
        dpath = _student_face_dir(code)
        if dpath.exists():
            shutil.rmtree(dpath)
    return jsonify({"ok": True})

@app.post("/api/students/enroll")
@roles_required("super_admin", "admin")
def api_students_enroll():
    """
    Accepts JSON: { student_code: str, images: [dataURL, ...] }
    Saves images into data/students/<code>/*.jpg
    """
    d = request.get_json(force=True) or {}
    code = (d.get("student_code") or "").strip()
    images = d.get("images") or []
    if not code or not images:
        return jsonify({"ok": False, "error": "student_code and images required"}), 400
    st = get_student_by_code(code)
    if not st:
        return jsonify({"ok": False, "error": "student not found"}), 404
    target = _student_face_dir(code)
    target.mkdir(parents=True, exist_ok=True)
    saved = 0
    for i, data_url in enumerate(images, start=1):
        try:
            if "," in data_url:
                data_url = data_url.split(",", 1)[1]
            arr = np.frombuffer(base64.b64decode(data_url), np.uint8)
            bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
            if bgr is None:
                continue
            # optional: center-crop & resize small to reduce storage
            out = bgr
            # write
            fname = f"cap_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{i:02d}.jpg"
            cv2.imwrite(str(target / fname), out, [int(cv2.IMWRITE_JPEG_QUALITY), 92])
            saved += 1
        except Exception:
            continue
    return jsonify({"ok": True, "saved": saved, "folder": str(target)})


# -------------------- Admin/Teacher: View / Export Attendance --------------------
@app.get("/api/attendance")
@login_required
def api_get_attendance():
    attend_date = request.args.get("date")
    student_code = request.args.get("student_code")
    role = session.get("role")
    user = current_user()

    sql = """SELECT a.id, a.attend_date, a.time_in, a.source,
                    s.student_code, s.full_name, s.class_name
             FROM attendance a
             JOIN students s ON s.id=a.student_id
             WHERE 1=1"""
    params = []
    if attend_date:
        sql += " AND a.attend_date=%s"; params.append(attend_date)
    if student_code:
        sql += " AND s.student_code=%s"; params.append(student_code)
    if role == "teacher" and user and user.get("class_name"):
        sql += " AND s.class_name=%s"; params.append(user["class_name"])
    sql += " ORDER BY a.time_in DESC"
    with get_conn() as c, c.cursor() as cur:
        cur.execute(sql, tuple(params))
        rows = cur.fetchall()
    return jsonify({"ok": True, "rows": rows})

@app.get("/api/attendance/export")
@roles_required("super_admin","admin")
def api_export_attendance():
    attend_date = request.args.get("date")
    sql = """SELECT a.attend_date, a.time_in, a.source,
                    s.student_code, s.full_name, s.class_name
             FROM attendance a JOIN students s ON s.id=a.student_id"""
    params = []
    if attend_date:
        sql += " WHERE a.attend_date=%s"; params.append(attend_date)
    sql += " ORDER BY a.time_in DESC"
    with get_conn() as c, c.cursor() as cur:
        cur.execute(sql, tuple(params))
        rows = cur.fetchall()

    s = io.StringIO()
    w = csv.writer(s)
    w.writerow(["attend_date","time_in","source","student_code","full_name","class_name"])
    for r in rows:
        w.writerow([r["attend_date"], r["time_in"], r["source"], r["student_code"], r["full_name"], r["class_name"]])
    s.seek(0)
    return send_file(
        io.BytesIO(s.getvalue().encode("utf-8")),
        mimetype="text/csv",
        as_attachment=True,
        download_name=f"attendance_{attend_date or 'all'}.csv"
    )

# -------------------- Config / Health / Reset --------------------
@app.get("/api/config")
@roles_required("super_admin","admin")
def api_config():
    _load_cfg()
    return jsonify({"ok": True, "config": cfg, "min_box_size": MIN_BOX_SIZE})

@app.post("/api/config/set")
@roles_required("super_admin","admin")
def api_config_set():
    global MIN_BOX_SIZE
    d = request.get_json(force=True) or {}
    if "lbph_threshold" in d:
        # clamp to safe band (prevents overly-permissive thresholds)
        thr = float(d["lbph_threshold"])
        thr = max(55.0, min(95.0, thr))
        cfg["lbph_threshold"] = thr
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(cfg, f, indent=2)
    if "min_box_size" in d:
        MIN_BOX_SIZE = int(d["min_box_size"])
    return jsonify({"ok": True, "config": cfg, "min_box_size": MIN_BOX_SIZE})

@app.get("/api/health")
@login_required
def api_health():
    return jsonify({
        "ok": True,
        "model_files": {
            "xml": MODEL_PATH.exists(),
            "labels": LABELS_PATH.exists(),
            "config": CONFIG_PATH.exists(),
        },
        "model_ready": (recognizer is not None) and bool(label_to_code),
        "students": len(label_to_code),
        "threshold": cfg.get("lbph_threshold", LBPH_DEFAULT),
        "role": session.get("role")
    })

@app.post("/api/reset_model")
@roles_required("super_admin")
def api_reset_model():
    try:
        if MODEL_PATH.exists(): MODEL_PATH.unlink()
        if LABELS_PATH.exists(): LABELS_PATH.unlink()
        load_model()
        return jsonify({"ok": True, "message": "Model removed. Train again."})
    except Exception as e:
        return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"}), 500

# -------------------- Run --------------------
if __name__ == "__main__":
    app.run(debug=True)
