import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { prisma } from '../config/database';
import { VisitorCheckpointAuthRequest, ScanAuthRequest } from '../middleware/auth';
import { emitToOwner, emitToApprover, recordNotification } from '../lib/events';

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret';

// Helper used by the scanner controllers below — pulls a normalized scope
// out of req.scanContext (populated by requireScanAccess). Falls back to the
// legacy checkpoint-only shape if a route is still using the older
// requireVisitorCheckpointAuth middleware so we don't break anything during
// the transition.
function scope(req: VisitorCheckpointAuthRequest & ScanAuthRequest) {
  if (req.scanContext) return req.scanContext;
  return {
    ownerId: req.ownerId!,
    actor: 'CHECKPOINT' as const,
    checkpointId: req.visitorCheckpointId,
  };
}

export async function visitorScannerLogin(req: Request, res: Response): Promise<void> {
  try {
    const { mobile, password } = req.body;

    if (!mobile || !password) {
      res.status(400).json({ error: 'Mobile and password are required' });
      return;
    }

    const mobileClean = mobile.replace(/[\s\-()]/g, '');
    const mobileVariants = [mobileClean];
    if (mobileClean.startsWith('+')) {
      mobileVariants.push(mobileClean.slice(1));
    } else {
      mobileVariants.push('+' + mobileClean);
    }

    const checkpoint = await prisma.visitorCheckpoint.findFirst({
      where: { mobile: { in: mobileVariants }, isActive: true },
    });

    if (!checkpoint) {
      res.status(401).json({ error: 'Invalid credentials' });
      return;
    }

    const isMatch = checkpoint.passwordHash.startsWith('$2')
      ? await bcrypt.compare(password, checkpoint.passwordHash)
      : password === checkpoint.passwordHash;
    if (!isMatch) {
      res.status(401).json({ error: 'Invalid credentials' });
      return;
    }

    // Workspace-suspension gate. The checkpoint's owning workspace can be
    // soft-suspended from the platform console; refuse to mint a scanner
    // token in that case so reception sees the block at sign-in instead of
    // landing in the scanner UI and 401-ing on every subsequent call.
    const ownerSuspension = await prisma.owner.findUnique({
      where: { id: checkpoint.ownerId },
      select: { suspendedAt: true, name: true, email: true },
    });
    if (ownerSuspension?.suspendedAt) {
      res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
      return;
    }

    const token = jwt.sign(
      { visitorCheckpointId: checkpoint.id, ownerId: checkpoint.ownerId },
      JWT_SECRET,
      { expiresIn: '30d' }
    );

    res.json({
      token,
      checkpoint: {
        id: checkpoint.id,
        name: checkpoint.name,
        personName: checkpoint.personName,
        mobile: checkpoint.mobile,
        ownerId: checkpoint.ownerId,
        createdAt: checkpoint.createdAt,
        org: { name: ownerSuspension?.name || null, email: ownerSuspension?.email || null },
      },
    });
  } catch (error) {
    console.error('visitorScannerLogin error:', error);
    res.status(500).json({ error: 'Failed to login' });
  }
}

export async function visitorScannerStats(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const { ownerId } = scope(req);
    // "Expected Today" = visitors still awaiting arrival (status EXPECTED)
    // whose visit is dated today — not a raw all-visitors count. Uses server
    // local day bounds, consistent with the rest of the app's date handling.
    const now = new Date();
    const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
    const [expected, arrived] = await Promise.all([
      prisma.visitor.count({
        where: { ownerId, status: 'EXPECTED', visitDate: { gte: startOfDay, lt: endOfDay } },
      }),
      prisma.visitor.count({ where: { ownerId, status: 'ARRIVED' } }),
    ]);
    res.json({ expected, arrived });
  } catch (error) {
    console.error('visitorScannerStats error:', error);
    res.status(500).json({ error: 'Failed to fetch stats' });
  }
}

export async function visitorCheckin(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const { shortId, confirmApprovalRequest, photoUrl } = req.body;
    const ctx = scope(req);

    if (!shortId) {
      res.status(400).json({ error: 'Visitor shortId is required' });
      return;
    }

    let visitor = await prisma.visitor.findFirst({
      where: { shortId, ownerId: ctx.ownerId },
    });

    if (!visitor) {
      res.status(404).json({ error: 'Visitor not found' });
      return;
    }

    // REJECTED is the most definitive "no" — surface it before anything
    // else so an old rejection doesn't get masked by an expiry message.
    if (visitor.status === 'REJECTED') {
      res.status(200).json({ ...visitor, alreadyRejected: true });
      return;
    }

    // Expiry check moved up so a past `expiresAt` wins over "already
    // arrived". A visitor who already checked in once and is now back
    // outside the validity window must hear "Entry expired", not
    // "already arrived". Flag with `passExpired: true` so the UI can
    // show a clear amber/red card with the expiry timestamp.
    const isExpired =
      visitor.status === 'EXPIRED' ||
      (visitor.expiresAt && new Date(visitor.expiresAt) < new Date());
    if (isExpired) {
      // Flip the row to EXPIRED if it wasn't already (lazy cron). Keeps
      // status reads consistent for the visitor list + audit log.
      let row = visitor;
      if (visitor.status !== 'EXPIRED') {
        row = await prisma.visitor.update({
          where: { id: visitor.id },
          data: { status: 'EXPIRED' },
        });
      }
      res.status(403).json({
        error: 'Entry expired',
        visitor: row,
        passExpired: true,
      });
      return;
    }

    // Other terminal states. ARRIVED comes AFTER the expiry check on
    // purpose — see comment above. CHECKED_OUT is treated like ARRIVED
    // for scanner UX (informational, not an error) but with a distinct
    // "already left" flag so the card can read accurately.
    if (visitor.status === 'CANCELLED') {
      // Cancelled by the owner OR auto-cancelled by the nightly sweep when
      // the visit date passed without anyone acting on the invite. Either
      // way the pass is dead — surface as "Entry expired" so reception
      // doesn't have to know the difference.
      res.status(403).json({ error: 'Entry expired — invite was cancelled', visitor, alreadyCancelled: true });
      return;
    }
    // Frequent + CUSTOM cadence: check the validity window and weekday
    // allow-list before doing anything else. If we're outside, reject
    // with a clear "not valid today" message without mutating state.
    // Applies BEFORE the re-arrive flip so a frequent visitor with an
    // expired custom window can't sneak in via the rescan path.
    if (visitor.isFrequent && visitor.frequencyType === 'CUSTOM') {
      const nowDate = new Date();
      if (visitor.frequencyValidFrom && nowDate < new Date(visitor.frequencyValidFrom)) {
        res.status(403).json({ error: 'Entry not valid yet — outside the allowed date range', visitor, passExpired: true });
        return;
      }
      if (visitor.frequencyValidUntil && nowDate > new Date(visitor.frequencyValidUntil)) {
        res.status(403).json({ error: 'Entry expired — outside the allowed date range', visitor, passExpired: true });
        return;
      }
      const weekdays = visitor.frequencyWeekdays || [];
      if (weekdays.length > 0 && !weekdays.includes(nowDate.getDay())) {
        res.status(403).json({ error: 'Entry not valid today — wrong weekday', visitor, passExpired: true });
        return;
      }
    }

    // Frequent visitors: scanning while ARRIVED or CHECKED_OUT is a new
    // entry, not an informational ping. Flip them BACK to EXPECTED so
    // the rest of this handler treats the scan exactly like a first
    // arrival — running the configured check-in policy (auto / live
    // approval / walk-in) afresh. The scan log row written below
    // gives the host a per-visit history.
    if (visitor.isFrequent && (visitor.status === 'ARRIVED' || visitor.status === 'CHECKED_OUT')) {
      visitor = await prisma.visitor.update({
        where: { id: visitor.id },
        data: { status: 'EXPECTED', arrivedAt: null, checkedOutAt: null, approvalRequestedAt: null, approvalNote: null },
      });
    } else if (visitor.status === 'CHECKED_OUT') {
      res.status(200).json({ ...visitor, alreadyCheckedOut: true });
      return;
    } else if (visitor.status === 'ARRIVED') {
      res.status(200).json({ ...visitor, alreadyArrived: true });
      return;
    }

    // Pre-approval visitors that were cleared in advance have status=EXPECTED
    // AND requiresApproval=true BUT isPreApproval=true — they should NOT be
    // routed through the live-approval preview/confirmation. They behave just
    // like auto-check-in visitors at scan time.
    const needsLiveApproval = visitor.requiresApproval && !visitor.isPreApproval;

    // Workspace photo-capture stage. When set to RECEPTION, reception must
    // capture a live photo before the approval request is sent — surfaced to
    // the client on the preview response and enforced on the commit below.
    // Loaded only on the live-approval path to avoid an extra query per scan.
    let captureStage: string = 'CREATION';
    if (needsLiveApproval) {
      const workspace = await prisma.owner.findUnique({
        where: { id: ctx.ownerId },
        select: { photoCaptureStage: true },
      });
      captureStage = workspace?.photoCaptureStage ?? 'CREATION';
    }

    // PREVIEW MODE: live-approval-required visitor scanned without an explicit
    // "request approval" confirmation → return their details but DON'T change
    // state. Reception can verify the person standing in front of them, then
    // tap "Request approval" which calls this endpoint again with the flag
    // (and, in RECEPTION mode, the freshly captured photo). `photoCaptureStage`
    // tells the UI whether to require that capture step.
    if (needsLiveApproval && visitor.status === 'EXPECTED' && !confirmApprovalRequest) {
      res.status(200).json({ ...visitor, approvalNeeded: true, photoCaptureStage: captureStage });
      return;
    }

    // RECEPTION photo-capture stage: the live identity photo must be captured
    // before the approval request goes out. Reject the commit if it's missing
    // so the approver never has to decide without a face to verify. Enforced
    // before we write the scan log / mutate state.
    let receptionPhotoUrl: string | null = null;
    if (needsLiveApproval && visitor.status === 'EXPECTED' && captureStage === 'RECEPTION') {
      if (!photoUrl || !String(photoUrl).trim()) {
        res.status(400).json({ error: 'Visitor photo is required before requesting approval', photoRequired: true });
        return;
      }
      const { savePhotoIfDataUrl } = await import('./visitors.controller');
      receptionPhotoUrl = await savePhotoIfDataUrl(photoUrl, visitor.shortId, req);
    }

    // From here on we're committing to a real check-in event → log the scan
    await prisma.visitorScanLog.create({
      data: {
        visitorId: visitor.id,
        checkpointId: ctx.checkpointId ?? null,
      },
    });

    // Approval-required visitor: park in AWAITING_APPROVAL, do not let them in yet
    if (needsLiveApproval && visitor.status === 'EXPECTED') {
      const updated = await prisma.visitor.update({
        where: { id: visitor.id },
        data: {
          status: 'AWAITING_APPROVAL',
          approvalRequestedAt: new Date(),
          // Persist the reception-captured photo against the visitor record so
          // it shows everywhere the photo is displayed (details, approvals,
          // reports) and rides along in the approver notifications/email below.
          ...(receptionPhotoUrl ? { photoUrl: receptionPhotoUrl } : {}),
        },
      });
      // Notify owner + assigned approver (if any) of new pending decision
      emitToOwner(ctx.ownerId, 'visitor.awaiting', { visitor: updated });
      await recordNotification({
        recipientType: 'OWNER', recipientId: ctx.ownerId,
        type: 'visitor.awaiting',
        title: `Approval needed for ${updated.name}`,
        body: `Scanned at reception — waiting for host approval · #${updated.shortId}`,
        link: '/visitors',
        push: { data: { visitorId: updated.id, shortId: updated.shortId, kind: 'visitor.awaiting' } },
      });
      // After the Admin/Approver merge, the assignee may live on either FK.
      // Both share the `approver:<id>` room key + APPROVER recipient type so
      // the existing approver UI/bell paths just work for admin-as-approver.
      const assignees = [updated.assignedApproverId, updated.assignedAdminId].filter((x): x is string => !!x);
      for (const id of assignees) {
        emitToApprover(id, 'visitor.awaiting', { visitor: updated });
        await recordNotification({
          recipientType: 'APPROVER', recipientId: id,
          type: 'visitor.awaiting',
          title: `${updated.name} is at reception`,
          body: `Scanned the QR — tap to approve or reject · #${updated.shortId}`,
          link: '/approver',
          push: { data: { visitorId: updated.id, shortId: updated.shortId, kind: 'visitor.awaiting' } },
        });
      }
      // Workspace-gated email to the assigned host/approver. Fire-and-
      // forget so a slow SMTP doesn't block the scan response.
      const { sendApproverRequest } = await import('./visitors.controller');
      sendApproverRequest(ctx.ownerId, updated, req).catch((e) =>
        console.error('sendApproverRequest failed', e)
      );
      // WhatsApp templates are never auto-sent — the host sends them manually
      // from the WhatsApp picker (POST /visitors/:id/whatsapp-send).
      res.status(202).json({ ...updated, approvalPending: true });
      return;
    }

    // Already awaiting approval — operator scanned again; surface current
    // state. For pre-approval visitors who never got cleared in advance,
    // re-emit the live notification so the approver sees fresh urgency now
    // that the visitor is physically at reception.
    if (visitor.status === 'AWAITING_APPROVAL') {
      if (visitor.isPreApproval) {
        emitToOwner(ctx.ownerId, 'visitor.awaiting', { visitor });
        const assignees = [visitor.assignedApproverId, visitor.assignedAdminId].filter((x): x is string => !!x);
        for (const id of assignees) {
          emitToApprover(id, 'visitor.awaiting', { visitor });
          await recordNotification({
            recipientType: 'APPROVER', recipientId: id,
            type: 'visitor.awaiting',
            title: `${visitor.name} is at reception`,
            body: `Pre-approval still pending — tap to approve or reject · #${visitor.shortId}`,
            link: '/approver',
            push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.awaiting' } },
          });
        }
      }
      res.status(202).json({ ...visitor, approvalPending: true });
      return;
    }

    // Normal path: directly mark arrived
    const updatedVisitor = await prisma.visitor.update({
      where: { id: visitor.id },
      data: { status: 'ARRIVED', arrivedAt: new Date() },
    });

    emitToOwner(ctx.ownerId, 'visitor.arrived', { visitor: updatedVisitor });
    await recordNotification({
      recipientType: 'OWNER', recipientId: ctx.ownerId,
      type: 'visitor.arrived',
      title: `${updatedVisitor.name} checked in`,
      body: `Auto check-in at reception — no approval required · #${updatedVisitor.shortId}`,
      link: '/visitors',
      push: { data: { visitorId: updatedVisitor.id, shortId: updatedVisitor.shortId, kind: 'visitor.arrived' } },
    });

    // Notify assigned approver/admin that the visitor has physically arrived
    const arrivedAssignees = [updatedVisitor.assignedApproverId, updatedVisitor.assignedAdminId].filter((x): x is string => !!x);
    for (const id of arrivedAssignees) {
      emitToApprover(id, 'visitor.arrived', { visitor: updatedVisitor });
      await recordNotification({
        recipientType: 'APPROVER', recipientId: id,
        type: 'visitor.arrived',
        title: `${updatedVisitor.name} has arrived`,
        body: `Visitor checked in at reception · #${updatedVisitor.shortId}`,
        link: '/approver',
        push: { data: { visitorId: updatedVisitor.id, shortId: updatedVisitor.shortId, kind: 'visitor.arrived' } },
      });
    }

    // Workspace-gated check-in confirmation email. No-op when the
    // automation toggle is off or the visitor has no email. Fire-and-
    // forget so a flaky SMTP doesn't block the scan response.
    const { sendCheckInConfirmation } = await import('./visitors.controller');
    sendCheckInConfirmation(ctx.ownerId, updatedVisitor).catch((e) =>
      console.error('sendCheckInConfirmation failed', e)
    );
    // WhatsApp templates are never auto-sent — the host sends them manually
    // from the WhatsApp picker (POST /visitors/:id/whatsapp-send).

    res.json(updatedVisitor);
  } catch (error: any) {
    console.error('visitorCheckin error:', error?.message || error);
    res.status(500).json({ error: 'Failed to check in visitor' });
  }
}

export async function visitorStatusByShortId(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const { ownerId } = scope(req);
    const { shortId } = req.params;
    const visitor = await prisma.visitor.findFirst({
      where: { shortId, ownerId },
    });
    if (!visitor) {
      res.status(404).json({ error: 'Visitor not found' });
      return;
    }
    res.json(visitor);
  } catch (error) {
    console.error('visitorStatusByShortId error:', error);
    res.status(500).json({ error: 'Failed to fetch visitor' });
  }
}

export async function createVisitorRequest(req: VisitorCheckpointAuthRequest, res: Response): Promise<void> {
  try {
    const { name, phone, email, company, reason, assignedApproverId } = req.body;

    if (!name || !reason) {
      res.status(400).json({ error: 'Name and reason are required' });
      return;
    }

    // Validate the assigned approver belongs to this owner
    let validApproverId: string | null = null;
    if (assignedApproverId) {
      const approver = await prisma.approver.findFirst({
        where: { id: assignedApproverId, ownerId: req.ownerId, isActive: true },
      });
      if (approver) validApproverId = approver.id;
    }

    // Create a Visitor row up-front so the walk-in appears in the main visitor
    // list (status=AWAITING_APPROVAL) the moment reception submits the
    // request. Approval/rejection later flips this row to ARRIVED/REJECTED.
    const { nanoid } = await import('nanoid');
    const { generateQRCodeBuffer } = await import('../utils/qrcode');
    const { saveUpload } = await import('../config/storage');
    const shortId = nanoid(10).toUpperCase();
    const qrBuffer = await generateQRCodeBuffer(shortId);
    const qrUrl = await saveUpload(qrBuffer, `visitors/qr/${shortId}.png`, 'image/png', req);
    const now = new Date();
    const visitor = await prisma.visitor.create({
      data: {
        ownerId: req.ownerId!,
        shortId,
        name,
        email: email || null,
        mobile: phone || null,
        reasonForVisit: reason,
        notes: company ? `Walk-in from ${company}` : null,
        visitDate: now,
        visitTime: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`,
        qrCodeUrl: qrUrl,
        status: 'AWAITING_APPROVAL',
        requiresApproval: true,
        approvalRequestedAt: now,
        assignedApproverId: validApproverId,
      },
    });
    // Record the scan that triggered the walk-in (audit + history).
    await prisma.visitorScanLog.create({
      data: { visitorId: visitor.id, checkpointId: req.visitorCheckpointId! },
    });

    const visitorRequest = await prisma.visitorRequest.create({
      data: {
        ownerId: req.ownerId!,
        checkpointId: req.visitorCheckpointId!,
        name,
        phone: phone || null,
        email: email || null,
        company: company || null,
        reason,
        assignedApproverId: validApproverId,
        visitorId: visitor.id,
      },
      include: {
        checkpoint: { select: { name: true, personName: true } },
        assignedApprover: { select: { id: true, name: true, email: true } },
      },
    });

    // Notify owner of new walk-in request
    emitToOwner(req.ownerId!, 'request.created', { request: visitorRequest });
    await recordNotification({
      recipientType: 'OWNER', recipientId: req.ownerId!,
      type: 'request.created',
      title: `New walk-in request: ${visitorRequest.name}`,
      body: visitorRequest.reason
        ? `Reception submitted a walk-in — ${visitorRequest.reason.slice(0, 100)}`
        : 'Reception submitted a walk-in — review and decide',
      link: '/visitors',
      push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.awaiting' } },
    });

    // Also notify the assigned approver if any
    if (validApproverId) {
      emitToApprover(validApproverId, 'request.created', { request: visitorRequest });
      await recordNotification({
        recipientType: 'APPROVER', recipientId: validApproverId,
        type: 'request.created',
        title: `Walk-in needs your approval: ${visitorRequest.name}`,
        body: visitorRequest.reason
          ? `Reception forwarded this walk-in to you — ${visitorRequest.reason.slice(0, 100)}`
          : 'Reception forwarded this walk-in to you — tap to approve or reject',
        link: '/visitors',
        push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.awaiting' } },
      });
    }

    // Workspace-gated email to the host/approver — same pattern as the
    // checkpoint approval flow above. Fire-and-forget.
    const { sendApproverRequest } = await import('./visitors.controller');
    sendApproverRequest(req.ownerId!, visitor as any, req).catch((e) =>
      console.error('sendApproverRequest failed', e)
    );
    // WhatsApp templates are never auto-sent — the host sends them manually
    // from the WhatsApp picker (POST /visitors/:id/whatsapp-send).

    res.status(201).json(visitorRequest);
  } catch (error) {
    console.error('createVisitorRequest error:', error);
    res.status(500).json({ error: 'Failed to create visitor request' });
  }
}

/** Auto check-in walk-in: creates a Visitor record directly marked ARRIVED. No approval needed. */
export async function walkInArrived(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const ctx = scope(req);
    const { name, phone, email, reasonForVisit, notes } = req.body;
    if (!name) { res.status(400).json({ error: 'Name is required' }); return; }
    if (!reasonForVisit || !String(reasonForVisit).trim()) {
      res.status(400).json({ error: 'Reason for visit is required' });
      return;
    }

    const { nanoid } = await import('nanoid');
    const { generateQRCodeBuffer } = await import('../utils/qrcode');
    const { saveUpload } = await import('../config/storage');

    const shortId = nanoid(10).toUpperCase();
    const qrBuffer = await generateQRCodeBuffer(shortId);
    const qrKey = `visitors/qr/${shortId}.png`;
    const qrUrl = await saveUpload(qrBuffer, qrKey, 'image/png', req);

    const now = new Date();
    const visitor = await prisma.visitor.create({
      data: {
        ownerId: ctx.ownerId,
        shortId,
        name,
        email: email || null,
        mobile: phone || null,
        reasonForVisit: String(reasonForVisit).trim(),
        notes: notes || null,
        visitDate: now,
        visitTime: `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`,
        qrCodeUrl: qrUrl,
        status: 'ARRIVED',
        arrivedAt: now,
      },
    });

    // Log the scan event for audit
    await prisma.visitorScanLog.create({
      data: { visitorId: visitor.id, checkpointId: ctx.checkpointId ?? null },
    });

    // Notify owner — visitor just walked in
    emitToOwner(ctx.ownerId, 'visitor.arrived', { visitor });
    await recordNotification({
      recipientType: 'OWNER', recipientId: ctx.ownerId,
      type: 'visitor.arrived',
      title: `${visitor.name} walked in`,
      body: `Reception logged a direct walk-in entry · #${visitor.shortId}`,
      link: '/visitors',
      push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.arrived' } },
    });

    // Notify assigned approver/admin if one was set on this walk-in
    const walkInAssignees = [visitor.assignedApproverId, visitor.assignedAdminId].filter((x): x is string => !!x);
    for (const id of walkInAssignees) {
      emitToApprover(id, 'visitor.arrived', { visitor });
      await recordNotification({
        recipientType: 'APPROVER', recipientId: id,
        type: 'visitor.arrived',
        title: `${visitor.name} walked in`,
        body: `Reception logged a direct walk-in entry · #${visitor.shortId}`,
        link: '/approver',
        push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.arrived' } },
      });
    }

    res.status(201).json(visitor);
  } catch (error) {
    console.error('walkInArrived error:', error);
    res.status(500).json({ error: 'Failed to register walk-in' });
  }
}

/**
 * Recent scan events.
 * - Checkpoint actor → scans logged by this checkpoint only (existing
 *   behavior, gives reception their personal history).
 * - Owner / Admin actor → all scans in the workspace, so the mobile app can
 *   show admins a workspace overview of what reception has been doing.
 */
export async function scanHistory(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const ctx = scope(req);
    const where =
      ctx.actor === 'CHECKPOINT' && ctx.checkpointId
        ? { checkpointId: ctx.checkpointId }
        : { visitor: { ownerId: ctx.ownerId } };
    const scans = await prisma.visitorScanLog.findMany({
      where,
      orderBy: { scannedAt: 'desc' },
      take: 100,
      include: {
        visitor: { select: { id: true, name: true, shortId: true, status: true, photoUrl: true, mobile: true, email: true, reasonForVisit: true } },
      },
    });
    res.json(scans);
  } catch (error) {
    console.error('scanHistory error:', error);
    res.status(500).json({ error: 'Failed to fetch scan history' });
  }
}

/** Approver list for the scanner UI — used to populate the "Route to" picker. */
export async function listApproversForScanner(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const { ownerId } = scope(req);
    const approvers = await prisma.approver.findMany({
      where: { ownerId, isActive: true },
      orderBy: { name: 'asc' },
      select: { id: true, name: true, email: true, designation: true, department: true },
    });
    res.json(approvers);
  } catch (error) {
    console.error('listApproversForScanner error:', error);
    res.status(500).json({ error: 'Failed to fetch approvers' });
  }
}

// Profile of the signed-in reception checkpoint. Backs the scanner's Profile
// panel so the operator can see their checkpoint name, contact and owning
// workspace without needing it baked into the login response (works for
// already-open sessions too, and stays fresh if an admin renames it).
export async function visitorScannerMe(req: VisitorCheckpointAuthRequest, res: Response): Promise<void> {
  try {
    const checkpoint = await prisma.visitorCheckpoint.findUnique({
      where: { id: req.visitorCheckpointId },
      select: {
        id: true,
        name: true,
        personName: true,
        mobile: true,
        isActive: true,
        createdAt: true,
        owner: { select: { name: true, email: true } },
      },
    });
    if (!checkpoint) {
      res.status(404).json({ error: 'Checkpoint not found' });
      return;
    }
    res.json({
      id: checkpoint.id,
      name: checkpoint.name,
      personName: checkpoint.personName,
      mobile: checkpoint.mobile,
      isActive: checkpoint.isActive,
      createdAt: checkpoint.createdAt,
      org: { name: checkpoint.owner?.name || null, email: checkpoint.owner?.email || null },
    });
  } catch (error) {
    console.error('visitorScannerMe error:', error);
    res.status(500).json({ error: 'Failed to load profile' });
  }
}

export async function listCheckpointRequests(req: VisitorCheckpointAuthRequest, res: Response): Promise<void> {
  try {
    const requests = await prisma.visitorRequest.findMany({
      where: {
        checkpointId: req.visitorCheckpointId,
        ownerId: req.ownerId,
      },
      orderBy: { createdAt: 'desc' },
      take: 50,
    });

    res.json(requests);
  } catch (error) {
    console.error('listCheckpointRequests error:', error);
    res.status(500).json({ error: 'Failed to fetch requests' });
  }
}

export async function completeVisitorRequest(req: VisitorCheckpointAuthRequest, res: Response): Promise<void> {
  try {
    const { id } = req.params;

    const existing = await prisma.visitorRequest.findFirst({
      where: { id, checkpointId: req.visitorCheckpointId, ownerId: req.ownerId },
    });

    if (!existing) {
      res.status(404).json({ error: 'Request not found' });
      return;
    }

    if (existing.status !== 'PENDING') {
      res.status(400).json({ error: 'Request is already processed' });
      return;
    }

    const updated = await prisma.visitorRequest.update({
      where: { id },
      data: { status: 'APPROVED', ownerNote: 'Auto-completed by checkpoint' },
    });

    res.json(updated);
  } catch (error) {
    console.error('completeVisitorRequest error:', error);
    res.status(500).json({ error: 'Failed to complete request' });
  }
}

export async function visitorLookup(req: ScanAuthRequest, res: Response): Promise<void> {
  try {
    const { ownerId } = scope(req);
    const { query } = req.params;

    if (!query) {
      res.status(400).json({ error: 'Search query is required' });
      return;
    }

    // Only surface visitors still relevant at reception — i.e. actionable /
    // in-progress. Terminal, completed passes (checked out, rejected,
    // cancelled, expired) are excluded here because they belong in the History
    // tab, not in a "who can I check in" search.
    const visitors = await prisma.visitor.findMany({
      where: {
        ownerId,
        status: { notIn: ['EXPIRED', 'REJECTED', 'CANCELLED', 'CHECKED_OUT'] },
        // Hide passes whose validity window has already closed even if the
        // nightly sweep hasn't flipped their status to EXPIRED yet. Frequent
        // visitors carry a null expiresAt and are always kept.
        OR: [
          { expiresAt: null },
          { expiresAt: { gte: new Date() } },
        ],
        AND: [
          {
            OR: [
              { shortId: { equals: query, mode: 'insensitive' } },
              { name: { contains: query, mode: 'insensitive' } },
              { mobile: { contains: query } },
            ],
          },
        ],
      },
      take: 20,
    });

    res.json(visitors);
  } catch (error) {
    console.error('visitorLookup error:', error);
    res.status(500).json({ error: 'Failed to search visitors' });
  }
}
