import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireRole } from "@/lib/auth";
import { gradeFromPercent, RESULT_OPERATIONAL_TYPES } from "@/lib/constants";
import { signResultToken } from "@/lib/verify-token";
import { ensureCodes, formatCode } from "@/lib/verify-code";
import { resolveVerificationSetting } from "@/lib/notify";
import { pushNotification } from "@/lib/notify-push";
import {
  buildSingleExamSheet,
  markTotal,
  sectionShiftNames,
  attendanceForStudents,
  activitiesForStudents,
  type SubjectCell,
  type SheetRow,
} from "@/lib/result-sheet";

/**
 * Exam Result module (Primary education).
 *
 *  GET                              → exams overview + result-system status
 *  GET ?examId=                     → Single format: one exam's sheet (+CQ/MCQ/Applied parts)
 *  GET ?agg=PERIODICAL&classItemId= → aggregate across the class's PERIODICAL exams
 *  GET ?agg=FINAL&classItemId=      → aggregate across ALL exams of the class
 *  GET ?agg=COMBINED&classItemId=   → Final Result: 3 exam sets (Tutorial+Semester each),
 *                                     user-defined marks ratios per set + pass-in ratio,
 *                                     weighted totals, per-set attendance & merit
 *  POST                             → publish / unpublish an exam's result
 */


export async function GET(req: Request) {
  const session = await requireRole("INSTITUTION_ADMIN");
  if (!session || !session.institutionId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const institutionId = session.institutionId;

  const url = new URL(req.url);
  const examIdParam = url.searchParams.get("examId");
  const aggParam = url.searchParams.get("agg"); // PERIODICAL | FINAL
  const classParam = url.searchParams.get("classItemId");
  const examIdsParam = url.searchParams.get("examIds"); // "1,7" — user-selected combination

  const institution = await prisma.institution.findUnique({
    where: { id: institutionId },
    select: { type: true, name: true, address: true, phone: true, logo: true, code: true },
  });
  const letterhead = {
    name: institution?.name ?? "",
    type: institution?.type ?? "",
    address: institution?.address ?? "",
    phone: institution?.phone ?? "",
    logo: institution?.logo ?? null,
    code: institution?.code ?? null,
  };

  /* ============ Final Result — 3 exam sets with user-defined ratios ============ */
  if (aggParam === "COMBINED") {
    if (!RESULT_OPERATIONAL_TYPES.includes(institution?.type ?? "")) {
      return NextResponse.json({ error: "RESULT_SYSTEM_NOT_OPERATIONAL" }, { status: 403 });
    }
    if (!classParam) {
      return NextResponse.json({ error: "classItemId required" }, { status: 400 });
    }
    const classItemId = Number(classParam);
    const weights = [1, 2, 3].map((i) => {
      const w = Number(url.searchParams.get(`w${i}`) ?? "0");
      return Number.isFinite(w) && w >= 0 ? w : 0;
    });
    const passPercentRaw = Number(url.searchParams.get("pass") ?? "0");
    const passPercent = Number.isFinite(passPercentRaw) && passPercentRaw >= 0 ? passPercentRaw : 0;
    const sectionFilter = Number(url.searchParams.get("sectionItemId") ?? "") || null;
    const shiftFilter = Number(url.searchParams.get("shiftItemId") ?? "") || null;

    // each set: "tutorialId,semesterId" (either side may be empty)
    const setParam = (i: number) => {
      const raw = url.searchParams.get(`set${i}`) ?? "";
      const [t, s] = raw.split(",").map((x) => Number(x.trim()));
      return { tutorial: Number.isFinite(t) && t > 0 ? t : null, semester: Number.isFinite(s) && s > 0 ? s : null };
    };
    const setPick = [setParam(1), setParam(2), setParam(3)];
    const examIds = setPick.flatMap((s) => [s.tutorial, s.semester]).filter((x): x is number => x !== null);
    if (examIds.length === 0) {
      return NextResponse.json({ institution: letterhead, aggregate: "COMBINED", combined: null, rows: [], exam: null });
    }

    const exams = await prisma.exam.findMany({
      where: { id: { in: examIds }, institutionId, classItemId },
      include: {
        schedules: {
          include: { subjectItem: { select: { name: true, nameBn: true } }, marks: true },
        },
      },
      orderBy: { startDate: "asc" },
    });
    const examById = new Map(exams.map((e) => [e.id, e]));
    const sets = setPick.map((pick, i) => ({
      label: i === 0 ? "1st" : i === 1 ? "2nd" : "Annual",
      weight: weights[i],
      tutorial: pick.tutorial && examById.has(pick.tutorial) ? examById.get(pick.tutorial)! : null,
      semester: pick.semester && examById.has(pick.semester) ? examById.get(pick.semester)! : null,
    }));
    if (sets.every((s) => !s.tutorial && !s.semester)) {
      return NextResponse.json({ institution: letterhead, aggregate: "COMBINED", combined: null, rows: [], exam: null });
    }

    const classItem = await prisma.academicItem.findUnique({
      where: { id: classItemId },
      select: { name: true, nameBn: true },
    });

    const students = await prisma.student.findMany({
      where: {
        institutionId,
        status: "ACTIVE",
        classItemId,
        ...(sectionFilter ? { sectionItemId: sectionFilter } : {}),
        ...(shiftFilter ? { shiftItemId: shiftFilter } : {}),
      },
      orderBy: { roll: "asc" },
      select: { id: true, roll: true, name: true, nameBn: true, guardianName: true, guardianNameBn: true, motherName: true, motherNameBn: true, photoUrl: true, sectionItemId: true, shiftItemId: true },
    });
    const ssMap = await sectionShiftNames(students);

    // subject union in first-seen order across the sets (tutorial, semester)
    type SetPaper = { full: number; marks: Map<number, number> };
    const subjects = new Map<
      string,
      {
        subject: string;
        subjectBn: string | null;
        papers: (SetPaper | null)[]; // [set1.tut, set1.sem, set2.tut, set2.sem, set3.tut, set3.sem]
      }
    >();
    const paperSlot = (sIdx: number, kind: "tutorial" | "semester") => sIdx * 2 + (kind === "tutorial" ? 0 : 1);
    for (const [sIdx, set] of sets.entries()) {
      for (const kind of ["tutorial", "semester"] as const) {
        const ex = set[kind];
        if (!ex) continue;
        for (const sch of ex.schedules) {
          const key = String(sch.subjectItemId ?? `e${ex.id}s${sch.id}`);
          let sub = subjects.get(key);
          if (!sub) {
            sub = {
              subject: sch.subjectItem?.name ?? "—",
              subjectBn: sch.subjectItem?.nameBn ?? null,
              papers: Array.from({ length: 6 }, () => null),
            };
            subjects.set(key, sub);
          }
          sub.papers[paperSlot(sIdx, kind)] = {
            full: sch.fullMarks,
            marks: new Map(sch.marks.map((m) => [m.studentId, markTotal(m)])),
          };
        }
      }
    }

    // per-set attendance over each set's exam date window
    const setWindows = sets.map((set) => {
      const ds = [set.tutorial, set.semester].filter((e): e is NonNullable<typeof e> => e !== null);
      if (ds.length === 0) return null;
      const start = ds.reduce((m, e) => (e.startDate < m ? e.startDate : m), ds[0].startDate);
      const end = ds.reduce((m, e) => ((e.endDate ?? e.startDate) > m ? e.endDate ?? e.startDate : m), ds[0].endDate ?? ds[0].startDate);
      return { start, end };
    });
    const attWhere = {
      institutionId,
      studentId: { in: students.map((s) => s.id) },
    };
    const attRecs = await prisma.studentAttendance.findMany({ where: attWhere, select: { studentId: true, date: true, status: true } });
    const perSetAttendance = students.map((st) =>
      setWindows.map((win) => {
        if (!win) return { academicDay: 0, present: 0, absent: 0 };
        const days = new Set<string>();
        let present = 0;
        let absent = 0;
        for (const r of attRecs) {
          if (r.studentId !== st.id) continue;
          const t = r.date.getTime();
          if (t < win.start.getTime() || t > win.end.getTime()) continue;
          days.add(r.date.toISOString().slice(0, 10));
          if (r.status === "PRESENT") present++;
          else if (r.status === "ABSENT") absent++;
        }
        return { academicDay: days.size, present, absent };
      })
    );

    const round2 = (x: number) => Math.round(x * 100) / 100;

    interface FinalSubject {
      key: string;
      subject: string;
      subjectBn: string | null;
      sets: { tutorial: number | null; semester: number | null; marks: number | null; full: number; highest: number | null }[];
      final: { c: number[]; total: number; totalFull: number; passed: boolean | null };
    }
    const rows = students.map((st, stuIdx) => {
      const finalSubjects: FinalSubject[] = Array.from(subjects.entries()).map(([key, sub]) => {
        const setCells = sets.map((_, sIdx) => {
          const tut = sub.papers[paperSlot(sIdx, "tutorial")];
          const sem = sub.papers[paperSlot(sIdx, "semester")];
          const tutorial = tut ? tut.marks.get(st.id) ?? null : null;
          const semester = sem ? sem.marks.get(st.id) ?? null : null;
          const marks = tutorial === null && semester === null ? null : (tutorial ?? 0) + (semester ?? 0);
          const full = (tut?.full ?? 0) + (sem?.full ?? 0);
          return { tutorial, semester, marks, full, highest: null as number | null };
        });
        const c = setCells.map((cell, sIdx) => (cell.marks !== null ? round2((cell.marks * sets[sIdx].weight) / 100) : 0));
        const total = round2(c.reduce((a, b) => a + b, 0));
        const totalFull = round2(
          setCells.reduce((a, cell, sIdx) => a + (cell.full * sets[sIdx].weight) / 100, 0)
        );
        const entered = setCells.some((cell) => cell.marks !== null);
        const passLine = (totalFull * passPercent) / 100;
        return {
          key,
          subject: sub.subject,
          subjectBn: sub.subjectBn,
          sets: setCells,
          final: { c, total, totalFull, passed: entered ? total >= passLine - 1e-9 : null },
        };
      });

      const perSetTotals = sets.map((_, sIdx) => {
        const vals = finalSubjects.map((sub) => sub.sets[sIdx].marks);
        return vals.some((v) => v !== null) ? round2(vals.reduce<number>((a, b) => a + (b ?? 0), 0)) : null;
      });
      const total = round2(finalSubjects.reduce((a, s) => a + s.final.total, 0));
      const totalFull = round2(finalSubjects.reduce((a, s) => a + s.final.totalFull, 0));
      const enteredAll = finalSubjects.every((s) => s.final.passed !== null);
      const failedSubjects = finalSubjects.filter((s) => s.final.passed === false).length;

      return {
        studentId: st.id,
        roll: st.roll,
        name: st.name,
        nameBn: st.nameBn,
        sectionName: st.sectionItemId ? ssMap.get(st.sectionItemId)?.name ?? null : null,
        sectionNameBn: st.sectionItemId ? ssMap.get(st.sectionItemId)?.nameBn ?? null : null,
        shiftName: st.shiftItemId ? ssMap.get(st.shiftItemId)?.name ?? null : null,
        shiftNameBn: st.shiftItemId ? ssMap.get(st.shiftItemId)?.nameBn ?? null : null,
        guardianName: st.guardianName,
        guardianNameBn: st.guardianNameBn,
        motherName: st.motherName,
        motherNameBn: st.motherNameBn,
        photoUrl: st.photoUrl,
        activities: [] as string[],
        subjects: finalSubjects,
        perSetTotals,
        total,
        totalFull,
        failedSubjects,
        result: !enteredAll ? ("INCOMPLETE" as const) : failedSubjects > 0 ? ("FAILED" as const) : ("PASSED" as const),
        position: null as number | null,
        attendance: perSetAttendance[stuIdx],
      };
    });

    // highest set marks per subject across the listed students
    for (const row of rows) {
      for (const sub of row.subjects) {
        sub.sets.forEach((cell, sIdx) => {
          const best = Math.max(
            ...rows.map((x) => x.subjects.find((y) => y.key === sub.key)?.sets[sIdx].marks ?? -1)
          );
          cell.highest = best >= 0 ? best : null;
        });
      }
    }

    const ranked = [...rows]
      .filter((r) => r.result !== "INCOMPLETE")
      .sort((a, b) => b.total - a.total);
    ranked.forEach((r, i) => {
      r.position = i + 1;
    });

    const examKey = `c:${classItemId}:${examIds
      .slice()
      .sort((a, b) => a - b)
      .join("-")}:${weights.join("/")}:${passPercent}`;
    for (const r of rows) {
      (r as { verifyToken?: string }).verifyToken = signResultToken({
        i: institutionId, k: examKey, s: r.studentId, t: r.total, g: "",
      });
    }
    const verification = await resolveVerificationSetting(institutionId);
    let combinedCodes: Map<number, string> = new Map();
    if (verification.toggles.result) {
      combinedCodes = await ensureCodes({
      institutionId,
      refKey: examKey,
      rows: rows.map((r) => ({ studentId: r.studentId, total: r.total, grade: "" })),
    });
    for (const r of rows) {
      const stored = combinedCodes.get(r.studentId);
      if (stored) (r as { verifyCode?: string }).verifyCode = formatCode(stored);
    }
    }

    return NextResponse.json({
      institution: letterhead,
      aggregate: "COMBINED",
      combined: {
        classItem: classItem ? { name: classItem.name, nameBn: classItem.nameBn } : null,
        weights,
        passPercent,
        sets: sets.map((set, i) => ({
          label: set.label,
          weight: set.weight,
          tutorial: set.tutorial ? { id: set.tutorial.id, name: set.tutorial.name, nameBn: set.tutorial.nameBn } : null,
          semester: set.semester ? { id: set.semester.id, name: set.semester.name, nameBn: set.semester.nameBn } : null,
          window: setWindows[i]
            ? { start: setWindows[i]!.start.toISOString(), end: setWindows[i]!.end.toISOString() }
            : null,
        })),
      },
      exam: null,
      rows,
    });
  }

  /* ================= Aggregate formats (Periodical / Final) ================= */
  if (aggParam === "PERIODICAL" || aggParam === "FINAL") {
    if (!RESULT_OPERATIONAL_TYPES.includes(institution?.type ?? "")) {
      return NextResponse.json({ error: "RESULT_SYSTEM_NOT_OPERATIONAL" }, { status: 403 });
    }
    if (!classParam) {
      return NextResponse.json({ error: "classItemId required" }, { status: 400 });
    }
    const classItemId = Number(classParam);

    // per-student collected cells across papers (per-request scratch)
    const perStudentCells = new Map<
      string,
      { obtaineds: (number | null)[]; cqs: number[]; mcqs: number[]; applieds: number[] }
    >();

    // PERIODICAL aggregates the class's periodical exams; FINAL aggregates all;
    // examIds narrows either to the user's selected combination (e.g. 1st Tutorial + 1st Term)
    const selectedIds = examIdsParam
      ? examIdsParam.split(",").map((x) => Number(x.trim())).filter((x) => Number.isFinite(x) && x > 0)
      : null;
    const baseWhere =
      aggParam === "PERIODICAL"
        ? { institutionId, classItemId, format: "PERIODICAL" as const }
        : { institutionId, classItemId };
    const exams = await prisma.exam.findMany({
      where: selectedIds ? { ...baseWhere, id: { in: selectedIds } } : baseWhere,
      include: {
        schedules: {
          include: {
            subjectItem: { select: { name: true, nameBn: true } },
            marks: true,
          },
        },
      },
      orderBy: { startDate: "asc" },
    });
    if (exams.length === 0) {
      return NextResponse.json({
        institution: letterhead,
        exam: null,
        rows: [],
        examNames: [],
        classItem: null,
      });
    }

    const classItem = await prisma.academicItem.findUnique({
      where: { id: classItemId },
      select: { name: true, nameBn: true },
    });

    const students = await prisma.student.findMany({
      where: { institutionId, status: "ACTIVE", classItemId },
      orderBy: { roll: "asc" },
      select: { id: true, roll: true, name: true, nameBn: true, guardianName: true, guardianNameBn: true, motherName: true, motherNameBn: true, photoUrl: true, sectionItemId: true, shiftItemId: true, groupItemId: true, dob: true },
    });
    const ssMap = await sectionShiftNames(students);
    const groupMap = await sectionShiftNames(students.map((st) => ({ sectionItemId: st.groupItemId, shiftItemId: null })));

    // stable exam order; per-subject slot list so perExam columns line up:
    // obtaineds for a subject are pushed exam-by-exam, so slots[i] is the
    // exam ordinal whose marks sit at obtaineds[i] for that subject.
    const examOrder = exams.map((e) => `e${e.id}`);
    const slotsBySubject = new Map<string, number[]>();
    for (const [ei, ex] of exams.entries()) {
      for (const sch of ex.schedules) {
        const skey = String(sch.subjectItemId ?? sch.id);
        const slots = slotsBySubject.get(skey) ?? [];
        slots.push(ei);
        slotsBySubject.set(skey, slots);
      }
    }

    // subject key → collected per-exam cells
    type Agg = {
      subject: string;
      subjectBn: string | null;
      fulls: number[]; // full marks per paper occurrence
      obtaineds: (number | null)[]; // per paper: obtained or null
      cqs: number[];
      mcqs: number[];
      applieds: number[];
    };
    const subjectAgg = new Map<string, Agg>();
    for (const ex of exams) {
      for (const sch of ex.schedules) {
        const key = String(sch.subjectItemId ?? sch.id);
        const agg = subjectAgg.get(key) ?? {
          subject: sch.subjectItem?.name ?? "—",
          subjectBn: sch.subjectItem?.nameBn ?? null,
          fulls: [],
          obtaineds: [],
          cqs: [],
          mcqs: [],
          applieds: [],
        };
        agg.fulls.push(sch.fullMarks);
        // per-student obtained for this paper
        for (const st of students) {
          const m = sch.marks.find((x) => x.studentId === st.id);
          const cellKey = `${key}:${st.id}`;
          const perStudent =
            perStudentCells.get(cellKey) ?? { obtaineds: [] as (number | null)[], cqs: [], mcqs: [], applieds: [] };
          perStudent.obtaineds.push(m ? markTotal(m) : null);
          if (m) {
            perStudent.cqs.push(m.cq ?? 0);
            perStudent.mcqs.push(m.mcq ?? 0);
            perStudent.applieds.push(m.applied ?? 0);
          }
          perStudentCells.set(cellKey, perStudent);
        }
        subjectAgg.set(key, agg);
      }
    }

    const activityMap = await activitiesForStudents(institutionId, students.map((x) => x.id));
    const { perStudent: attendMap, academicDay } = await attendanceForStudents(
      institutionId,
      students.map((x) => x.id)
    );

    const rows: SheetRow[] = students.map((st) => {
      const subjects: SubjectCell[] = [];
      for (const [key, agg] of subjectAgg) {
        const cell = perStudentCells.get(`${key}:${st.id}`)!;
        const papers = agg.fulls.length;
        // raw obtained per exam for side-by-side columns (aligned to examOrder)
        const slots = slotsBySubject.get(key) ?? [];
        const perExam = examOrder.map((_, examIdx) => {
          const pos = slots.indexOf(examIdx);
          return pos === -1 ? null : (cell.obtaineds[pos] ?? null);
        });
        const graded = cell.obtaineds.filter((x): x is number => x !== null);
        // template semantics: combined marks = SUM of the exam columns,
        // subject full marks = sum of the papers' full marks
        const sumFull = agg.fulls.reduce((a, b) => a + b, 0);
        const obtained = graded.length === papers ? graded.reduce((a, b) => a + b, 0) : null;
        const percent = obtained !== null && sumFull > 0 ? (obtained / sumFull) * 100 : null;
        const passLine = Math.ceil(sumFull * 0.33);
        const perExamFull = exams.map((_, examIdx) => {
          const pos = slots.indexOf(examIdx);
          return pos === -1 ? 0 : agg.fulls[pos];
        });
        subjects.push({
          scheduleId: Number(key),
          subject: agg.subject,
          subjectBn: agg.subjectBn,
          full: sumFull,
          obtained,
          cq: graded.length === papers ? cell.cqs.reduce((a, b) => a + b, 0) : null,
          mcq: graded.length === papers ? cell.mcqs.reduce((a, b) => a + b, 0) : null,
          applied: graded.length === papers ? cell.applieds.reduce((a, b) => a + b, 0) : null,
          percent,
          grade: percent !== null ? gradeFromPercent(percent).grade : null,
          failed: obtained !== null ? obtained < passLine : false,
          perExam,
          perExamFull,
        });
      }

      const totalFull = subjects.reduce((x, y) => x + y.full, 0);
      const totalObtained = subjects.reduce((x, y) => x + (y.obtained ?? 0), 0);
      const percent = totalFull > 0 ? (totalObtained / totalFull) * 100 : 0;
      const overall = gradeFromPercent(percent);
      const failedSubjects = subjects.filter((y) => y.failed).length;
      const missingSubjects = subjects.filter((y) => y.obtained === null).length;

      return {
        studentId: st.id,
        roll: st.roll,
        name: st.name,
        nameBn: st.nameBn,
        sectionName: st.sectionItemId ? ssMap.get(st.sectionItemId)?.name ?? null : null,
        sectionNameBn: st.sectionItemId ? ssMap.get(st.sectionItemId)?.nameBn ?? null : null,
        shiftName: st.shiftItemId ? ssMap.get(st.shiftItemId)?.name ?? null : null,
        shiftNameBn: st.shiftItemId ? ssMap.get(st.shiftItemId)?.nameBn ?? null : null,
        groupName: st.groupItemId ? groupMap.get(st.groupItemId)?.name ?? null : null,
        dob: st.dob,
        subjects,
        totalFull,
        totalObtained,
        percent,
        grade: overall.grade,
        point: overall.point,
        failedSubjects,
        missingSubjects,
        result:
          missingSubjects > 0 ? "INCOMPLETE" : failedSubjects > 0 ? "FAILED" : "PASSED",
        position: null,
        activities: activityMap.get(st.id) ?? [],
        guardianName: st.guardianName,
        guardianNameBn: st.guardianNameBn,
        motherName: st.motherName,
        motherNameBn: st.motherNameBn,
        photoUrl: st.photoUrl,
        attendance: {
          academicDay,
          present: attendMap.get(st.id)?.present ?? 0,
          absent: attendMap.get(st.id)?.absent ?? 0,
        },
      };
    });

    // highest combined marks per subject across the class (Highest column)
    for (const row of rows) {
      for (const sub of row.subjects) {
        const best = Math.max(
          ...rows.map((x) => x.subjects.find((y) => y.scheduleId === sub.scheduleId)?.obtained ?? -1)
        );
        sub.highest = best >= 0 ? best : null;
      }
    }

    const ranked = [...rows]
      .filter((r) => r.missingSubjects === 0)
      .sort((a, b) => b.totalObtained - a.totalObtained);
    ranked.forEach((r, i) => {
      r.position = i + 1;
    });

    // signed verification tokens (QR) — exam key from the combined exam ids
    const examKey = `a:${aggParam}:${classItemId}:${exams.map((e) => e.id).sort((x, y) => x - y).join("-")}`;
    for (const r of rows) {
      r.verifyToken = signResultToken({
        i: institutionId, k: examKey, s: r.studentId,
        t: r.totalObtained, g: r.grade,
      });
    }
    const verification = await resolveVerificationSetting(institutionId);
    let aggCodes: Map<number, string> = new Map();
    if (verification.toggles.result) {
      aggCodes = await ensureCodes({
      institutionId,
      refKey: examKey,
      label: exams[0]?.name ?? null,
      rows: rows.map((r) => ({ studentId: r.studentId, total: r.totalObtained, grade: r.grade })),
    });
    for (const r of rows) {
      const stored = aggCodes.get(r.studentId);
      if (stored) r.verifyCode = formatCode(stored);
    }
    }

    return NextResponse.json({
      institution: letterhead,
      aggregate: aggParam,
      examNames: exams.map((e) => ({ name: e.name, nameBn: e.nameBn, format: e.format })),
      classItem: classItem ? { name: classItem.name, nameBn: classItem.nameBn } : null,
      // combined date range across the aggregated exams (marksheets + merit print)
      period: {
        start: exams.reduce((m, e) => (e.startDate < m ? e.startDate : m), exams[0].startDate).toISOString(),
        end: exams
          .reduce((m, e) => ((e.endDate ?? e.startDate) > m ? (e.endDate ?? e.startDate) : m), exams[0].endDate ?? exams[0].startDate)
          .toISOString(),
      },
      exam: null,
      rows,
    });
  }

  /* ================= Single format: one exam ================= */
  if (examIdParam) {
    if (!RESULT_OPERATIONAL_TYPES.includes(institution?.type ?? "")) {
      return NextResponse.json({ error: "RESULT_SYSTEM_NOT_OPERATIONAL" }, { status: 403 });
    }
    const built = await buildSingleExamSheet(institutionId, Number(examIdParam));
    if (!built) return NextResponse.json({ error: "Not found" }, { status: 404 });

    return NextResponse.json({
      optionalSubjectIds: built.optionalSubjectIds,
      institution: letterhead,
      exam: built.exam,
      rows: built.rows,
    });
  }

  /* ================= Overview ================= */
  const exams = await prisma.exam.findMany({
    where: { institutionId },
    orderBy: { startDate: "desc" },
    include: {
      classItem: { select: { id: true, name: true, nameBn: true } },
      schedules: { select: { id: true, fullMarks: true, marks: { select: { studentId: true } } } },
    },
  });

  const list = exams.map((e) => ({
    id: e.id,
    name: e.name,
    nameBn: e.nameBn,
    format: e.format,
    classItemId: e.classItem?.id ?? null,
    className: e.classItem?.name ?? null,
    classNameBn: e.classItem?.nameBn ?? null,
    startDate: e.startDate,
    status: e.status,
    resultPublished: e.resultPublished,
    subjects: e.schedules.length,
    marksEntered: e.schedules.reduce((s, x) => s + x.marks.length, 0),
  }));

  // classes that have aggregate-able exams (periodical and/or any for final)
  const classIds = new Set(
    exams.map((e) => e.classItemId).filter((x): x is number => x !== null)
  );
  const periodicalClassIds = new Set(
    exams.filter((e) => e.format === "PERIODICAL" && e.classItemId !== null).map((e) => e.classItemId as number)
  );

  const classItems = await prisma.academicItem.findMany({
    where: { id: { in: Array.from(classIds) } },
    select: { id: true, name: true, nameBn: true },
  });

  // section / shift academic items linked to this institution (Final Result filter form)
  const [sectionItems, shiftItems] = await Promise.all(
    (["SECTION", "SHIFT"] as const).map((category) =>
      prisma.academicItem.findMany({
        where: {
          category,
          selections: { some: { institutionId } },
        },
        select: { id: true, name: true, nameBn: true },
        orderBy: { name: "asc" },
      })
    )
  );

  // the principal's uploaded signature (HR) — printed on the result sheets
  const principal = await prisma.staff.findFirst({
    where: { institutionId, designation: "PRINCIPAL", status: "ACTIVE", signatureUrl: { not: null } },
    select: { name: true, signatureUrl: true },
    orderBy: { id: "asc" },
  });
  // the class-teacher signature: first ACTIVE staff whose designation
  // mentions "teacher" and who has an uploaded signature image
  const staffWithSignatures = await prisma.staff.findMany({
    where: { institutionId, status: "ACTIVE", signatureUrl: { not: null } },
    select: { name: true, signatureUrl: true, designation: true },
    orderBy: { id: "asc" },
  });
  const teacher = staffWithSignatures.find((s) => /teacher/i.test(s.designation));

  return NextResponse.json({
    institution: { type: institution?.type, name: institution?.name },
    exams: list,
    classes: classItems.map((c) => ({
      ...c,
      hasPeriodical: periodicalClassIds.has(c.id),
    })),
    sectionItems,
    shiftItems,
    principalSignature: principal?.signatureUrl ?? null,
    principalName: principal?.name ?? null,
    teacherSignature: teacher?.signatureUrl ?? null,
    teacherName: teacher?.name ?? null,
  });
}

/** Publish / unpublish an exam result (locks the marks tab while published). */
export async function POST(req: Request) {
  const session = await requireRole("INSTITUTION_ADMIN");
  if (!session || !session.institutionId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const guard = await prisma.institution.findUnique({
    where: { id: session.institutionId },
    select: { type: true },
  });
  if (!RESULT_OPERATIONAL_TYPES.includes(guard?.type ?? "")) {
    return NextResponse.json({ error: "RESULT_SYSTEM_NOT_OPERATIONAL" }, { status: 403 });
  }
  try {
    const body = await req.json();
    const examId = Number(body.examId);
    const publish = Boolean(body.publish);

    const exam = await prisma.exam.findFirst({
      where: { id: examId, institutionId: session.institutionId },
    });
    if (!exam) return NextResponse.json({ error: "Not found" }, { status: 404 });

    await prisma.exam.update({
      where: { id: examId },
      data: {
        resultPublished: publish,
        resultPublishedAt: publish ? new Date() : null,
        ...(publish ? { status: "FINISHED" as never } : {}),
      },
    });

    // push to the class's students + parents when results go live
    if (publish) {
      await pushNotification({
        institutionId: session.institutionId,
        kind: "RESULT",
        title: `Results published — ${exam.name}`,
        body: "The results of this exam have been published. Open Results to see the marksheet.",
        link: "/me/results",
        audience: ["students", "parents"],
        classItemId: exam.classItemId,
        refId: exam.id,
        createdById: session.userId,
      });
    }
    return NextResponse.json({ success: true, resultPublished: publish });
  } catch {
    return NextResponse.json({ error: "Could not update" }, { status: 500 });
  }
}
