from html import escape
from pathlib import Path
import sys
from urllib.parse import quote, unquote

from fastapi import FastAPI, Form, HTTPException, Query
from fastapi.responses import HTMLResponse, RedirectResponse


BASE_DIR = Path(__file__).resolve().parent
PROJECT_DIR = BASE_DIR.parent
ML_DIR = PROJECT_DIR / "ml"

if str(ML_DIR) not in sys.path:
    sys.path.append(str(ML_DIR))

from database import (  # noqa: E402
    create_user,
    delete_prediction_result,
    delete_user,
    get_database_label,
    get_prediction_result,
    get_user_by_id,
    init_db,
    list_prediction_results,
    list_users,
    save_prediction_result,
    update_prediction_result,
    update_user,
)


app = FastAPI(title="ConfiVoice Admin Web")


@app.on_event("startup")
def startup():
    init_db()


@app.get("/")
def read_root():
    return {
        "name": "ConfiVoice Admin Web",
        "status": "ok",
        "admin_page": "/admin",
        "database": get_database_label(),
    }


@app.get("/predictions")
def read_predictions(limit: int = 500):
    return {
        "database": get_database_label(),
        "data": list_prediction_results(limit=limit),
    }


@app.post("/admin/predictions")
def create_prediction_from_admin(
    student_name: str = Form(...),
    student_gender: str = Form(""),
    predicted_label: str = Form("PD"),
    description: str = Form(""),
    confidence_percent: float = Form(0),
    probability_pd_percent: float = Form(0),
    probability_tpd_percent: float = Form(0),
    is_valid_audio: str = Form("1"),
    error_message: str = Form(""),
    audio_duration: float = Form(0),
):
    student_name = student_name.strip()
    if not student_name:
        raise HTTPException(status_code=400, detail="Nama siswa/i wajib diisi.")

    save_prediction_result(
        student_name,
        _result_payload(
            predicted_label=predicted_label,
            student_gender=student_gender,
            description=description,
            confidence_percent=confidence_percent,
            probability_pd_percent=probability_pd_percent,
            probability_tpd_percent=probability_tpd_percent,
            is_valid_audio=is_valid_audio,
            error_message=error_message,
            audio_duration=audio_duration,
        ),
    )
    return _redirect_admin()


@app.get("/admin/predictions/new", response_class=HTMLResponse)
def read_new_prediction_page():
    return _html_page(
        title="Tambah Data",
        active="prediction-new",
        content=f"""
        <section class="page-head">
          <div>
            <h1>Tambah Data</h1>
            <p>Tambahkan hasil analisis secara manual jika diperlukan.</p>
          </div>
        </section>
        <section class="panel">
          {_prediction_form(action="/admin/predictions", submit_label="Tambah Data")}
        </section>
        """,
    )


@app.get("/admin/predictions/{prediction_id}/edit", response_class=HTMLResponse)
def read_edit_prediction_page(prediction_id: int):
    row = get_prediction_result(prediction_id)
    if row is None:
        raise HTTPException(status_code=404, detail="Data tidak ditemukan.")

    return _html_page(
        title=f"Edit Data #{prediction_id}",
        active="dashboard",
        content=f"""
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>Edit Data #{prediction_id}</h2>
              <p>Ubah hasil analisis yang tersimpan di admin.</p>
            </div>
            <a class="button secondary" href="/admin">Kembali</a>
          </div>
          {_prediction_form(row=row, action=f"/admin/predictions/{prediction_id}/edit", submit_label="Simpan Perubahan")}
        </section>
        """,
    )


@app.post("/admin/predictions/{prediction_id}/edit")
def update_prediction_from_admin(
    prediction_id: int,
    student_name: str = Form(...),
    student_gender: str = Form(""),
    predicted_label: str = Form("PD"),
    description: str = Form(""),
    confidence_percent: float = Form(0),
    probability_pd_percent: float = Form(0),
    probability_tpd_percent: float = Form(0),
    is_valid_audio: str = Form("1"),
    error_message: str = Form(""),
    audio_duration: float = Form(0),
):
    student_name = student_name.strip()
    if not student_name:
        raise HTTPException(status_code=400, detail="Nama siswa/i wajib diisi.")

    updated = update_prediction_result(
        prediction_id,
        _db_values(
            student_name=student_name,
            student_gender=student_gender,
            predicted_label=predicted_label,
            description=description,
            confidence_percent=confidence_percent,
            probability_pd_percent=probability_pd_percent,
            probability_tpd_percent=probability_tpd_percent,
            is_valid_audio=is_valid_audio,
            error_message=error_message,
            audio_duration=audio_duration,
        ),
    )
    if not updated:
        raise HTTPException(status_code=404, detail="Data tidak ditemukan.")

    return _redirect_admin()


@app.post("/admin/predictions/{prediction_id}/delete")
def delete_prediction_from_admin(prediction_id: int):
    delete_prediction_result(prediction_id)
    return _redirect_admin()


@app.get("/admin/users", response_class=HTMLResponse)
def read_users_page():
    users = list_users(limit=500)
    return _html_page(
        title="Tambah User",
        active="users",
        content=f"""
        <section class="page-head">
          <div>
            <h1>Tambah User</h1>
            <p>Kelola akun guru atau pengguna aplikasi ConfiVoice.</p>
          </div>
        </section>
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>Tambah User</h2>
              <p>Username akan disimpan huruf kecil, password disimpan sebagai hash.</p>
            </div>
          </div>
          {_user_form(action="/admin/users", submit_label="Tambah User", password_required=True)}
        </section>
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>CRUD User</h2>
              <p>Edit atau hapus user yang sudah terdaftar.</p>
            </div>
          </div>
          {_user_table(users)}
        </section>
        """,
    )


@app.post("/admin/users")
def create_user_from_admin(
    full_name: str = Form(...),
    username: str = Form(...),
    password: str = Form(...),
):
    full_name = full_name.strip()
    username = username.strip()
    password = password.strip()
    if not full_name or not username or not password:
        raise HTTPException(status_code=400, detail="Nama, username, dan password wajib diisi.")

    try:
        create_user(full_name, username, password)
    except Exception as error:
        raise HTTPException(
            status_code=400,
            detail="Username sudah dipakai atau data user tidak valid.",
        ) from error
    return RedirectResponse(url="/admin/users", status_code=303)


@app.get("/admin/users/{user_id}/edit", response_class=HTMLResponse)
def read_edit_user_page(user_id: int):
    user = get_user_by_id(user_id)
    if user is None:
        raise HTTPException(status_code=404, detail="User tidak ditemukan.")

    return _html_page(
        title=f"Edit User #{user_id}",
        active="users",
        content=f"""
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>Edit User #{user_id}</h2>
              <p>Kosongkan password jika tidak ingin mengganti password.</p>
            </div>
            <a class="button secondary" href="/admin/users">Kembali</a>
          </div>
          {_user_form(row=user, action=f"/admin/users/{user_id}/edit", submit_label="Simpan User")}
        </section>
        """,
    )


@app.post("/admin/users/{user_id}/edit")
def update_user_from_admin(
    user_id: int,
    full_name: str = Form(...),
    username: str = Form(...),
    password: str = Form(""),
):
    full_name = full_name.strip()
    username = username.strip()
    if not full_name or not username:
        raise HTTPException(status_code=400, detail="Nama dan username wajib diisi.")

    try:
        updated = update_user(user_id, full_name, username, password)
    except Exception as error:
        raise HTTPException(
            status_code=400,
            detail="Username sudah dipakai atau data user tidak valid.",
        ) from error
    if not updated:
        raise HTTPException(status_code=404, detail="User tidak ditemukan.")
    return RedirectResponse(url="/admin/users", status_code=303)


@app.post("/admin/users/{user_id}/delete")
def delete_user_from_admin(user_id: int):
    delete_user(user_id)
    return RedirectResponse(url="/admin/users", status_code=303)


@app.get("/admin/logout", response_class=HTMLResponse)
def read_logout_page():
    return _html_page(
        title="Logout",
        active="logout",
        content="""
        <section class="panel logout-panel">
          <h1>Logout</h1>
          <p>Sesi admin ditutup. Saat ini web admin belum memakai session login khusus, jadi halaman ini menjadi pintu keluar visual.</p>
          <a class="button" href="/admin">Kembali ke Dashboard</a>
        </section>
        """,
    )


@app.get("/admin/students/{student_name}", response_class=HTMLResponse)
def read_student_predictions_page(student_name: str):
    decoded_name = unquote(student_name)
    rows = [
        row
        for row in list_prediction_results(limit=500)
        if row["student_name"] == decoded_name
    ]
    total = len(rows)
    pd_count = sum(1 for row in rows if row["predicted_label"] == "PD")
    tpd_count = sum(1 for row in rows if row["predicted_label"] == "TPD")
    avg_pd = _average_pd_ratio(rows)

    return _html_page(
        title=f"Analisis {decoded_name}",
        active="dashboard",
        content=f"""
        <section class="page-head">
          <div>
            <h1>{escape(decoded_name)}</h1>
            <p>Folder hasil analisis siswa/i.</p>
          </div>
          <a class="button secondary" href="/admin">Kembali</a>
        </section>
        <section class="stats">
          <div class="stat"><span>Total Prediksi</span><strong>{total}</strong></div>
          <div class="stat"><span>Percaya Diri</span><strong>{pd_count}</strong></div>
          <div class="stat"><span>Tidak Percaya Diri</span><strong>{tpd_count}</strong></div>
          <div class="stat"><span>Rata-rata PD</span><strong>{avg_pd * 100:.1f}%</strong></div>
        </section>
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>Isi Folder Analisis</h2>
              <p>Edit atau hapus hasil prediksi milik siswa/i ini.</p>
            </div>
          </div>
          {_prediction_table(rows)}
        </section>
        """,
    )


@app.get("/admin", response_class=HTMLResponse)
def read_admin_page(
    q: str = Query("", alias="q"),
    label: str = Query("", alias="label"),
):
    rows = list_prediction_results(limit=500)
    clean_query = q.strip()
    clean_label = label.strip().upper()
    if clean_label not in {"PD", "TPD"}:
        clean_label = ""
    filtered_rows = _filter_prediction_rows(rows, clean_query, clean_label)
    total = len(filtered_rows)
    pd_count = sum(1 for row in filtered_rows if row["predicted_label"] == "PD")
    tpd_count = sum(1 for row in filtered_rows if row["predicted_label"] == "TPD")
    avg_pd = _average_pd_ratio(filtered_rows)
    grouped_rows = _group_predictions_by_student(filtered_rows)

    return _html_page(
        title="Admin ConfiVoice",
        active="dashboard",
        content=f"""
        <section class="page-head">
          <div>
            <h1>Admin ConfiVoice</h1>
            <p>Dashboard hasil analisis percaya diri siswa/i dari aplikasi Flutter.</p>
          </div>
        </section>
        <section class="stats">
          <div class="stat"><span>Total Data Tampil</span><strong data-stat="total">{total}</strong></div>
          <div class="stat"><span>Percaya Diri</span><strong data-stat="pd">{pd_count}</strong></div>
          <div class="stat"><span>Tidak Percaya Diri</span><strong data-stat="tpd">{tpd_count}</strong></div>
          <div class="stat"><span>Rata-rata Percaya Diri</span><strong data-stat="avg-pd">{avg_pd * 100:.1f}%</strong></div>
        </section>
        <section class="panel">
          <div class="panel-head">
            <div>
              <h2>Folder Siswa/i</h2>
              <p>Pilih nama siswa/i untuk melihat semua hasil analisisnya.</p>
            </div>
          </div>
          {_dashboard_filter_form(clean_query, clean_label)}
          {_student_folder_grid(grouped_rows)}
        </section>
        """,
    )


def _redirect_admin():
    return RedirectResponse(url="/admin", status_code=303)


def _result_payload(
    *,
    predicted_label,
    student_gender,
    description,
    confidence_percent,
    probability_pd_percent,
    probability_tpd_percent,
    is_valid_audio,
    error_message,
    audio_duration,
):
    return {
        "predicted_label": predicted_label,
        "student_gender": _clean_student_gender(student_gender),
        "description": description,
        "confidence": _percent_to_ratio(confidence_percent),
        "probability_pd": _percent_to_ratio(probability_pd_percent),
        "probability_tpd": _percent_to_ratio(probability_tpd_percent),
        "is_valid_audio": is_valid_audio == "1",
        "error_message": error_message.strip() or None,
        "audio_quality": {"duration": audio_duration},
        "voice_indicators": {},
    }


def _db_values(
    *,
    student_name,
    student_gender,
    predicted_label,
    description,
    confidence_percent,
    probability_pd_percent,
    probability_tpd_percent,
    is_valid_audio,
    error_message,
    audio_duration,
):
    return (
        student_name,
        _clean_student_gender(student_gender),
        predicted_label.strip() or None,
        description.strip() or None,
        _percent_to_ratio(confidence_percent),
        _percent_to_ratio(probability_pd_percent),
        _percent_to_ratio(probability_tpd_percent),
        1 if is_valid_audio == "1" else 0,
        error_message.strip() or None,
        float(audio_duration or 0),
        0,
        0,
        0,
        0,
        0,
    )


def _percent_to_ratio(value):
    return max(0, min(float(value or 0), 100)) / 100


def _ratio_to_percent(value):
    return f"{float(value or 0) * 100:.2f}"


def _clean_student_gender(value):
    gender = (value or "").strip()
    return gender if gender in {"Laki-laki", "Perempuan"} else None


def _student_gender_label(value):
    return value or "Belum diisi"


def _pd_probability(row):
    return float(row.get("probability_pd") or 0)


def _average_pd_ratio(rows):
    return sum(_pd_probability(row) for row in rows) / len(rows) if rows else 0


def _filter_prediction_rows(rows, query, label):
    query = query.strip().lower()
    label = label.strip().upper()

    filtered_rows = rows
    if query:
        filtered_rows = [
            row
            for row in filtered_rows
            if query in (row["student_name"] or "").lower()
        ]
    if label in {"PD", "TPD"}:
        filtered_rows = [
            row
            for row in filtered_rows
            if (row["predicted_label"] or "").upper() == label
        ]
    return filtered_rows


def _dashboard_filter_form(query, label):
    all_selected = "selected" if not label else ""
    pd_selected = "selected" if label == "PD" else ""
    tpd_selected = "selected" if label == "TPD" else ""
    return f"""
    <form class="filter-bar" method="get" action="/admin" data-auto-filter>
      <label>
        <span>Cari nama siswa/i</span>
        <input name="q" value="{escape(query)}" placeholder="Ketik nama siswa/i...">
      </label>
      <label>
        <span>Filter hasil</span>
        <select name="label">
          <option value="" {all_selected}>Semua</option>
          <option value="PD" {pd_selected}>Percaya Diri</option>
          <option value="TPD" {tpd_selected}>Tidak Percaya Diri</option>
        </select>
      </label>
      <div class="filter-actions">
        <a class="button secondary" href="/admin">Reset</a>
      </div>
    </form>
    """


def _group_predictions_by_student(rows):
    groups = {}
    for row in rows:
        student_name = row["student_name"] or "Tanpa Nama"
        groups.setdefault(student_name, []).append(row)
    return sorted(groups.items(), key=lambda item: item[0].lower())


def _student_folder_grid(grouped_rows):
    if not grouped_rows:
        return '<div class="empty folder-empty" data-folder-empty>Belum ada folder siswa/i.</div>'

    cards = []
    for student_name, rows in grouped_rows:
        total = len(rows)
        latest = rows[0]
        gender = _student_gender_label(latest.get("student_gender"))
        label = latest.get("predicted_label") or "-"
        pd_count = sum(1 for row in rows if (row.get("predicted_label") or "").upper() == "PD")
        tpd_count = sum(1 for row in rows if (row.get("predicted_label") or "").upper() == "TPD")
        pd_sum = sum(_pd_probability(row) for row in rows)
        labels = " ".join(
            sorted({(row.get("predicted_label") or "").upper() for row in rows})
        )
        latest_pd = _pd_probability(latest) * 100
        pd_average = _average_pd_ratio(rows) * 100
        folder_url = f"/admin/students/{quote(student_name, safe='')}"
        cards.append(
            f"""
            <a class="student-folder" href="{folder_url}" data-student-name="{escape(student_name.lower())}" data-labels="{escape(labels)}" data-total="{total}" data-pd-count="{pd_count}" data-tpd-count="{tpd_count}" data-pd-sum="{pd_sum:.8f}">
              <div class="folder-icon">{escape(student_name[:1].upper() or "?")}</div>
              <div class="folder-content">
                <strong>{escape(student_name)}</strong>
                <span>{escape(gender)} | {total} hasil analisis</span>
                <small>Label terakhir: {escape(label)} | PD terakhir {latest_pd:.1f}% | Rata-rata PD {pd_average:.1f}%</small>
              </div>
            </a>
            """
        )

    return (
        f'<div class="folder-grid" data-folder-grid>{"".join(cards)}</div>'
        '<div class="empty folder-empty is-hidden" data-filter-empty>'
        "Tidak ada folder siswa/i yang sesuai filter."
        "</div>"
    )


def _prediction_form(row=None, action="/admin/predictions", submit_label="Simpan"):
    row = row or {}
    student_name = escape(str(row.get("student_name") or ""))
    student_gender = str(row.get("student_gender") or "")
    predicted_label = str(row.get("predicted_label") or "PD")
    description = escape(str(row.get("description") or ""))
    confidence = _ratio_to_percent(row.get("confidence"))
    probability_pd = _ratio_to_percent(row.get("probability_pd"))
    probability_tpd = _ratio_to_percent(row.get("probability_tpd"))
    audio_duration = float(row.get("audio_duration") or 0)
    error_message = escape(str(row.get("error_message") or ""))
    valid_audio = str(int(row.get("is_valid_audio", 1))) == "1"
    pd_selected = "selected" if predicted_label == "PD" else ""
    tpd_selected = "selected" if predicted_label == "TPD" else ""
    male_selected = "selected" if student_gender == "Laki-laki" else ""
    female_selected = "selected" if student_gender == "Perempuan" else ""
    valid_selected = "selected" if valid_audio else ""
    invalid_selected = "selected" if not valid_audio else ""

    return f"""
    <form class="form-grid" method="post" action="{escape(action)}">
      <label>
        <span>Nama siswa/i</span>
        <input name="student_name" value="{student_name}" required>
      </label>
      <label>
        <span>Jenis kelamin</span>
        <select name="student_gender">
          <option value="">Pilih jenis kelamin</option>
          <option value="Laki-laki" {male_selected}>Laki-laki</option>
          <option value="Perempuan" {female_selected}>Perempuan</option>
        </select>
      </label>
      <label>
        <span>Label</span>
        <select name="predicted_label">
          <option value="PD" {pd_selected}>PD</option>
          <option value="TPD" {tpd_selected}>TPD</option>
        </select>
      </label>
      <label>
        <span>Confidence (%)</span>
        <input name="confidence_percent" type="number" min="0" max="100" step="0.01" value="{confidence}">
      </label>
      <label>
        <span>PD (%)</span>
        <input name="probability_pd_percent" type="number" min="0" max="100" step="0.01" value="{probability_pd}">
      </label>
      <label>
        <span>TPD (%)</span>
        <input name="probability_tpd_percent" type="number" min="0" max="100" step="0.01" value="{probability_tpd}">
      </label>
      <label>
        <span>Durasi audio (detik)</span>
        <input name="audio_duration" type="number" min="0" step="0.01" value="{audio_duration:.2f}">
      </label>
      <label>
        <span>Status audio</span>
        <select name="is_valid_audio">
          <option value="1" {valid_selected}>Valid</option>
          <option value="0" {invalid_selected}>Tidak valid</option>
        </select>
      </label>
      <label class="wide">
        <span>Keterangan</span>
        <input name="description" value="{description}">
      </label>
      <label class="wide">
        <span>Pesan error audio</span>
        <input name="error_message" value="{error_message}">
      </label>
      <div class="form-actions">
        <button class="button" type="submit">{escape(submit_label)}</button>
      </div>
    </form>
    """


def _prediction_table(rows):
    table_rows = []
    for row in rows:
        valid_text = "Valid" if row["is_valid_audio"] else "Tidak valid"
        table_rows.append(
            "<tr>"
            f"<td>{row['id']}</td>"
            f"<td>{escape(str(row['created_at']))}</td>"
            f"<td>{escape(row['student_name'])}</td>"
            f"<td>{escape(_student_gender_label(row.get('student_gender')))}</td>"
            f"<td><span class='badge'>{escape(row['predicted_label'] or '-')}</span></td>"
            f"<td>{escape(row['description'] or '-')}</td>"
            f"<td>{float(row['confidence'] or 0) * 100:.2f}%</td>"
            f"<td>{float(row['probability_pd'] or 0) * 100:.2f}%</td>"
            f"<td>{float(row['probability_tpd'] or 0) * 100:.2f}%</td>"
            f"<td>{float(row['audio_duration'] or 0):.2f} dtk</td>"
            f"<td>{valid_text}</td>"
            "<td class='row-actions'>"
            f"<a class='small-button' href='/admin/predictions/{row['id']}/edit'>Edit</a>"
            f"<form method='post' action='/admin/predictions/{row['id']}/delete' "
            "onsubmit=\"return confirm('Hapus data ini?');\">"
            "<button class='small-button danger' type='submit'>Hapus</button>"
            <div class="modal-backdrop is-hidden" data-delete-user-modal>
          <section class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-user-title">
            <h2 id="delete-user-title">Hapus user?</h2>
            <p data-delete-user-message>User ini akan dihapus dari database.</p>
            <div class="modal-actions">
              <button class="button secondary" type="button" data-delete-user-cancel>Batal</button>
              <button class="button danger" type="button" data-delete-user-confirm>Hapus</button>
            </div>
          </section>
        </div>
            "</form>"
            "</td>"
            "</tr>"
        )

    body = "\n".join(table_rows) or (
        '<tr><td colspan="12" class="empty">Belum ada hasil prediksi.</td></tr>'
    )

    return f"""
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Waktu</th>
            <th>Nama siswa/i</th>
            <th>Jenis kelamin</th>
            <th>Label</th>
            <th>Keterangan</th>
            <th>Confidence</th>
            <th>PD</th>
            <th>TPD</th>
            <th>Durasi</th>
            <th>Status audio</th>
            <th>Aksi</th>
          </tr>
        </thead>
        <tbody>{body}</tbody>
      </table>
    </div>
    """


def _user_form(row=None, action="/admin/users", submit_label="Simpan", password_required=False):
    row = row or {}
    full_name = escape(str(row.get("full_name") or ""))
    username = escape(str(row.get("username") or ""))
    required = "required" if password_required else ""
    password_hint = "Wajib diisi untuk user baru." if password_required else "Kosongkan jika tidak diganti."

    return f"""
    <form class="form-grid user-form" method="post" action="{escape(action)}">
      <label>
        <span>Nama lengkap</span>
        <input name="full_name" value="{full_name}" required>
      </label>
      <label>
        <span>Username</span>
        <input name="username" value="{username}" required>
      </label>
      <label>
        <span>Password</span>
        <input name="password" type="password" {required}>
        <small>{password_hint}</small>
      </label>
      <div class="form-actions">
        <button class="button" type="submit">{escape(submit_label)}</button>
      </div>
    </form>
    """


def _user_table(users):
    table_rows = []
    for user in users:
        table_rows.append(
            "<tr>"
            f"<td>{user['id']}</td>"
            f"<td>{escape(str(user['created_at']))}</td>"
            f"<td>{escape(user['full_name'])}</td>"
            f"<td>{escape(user['username'])}</td>"
            "<td class='row-actions'>"
            f"<a class='small-button' href='/admin/users/{user['id']}/edit'>Edit</a>"
            f"<form method='post' action='/admin/users/{user['id']}/delete' "
            f"data-delete-user-form data-user-name=\"{escape(user['full_name'])}\">"
            "<button class='small-button danger' type='submit'>Hapus</button>"
            "</form>"
            "</td>"
            "</tr>"
        )

    body = "\n".join(table_rows) or (
        '<tr><td colspan="5" class="empty">Belum ada user terdaftar.</td></tr>'
    )

    return f"""
    <div class="table-wrap">
      <table class="user-table">
        <thead>
          <tr>
            <th>ID</th>
            <th>Dibuat</th>
            <th>Nama lengkap</th>
            <th>Username</th>
            <th>Aksi</th>
          </tr>
        </thead>
        <tbody>{body}</tbody>
      </table>
    </div>
    """


def _nav_active(active, name):
    return " active" if active == name else ""


def _sidebar(active):
    update_open = " open" if active in {"prediction-new", "users"} else ""
    return f"""
    <aside class="sidebar">
      <div class="brand">
        <span class="brand-mark">CV</span>
        <div>
          <strong>ConfiVoice</strong>
          <small>Admin Panel</small>
        </div>
      </div>
      <nav class="nav">
        <a class="nav-link{_nav_active(active, 'dashboard')}" href="/admin">
          <span>Dashboard</span>
        </a>
        <details class="nav-group"{update_open}>
          <summary>Update</summary>
          <a class="nav-link{_nav_active(active, 'prediction-new')}" href="/admin/predictions/new">Tambah Data</a>
          <a class="nav-link{_nav_active(active, 'users')}" href="/admin/users">Tambah User</a>
        </details>
      </nav>
      <a class="nav-link logout{_nav_active(active, 'logout')}" href="/admin/logout">Keluar</a>
    </aside>
    """


def _html_page(title, content, active="dashboard"):
    return f"""
    <!doctype html>
    <html lang="id">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>{escape(title)}</title>
        <style>
          :root {{
            font-family: Inter, Arial, sans-serif;
            background: #020817;
            color: #f8fbff;
          }}
          * {{
            box-sizing: border-box;
          }}
          body {{
            margin: 0;
            min-height: 100vh;
            background:
              radial-gradient(circle at top right, rgba(37, 99, 235, 0.26), transparent 32rem),
              linear-gradient(135deg, #020817 0%, #071426 58%, #0b1d35 100%);
          }}
          .app-shell {{
            display: grid;
            grid-template-columns: 260px minmax(0, 1fr);
            min-height: 100vh;
          }}
          .sidebar {{
            position: sticky;
            top: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
            padding: 22px 16px;
            background: #030b18;
            border-right: 1px solid #173456;
            color: #dbeafe;
          }}
          .brand {{
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 18px;
          }}
          .brand-mark {{
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 42px;
            height: 42px;
            border-radius: 8px;
            background: linear-gradient(135deg, #2563eb, #60a5fa);
            color: #ffffff;
            font-weight: 800;
          }}
          .brand strong, .brand small {{
            display: block;
          }}
          .brand small {{
            margin-top: 2px;
            color: #8db8f5;
            font-size: 12px;
          }}
          .nav {{
            display: grid;
            gap: 8px;
          }}
          .nav-link, .nav-group summary {{
            display: flex;
            align-items: center;
            width: 100%;
            min-height: 42px;
            padding: 10px 12px;
            border-radius: 8px;
            color: #a8c7f6;
            text-decoration: none;
            font-weight: 700;
            cursor: pointer;
          }}
          .nav-link:hover, .nav-group summary:hover, .nav-link.active {{
            background: #12345b;
            color: #ffffff;
          }}
          .nav-group {{
            border-radius: 8px;
          }}
          .nav-group summary {{
            list-style: none;
          }}
          .nav-group summary::-webkit-details-marker {{
            display: none;
          }}
          .nav-group summary::after {{
            content: "v";
            margin-left: auto;
            font-size: 12px;
          }}
          .nav-group:not([open]) summary::after {{
            content: ">";
          }}
          .nav-group .nav-link {{
            min-height: 38px;
            margin-top: 6px;
            padding-left: 24px;
            font-size: 14px;
            font-weight: 600;
          }}
          .logout {{
            margin-top: auto;
            color: #fecaca;
          }}
          .logout:hover, .logout.active {{
            background: #571d2b;
            color: #ffffff;
          }}
          main {{
            width: 100%;
            max-width: 1440px;
            padding: 24px;
          }}
          .page-head {{
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 16px;
            margin-bottom: 18px;
          }}
          h1, h2 {{
            margin: 0 0 6px;
          }}
          h1 {{
            font-size: 28px;
          }}
          h2 {{
            font-size: 20px;
          }}
          p {{
            margin: 0;
            color: #9fb3ca;
          }}
          .stats {{
            display: grid;
            grid-template-columns: repeat(4, minmax(140px, 1fr));
            gap: 12px;
            margin-bottom: 16px;
          }}
          .stat, .panel {{
            background: rgba(8, 18, 34, 0.94);
            border: 1px solid #1d3558;
            border-radius: 8px;
            box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
          }}
          .stat {{
            padding: 14px;
          }}
          .stat span {{
            display: block;
            color: #8ca6c3;
            font-size: 13px;
            margin-bottom: 6px;
          }}
          .stat strong {{
            font-size: 24px;
            color: #ffffff;
          }}
          .panel {{
            margin-bottom: 16px;
            padding: 16px;
          }}
          .panel-head {{
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            margin-bottom: 14px;
          }}
          .form-grid {{
            display: grid;
            grid-template-columns: repeat(4, minmax(160px, 1fr));
            gap: 12px;
          }}
          .filter-bar {{
            display: grid;
            grid-template-columns: minmax(220px, 1fr) minmax(180px, 260px) auto;
            gap: 12px;
            align-items: end;
            margin-bottom: 16px;
            padding: 14px;
            border: 1px solid #1d3558;
            border-radius: 8px;
            background: rgba(11, 22, 40, 0.78);
          }}
          .filter-actions {{
            display: flex;
            gap: 8px;
          }}
          .filter-actions .button {{
            white-space: nowrap;
          }}
          label span {{
            display: block;
            color: #9fb3ca;
            font-size: 13px;
            margin-bottom: 6px;
          }}
          small {{
            display: block;
            margin-top: 5px;
            color: #8ca6c3;
            font-size: 12px;
          }}
          input, select {{
            width: 100%;
            min-height: 40px;
            border: 1px solid #24446d;
            border-radius: 8px;
            padding: 9px 10px;
            font: inherit;
            background: #0b1628;
            color: #f8fbff;
          }}
          input:focus, select:focus {{
            border-color: #60a5fa;
            outline: 2px solid rgba(96, 165, 250, 0.18);
          }}
          .wide {{
            grid-column: span 2;
          }}
          .form-actions {{
            display: flex;
            align-items: end;
          }}
          .button, .small-button {{
            display: inline-flex;
            align-items: center;
            justify-content: center;
            border: 0;
            border-radius: 8px;
            background: #3b82f6;
            color: #ffffff;
            font-weight: 700;
            text-decoration: none;
            cursor: pointer;
          }}
          .button {{
            min-height: 40px;
            padding: 9px 14px;
          }}
          .button:hover, .small-button:hover {{
            background: #2563eb;
          }}
          .button.secondary, .small-button {{
            background: #122a4a;
            color: #bfdbfe;
            border: 1px solid #24446d;
          }}
          .button.secondary:hover, .small-button:hover {{
            background: #173b68;
          }}
          .small-button {{
            min-height: 32px;
            padding: 7px 10px;
            font-size: 13px;
          }}
          .small-button.danger {{
            background: #421623;
            color: #fecaca;
            border-color: #7f1d1d;
          }}
          .small-button.danger:hover {{
            background: #5f1b2a;
          }}
          .button.danger {{
            background: #7f1d1d;
            color: #ffffff;
          }}
          .button.danger:hover {{
            background: #991b1b;
          }}
          .table-wrap {{
            overflow-x: auto;
            border: 1px solid #1d3558;
            border-radius: 8px;
          }}
          table {{
            width: 100%;
            border-collapse: collapse;
            min-width: 1240px;
          }}
          th, td {{
            padding: 12px 14px;
            border-bottom: 1px solid #182b49;
            text-align: left;
            white-space: nowrap;
          }}
          th {{
            background: #0b1b31;
            color: #bfdbfe;
            font-size: 13px;
            text-transform: uppercase;
          }}
          td {{
            color: #e5eefc;
          }}
          tr:last-child td {{
            border-bottom: 0;
          }}
          .badge {{
            display: inline-block;
            min-width: 40px;
            padding: 4px 8px;
            border-radius: 999px;
            background: #12345b;
            color: #bfdbfe;
            text-align: center;
            font-weight: 700;
          }}
          .empty {{
            text-align: center;
            color: #8ca6c3;
          }}
          .folder-empty {{
            padding: 22px;
            border: 1px dashed #24446d;
            border-radius: 8px;
          }}
          .is-hidden {{
            display: none !important;
          }}
          .folder-grid {{
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
            gap: 12px;
          }}
          .student-folder {{
            display: flex;
            align-items: center;
            gap: 14px;
            min-height: 118px;
            padding: 16px;
            border: 1px solid #24446d;
            border-radius: 8px;
            background: linear-gradient(135deg, rgba(18, 42, 74, 0.96), rgba(8, 18, 34, 0.96));
            color: #f8fbff;
            text-decoration: none;
            transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
          }}
          .student-folder:hover {{
            transform: translateY(-2px);
            border-color: #60a5fa;
            background: linear-gradient(135deg, rgba(29, 78, 216, 0.34), rgba(8, 18, 34, 0.98));
          }}
          .folder-icon {{
            display: inline-flex;
            align-items: center;
            justify-content: center;
            flex: 0 0 auto;
            width: 58px;
            height: 58px;
            border-radius: 8px;
            background: #12345b;
            color: #93c5fd;
            font-size: 24px;
            font-weight: 800;
          }}
          .folder-content {{
            display: grid;
            gap: 5px;
            min-width: 0;
          }}
          .folder-content strong {{
            overflow: hidden;
            color: #ffffff;
            font-size: 18px;
            text-overflow: ellipsis;
            white-space: nowrap;
          }}
          .folder-content span {{
            color: #bfdbfe;
          }}
          .folder-content small {{
            margin: 0;
          }}
          .row-actions {{
            display: flex;
            align-items: center;
            gap: 8px;
          }}
          .row-actions form {{
            margin: 0;
          }}
          .user-table {{
            min-width: 760px;
          }}
          .logout-panel {{
            max-width: 640px;
          }}
          .logout-panel .button {{
            margin-top: 16px;
          }}
          .modal-backdrop {{
            position: fixed;
            inset: 0;
            z-index: 50;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
            background: rgba(2, 8, 23, 0.72);
            backdrop-filter: blur(8px);
          }}
          .modal {{
            width: min(420px, 100%);
            padding: 22px;
            border: 1px solid #24446d;
            border-radius: 8px;
            background: #081222;
            box-shadow: 0 24px 80px rgba(0, 0, 0, 0.42);
          }}
          .modal h2 {{
            margin-bottom: 8px;
          }}
          .modal p {{
            line-height: 1.55;
          }}
          .modal-actions {{
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 20px;
          }}
          .modal-actions .button {{
            min-width: 88px;
          }}
          @media (max-width: 980px) {{
            .app-shell {{
              grid-template-columns: 1fr;
            }}
            .sidebar {{
              position: static;
              height: auto;
              min-height: 0;
            }}
            .logout {{
              margin-top: 8px;
            }}
            .form-grid {{
              grid-template-columns: repeat(2, minmax(140px, 1fr));
            }}
            .filter-bar {{
              grid-template-columns: 1fr 1fr;
            }}
            .filter-actions {{
              grid-column: span 2;
            }}
            .wide {{
              grid-column: span 2;
            }}
          }}
          @media (max-width: 760px) {{
            main {{
              padding: 14px;
            }}
            .page-head {{
              align-items: stretch;
              flex-direction: column;
            }}
            .stats {{
              grid-template-columns: repeat(2, minmax(120px, 1fr));
            }}
            .form-grid {{
              grid-template-columns: 1fr;
            }}
            .filter-bar {{
              grid-template-columns: 1fr;
            }}
            .filter-actions {{
              grid-column: span 1;
            }}
            .filter-actions .button {{
              flex: 1;
            }}
            .wide {{
              grid-column: span 1;
            }}
            .panel-head {{
              display: block;
            }}
            .panel-head .button {{
              margin-top: 12px;
            }}
          }}
        </style>
      </head>
      <body>
        <div class="app-shell">
          {_sidebar(active)}
          <main>
            {content}
          </main>
        </div>
        <div class="modal-backdrop is-hidden" data-delete-user-modal>
          <section class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-user-title">
            <h2 id="delete-user-title">Hapus user?</h2>
            <p data-delete-user-message>User ini akan dihapus dari database.</p>
            <div class="modal-actions">
              <button class="button secondary" type="button" data-delete-user-cancel>Batal</button>
              <button class="button danger" type="button" data-delete-user-confirm>Hapus</button>
            </div>
          </section>
        </div>
        <script>
          const autoFilterForm = document.querySelector('[data-auto-filter]');
          if (autoFilterForm) {{
            const searchInput = autoFilterForm.querySelector('input[name="q"]');
            const labelSelect = autoFilterForm.querySelector('select[name="label"]');
            const folders = [...document.querySelectorAll('.student-folder')];
            const emptyState = document.querySelector('[data-filter-empty]');
            const totalStat = document.querySelector('[data-stat="total"]');
            const pdStat = document.querySelector('[data-stat="pd"]');
            const tpdStat = document.querySelector('[data-stat="tpd"]');
            const avgPdStat = document.querySelector('[data-stat="avg-pd"]');

            const applyLocalFilter = () => {{
              const query = (searchInput?.value || '').trim().toLowerCase();
              const label = (labelSelect?.value || '').trim().toUpperCase();
              let visibleCount = 0;
              let totalCount = 0;
              let pdCount = 0;
              let tpdCount = 0;
              let pdSum = 0;

              folders.forEach((folder) => {{
                const studentName = folder.dataset.studentName || '';
                const labels = (folder.dataset.labels || '').split(' ');
                const matchesName = !query || studentName.includes(query);
                const matchesLabel = !label || labels.includes(label);
                const shouldShow = matchesName && matchesLabel;
                folder.classList.toggle('is-hidden', !shouldShow);
                if (shouldShow) {{
                  visibleCount += 1;
                  totalCount += Number(folder.dataset.total || 0);
                  pdCount += Number(folder.dataset.pdCount || 0);
                  tpdCount += Number(folder.dataset.tpdCount || 0);
                  pdSum += Number(folder.dataset.pdSum || 0);
                }}
              }});

              emptyState?.classList.toggle('is-hidden', visibleCount !== 0);
              if (totalStat) totalStat.textContent = String(totalCount);
              if (pdStat) pdStat.textContent = String(pdCount);
              if (tpdStat) tpdStat.textContent = String(tpdCount);
              if (avgPdStat) {{
                avgPdStat.textContent = totalCount ? `${{(pdSum / totalCount * 100).toFixed(1)}}%` : '0.0%';
              }}
            }};

            autoFilterForm.addEventListener('submit', (event) => {{
              event.preventDefault();
              applyLocalFilter();
            }});
            searchInput?.addEventListener('input', applyLocalFilter);
            labelSelect?.addEventListener('change', applyLocalFilter);
            applyLocalFilter();
          }}

          const deleteUserModal = document.querySelector('[data-delete-user-modal]');
          const deleteUserMessage = document.querySelector('[data-delete-user-message]');
          const deleteUserCancel = document.querySelector('[data-delete-user-cancel]');
          const deleteUserConfirm = document.querySelector('[data-delete-user-confirm]');
          let pendingDeleteUserForm = null;

          document.querySelectorAll('[data-delete-user-form]').forEach((form) => {{
            form.addEventListener('submit', (event) => {{
              event.preventDefault();
              pendingDeleteUserForm = form;
              const userName = form.dataset.userName || 'user ini';
              if (deleteUserMessage) {{
                deleteUserMessage.textContent = `Yakin ingin menghapus ${{userName}}? Data user akan dihapus dari database.`;
              }}
              deleteUserModal?.classList.remove('is-hidden');
            }});
          }});

          const closeDeleteUserModal = () => {{
            pendingDeleteUserForm = null;
            deleteUserModal?.classList.add('is-hidden');
          }};

          deleteUserCancel?.addEventListener('click', closeDeleteUserModal);
          deleteUserModal?.addEventListener('click', (event) => {{
            if (event.target === deleteUserModal) {{
              closeDeleteUserModal();
            }}
          }});
          deleteUserConfirm?.addEventListener('click', () => {{
            const form = pendingDeleteUserForm;
            closeDeleteUserModal();
            form?.submit();
          }});
        </script>
      </body>
    </html>
    """
