# -*- coding: utf-8 -*-
"""Generate a multi-page draw.io file with native sequence-diagram shapes
for the Madrasah academic information system."""
import html
import os

# ---- layout constants -------------------------------------------------------
FIRST_CX = 90          # center x of first lifeline
SPACING = 200          # horizontal gap between lifelines
HEAD_W = 150           # participant header width
HEAD_H = 40            # participant header height
TOP_Y = 40             # y of participant header top
MSG_START = 120        # y of first message
STEP = 46              # vertical gap per slot
BOTTOM_PAD = 30

LIFELINE = ("shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;"
            "container=1;collapsible=0;recursiveResize=0;outlineConnect=0;"
            "fillColor=#dae8fc;strokeColor=#6c8ebf;size={size};")
LIFELINE_ACTOR = ("shape=umlLifeline;participant=umlActor;perimeter=lifelinePerimeter;"
                  "whiteSpace=wrap;html=1;container=1;collapsible=0;recursiveResize=0;"
                  "outlineConnect=0;fillColor=#d5e8d4;strokeColor=#82b366;size={size};")
CALL = ("html=1;align=center;verticalAlign=bottom;endArrow=block;rounded=0;"
        "exitX=0.5;exitY={ey};exitDx=0;exitDy=0;entryX=0.5;entryY={ny};entryDx=0;entryDy=0;")
RET = ("html=1;align=center;verticalAlign=bottom;endArrow=open;dashed=1;rounded=0;"
       "exitX=0.5;exitY={ey};exitDx=0;exitDy=0;entryX=0.5;entryY={ny};entryDx=0;entryDy=0;")
NOTE = ("rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=#fff2cc;strokeColor=#d6b656;"
        "verticalAlign=middle;align=center;")
FRAG = ("rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=none;strokeColor=#9673a6;"
        "align=left;verticalAlign=middle;fontStyle=2;fontColor=#9673a6;spacingLeft=6;")


def esc(s):
    return html.escape(s, quote=True)


def build_page(name, pid, participants, flow):
    """participants: list of (key, label, is_actor)
       flow: list of tuples:
         ('call', src, dst, label)
         ('ret',  src, dst, label)
         ('self', key, label)
         ('frag', text)
    """
    # count slots to size lifelines
    nslots = len(flow)
    life_bottom = MSG_START + nslots * STEP + BOTTOM_PAD
    life_h = life_bottom - TOP_Y

    cx = {}
    cells = []
    cid = 0

    def nid():
        nonlocal cid
        cid += 1
        return f"n{pid}_{cid}"

    # participants
    pkey_id = {}
    for i, (key, label, is_actor) in enumerate(participants):
        center = FIRST_CX + i * SPACING
        cx[key] = center
        x = center - HEAD_W / 2
        style = (LIFELINE_ACTOR if is_actor else LIFELINE).format(size=HEAD_H)
        myid = nid()
        pkey_id[key] = myid
        cells.append(
            f'<mxCell id="{myid}" value="{esc(label)}" style="{style}" vertex="1" parent="1">'
            f'<mxGeometry x="{x:.0f}" y="{TOP_Y}" width="{HEAD_W}" height="{life_h:.0f}" as="geometry"/></mxCell>'
        )

    def frac(y):
        return round((y - TOP_Y) / life_h, 4)

    y = MSG_START
    for item in flow:
        kind = item[0]
        if kind in ("call", "ret"):
            _, src, dst, label = item
            tmpl = CALL if kind == "call" else RET
            style = tmpl.format(ey=frac(y), ny=frac(y))
            e = nid()
            cells.append(
                f'<mxCell id="{e}" value="{esc(label)}" style="{style}" edge="1" parent="1" '
                f'source="{pkey_id[src]}" target="{pkey_id[dst]}">'
                f'<mxGeometry relative="1" as="geometry"/></mxCell>'
            )
        elif kind == "self":
            _, key, label = item
            c = cx[key]
            e = nid()
            cells.append(
                f'<mxCell id="{e}" value="{esc(label)}" style="{NOTE}" vertex="1" parent="1">'
                f'<mxGeometry x="{c-55:.0f}" y="{y-15:.0f}" width="110" height="30" as="geometry"/></mxCell>'
            )
        elif kind == "frag":
            _, text = item
            left = FIRST_CX - HEAD_W / 2 - 10
            right = FIRST_CX + (len(participants) - 1) * SPACING + HEAD_W / 2 + 10
            w = right - left
            e = nid()
            cells.append(
                f'<mxCell id="{e}" value="{esc(text)}" style="{FRAG}" vertex="1" parent="1">'
                f'<mxGeometry x="{left:.0f}" y="{y-14:.0f}" width="{w:.0f}" height="28" as="geometry"/></mxCell>'
            )
        y += STEP

    page_w = FIRST_CX + (len(participants) - 1) * SPACING + HEAD_W
    body = "".join(cells)
    return (
        f'<diagram name="{esc(name)}" id="{pid}">'
        f'<mxGraphModel dx="1400" dy="900" grid="0" gridSize="10" guides="1" tooltips="1" '
        f'connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="{page_w+80:.0f}" '
        f'pageHeight="{life_bottom+80:.0f}" math="0" shadow="0">'
        f'<root><mxCell id="0"/><mxCell id="1" parent="0"/>{body}</root>'
        f'</mxGraphModel></diagram>'
    )


# Common participant sets ------------------------------------------------------
def P(*keys):
    table = {
        "AD": ("AD", "Admin", True),
        "GU": ("GU", "Guru", True),
        "SW": ("SW", "Siswa", True),
        "US": ("US", "Pengguna", True),
        "H":  ("H", "Halaman (View)", False),
        "R":  ("R", "Router + Middleware", False),
        "C":  ("C", "Controller", False),
        "M":  ("M", "Model (Eloquent)", False),
        "DB": ("DB", "Database", False),
    }
    return [table[k] for k in keys]


pages = []

# 0. Login
pages.append(build_page("0. Login", "P0", P("US", "H", "R", "C", "M", "DB"), [
    ("call", "US", "H", "Isi email & password"),
    ("call", "H", "R", "POST /login"),
    ("call", "R", "C", "store(LoginRequest)"),
    ("call", "C", "M", "Auth::attempt(kredensial)"),
    ("call", "M", "DB", "SELECT users WHERE email=?"),
    ("ret", "DB", "M", "data user"),
    ("ret", "M", "C", "hasil autentikasi"),
    ("frag", "alt  [kredensial valid]"),
    ("self", "C", "session regenerate"),
    ("ret", "C", "H", "redirect dashboard sesuai role"),
    ("ret", "H", "US", "tampil dashboard"),
    ("frag", "else  [kredensial salah]"),
    ("ret", "C", "H", "kembali + pesan error"),
    ("ret", "H", "US", "tampil pesan gagal"),
]))

# 1. Mengelola Data Siswa
pages.append(build_page("1. Kelola Data Siswa", "P1", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka menu Data Siswa"),
    ("call", "H", "R", "GET /students"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Student::with(user)->get()"),
    ("call", "M", "DB", "SELECT students JOIN users"),
    ("ret", "DB", "C", "data siswa"),
    ("ret", "C", "H", "JSON daftar siswa"),
    ("ret", "H", "AD", "tampil tabel"),
    ("frag", "alt  [Tambah]"),
    ("call", "AD", "H", "Isi form, Simpan"),
    ("call", "H", "C", "POST /students"),
    ("self", "C", "validate()"),
    ("call", "C", "M", "User::create + Student::create"),
    ("call", "M", "DB", "INSERT users + students (transaksi)"),
    ("ret", "C", "H", "JSON siswa (201)"),
    ("frag", "else  [Ubah]"),
    ("call", "H", "C", "PUT /students/{id}"),
    ("call", "C", "M", "user->update + student->update"),
    ("call", "M", "DB", "UPDATE users + students"),
    ("ret", "C", "H", "JSON siswa"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /students/{id}"),
    ("call", "C", "M", "student->delete + user->delete"),
    ("call", "M", "DB", "DELETE students + users"),
    ("ret", "C", "H", "JSON {deleted}"),
]))

# 2. Kelola Data Guru/Kelas/Mapel (generik)
pages.append(build_page("2. Kelola Data Guru-Kelas-Mapel", "P2", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka menu Data Master"),
    ("call", "H", "R", "GET /teachers | /classes | /subjects"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Model::latest()->get()"),
    ("call", "M", "DB", "SELECT data"),
    ("ret", "C", "H", "JSON daftar"),
    ("ret", "H", "AD", "tampil tabel"),
    ("frag", "alt  [Tambah]"),
    ("call", "H", "C", "POST (store)"),
    ("self", "C", "validate()"),
    ("call", "C", "M", "Model::create(data)"),
    ("call", "M", "DB", "INSERT"),
    ("ret", "C", "H", "JSON (201)"),
    ("frag", "else  [Ubah]"),
    ("call", "H", "C", "PUT /{id} (update)"),
    ("call", "C", "M", "model->update(data)"),
    ("call", "M", "DB", "UPDATE"),
    ("ret", "C", "H", "JSON"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /{id}"),
    ("call", "C", "M", "model->delete()"),
    ("call", "M", "DB", "DELETE"),
    ("ret", "C", "H", "JSON {message}"),
]))

# 3. Atur Jadwal Pelajaran
pages.append(build_page("3. Atur Jadwal Pelajaran", "P3", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka menu Jadwal"),
    ("call", "H", "R", "GET /schedules"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Schedule::orderByRaw(hari)->get()"),
    ("call", "M", "DB", "SELECT schedules"),
    ("ret", "C", "H", "JSON jadwal"),
    ("ret", "H", "AD", "tampil jadwal"),
    ("frag", "alt  [Tambah]"),
    ("call", "H", "C", "POST /schedules"),
    ("self", "C", "validate()"),
    ("call", "C", "M", "Schedule::create(data)"),
    ("call", "M", "DB", "INSERT schedules"),
    ("ret", "C", "H", "JSON (201)"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /schedules/{id}"),
    ("call", "C", "M", "schedule->delete()"),
    ("call", "M", "DB", "DELETE schedules"),
    ("ret", "C", "H", "JSON {message}"),
]))

# 4. Kelola Pengumuman
pages.append(build_page("4. Kelola Pengumuman", "P4", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka menu Pengumuman"),
    ("call", "H", "R", "GET /announcements"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Announcement::latest()->get()"),
    ("call", "M", "DB", "SELECT announcements"),
    ("ret", "C", "H", "JSON daftar"),
    ("ret", "H", "AD", "tampil daftar"),
    ("frag", "alt  [Buat]"),
    ("call", "H", "C", "POST /announcements"),
    ("self", "C", "validate()"),
    ("call", "C", "M", "Announcement::create(data)"),
    ("call", "M", "DB", "INSERT"),
    ("ret", "C", "H", "JSON"),
    ("frag", "else  [Ubah]"),
    ("call", "H", "C", "PUT /{id}"),
    ("call", "C", "M", "announcement->update()"),
    ("call", "M", "DB", "UPDATE"),
    ("ret", "C", "H", "JSON"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /{id}"),
    ("call", "C", "M", "announcement->delete()"),
    ("call", "M", "DB", "DELETE"),
    ("ret", "C", "H", "JSON {message}"),
]))

# 5. Lihat Rekap Nilai (Admin)
pages.append(build_page("5. Lihat Rekap Nilai (Admin)", "P5", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka Rekap Nilai"),
    ("call", "H", "R", "GET /grades"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Grade::latest()->get()"),
    ("call", "M", "DB", "SELECT grades"),
    ("ret", "DB", "M", "data nilai"),
    ("ret", "C", "H", "JSON daftar nilai"),
    ("ret", "H", "AD", "tampil rekap nilai"),
]))

# 6. Lihat Rekap Absensi (Admin)
pages.append(build_page("6. Lihat Rekap Absensi (Admin)", "P6", P("AD", "H", "R", "C", "M", "DB"), [
    ("call", "AD", "H", "Buka Rekap Absensi"),
    ("call", "H", "R", "GET /attendances"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Attendance::latest()->get()"),
    ("call", "M", "DB", "SELECT attendances"),
    ("ret", "DB", "M", "data absensi"),
    ("ret", "C", "H", "JSON daftar absensi"),
    ("ret", "H", "AD", "tampil rekap absensi"),
]))

# 7. Lihat Jadwal Mengajar (Guru)
pages.append(build_page("7. Lihat Jadwal Mengajar (Guru)", "P7", P("GU", "H", "R", "C", "M", "DB"), [
    ("call", "GU", "H", "Buka Jadwal Mengajar"),
    ("call", "H", "R", "GET /schedules"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Schedule::orderByRaw(hari)->get()"),
    ("call", "M", "DB", "SELECT schedules"),
    ("ret", "DB", "M", "data jadwal"),
    ("ret", "C", "H", "JSON jadwal"),
    ("ret", "H", "GU", "tampil jadwal mengajar"),
]))

# 8. Kelola Nilai (Guru)
pages.append(build_page("8. Kelola Nilai Akademik (Guru)", "P8", P("GU", "H", "R", "C", "M", "DB"), [
    ("call", "GU", "H", "Buka Input Nilai"),
    ("call", "H", "R", "GET /grades"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Grade::latest()->get()"),
    ("call", "M", "DB", "SELECT grades"),
    ("ret", "C", "H", "JSON daftar nilai"),
    ("ret", "H", "GU", "tampil tabel nilai"),
    ("frag", "alt  [Simpan]"),
    ("call", "H", "C", "POST /grades"),
    ("self", "C", "validate + resolveStudentId"),
    ("call", "C", "M", "Grade::create(data)"),
    ("call", "M", "DB", "INSERT grades"),
    ("ret", "C", "H", "JSON nilai (201)"),
    ("frag", "else  [Ubah]"),
    ("call", "H", "C", "PUT /grades/{id}"),
    ("call", "C", "M", "grade->update(data)"),
    ("call", "M", "DB", "UPDATE grades"),
    ("ret", "C", "H", "JSON nilai"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /grades/{id}"),
    ("call", "C", "M", "grade->delete()"),
    ("call", "M", "DB", "DELETE grades"),
    ("ret", "C", "H", "JSON {message}"),
]))

# 9. Kelola Absensi (Guru)
pages.append(build_page("9. Kelola Absensi Kelas (Guru)", "P9", P("GU", "H", "R", "C", "M", "DB"), [
    ("call", "GU", "H", "Buka Absensi"),
    ("call", "H", "R", "GET /attendances"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Attendance::latest()->get()"),
    ("call", "M", "DB", "SELECT attendances"),
    ("ret", "C", "H", "JSON daftar absensi"),
    ("ret", "H", "GU", "tampil rekap absensi"),
    ("frag", "alt  [Simpan]"),
    ("call", "H", "C", "POST /attendances"),
    ("self", "C", "validate()"),
    ("call", "C", "M", "Attendance::updateOrCreate()"),
    ("call", "M", "DB", "INSERT / UPDATE attendances"),
    ("ret", "C", "H", "JSON absensi"),
    ("frag", "else  [Ubah]"),
    ("call", "H", "C", "PUT /attendances/{id}"),
    ("call", "C", "M", "attendance->update()"),
    ("call", "M", "DB", "UPDATE attendances"),
    ("ret", "C", "H", "JSON absensi"),
    ("frag", "else  [Hapus]"),
    ("call", "H", "C", "DELETE /attendances/{id}"),
    ("call", "C", "M", "attendance->delete()"),
    ("call", "M", "DB", "DELETE attendances"),
    ("ret", "C", "H", "JSON {message}"),
]))

# 10. Lihat Pengumuman (Guru/Siswa)
pages.append(build_page("10. Lihat Pengumuman", "P10", P("US", "H", "R", "C", "M", "DB"), [
    ("call", "US", "H", "Buka menu Pengumuman"),
    ("call", "H", "R", "GET /announcements"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Announcement::latest()->get()"),
    ("call", "M", "DB", "SELECT announcements"),
    ("ret", "DB", "M", "data pengumuman"),
    ("ret", "C", "H", "JSON daftar"),
    ("ret", "H", "US", "tampil pengumuman"),
]))

# 11. Ringkasan Akademik (Siswa dashboard)
pages.append(build_page("11. Ringkasan Akademik (Siswa)", "P11", P("SW", "H", "R", "C", "M", "DB"), [
    ("call", "SW", "H", "Buka dashboard siswa"),
    ("call", "H", "R", "GET /siswa/dashboard"),
    ("call", "R", "C", "closure siswa.dashboard"),
    ("call", "C", "M", "Student::where(user_id)->first()"),
    ("call", "M", "DB", "SELECT students"),
    ("call", "C", "M", "Grade::where(student_id)->get()"),
    ("call", "M", "DB", "SELECT grades"),
    ("call", "C", "M", "Attendance::where(class_name)->get()"),
    ("call", "M", "DB", "SELECT attendances"),
    ("call", "C", "M", "Announcement::latest()->get()"),
    ("call", "M", "DB", "SELECT announcements"),
    ("ret", "DB", "M", "semua data"),
    ("ret", "C", "H", "view siswa.dashboard"),
    ("ret", "H", "SW", "tampil ringkasan akademik"),
]))

# 12. Lihat Nilai & Raport (Siswa) + extend
pages.append(build_page("12. Lihat Nilai & Raport (Siswa)", "P12", P("SW", "H", "R", "C", "M", "DB"), [
    ("call", "SW", "H", "Buka Nilai & Raport"),
    ("call", "H", "R", "GET /grades (milik siswa)"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Grade::where(student_id)->get()"),
    ("call", "M", "DB", "SELECT grades WHERE student_id=?"),
    ("ret", "DB", "M", "data nilai siswa"),
    ("ret", "C", "H", "JSON nilai"),
    ("ret", "H", "SW", "tampil daftar nilai"),
    ("frag", "opt  <<extend>> Detail Komponen Nilai"),
    ("call", "SW", "H", "klik mata pelajaran"),
    ("self", "H", "tampil rincian komponen"),
    ("ret", "H", "SW", "tampil detail nilai"),
]))

# 13. Riwayat Absensi (Siswa)
pages.append(build_page("13. Riwayat Absensi (Siswa)", "P13", P("SW", "H", "R", "C", "M", "DB"), [
    ("call", "SW", "H", "Buka Riwayat Absensi"),
    ("call", "H", "R", "GET /attendances (kelas siswa)"),
    ("call", "R", "C", "index()"),
    ("call", "C", "M", "Attendance::where(class_name)->get()"),
    ("call", "M", "DB", "SELECT attendances WHERE class_name=?"),
    ("ret", "DB", "M", "data absensi kelas"),
    ("ret", "C", "H", "JSON absensi"),
    ("ret", "H", "SW", "tampil riwayat kehadiran"),
]))

# 14. Kelola Profil (opsional)
pages.append(build_page("14. Kelola Profil (opsional)", "P14", P("US", "H", "R", "C", "M", "DB"), [
    ("call", "US", "H", "Buka menu Profil"),
    ("call", "H", "R", "GET /user/profile"),
    ("call", "R", "C", "show()"),
    ("call", "C", "M", "request->user()"),
    ("call", "M", "DB", "SELECT users WHERE id=?"),
    ("ret", "C", "H", "JSON profil"),
    ("ret", "H", "US", "tampil data profil"),
    ("frag", "opt  [Perbarui profil / password]"),
    ("call", "H", "C", "PUT /user/profile"),
    ("self", "C", "validate + Hash::check"),
    ("call", "C", "M", "user->save()"),
    ("call", "M", "DB", "UPDATE users"),
    ("ret", "C", "H", "JSON {berhasil diperbarui}"),
    ("ret", "H", "US", "notifikasi sukses"),
]))

out_dir = r"C:\Users\FITO\Documents\SKRIPSI FITO\Daftar Gambar"
out_path = os.path.join(out_dir, "sequence_diagram_madrasah.drawio")
xml = ('<mxfile host="app.diagrams.net" agent="generated" version="29.0.3">'
       + "".join(pages) + "</mxfile>")
with open(out_path, "w", encoding="utf-8") as f:
    f.write(xml)
print("OK ->", out_path)
print("pages:", len(pages))
