/**
 * ZKTeco iclock ADMS protocol endpoints.
 *
 * ZKTeco device firmware hardcodes these three paths — they are NOT configurable:
 *   GET  /iclock/cdata?SN=<serial>&options=all  → device init/heartbeat
 *   POST /iclock/cdata?SN=<serial>&table=ATTLOG → attendance record push
 *   GET  /iclock/getrequest?SN=<serial>         → pending command poll
 *
 * Configure the device with:
 *   Server Address: <your-server-ip-or-domain>
 *   Server Port:    3333  (or 80/443 in production via nginx)
 */
import type { FastifyInstance } from 'fastify';
import { eq, and } from 'drizzle-orm';
import { devices, students } from '../../db/schema/index.js';
import { processRawScan } from '../attendance/attendance.service.js';

export default async function iclockRoutes(fastify: FastifyInstance) {
  // ZKTeco devices send body as text/plain (or sometimes with no Content-Type)
  fastify.addContentTypeParser('text/plain', { parseAs: 'string' }, (_req, body, done) => {
    done(null, body);
  });
  fastify.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_req, body, done) => {
    done(null, body);
  });

  // ── Device initialisation / heartbeat ──────────────────────────────────────
  // Device calls this when it first connects and periodically to stay "online".
  // We respond with the device's operating parameters.
  fastify.get('/cdata', async (request, reply) => {
    const q = request.query as Record<string, string>;
    const serialNo = q['SN'];

    if (!serialNo) {
      return reply.code(400).header('Content-Type', 'text/plain').send('ERROR: Missing SN');
    }

    const [device] = await fastify.db
      .select()
      .from(devices)
      .where(eq(devices.serialNo, serialNo))
      .limit(1);

    if (device) {
      await fastify.db
        .update(devices)
        .set({ status: 'ONLINE', lastSeen: new Date(), updatedAt: new Date() })
        .where(eq(devices.id, device.id));
    } else {
      fastify.log.warn({ serialNo }, 'iclock init from unregistered device — responding anyway');
    }

    // ZKTeco expects this exact multi-line text format
    const config = [
      'GET OPTION FROM: SERVER',
      'ATTLOGStamp=None',
      'OPERLOGStamp=9999',
      'ErrorDelay=30',
      'Delay=10',
      'TransTimes=00:00;14:05',
      'TransInterval=1',
      'Realtime=1',
      'Encrypt=None',
    ].join('\n');

    return reply.header('Content-Type', 'text/plain').send(config);
  });

  // ── Attendance record push ─────────────────────────────────────────────────
  // Device POSTs a batch of tab-separated ATTLOG lines:
  //   <biometric_id>\t<YYYY-MM-DD HH:MM:SS>\t<verify_state>\t<verify_type>\t...\n
  fastify.post('/cdata', async (request, reply) => {
    const q = request.query as Record<string, string>;
    const serialNo = q['SN'];
    const table = q['table'] ?? 'ATTLOG';

    if (!serialNo) {
      return reply.code(400).header('Content-Type', 'text/plain').send('ERROR: Missing SN');
    }

    // Non-ATTLOG tables (OPERLOG, etc.) — acknowledge and ignore
    if (table !== 'ATTLOG') {
      return reply.header('Content-Type', 'text/plain').send('OK');
    }

    const [device] = await fastify.db
      .select()
      .from(devices)
      .where(eq(devices.serialNo, serialNo))
      .limit(1);

    if (!device) {
      fastify.log.warn({ serialNo }, 'ADMS push from unregistered device');
      return reply.code(403).header('Content-Type', 'text/plain').send('ERROR: Device not registered');
    }

    await fastify.db
      .update(devices)
      .set({ status: 'ONLINE', lastSeen: new Date(), updatedAt: new Date() })
      .where(eq(devices.id, device.id));

    const body = (request.body as string) ?? '';
    const lines = body.split('\n').filter((l) => l.trim().length > 0);

    let processed = 0;
    const skipped: string[] = [];

    for (const line of lines) {
      const parts = line.trim().split('\t');
      if (parts.length < 2) continue;

      const [userId, datetimeStr] = parts;
      if (!userId || !datetimeStr) continue;

      try {
        // ZKTeco format: "2024-01-15 09:30:00" — replace space with T for ISO parse
        const scanTime = new Date(datetimeStr.replace(' ', 'T'));
        if (isNaN(scanTime.getTime())) {
          skipped.push(`bad datetime: ${datetimeStr}`);
          continue;
        }

        const [student] = await fastify.db
          .select()
          .from(students)
          .where(and(eq(students.tenantId, device.tenantId), eq(students.biometricId, userId)))
          .limit(1);

        if (!student) {
          skipped.push(`unknown id: ${userId}`);
          continue;
        }

        await processRawScan(fastify, {
          tenantId: device.tenantId,
          studentId: student.id,
          deviceId: device.id,
          scanTime,
        });

        processed++;
      } catch (err) {
        fastify.log.error({ err, line }, 'Error processing ATTLOG line');
        skipped.push(`error: ${line}`);
      }
    }

    fastify.log.info({ serialNo, processed, skipped: skipped.length }, 'ATTLOG push processed');
    return reply.header('Content-Type', 'text/plain').send(`OK: ${processed}`);
  });

  // ── Command poll ───────────────────────────────────────────────────────────
  // Device polls this every few seconds looking for server-side commands
  // (enroll user, delete user, reboot, etc.).
  // We return an empty command response — command support can be added later.
  fastify.get('/getrequest', async (request, reply) => {
    const q = request.query as Record<string, string>;
    const serialNo = q['SN'];

    if (serialNo) {
      // Keep last_seen fresh without fetching the full row
      await fastify.db
        .update(devices)
        .set({ lastSeen: new Date() })
        .where(eq(devices.serialNo, serialNo));
    }

    return reply.header('Content-Type', 'text/plain').send('C:0:');
  });
}
