import { prisma } from "@/lib/prisma";
import { gradeFromPercent } from "@/lib/constants";
import { signResultToken } from "@/lib/verify-token";
import { ensureCodes, formatCode } from "@/lib/verify-code";
import { resolveVerificationSetting } from "@/lib/notify";

/* Single-exam result sheet (marksheet) computation — shared by the portal
   results API and the member (/me) marksheet endpoint so students receive
   exactly the same rows the institution prints. */

export interface SubjectCell {
  scheduleId: number;
  subject: string;
  subjectBn: string | null;
  subjectItemId?: number | null;
  full: number;
  mcqFull?: number | null;
  cqFull?: number | null;
  appliedFull?: number | null;
  /** schedule-configured minimum passing marks per part and total */
  mcqPass?: number | null;
  cqPass?: number | null;
  appliedPass?: number | null;
  totalPass?: number | null;
  obtained: number | null;
  cq: number | null;
  mcq: number | null;
  applied: number | null;
  percent: number | null;
  grade: string | null;
  failed: boolean;
  perExam?: (number | null)[]; // aggregate formats: raw obtained per exam (examNames order)
  perExamFull?: number[]; // aggregate: full marks of each exam paper
  highest?: number | null; // highest combined marks in this subject across the class
}

export interface SheetRow {
  studentId: number;
  roll: string;
  name: string;
  nameBn: string | null;
  sectionName?: string | null;
  sectionNameBn?: string | null;
  shiftName?: string | null;
  shiftNameBn?: string | null;
  subjects: SubjectCell[];
  totalFull: number;
  totalObtained: number;
  percent: number;
  grade: string;
  point: number;
  failedSubjects: number;
  missingSubjects: number;
  result: "PASSED" | "FAILED" | "INCOMPLETE";
  position: number | null;
  activities: string[]; // recent activity titles this student took part in
  guardianName?: string;
  guardianNameBn?: string | null;
  motherName?: string | null;
  motherNameBn?: string | null;
  photoUrl?: string | null;
  attendance?: { academicDay: number; present: number; absent: number };
  verifyToken?: string; // signed QR token for copy verification (legacy)
  verifyCode?: string; // short EMS-XXXX-XXXX printed as the QR text
}

export function markTotal(m: { total: number | null; cq: number | null; mcq: number | null; applied: number | null }) {
  return m.total ?? (m.cq ?? 0) + (m.mcq ?? 0) + (m.applied ?? 0);
}

/** name lookup for the students' section / shift academic items (marksheet fields) */
export async function sectionShiftNames(students: { sectionItemId: number | null; shiftItemId: number | null }[]) {
  const ids = new Set<number>();
  for (const st of students) {
    if (st.sectionItemId) ids.add(st.sectionItemId);
    if (st.shiftItemId) ids.add(st.shiftItemId);
  }
  if (ids.size === 0) return new Map<number, { name: string; nameBn: string | null }>();
  const items = await prisma.academicItem.findMany({
    where: { id: { in: Array.from(ids) } },
    select: { id: true, name: true, nameBn: true },
  });
  return new Map(items.map((i) => [i.id, { name: i.name, nameBn: i.nameBn }]));
}

export async function attendanceForStudents(institutionId: number, studentIds: number[]) {
  if (studentIds.length === 0) return { perStudent: new Map<number, { present: number; absent: number }>(), academicDay: 0 };
  const recs = await prisma.studentAttendance.findMany({
    where: { institutionId, studentId: { in: studentIds } },
    select: { studentId: true, date: true, status: true },
  });
  // class academic days = distinct attendance dates across the queried students
  const days = new Set<string>();
  // one entry per date per student — duplicate records on the same day must
  // not inflate the counts; a PRESENT record that day wins over an ABSENT one
  const perStudentDays = new Map<number, Map<string, boolean>>();
  for (const r of recs) {
    const day = r.date.toISOString().slice(0, 10);
    days.add(day);
    let byDay = perStudentDays.get(r.studentId);
    if (!byDay) {
      byDay = new Map();
      perStudentDays.set(r.studentId, byDay);
    }
    byDay.set(day, byDay.get(day) === true ? true : r.status === "PRESENT");
  }
  const perStudent = new Map<number, { present: number; absent: number }>();
  for (const [studentId, byDay] of perStudentDays) {
    let present = 0;
    let absent = 0;
    for (const attended of byDay.values()) {
      if (attended) present += 1;
      else absent += 1;
    }
    perStudent.set(studentId, { present, absent });
  }
  return { perStudent, academicDay: days.size };
}

export async function activitiesForStudents(institutionId: number, studentIds: number[]) {
  if (studentIds.length === 0) return new Map<number, string[]>();
  const recs = await prisma.studentActivityRecipient.findMany({
    where: { studentId: { in: studentIds }, activity: { institutionId } },
    orderBy: { activity: { activityDate: "desc" } },
    take: 150,
    select: { studentId: true, activity: { select: { title: true } } },
  });
  const map = new Map<number, string[]>();
  for (const r of recs) {
    const list = map.get(r.studentId) ?? [];
    if (list.length < 3) list.push(r.activity.title);
    map.set(r.studentId, list);
  }
  return map;
}

export interface SingleExamSheet {
  exam: {
    id: number;
    name: string;
    nameBn: string | null;
    format: string;
    className: string | null;
    classNameBn: string | null;
    sectionName: string | null;
    sectionNameBn: string | null;
    shiftName: string | null;
    shiftNameBn: string | null;
    startDate: Date;
    endDate: Date | null;
    resultPublished: boolean;
    resultPublishedAt: Date | null;
  };
  rows: SheetRow[];
  optionalSubjectIds: number[];
}

/** Full single-exam sheet for one exam: every active student of the exam's
    class with per-subject cells, highest marks, merit position and the
    verification QR codes (token + short code when enabled). */
export async function buildSingleExamSheet(institutionId: number, examId: number): Promise<SingleExamSheet | null> {
  const exam = await prisma.exam.findFirst({
    where: { id: examId, institutionId },
    include: {
      classItem: { select: { name: true, nameBn: true } },
      sectionItem: { select: { name: true, nameBn: true } },
      shiftItem: { select: { name: true, nameBn: true } },
      schedules: {
        orderBy: { date: "asc" },
        include: {
          subjectItem: { select: { name: true, nameBn: true } },
          marks: true,
        },
      },
    },
  });
  if (!exam) return null;

  const students = await prisma.student.findMany({
    where: {
      institutionId,
      status: "ACTIVE",
      classItemId: exam.classItemId ?? undefined,
      sectionItemId: exam.sectionItemId ?? undefined,
    },
    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);

  const markMap = new Map<string, { total: number; cq: number | null; mcq: number | null; applied: number | null }>();
  for (const sch of exam.schedules) {
    for (const m of sch.marks) {
      markMap.set(`${sch.id}:${m.studentId}`, {
        total: markTotal(m),
        cq: m.cq,
        mcq: m.mcq,
        applied: m.applied,
      });
    }
  }

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

  const rows: SheetRow[] = students.map((s) => {
    const subjects: SubjectCell[] = exam.schedules.map((sch) => {
      const full = sch.fullMarks;
      const m = markMap.get(`${sch.id}:${s.id}`);
      const has = m !== undefined;
      const percent = has && full > 0 ? (m.total / full) * 100 : null;
      const passMarks = sch.totalPassMarks ?? Math.ceil(full * 0.33);
      return {
        scheduleId: sch.id,
        subject: sch.subjectItem?.name ?? "—",
        subjectBn: sch.subjectItem?.nameBn ?? null,
        subjectItemId: sch.subjectItemId ?? null,
        full,
        mcqFull: sch.mcqMarks ?? null,
        cqFull: sch.cqMarks ?? null,
        appliedFull: sch.appliedMarks ?? null,
        mcqPass: sch.mcqPassMarks ?? null,
        cqPass: sch.cqPassMarks ?? null,
        appliedPass: sch.appliedPassMarks ?? null,
        totalPass: sch.totalPassMarks ?? null,
        obtained: has ? m.total : null,
        cq: has ? m.cq : null,
        mcq: has ? m.mcq : null,
        applied: has ? m.applied : null,
        percent,
        grade: percent !== null ? gradeFromPercent(percent).grade : null,
        failed: has ? m.total < passMarks : false,
      };
    });

    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: s.id,
      roll: s.roll,
      name: s.name,
      nameBn: s.nameBn,
      sectionName: (s.sectionItemId ? ssMap.get(s.sectionItemId)?.name : undefined) ?? exam.sectionItem?.name ?? null,
      sectionNameBn: (s.sectionItemId ? ssMap.get(s.sectionItemId)?.nameBn : undefined) ?? exam.sectionItem?.nameBn ?? null,
      shiftName: (s.shiftItemId ? ssMap.get(s.shiftItemId)?.name : undefined) ?? exam.shiftItem?.name ?? null,
      shiftNameBn: (s.shiftItemId ? ssMap.get(s.shiftItemId)?.nameBn : undefined) ?? exam.shiftItem?.nameBn ?? null,
      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(s.id) ?? [],
      guardianName: s.guardianName,
      motherName: s.motherName,
      photoUrl: s.photoUrl,
      attendance: {
        academicDay: academicDayS,
        present: attendMapS.get(s.id)?.present ?? 0,
        absent: attendMapS.get(s.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)
  const examKey = `e:${exam.id}`;
  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);
  if (verification.toggles.result) {
    const examCodes = await ensureCodes({
      institutionId,
      refKey: examKey,
      label: exam.name,
      rows: rows.map((r) => ({ studentId: r.studentId, total: r.totalObtained, grade: r.grade })),
    });
    for (const r of rows) {
      const stored = examCodes.get(r.studentId);
      if (stored) r.verifyCode = formatCode(stored);
    }
  }

  // optional-subject pool for the exam's class (With-Optional sheet type)
  const optionalPool = await prisma.classOptionalSubject.findMany({
    where: { institutionId, classItemId: exam.classItemId ?? -1 },
    select: { subjectItemId: true },
  });

  return {
    exam: {
      id: exam.id,
      name: exam.name,
      nameBn: exam.nameBn,
      format: exam.format,
      className: exam.classItem?.name ?? null,
      classNameBn: exam.classItem?.nameBn ?? null,
      sectionName: exam.sectionItem?.name ?? null,
      sectionNameBn: exam.sectionItem?.nameBn ?? null,
      shiftName: exam.shiftItem?.name ?? null,
      shiftNameBn: exam.shiftItem?.nameBn ?? null,
      startDate: exam.startDate,
      endDate: exam.endDate,
      resultPublished: exam.resultPublished,
      resultPublishedAt: exam.resultPublishedAt,
    },
    rows,
    optionalSubjectIds: optionalPool.map((o) => o.subjectItemId),
  };
}

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

export async function principalSignatureFor(institutionId: number) {
  const principal = await prisma.staff.findFirst({
    where: { institutionId, designation: "PRINCIPAL", status: "ACTIVE", signatureUrl: { not: null } },
    select: { name: true, signatureUrl: true },
    orderBy: { id: "asc" },
  });
  return { principalSignature: principal?.signatureUrl ?? null, principalName: principal?.name ?? null };
}
