import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { prisma } from '../config/database';

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

export interface AuthRequest extends Request {
  ownerId?: string;
  // When the requester is an admin (or sub-admin), adminId is set and
  // ownerId points at their parent owner. When the requester is the
  // workspace owner themselves, adminId is undefined.
  adminId?: string;
  // Parent admin id, when this session is a sub-admin (receptionist).
  // Their data view is scoped to this parent's tree. Undefined for
  // top-tier admins and owners.
  parentAdminId?: string;
  adminFlags?: {
    canManageVisitors: boolean;
    canManageApprovers: boolean;
    canManageSettings: boolean;
    canApproveRequests: boolean;
    canScanCheckpoint: boolean;
    canManageSubAdmins: boolean;
    canSeeAllVisitors: boolean;
  };
}

export async function requireAuth(
  req: AuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }

  const token = header.slice(7);
  let payload: {
    ownerId: string;
    adminId?: string;
    parentAdminId?: string;
    adminFlags?: AuthRequest['adminFlags'];
  };
  try {
    payload = jwt.verify(token, JWT_SECRET) as typeof payload;
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
    return;
  }

  // Workspace-suspension check. When the platform console marks an Owner
  // as suspended, every authed request from that workspace is rejected
  // until they're unsuspended. Single indexed lookup; cheap. Skipped if
  // the DB happens to be unreachable so we don't lock everyone out on
  // a transient error.
  try {
    const owner = await prisma.owner.findUnique({
      where: { id: payload.ownerId },
      select: { suspendedAt: true },
    });
    if (owner?.suspendedAt) {
      res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
      return;
    }
  } catch { /* fall through */ }

  req.ownerId = payload.ownerId;
  if (payload.adminId) {
    req.adminId = payload.adminId;
    req.parentAdminId = payload.parentAdminId;
    req.adminFlags = payload.adminFlags;
  }
  next();
}

// Owner soft-suspension check. Runs after requireAuth — short-circuits
// with 403 when the workspace is suspended from the platform console.
// Skipped for owners hitting their own /account endpoints so a
// suspended owner can still see they're blocked but not delete their
// own data unexpectedly.
export async function blockIfSuspended(
  req: AuthRequest,
  res: Response,
  next: NextFunction,
): Promise<void> {
  try {
    if (!req.ownerId) return next();
    const owner = await prisma.owner.findUnique({
      where: { id: req.ownerId },
      select: { suspendedAt: true },
    });
    if (owner?.suspendedAt) {
      res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
      return;
    }
    next();
  } catch {
    next();
  }
}

// Sub-admin gate. Returns true when the session is a receptionist (sub-
// admin) — i.e. an admin whose JWT carries parentAdminId. Top-tier
// admins and owners return false.
export function isSubAdminSession(req: AuthRequest): boolean {
  return !!(req.adminId && req.parentAdminId);
}

// Platform-level admin auth (the vendor managing all customer orgs).
// JWT carries `platformAdminId` instead of ownerId so the customer
// requireAuth path never accidentally accepts a platform token.
export interface PlatformAuthRequest extends Request {
  platformAdminId?: string;
}
export function requirePlatformAuth(
  req: PlatformAuthRequest,
  res: Response,
  next: NextFunction,
): void {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }
  const token = header.slice(7);
  try {
    const payload = jwt.verify(token, JWT_SECRET) as { platformAdminId?: string };
    if (!payload.platformAdminId) {
      res.status(401).json({ error: 'Not a platform admin token' });
      return;
    }
    req.platformAdminId = payload.platformAdminId;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired platform token' });
  }
}

// Dual-mode auth used by the few customer endpoints that the platform
// admin also legitimately reads (visitors list, counts, admins list)
// to give them a cross-org view. Accepts either a customer or a
// platform token; controllers branch on `req.platformAdminId` to drop
// the ownerId filter when so. Customer-only endpoints (add, edit,
// delete) keep using `requireAuth` so a platform token can't mutate
// arbitrary workspace data.
export interface MixedAuthRequest extends AuthRequest, PlatformAuthRequest {}
export async function requireAuthOrPlatform(
  req: MixedAuthRequest,
  res: Response,
  next: NextFunction,
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }
  const token = header.slice(7);
  try {
    const payload = jwt.verify(token, JWT_SECRET) as any;
    if (payload.platformAdminId) {
      req.platformAdminId = payload.platformAdminId;
      next();
      return;
    }
    if (payload.ownerId) {
      // Reuse requireAuth's suspension check by deferring to it.
      // Set the same fields it would.
      try {
        const owner = await prisma.owner.findUnique({
          where: { id: payload.ownerId },
          select: { suspendedAt: true },
        });
        if (owner?.suspendedAt) {
          res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
          return;
        }
      } catch { /* fall through */ }
      req.ownerId = payload.ownerId;
      if (payload.adminId) {
        req.adminId = payload.adminId;
        req.parentAdminId = payload.parentAdminId;
        req.adminFlags = payload.adminFlags;
      }
      next();
      return;
    }
    res.status(401).json({ error: 'Token does not grant access' });
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Tighter gate for endpoints only the workspace owner (super admin) may use,
// e.g. managing other admins. Run AFTER `requireAuth`.
export function requireOwnerOnly(
  req: AuthRequest,
  res: Response,
  next: NextFunction
): void {
  if (req.adminId) {
    res.status(403).json({ error: 'Only the workspace owner can perform this action' });
    return;
  }
  next();
}

// Owner OR an admin with canManageSubAdmins. Used to gate the
// admin-CRUD endpoints so admins-with-flag can manage their own
// receptionists. Per-row authorisation (only edit your own sub-admins)
// is enforced inside the controllers, not here.
export function requireOwnerOrSubAdminManager(
  req: AuthRequest,
  res: Response,
  next: NextFunction,
): void {
  if (!req.adminId) { next(); return; } // owner — always allowed
  if (req.adminFlags?.canManageSubAdmins) { next(); return; }
  res.status(403).json({ error: 'Only the workspace owner or an admin with sub-admin management can do this' });
}

// Feature-flag gate for sub-admin writes. Owners always pass.
export function requireFlag(flag: keyof NonNullable<AuthRequest['adminFlags']>) {
  return (req: AuthRequest, res: Response, next: NextFunction): void => {
    if (!req.adminId) { next(); return; } // owner — full access
    if (req.adminFlags?.[flag]) { next(); return; }
    res.status(403).json({ error: `Your admin role doesn't allow this action` });
  };
}

export interface ApproverAuthRequest extends Request {
  approverId?: string;
  ownerId?: string;
}

export async function requireApproverAuth(
  req: ApproverAuthRequest,
  res: Response,
  next: NextFunction,
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }

  const token = header.slice(7);
  let payload: { approverId: string; ownerId: string };
  try {
    payload = jwt.verify(token, JWT_SECRET) as { approverId: string; ownerId: string };
  } catch {
    res.status(401).json({ error: 'Invalid or expired approver token' });
    return;
  }

  // Workspace-suspension gate. Mirrors requireAuth so a suspended owner's
  // approvers also lose access on the next request — they bounce out of
  // the approver portal instead of seeing stale data they can't act on.
  try {
    const owner = await prisma.owner.findUnique({
      where: { id: payload.ownerId },
      select: { suspendedAt: true },
    });
    if (owner?.suspendedAt) {
      res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
      return;
    }
  } catch { /* fall through — don't lock everyone out on a transient DB error */ }

  req.approverId = payload.approverId;
  req.ownerId = payload.ownerId;
  next();
}

// Permissive auth for the device-token registration endpoint — the single URL
// that every user type calls regardless of role. Accepts owner/admin, approver,
// and checkpoint JWTs without any flag checks (push registration is harmless).
// Do NOT use on endpoints that mutate or read sensitive data.
export async function requireAnyAuth(
  req: AuthRequest & ApproverAuthRequest & VisitorCheckpointAuthRequest,
  res: Response,
  next: NextFunction,
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }
  const token = header.slice(7);
  let payload: any;
  try {
    payload = jwt.verify(token, JWT_SECRET);
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
    return;
  }

  if (payload.visitorCheckpointId) {
    req.visitorCheckpointId = payload.visitorCheckpointId;
    req.ownerId = payload.ownerId;
    next();
    return;
  }
  if (payload.approverId) {
    req.approverId = payload.approverId;
    req.ownerId = payload.ownerId;
    next();
    return;
  }
  if (payload.ownerId) {
    req.ownerId = payload.ownerId;
    if (payload.adminId) req.adminId = payload.adminId;
    next();
    return;
  }
  res.status(401).json({ error: 'Unrecognized token type' });
}

export interface VisitorCheckpointAuthRequest extends Request {
  visitorCheckpointId?: string;
  ownerId?: string;
}

export async function requireVisitorCheckpointAuth(
  req: VisitorCheckpointAuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }

  const token = header.slice(7);
  let payload: { visitorCheckpointId: string; ownerId: string };
  try {
    payload = jwt.verify(token, JWT_SECRET) as {
      visitorCheckpointId: string;
      ownerId: string;
    };
  } catch {
    res.status(401).json({ error: 'Invalid or expired visitor checkpoint token' });
    return;
  }

  // The token can outlive the checkpoint — admins can delete or disable
  // a checkpoint without the operator's device knowing. Verify the row
  // still exists on every request so the scanner gets a clean 401 on the
  // next call (or page refresh), which the frontend interceptor turns
  // into a logout.
  try {
    const cp = await prisma.visitorCheckpoint.findUnique({
      where: { id: payload.visitorCheckpointId },
      select: { id: true, isActive: true },
    });
    if (!cp) {
      res.status(401).json({ error: 'Checkpoint no longer exists', code: 'CHECKPOINT_REVOKED' });
      return;
    }
    if (!cp.isActive) {
      res.status(401).json({ error: 'Checkpoint is disabled', code: 'CHECKPOINT_DISABLED' });
      return;
    }
    // Workspace-suspension gate. Mirrors requireAuth so a suspended owner
    // can't keep their reception scanner running on the side.
    const owner = await prisma.owner.findUnique({
      where: { id: payload.ownerId },
      select: { suspendedAt: true },
    });
    if (owner?.suspendedAt) {
      res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
      return;
    }
  } catch {
    res.status(500).json({ error: 'Failed to validate checkpoint session' });
    return;
  }

  req.visitorCheckpointId = payload.visitorCheckpointId;
  req.ownerId = payload.ownerId;
  next();
}

// Unified scan-access middleware. Lets either a logged-in reception
// VisitorCheckpoint OR a workspace Owner / Admin (with canScanCheckpoint)
// hit the scanner endpoints, so the mobile app can offer the scan tab to
// owners/admins without forcing them to log in as a separate checkpoint
// identity.
//
// Normalizes the auth into `req.scanContext` so the controllers don't have
// to care which kind of token landed:
//   { ownerId, actor: 'CHECKPOINT' | 'OWNER' | 'ADMIN',
//     checkpointId? (CHECKPOINT only), adminId? (ADMIN only) }
export type ScanActor = 'CHECKPOINT' | 'OWNER' | 'ADMIN' | 'APPROVER';
export interface ScanContext {
  ownerId: string;
  actor: ScanActor;
  checkpointId?: string;
  adminId?: string;
  approverId?: string;
}
export interface ScanAuthRequest extends Request {
  scanContext?: ScanContext;
  // Mirror the checkpoint-specific fields so legacy callers still see them.
  visitorCheckpointId?: string;
  ownerId?: string;
  adminId?: string;
  approverId?: string;
  adminFlags?: AuthRequest['adminFlags'];
}

export async function requireScanAccess(
  req: ScanAuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    res.status(401).json({ error: 'Missing or invalid authorization header' });
    return;
  }
  const token = header.slice(7);

  let payload: any;
  try {
    payload = jwt.verify(token, JWT_SECRET);
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
    return;
  }

  // Workspace-suspension gate. Every token shape that reaches this
  // middleware (checkpoint / approver / owner / admin) carries the
  // workspace ownerId on its payload — short-circuit here so any scanner
  // call from a suspended workspace gets a clean 403 regardless of how
  // the operator authed.
  if (payload.ownerId) {
    try {
      const owner = await prisma.owner.findUnique({
        where: { id: payload.ownerId },
        select: { suspendedAt: true },
      });
      if (owner?.suspendedAt) {
        res.status(403).json({ error: 'Workspace suspended. Contact support to restore access.' });
        return;
      }
    } catch { /* fall through — don't lock everyone out on a transient DB error */ }
  }

  // Checkpoint token — has visitorCheckpointId.
  if (payload.visitorCheckpointId) {
    // Same revocation check as requireVisitorCheckpointAuth: if an admin
    // deleted or disabled the checkpoint, this token is dead even though
    // the JWT itself is still cryptographically valid.
    try {
      const cp = await prisma.visitorCheckpoint.findUnique({
        where: { id: payload.visitorCheckpointId },
        select: { id: true, isActive: true },
      });
      if (!cp) {
        res.status(401).json({ error: 'Checkpoint no longer exists', code: 'CHECKPOINT_REVOKED' });
        return;
      }
      if (!cp.isActive) {
        res.status(401).json({ error: 'Checkpoint is disabled', code: 'CHECKPOINT_DISABLED' });
        return;
      }
    } catch {
      res.status(500).json({ error: 'Failed to validate checkpoint session' });
      return;
    }
    req.scanContext = {
      ownerId: payload.ownerId,
      actor: 'CHECKPOINT',
      checkpointId: payload.visitorCheckpointId,
    };
    req.visitorCheckpointId = payload.visitorCheckpointId;
    req.ownerId = payload.ownerId;
    next();
    return;
  }

  // Approver token — has approverId. Gated by approverFlags.canScanCheckpoint.
  if (payload.approverId) {
    if (!payload.approverFlags?.canScanCheckpoint) {
      res.status(403).json({ error: "Your approver role doesn't allow scanning at reception" });
      return;
    }
    req.scanContext = {
      ownerId: payload.ownerId,
      actor: 'APPROVER',
      approverId: payload.approverId,
    };
    req.approverId = payload.approverId;
    req.ownerId = payload.ownerId;
    next();
    return;
  }

  // Owner / Admin token — has ownerId, optionally adminId + adminFlags.
  if (payload.ownerId) {
    if (payload.adminId) {
      if (!payload.adminFlags?.canScanCheckpoint) {
        res.status(403).json({ error: "Your admin role doesn't allow scanning at reception" });
        return;
      }
      req.scanContext = {
        ownerId: payload.ownerId,
        actor: 'ADMIN',
        adminId: payload.adminId,
      };
      req.adminId = payload.adminId;
      req.adminFlags = payload.adminFlags;
    } else {
      req.scanContext = { ownerId: payload.ownerId, actor: 'OWNER' };
    }
    req.ownerId = payload.ownerId;
    next();
    return;
  }

  res.status(401).json({ error: 'Token does not grant scanner access' });
}
