import ZKLib from "zklib-js";
import { prisma } from "@/lib/prisma";
import { ingestPunches, type PunchInput } from "@/lib/device-punch";

/* ZKTeco edge — direct poll over the device protocol (TCP first, UDP
   fallback inside zklib-js). Works with the standard ZK protocol family
   (K14/K20/K28/F18/… speedface included) on port 4370. */

export interface ZkPollResult {
  ok: boolean;
  serialNumber?: string;
  deviceUsers?: number;
  newPunches?: number;
  totalPunches?: number;
  created?: number;
  matched?: number;
  unmatched?: number;
  duplicates?: number;
  error?: string;
}

function toError(err: unknown): string {
  // zklib-js throws plain ZKError objects ({ err: { code, message }, … }),
  // not Error instances — serialize and scan for connection codes too
  const text = err instanceof Error ? err.message : (() => {
    try {
      return JSON.stringify(err);
    } catch {
      return String(err ?? "");
    }
  })();
  if (/ECONNREFUSED|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|ENETUNREACH|EACCES|timeout/i.test(text)) {
    return "DEVICE_UNREACHABLE";
  }
  return (err instanceof Error ? err.message : text).slice(0, 120) || "DEVICE_ERROR";
}

/** Connect + report device info without ingesting (Test Connection). */
export async function zkTest(machine: { ipAddress: string | null; port: number }): Promise<ZkPollResult> {
  if (!machine.ipAddress) return { ok: false, error: "IP_REQUIRED" };
  const zk = new ZKLib(machine.ipAddress, machine.port, 6000, 6000);
  try {
    await zk.createSocket();
    const info = (await zk.getInfo()) as { serialNumber?: string; userCounts?: number; logCounts?: number };
    return {
      ok: true,
      serialNumber: info?.serialNumber,
      deviceUsers: info?.userCounts,
      totalPunches: info?.logCounts,
    };
  } catch (err) {
    return { ok: false, error: toError(err) };
  } finally {
    await zk.disconnect().catch(() => undefined);
  }
}

/** Pull users + attendance logs and feed the universal pipeline. */
export async function zkSync(machineId: number): Promise<ZkPollResult> {
  const machine = await prisma.attendanceMachine.findUnique({ where: { id: machineId } });
  if (!machine) return { ok: false, error: "MACHINE_NOT_FOUND" };
  if (!machine.ipAddress) return { ok: false, error: "IP_REQUIRED" };

  const zk = new ZKLib(machine.ipAddress, machine.port, 6000, 6000);
  try {
    await zk.createSocket();
    const info = (await zk.getInfo()) as { serialNumber?: string; userCounts?: number };

    const logs = (await zk.getAttendances()) as { data?: { deviceUserId: number | string; recordTime: string }[] };

    // badge/user-id map from the device (helps manual mapping later)
    try {
      const users = (await zk.getUsers()) as { data?: { userId?: string; name?: string }[] };
      for (const u of users.data ?? []) {
        const ident = String(u.userId ?? "").trim().toUpperCase();
        if (!ident) continue;
        await prisma.attendanceCredential.upsert({
          where: { institutionId_identifier: { institutionId: machine.institutionId, identifier: ident } },
          create: { institutionId: machine.institutionId, identifier: ident, machineUserId: u.name ?? null },
          update: { machineUserId: u.name ?? null },
        }).catch(() => undefined);
      }
    } catch {
      /* user list is optional */
    }

    const punches: PunchInput[] = (logs.data ?? [])
      .map((log) => ({
        institutionId: machine.institutionId,
        machineId: machine.id,
        source: "ZK_POLL" as const,
        identifier: String(log.deviceUserId ?? ""),
        punchedAt: new Date(log.recordTime),
        machineUserId: String(log.deviceUserId ?? ""),
        raw: { deviceUserId: log.deviceUserId, recordTime: log.recordTime },
      }))
      .filter((p) => p.identifier && !isNaN(p.punchedAt.getTime()));

    const result = await ingestPunches(punches);
    await prisma.attendanceMachine.update({
      where: { id: machine.id },
      data: { lastSeenAt: new Date(), serialNumber: info?.serialNumber ?? machine.serialNumber },
    });

    return {
      ok: true,
      serialNumber: info?.serialNumber,
      deviceUsers: info?.userCounts,
      totalPunches: logs.data?.length,
      ...result,
    };
  } catch (err) {
    return { ok: false, error: toError(err) };
  } finally {
    await zk.disconnect().catch(() => undefined);
  }
}
