import { Request, Response } from 'express';
import { nanoid } from 'nanoid';
import { prisma } from '../config/database';
import { AuthRequest } from '../middleware/auth';
import { generateQRCodeBuffer } from '../utils/qrcode';
import { saveUpload } from '../config/storage';
import { emitToOwner, emitToApprover, recordNotification } from '../lib/events';
import { defaultExpiry } from '../lib/visitExpiry';

// Public URL the QR poster encodes. The visitor's phone camera opens this
// directly. Derive it strictly from the admin's incoming request so the QR
// always points at the same origin the admin is using — no hard-coded
// localhost fallback that could ship a broken poster to production.
//   1. Origin header (browsers set this on POST reliably)
//   2. Referer header
//   3. FRONTEND_URL env (last-resort override for CLI / cron)
// If none resolve we throw — the controller turns that into a 400 with a
// clear message rather than silently encoding "localhost" into the PNG.
function isPublicOrigin(url: string): boolean {
  if (!/^https?:\/\//i.test(url)) return false;
  try { return !['localhost', '127.0.0.1', '::1'].includes(new URL(url).hostname); } catch { return false; }
}

function publicAppUrl(req: AuthRequest): string {
  const origin = req.headers.origin;
  if (typeof origin === 'string') {
    const firstPublic = origin.split(',').map((o) => o.trim()).find(isPublicOrigin);
    if (firstPublic) return firstPublic.replace(/\/+$/, '');
  }
  const referer = req.headers.referer;
  if (typeof referer === 'string' && isPublicOrigin(referer)) {
    try { return new URL(referer).origin; } catch { /* fall through */ }
  }
  const env = process.env.FRONTEND_URL;
  if (env && env !== '*' && /^https?:\/\//i.test(env)) return env.replace(/\/+$/, '');
  throw new Error("Couldn't determine the public app URL for the QR poster — open this page in a browser so the Origin header is sent, or set FRONTEND_URL on the server.");
}

// ─── Authenticated CRUD ─────────────────────────────────────────────────────

// Per-action walk-in QR permission — owner always allowed; a sub-admin needs
// the specific flag on their Admin row. DB lookup because these flags live on
// the Admin row, not in the JWT (so grants take effect without re-login).
// Mirrors the visitor Add/Delete gating pattern.
async function hasWalkInFlag(
  req: AuthRequest,
  flag: 'canViewWalkInQr' | 'canAddWalkInQr' | 'canDeleteWalkInQr',
): Promise<boolean> {
  if (!req.adminId) return true; // owner session
  const admin = await prisma.admin.findFirst({
    where: { id: req.adminId, ownerId: req.ownerId, isActive: true },
    select: { canViewWalkInQr: true, canAddWalkInQr: true, canDeleteWalkInQr: true },
  });
  if (!admin) return false;
  return admin[flag];
}

// Access to the Walk-in QR section (the list). Any of the three walk-in
// flags grants it, so a user who can add or delete isn't locked out of the
// page they need. Owner always allowed.
async function canAccessWalkInSection(req: AuthRequest): Promise<boolean> {
  if (!req.adminId) return true; // owner session
  const admin = await prisma.admin.findFirst({
    where: { id: req.adminId, ownerId: req.ownerId, isActive: true },
    select: { canViewWalkInQr: true, canAddWalkInQr: true, canDeleteWalkInQr: true },
  });
  if (!admin) return false;
  return admin.canViewWalkInQr || admin.canAddWalkInQr || admin.canDeleteWalkInQr;
}

// Owner sees every walk-in QR in the workspace. Admins only see QRs they
// created. Receptionists (sub-admins) also see their parent admin's
// posters so they can fetch / re-print them when needed.
function walkInScopeForRequest(req: AuthRequest): Record<string, any> {
  const base: Record<string, any> = { ownerId: req.ownerId };
  if (req.adminId) {
    if (req.parentAdminId) {
      base.createdByAdminId = { in: [req.adminId, req.parentAdminId] };
    } else {
      base.createdByAdminId = req.adminId;
    }
  }
  return base;
}

export async function listWalkInQRs(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!(await canAccessWalkInSection(req))) { res.status(403).json({ error: 'You do not have permission to view walk-in QRs' }); return; }
    const [qrs, owner] = await Promise.all([
      prisma.walkInQR.findMany({
        where: walkInScopeForRequest(req),
        orderBy: { createdAt: 'desc' },
        include: {
          assignedAdmin: { select: { id: true, name: true, email: true } },
          createdByAdmin: { select: { id: true, name: true, email: true } },
        },
      }),
      prisma.owner.findUnique({
        where: { id: req.ownerId! },
        select: { id: true, name: true, email: true },
      }),
    ]);

    // Resolve who created each QR into a single `addedBy` field so the
    // frontend doesn't have to special-case "owner vs admin" rendering —
    // same pattern as Approvers list. Owner-created QRs show the
    // workspace owner; admin-created QRs show the admin.
    const ownerLabel = owner ? (owner.name || owner.email.split('@')[0]) : 'Workspace owner';
    const enriched = qrs.map((qr) => ({
      ...qr,
      addedBy: qr.createdByAdmin
        ? { id: qr.createdByAdmin.id, name: qr.createdByAdmin.name, email: qr.createdByAdmin.email, role: 'ADMIN' as const }
        : { id: owner?.id || null, name: ownerLabel, email: owner?.email || '', role: 'OWNER' as const },
    }));

    res.json(enriched);
  } catch (e) {
    console.error('listWalkInQRs', e);
    res.status(500).json({ error: 'Failed to fetch walk-in QRs' });
  }
}

export async function createWalkInQR(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!(await hasWalkInFlag(req, 'canAddWalkInQr'))) { res.status(403).json({ error: 'You do not have permission to create walk-in QRs' }); return; }
    const { label, assignedAdminId, requiresApproval } = req.body;
    if (!label || !String(label).trim()) { res.status(400).json({ error: 'Label is required' }); return; }

    // Validate the assignee belongs to this workspace if provided.
    let validAdminId: string | null = null;
    if (assignedAdminId) {
      const a = await prisma.admin.findFirst({
        where: { id: assignedAdminId, ownerId: req.ownerId, isActive: true },
      });
      if (!a) { res.status(400).json({ error: 'Assignee not found in this workspace' }); return; }
      validAdminId = a.id;
    }

    const code = nanoid(12).toLowerCase().replace(/[^a-z0-9]/g, '');
    const qrBuffer = await generateQRCodeBuffer(`${publicAppUrl(req)}/walk-in/${code}`);
    const qrCodeUrl = await saveUpload(qrBuffer, `walk-in-qr/${code}.png`, 'image/png', req);

    const qr = await prisma.walkInQR.create({
      data: {
        ownerId: req.ownerId!,
        assignedAdminId: validAdminId,
        createdByAdminId: req.adminId || null,
        label: String(label).trim(),
        code,
        qrCodeUrl,
        isActive: true,
        // Default to requiring approval (matches the existing flow); auto
        // check-in is an explicit opt-in per QR.
        requiresApproval: requiresApproval === false ? false : true,
      },
      include: {
        assignedAdmin: { select: { id: true, name: true, email: true } },
        createdByAdmin: { select: { id: true, name: true, email: true } },
      },
    });

    res.status(201).json({ ...qr, publicUrl: `${publicAppUrl(req)}/walk-in/${code}` });
  } catch (e: any) {
    console.error('createWalkInQR', e);
    res.status(500).json({ error: e?.message || 'Failed to create walk-in QR' });
  }
}

export async function updateWalkInQR(req: AuthRequest, res: Response): Promise<void> {
  try {
    // No edit UI exists today; the PATCH endpoint is gated on the add flag
    // for consistency (see plan) so it's not left on the removed
    // canManageSettings guard.
    if (!(await hasWalkInFlag(req, 'canAddWalkInQr'))) { res.status(403).json({ error: 'Forbidden' }); return; }
    const existing = await prisma.walkInQR.findFirst({
      where: { id: req.params.id, ...walkInScopeForRequest(req) },
    });
    if (!existing) { res.status(404).json({ error: 'Walk-in QR not found' }); return; }

    const { label, assignedAdminId, isActive, requiresApproval } = req.body;
    const data: any = {};
    if (label !== undefined) {
      if (!String(label).trim()) { res.status(400).json({ error: 'Label cannot be empty' }); return; }
      data.label = String(label).trim();
    }
    if (assignedAdminId !== undefined) {
      if (assignedAdminId === null || assignedAdminId === '') {
        data.assignedAdminId = null;
      } else {
        const a = await prisma.admin.findFirst({
          where: { id: assignedAdminId, ownerId: req.ownerId, isActive: true },
        });
        if (!a) { res.status(400).json({ error: 'Assignee not found in this workspace' }); return; }
        data.assignedAdminId = a.id;
      }
    }
    if (isActive !== undefined) data.isActive = !!isActive;
    if (requiresApproval !== undefined) data.requiresApproval = !!requiresApproval;

    const qr = await prisma.walkInQR.update({
      where: { id: existing.id },
      data,
      include: {
        assignedAdmin: { select: { id: true, name: true, email: true } },
        createdByAdmin: { select: { id: true, name: true, email: true } },
      },
    });
    res.json(qr);
  } catch (e) {
    console.error('updateWalkInQR', e);
    res.status(500).json({ error: 'Failed to update walk-in QR' });
  }
}

export async function deleteWalkInQR(req: AuthRequest, res: Response): Promise<void> {
  try {
    if (!(await hasWalkInFlag(req, 'canDeleteWalkInQr'))) { res.status(403).json({ error: 'Forbidden' }); return; }
    const existing = await prisma.walkInQR.findFirst({
      where: { id: req.params.id, ...walkInScopeForRequest(req) },
    });
    if (!existing) { res.status(404).json({ error: 'Walk-in QR not found' }); return; }
    await prisma.walkInQR.delete({ where: { id: existing.id } });
    res.json({ message: 'Walk-in QR deleted' });
  } catch (e) {
    console.error('deleteWalkInQR', e);
    res.status(500).json({ error: 'Failed to delete walk-in QR' });
  }
}

// ─── Public (no auth) — what the walk-in visitor's phone hits ───────────────

// Simple in-memory rate limiter. Not durable across restarts, but keeps
// casual flooding under control without adding a new dependency.
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX = 5;
const rateBuckets = new Map<string, number[]>();
function rateLimit(key: string): boolean {
  const now = Date.now();
  const list = (rateBuckets.get(key) || []).filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
  if (list.length >= RATE_LIMIT_MAX) return false;
  list.push(now);
  rateBuckets.set(key, list);
  return true;
}

export async function getPublicWalkInQR(req: Request, res: Response): Promise<void> {
  try {
    const { code } = req.params;
    const qr = await prisma.walkInQR.findUnique({
      where: { code },
      include: {
        assignedAdmin: { select: { name: true } },
        // Workspace identity drives the "Welcome to {org}" header per
        // step-6 spec 16c. Owner.name doubles as workspace name today.
        owner: { select: { name: true, email: true } },
      },
    });
    if (!qr || !qr.isActive) { res.status(404).json({ error: 'This walk-in QR is not active' }); return; }

    const reasons = await prisma.visitorReason.findMany({
      where: { ownerId: qr.ownerId },
      orderBy: { name: 'asc' },
      select: { id: true, name: true },
    });
    const orgName = qr.owner?.name?.trim()
      || (qr.owner?.email ? qr.owner.email.split('@')[0] : 'our office');
    res.json({
      orgName,
      label: qr.label,
      hostName: qr.assignedAdmin?.name || 'Host',
      reasons,
    });
  } catch (e) {
    console.error('getPublicWalkInQR', e);
    res.status(500).json({ error: 'Failed to load form' });
  }
}

export async function submitPublicWalkInQR(req: Request, res: Response): Promise<void> {
  try {
    const { code } = req.params;
    const ip = (req.ip || req.headers['x-forwarded-for'] || 'unknown').toString();
    if (!rateLimit(`${ip}|${code}`)) {
      res.status(429).json({ error: 'Too many submissions from this device. Try again later.' });
      return;
    }

    const qr = await prisma.walkInQR.findUnique({ where: { code } });
    if (!qr || !qr.isActive) { res.status(404).json({ error: 'This walk-in QR is not active' }); return; }

    const { name, mobile, email, reasonForVisit, photoUrl } = req.body;
    if (!name || !String(name).trim()) { res.status(400).json({ error: 'Full name is required' }); return; }
    if (!reasonForVisit || !String(reasonForVisit).trim()) { res.status(400).json({ error: 'Reason for visit is required' }); return; }

    // Materialise the visitor in the same shape as a live-approval Add Visitor:
    // EXPECTED, requiresApproval=true, isPreApproval=false. The scan at reception
    // will trigger the assignedAdmin's approval flow (see visitor-scanner.controller).
    const shortId = nanoid(10).toUpperCase();
    const qrBuffer = await generateQRCodeBuffer(shortId);
    const personalQrUrl = await saveUpload(qrBuffer, `visitors/qr/${shortId}.png`, 'image/png', req);

    // Optional self-photo the visitor added on the registration form. Decodes
    // the base64 data URL to a file on disk and stores the relative path on the
    // visitor (shown in visitor details, lists, and reports like any photo).
    const { savePhotoIfDataUrl } = await import('./visitors.controller');
    const savedPhotoUrl = await savePhotoIfDataUrl(photoUrl, shortId, req);

    const now = new Date();
    const visitTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;

    // Auto check-in QRs create the visitor with requiresApproval=false; the
    // reception scan then flows straight to ARRIVED. Approval-required QRs
    // keep the current live-approval shape — scan parks them in
    // AWAITING_APPROVAL until the host decides.
    const visitor = await prisma.visitor.create({
      data: {
        ownerId: qr.ownerId,
        shortId,
        name: String(name).trim(),
        email: email ? String(email).trim() : null,
        mobile: mobile ? String(mobile).trim() : null,
        reasonForVisit: String(reasonForVisit).trim(),
        photoUrl: savedPhotoUrl,
        visitDate: now,
        visitTime,
        qrCodeUrl: personalQrUrl,
        status: 'EXPECTED',
        requiresApproval: qr.requiresApproval,
        isPreApproval: false,
        assignedAdminId: qr.assignedAdminId,
        // Self-registered passes get the same 24h default as Add Visitor —
        // keeps the system from holding stale rows indefinitely.
        expiresAt: defaultExpiry(now),
      },
    });

    // Awareness ping for the owner (always) + the assigned host (if any).
    // The notification copy distinguishes auto vs approval-required so the
    // recipient knows whether they need to act when reception scans.
    const isAuto = !qr.requiresApproval;
    const bodyForOwner = isAuto
      ? `Self-registered via "${qr.label}" — auto check-in on scan, no approval needed · #${visitor.shortId}`
      : `Self-registered via "${qr.label}" — host approval will be needed on scan · #${visitor.shortId}`;
    const bodyForHost = isAuto
      ? `Self-registered via "${qr.label}" — auto check-in on scan, no action needed · #${visitor.shortId}`
      : `Self-registered via "${qr.label}" — you'll be asked to approve when they scan · #${visitor.shortId}`;

    emitToOwner(qr.ownerId, 'visitor.awaiting', { visitor });
    await recordNotification({
      recipientType: 'OWNER',
      recipientId: qr.ownerId,
      type: 'visitor.awaiting',
      title: `New visitor: ${visitor.name}`,
      body: bodyForOwner,
      link: '/visitors',
      push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.awaiting' } },
    });
    if (qr.assignedAdminId) {
      emitToApprover(qr.assignedAdminId, 'visitor.awaiting', { visitor });
      await recordNotification({
        recipientType: 'APPROVER',
        recipientId: qr.assignedAdminId,
        type: 'visitor.awaiting',
        title: `New visitor: ${visitor.name}`,
        body: bodyForHost,
        link: '/visitors',
        push: { data: { visitorId: visitor.id, shortId: visitor.shortId, kind: 'visitor.awaiting' } },
      });
    }

    res.status(201).json({
      shortId: visitor.shortId,
      name: visitor.name,
      qrCodeUrl: visitor.qrCodeUrl,
      hostHint: qr.label,
    });
  } catch (e) {
    console.error('submitPublicWalkInQR', e);
    res.status(500).json({ error: 'Failed to submit registration' });
  }
}
