import type { NextFunction, Request, Response } from 'express';
import pool from '../db';
import { findUserById, type RmUser } from '../models/user';

type AdminUser = Pick<RmUser, 'id' | 'email' | 'is_weatherman' | 'email_verified'>;
type AuditTableAvailability = 'unknown' | 'present' | 'missing';

const AUDIT_TABLE_RETRY_MS = 60_000;

declare global {
  namespace Express {
    interface Request {
      adminUser?: AdminUser;
    }
  }
}

function parseCsvSet(value: string | undefined, normalize?: (entry: string) => string): Set<string> {
  return new Set(
    (value || '')
      .split(',')
      .map((entry) => entry.trim())
      .filter(Boolean)
      .map((entry) => (normalize ? normalize(entry) : entry)),
  );
}

let auditTableAvailability: AuditTableAvailability = 'unknown';
let nextAuditTableCheckAt = 0;
let auditTableMissingWarned = false;

function shouldEnforceAdminAllowlist(): boolean {
  const explicit = (process.env.ENFORCE_ADMIN_ALLOWLIST || '').trim().toLowerCase();
  if (process.env.NODE_ENV === 'production') return true;
  return ['1', 'true', 'yes', 'on'].includes(explicit);
}

function getAdminAllowlists(): { emails: Set<string>; userIds: Set<string> } {
  return {
    emails: parseCsvSet(process.env.ADMIN_EMAILS, (entry) => entry.toLowerCase()),
    userIds: parseCsvSet(process.env.ADMIN_USER_IDS),
  };
}

function getClientIp(req: Request): string | null {
  const forwarded = req.headers['x-forwarded-for'];
  if (forwarded) {
    const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(',')[0].trim();
    return first || null;
  }

  return req.ip || req.socket.remoteAddress || null;
}

function isMissingAuditTableError(err: unknown): boolean {
  return !!err && typeof err === 'object' && 'code' in err && (err as { code?: string }).code === '42P01';
}

async function writeAdminAuditEntry(entry: {
  userId: string | null;
  userEmail: string | null;
  area: string;
  action: string;
  allowed: boolean;
  statusCode: number;
  ipAddress: string | null;
  userAgent: string | null;
  metadata?: Record<string, unknown>;
}): Promise<void> {
  if (auditTableAvailability === 'missing' && Date.now() < nextAuditTableCheckAt) {
    return;
  }

  try {
    await pool.query(
      `INSERT INTO rm_admin_action_audit
         (user_id, user_email, area, action, allowed, status_code, ip_address, user_agent, metadata)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
      [
        entry.userId,
        entry.userEmail,
        entry.area,
        entry.action,
        entry.allowed,
        entry.statusCode,
        entry.ipAddress,
        entry.userAgent,
        JSON.stringify(entry.metadata || {}),
      ],
    );
    auditTableAvailability = 'present';
  } catch (err) {
    if (isMissingAuditTableError(err)) {
      auditTableAvailability = 'missing';
      nextAuditTableCheckAt = Date.now() + AUDIT_TABLE_RETRY_MS;
      if (!auditTableMissingWarned) {
        auditTableMissingWarned = true;
        console.warn('[admin-audit] Audit table missing. Skipping admin audit writes until the migration is applied.');
      }
      return;
    }

    auditTableAvailability = 'unknown';
    console.error('[admin-audit] Failed to write audit entry:', err);
  }
}

function getActionLabel(req: Request): string {
  return `${req.method} ${req.baseUrl || ''}${req.path || ''}`.trim();
}

async function denyAdminRequest(
  req: Request,
  res: Response,
  statusCode: number,
  error: string,
  user: AdminUser | null,
  reason: string,
): Promise<void> {
  await writeAdminAuditEntry({
    userId: user?.id || req.user?.userId || null,
    userEmail: user?.email || req.user?.email || null,
    area: 'admin-access',
    action: getActionLabel(req),
    allowed: false,
    statusCode,
    ipAddress: getClientIp(req),
    userAgent: req.get('user-agent') || null,
    metadata: {
      reason,
      originalUrl: req.originalUrl,
    },
  });

  res.status(statusCode).json({ error });
}

export async function requireAdminAccess(req: Request, res: Response, next: NextFunction): Promise<void> {
  const userId = req.user?.userId;
  if (!userId) {
    await denyAdminRequest(req, res, 401, 'Authentication required', null, 'missing_auth');
    return;
  }

  const user = await findUserById(userId);
  if (!user?.is_weatherman) {
    await denyAdminRequest(req, res, 403, 'Admin access required', user || null, 'missing_role');
    return;
  }

  if (!user.email_verified) {
    await denyAdminRequest(req, res, 403, 'Verified email required for admin access', user, 'email_unverified');
    return;
  }

  const { emails, userIds } = getAdminAllowlists();
  const allowlistConfigured = emails.size > 0 || userIds.size > 0;
  const enforceAllowlist = shouldEnforceAdminAllowlist();

  if (enforceAllowlist && !allowlistConfigured) {
    await denyAdminRequest(req, res, 403, 'Admin allowlist not configured', user, 'allowlist_missing');
    return;
  }

  if (enforceAllowlist || allowlistConfigured) {
    const email = user.email.toLowerCase();
    if (!emails.has(email) && !userIds.has(user.id)) {
      await denyAdminRequest(req, res, 403, 'Admin allowlist required', user, 'allowlist_denied');
      return;
    }
  }

  req.adminUser = {
    id: user.id,
    email: user.email,
    is_weatherman: user.is_weatherman,
    email_verified: user.email_verified,
  };
  next();
}

export function auditAdminAccess(area: string) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const startedAt = Date.now();

    res.on('finish', () => {
      if (!req.adminUser) return;
      void writeAdminAuditEntry({
        userId: req.adminUser.id,
        userEmail: req.adminUser.email,
        area,
        action: getActionLabel(req),
        allowed: res.statusCode < 400,
        statusCode: res.statusCode,
        ipAddress: getClientIp(req),
        userAgent: req.get('user-agent') || null,
        metadata: {
          originalUrl: req.originalUrl,
          durationMs: Date.now() - startedAt,
        },
      });
    });

    next();
  };
}
