import crypto from "crypto";
import { prisma } from "@/lib/prisma";

/* Universal punch pipeline — every source (ZK poll, HTTP push, CSV import)
   feeds punches through here:

     ingest → dedupe → identify (credential → roll → cardNo → registration
     → employeeCode → UNMATCHED queue) → attendance processing
     (daily row PRESENT with first-in / last-out times, students AND staff)

   Brand differences live only at the edge (how a punch reaches ingest). */

export interface PunchInput {
  institutionId: number;
  machineId?: number | null;
  source: "ZK_POLL" | "HTTP_PUSH" | "CSV";
  identifier: string; // badge / card number / uid as the machine knows it
  punchedAt: Date;
  machineUserId?: string | null; // device user id, when the source has one
  raw?: unknown;
}

export interface IngestResult {
  received: number;
  created: number;
  duplicates: number;
  matched: number;
  unmatched: number;
}

function dedupeKey(p: PunchInput) {
  // source-scoped (not machine-scoped): re-importing the same CSV dedupes, and
  // the same physical punch arriving via two edges doesn't double-count
  return `${p.source}:${p.identifier.trim().toUpperCase()}:${p.punchedAt.toISOString()}`;
}

/** identifier → person, using the remembered mapping first then natural keys */
type Resolved = { studentId?: number; staffId?: number };

async function resolveIdentity(institutionId: number, identifier: string, machineUserId?: string | null): Promise<Resolved> {
  const id = identifier.trim();
  const upper = id.toUpperCase();
  const cred = await prisma.attendanceCredential.findUnique({
    where: { institutionId_identifier: { institutionId, identifier: upper } },
  });
  if (cred && (cred.studentId || cred.staffId)) {
    return { studentId: cred.studentId ?? undefined, staffId: cred.staffId ?? undefined };
  }
  // natural keys: student roll / RFID card no / registration, then staff code
  // (cardNo is the printed ID no — identification only, never punched)
  const student = await prisma.student.findFirst({
    where: {
      institutionId,
      OR: [{ roll: id }, { rfidCardNo: id }, ...(id ? [{ registrationNo: id }] : [])],
    },
    select: { id: true },
  });
  if (student) return { studentId: student.id };

  const staff = await prisma.staff.findFirst({
    where: { institutionId, employeeCode: id },
    select: { id: true },
  });
  if (staff) return { staffId: staff.id };

  void machineUserId;
  return {};
}


/** device rows keep first-in / last-out: min/max over all punches of the day */
function spanTimes(a: Date | null, b: Date | null, punch: Date) {
  const inTime = a && a < punch ? a : punch;
  const outTime = b && b > punch ? b : punch;
  return { inTime, outTime };
}

/** one person's punch → the daily attendance row (students + staff) */
async function applyToAttendance(
  institutionId: number,
  who: Resolved,
  punchedAt: Date
) {
  const date = new Date(Date.UTC(
    punchedAt.getFullYear(), punchedAt.getMonth(), punchedAt.getDate()
  ));

  if (who.studentId) {
    // daily row (subjectItemId null) — a real punch is the strongest evidence
    // of presence, so it marks PRESENT and refreshes the in/out times.
    // (find-then-write: Prisma's compound-unique input won't take a null
    // subjectItemId directly)
    const existing = await prisma.studentAttendance.findFirst({
      where: { studentId: who.studentId, date, subjectItemId: null },
    });
    if (existing && existing.source === "DEVICE") {
      await prisma.studentAttendance.update({
        where: { id: existing.id },
        data: spanTimes(existing.inTime, existing.outTime, punchedAt),
      });
    } else if (existing) {
      await prisma.studentAttendance.update({
        where: { id: existing.id },
        data: { status: "PRESENT", inTime: punchedAt, outTime: punchedAt, source: "DEVICE" },
      });
    } else {
      await prisma.studentAttendance.create({
        data: {
          institutionId,
          studentId: who.studentId,
          date,
          status: "PRESENT",
          inTime: punchedAt,
          outTime: punchedAt,
          source: "DEVICE",
        },
      });
    }
  }

  if (who.staffId) {
    const existing = await prisma.staffAttendance.findUnique({
      where: { staffId_date: { staffId: who.staffId, date } },
    });
    if (existing && existing.source === "DEVICE") {
      await prisma.staffAttendance.update({
        where: { id: existing.id },
        data: spanTimes(existing.inTime, existing.outTime, punchedAt),
      });
    } else {
      await prisma.staffAttendance.upsert({
        where: { staffId_date: { staffId: who.staffId, date } },
        create: {
          institutionId,
          staffId: who.staffId,
          date,
          status: "PRESENT",
          inTime: punchedAt,
          outTime: punchedAt,
          source: "DEVICE",
        },
        update: {
          status: "PRESENT",
          inTime: punchedAt,
          outTime: punchedAt,
          source: "DEVICE",
        },
      });
    }
  }
}

/** Feed a batch of punches through the pipeline (idempotent per punch). */
export async function ingestPunches(punches: PunchInput[]): Promise<IngestResult> {
  const out: IngestResult = { received: punches.length, created: 0, duplicates: 0, matched: 0, unmatched: 0 };

  for (const p of punches) {
    const identifier = p.identifier.trim();
    if (!identifier || !(p.punchedAt instanceof Date) || isNaN(p.punchedAt.getTime())) continue;

    try {
      const who = await resolveIdentity(p.institutionId, identifier, p.machineUserId);
      const row = await prisma.rawPunch.create({
        data: {
          institutionId: p.institutionId,
          machineId: p.machineId ?? null,
          source: p.source,
          identifier: identifier.toUpperCase(),
          punchedAt: p.punchedAt,
          raw: (p.raw ?? undefined) as never,
          matchStatus: who.studentId || who.staffId ? "MATCHED" : "UNMATCHED",
          studentId: who.studentId,
          staffId: who.staffId,
          processedAt: who.studentId || who.staffId ? new Date() : null,
          dedupeKey: dedupeKey({ ...p, identifier }),
        },
      });
      out.created++;
      if (who.studentId || who.staffId) {
        out.matched++;
        await applyToAttendance(p.institutionId, who, p.punchedAt);
      } else {
        out.unmatched++;
      }
      if (p.machineId) {
        await prisma.attendanceMachine.update({
          where: { id: p.machineId },
          data: { lastPunchAt: p.punchedAt },
        }).catch(() => undefined);
      }
      void row;
    } catch (err) {
      if ((err as { code?: string }).code === "P2002") {
        out.duplicates++; // already ingested on an earlier sync/import
      } else {
        throw err;
      }
    }
  }

  return out;
}

/** Admin mapped an UNMATCHED punch to a person: remember the identifier and
    re-apply this punch (and every other unmatched punch of the same
    identifier) to attendance. */
export async function mapPunchToPerson(opts: {
  institutionId: number;
  punchId: number;
  studentId?: number | null;
  staffId?: number | null;
}) {
  const punch = await prisma.rawPunch.findFirst({
    where: { id: opts.punchId, institutionId: opts.institutionId },
  });
  if (!punch) return { ok: false as const, error: "PUNCH_NOT_FOUND" };

  const applied = await registerCredential({
    institutionId: opts.institutionId,
    identifier: punch.identifier,
    studentId: opts.studentId,
    staffId: opts.staffId,
  });

  return { ok: true as const, applied: applied.applied };
}

/** Remember an identifier → person mapping (admin-set) and re-apply every
    UNMATCHED punch of that identifier to attendance. Used both when an admin
    maps an unmatched punch and when they pre-register a card/ID from the
    Card & ID Registry. */
export async function registerCredential(opts: {
  institutionId: number;
  identifier: string;
  studentId?: number | null;
  staffId?: number | null;
  machineUserId?: string | null;
}) {
  const identifier = opts.identifier.trim().toUpperCase();
  await prisma.attendanceCredential.upsert({
    where: {
      institutionId_identifier: {
        institutionId: opts.institutionId,
        identifier,
      },
    },
    create: {
      institutionId: opts.institutionId,
      identifier,
      studentId: opts.studentId ?? null,
      staffId: opts.staffId ?? null,
      machineUserId: opts.machineUserId?.trim() || null,
      source: "MANUAL",
    },
    update: {
      studentId: opts.studentId ?? null,
      staffId: opts.staffId ?? null,
      machineUserId: opts.machineUserId?.trim() || null,
      source: "MANUAL",
    },
  });

  // re-process every unmatched punch with this identifier
  const pending = await prisma.rawPunch.findMany({
    where: { institutionId: opts.institutionId, identifier, matchStatus: "UNMATCHED" },
    orderBy: { punchedAt: "asc" },
  });
  for (const p of pending) {
    if (opts.studentId) await applyToAttendance(opts.institutionId, { studentId: opts.studentId }, p.punchedAt);
    if (opts.staffId) await applyToAttendance(opts.institutionId, { staffId: opts.staffId }, p.punchedAt);
    await prisma.rawPunch.update({
      where: { id: p.id },
      data: {
        matchStatus: "MATCHED",
        studentId: opts.studentId ?? null,
        staffId: opts.staffId ?? null,
        processedAt: new Date(),
      },
    });
  }

  return { applied: pending.length };
}

export function generateApiKey() {
  return "ems_" + crypto.randomBytes(18).toString("hex");
}
