/**
 * Audit Middleware
 *
 * Factory function that returns an Express middleware for a given
 * (resourceType, action) pair.  Designed to sit after `authenticate`
 * in the middleware chain.
 *
 * Usage examples:
 *
 *   // Simple: no old_value needed
 *   router.post('/', authenticate, auditMiddleware(AuditResourceType.MEMBER, AuditAction.CREATE), createMember);
 *
 *   // UPDATE: controller must populate res.locals.auditOldValue before responding
 *   router.put('/:id', authenticate, auditMiddleware(AuditResourceType.MEMBER, AuditAction.UPDATE), updateMember);
 *
 *   // DELETE: controller must populate res.locals.auditOldValue (the entity before deletion)
 *   router.delete('/:id', authenticate, auditMiddleware(AuditResourceType.MEMBER, AuditAction.DELETE), deleteMember);
 *
 * Pattern for capturing old_value in controllers:
 *
 *   async function updateMember(req: Request, res: Response): Promise<void> {
 *     const existing = await memberService.findById(req.params.id);
 *     res.locals.auditOldValue = existing;   // <-- set BEFORE modifying
 *
 *     const updated = await memberService.update(req.params.id, req.body);
 *     res.locals.auditResourceId = req.params.id;
 *
 *     res.status(200).json({ success: true, data: updated });
 *     // The middleware intercepts res.json() and logs AFTER the response is sent.
 *   }
 *
 * For PERMISSION_DENIED events, use auditPermissionDenied() directly in the
 * requirePermission middleware — see auth.middleware.ts.
 */

import { Request, Response, NextFunction } from 'express';
import auditService, { AuditAction, AuditResourceType } from '../services/audit.service';
import logger from '../utils/logger';

// ---------------------------------------------------------------------------
// IP extraction helper
// ---------------------------------------------------------------------------

/**
 * Extracts the real client IP address, respecting X-Forwarded-For when set
 * by a trusted reverse proxy (Nginx / Traefik in the Docker stack).
 *
 * Trust only the leftmost IP in X-Forwarded-For to avoid spoofing.
 * If you run without a reverse proxy, req.ip is sufficient.
 */
export function extractIp(req: Request): string | null {
  const forwarded = req.headers['x-forwarded-for'];
  if (forwarded) {
    const ips = (typeof forwarded === 'string' ? forwarded : forwarded[0]).split(',');
    const ip = ips[0].trim();
    return ip || req.ip || null;
  }
  return req.ip || null;
}

/**
 * Extracts User-Agent, truncated to 500 chars to avoid bloating the DB.
 */
export function extractUserAgent(req: Request): string | null {
  const ua = req.headers['user-agent'];
  if (!ua) return null;
  return ua.substring(0, 500);
}

// ---------------------------------------------------------------------------
// res.locals contract (populated by controllers before res.json())
// ---------------------------------------------------------------------------

/**
 * Extend Express locals so TypeScript knows what the audit middleware reads.
 *
 * Controllers set these before calling res.json() / res.status().json():
 *   res.locals.auditOldValue    — entity state before the operation (UPDATE, DELETE)
 *   res.locals.auditNewValue    — override for new value (optional; defaults to response body)
 *   res.locals.auditResourceId  — override for resource ID (optional; defaults to req.params.id)
 *   res.locals.auditMetadata    — any extra context object
 *   res.locals.auditSkip        — set to true to suppress logging for this request
 */
declare global {
  namespace Express {
    interface Locals {
      auditOldValue?:   Record<string, unknown> | null;
      auditNewValue?:   Record<string, unknown> | null;
      auditResourceId?: string;
      auditMetadata?:   Record<string, unknown>;
      auditSkip?:       boolean;
    }
  }
}

// ---------------------------------------------------------------------------
// Middleware factory
// ---------------------------------------------------------------------------

/**
 * auditMiddleware — returns Express middleware that fires an audit log entry
 * after the response has been sent (fire-and-forget; never delays the response).
 *
 * @param resourceType  The type of entity being operated on.
 * @param action        The action being performed.
 */
export function auditMiddleware(
  resourceType: AuditResourceType,
  action: AuditAction
) {
  return function auditHandler(
    req: Request,
    res: Response,
    next: NextFunction
  ): void {
    // Intercept res.json() so we can capture the response body after it is
    // set, without delaying the response to the client.
    const originalJson = res.json.bind(res);

    res.json = function auditInterceptedJson(body: unknown): Response {
      // Call the original json() — this sends the response immediately.
      const result = originalJson(body);

      // Only log on successful responses (2xx).  Failed requests are not
      // meaningful audit events for data mutations (the data did not change).
      if (res.statusCode >= 200 && res.statusCode < 300 && !res.locals.auditSkip) {
        const userId    = req.user?.id    ?? null;
        const userEmail = req.user?.email ?? null;
        const ipAddress = extractIp(req);
        const userAgent = extractUserAgent(req);

        // Resource ID: prefer controller override, then route param, then body id.
        const resourceId =
          res.locals.auditResourceId ??
          (req.params as Record<string, string>).id ??
          (body !== null && typeof body === 'object'
            ? ((body as Record<string, unknown>)?.data as Record<string, unknown>)?.id as string | undefined
            : undefined) ??
          null;

        // old_value: always from res.locals (controller must set it for UPDATE/DELETE).
        const oldValue = res.locals.auditOldValue ?? null;

        // new_value: prefer controller override, else use the response body data.
        // We extract the `data` property if it exists (matches our { success, data } envelope).
        let newValue: Record<string, unknown> | null = res.locals.auditNewValue ?? null;
        if (newValue === null && action !== AuditAction.DELETE) {
          const bodyObj = body as Record<string, unknown> | null;
          newValue =
            (bodyObj?.data as Record<string, unknown> | undefined) ??
            (typeof bodyObj === 'object' && bodyObj !== null ? bodyObj : null);
        }

        // Fire-and-forget — never await in a middleware.
        auditService.logAudit({
          user_id:       userId,
          user_email:    userEmail,
          action,
          resource_type: resourceType,
          resource_id:   resourceId ? String(resourceId) : null,
          old_value:     oldValue,
          new_value:     newValue,
          ip_address:    ipAddress,
          user_agent:    userAgent,
          metadata:      res.locals.auditMetadata ?? {},
        }).catch((err) => {
          // Belt-and-suspenders: logAudit() never rejects, but just in case.
          logger.error('auditMiddleware: unexpected logAudit rejection', { err });
        });
      }

      return result;
    };

    next();
  };
}

// ---------------------------------------------------------------------------
// Standalone helpers for use outside of route middleware chains
// ---------------------------------------------------------------------------

/**
 * auditPermissionDenied
 *
 * Call this inside requirePermission() when access is denied.
 * Does NOT depend on res.json interception — fires directly.
 */
export function auditPermissionDenied(
  req: Request,
  resourceType: AuditResourceType,
  resourceId?: string,
  metadata?: Record<string, unknown>
): void {
  auditService.logAudit({
    user_id:       req.user?.id    ?? null,
    user_email:    req.user?.email ?? null,
    action:        AuditAction.PERMISSION_DENIED,
    resource_type: resourceType,
    resource_id:   resourceId ?? null,
    old_value:     null,
    new_value:     null,
    ip_address:    extractIp(req),
    user_agent:    extractUserAgent(req),
    metadata:      {
      attempted_path:   req.path,
      attempted_method: req.method,
      ...metadata,
    },
  }).catch(() => {/* swallowed — audit must not affect the 403 response */});
}

/**
 * auditExport
 *
 * Call this immediately before streaming an export response.
 */
export function auditExport(
  req: Request,
  resourceType: AuditResourceType,
  metadata: Record<string, unknown> = {}
): void {
  auditService.logAudit({
    user_id:       req.user?.id    ?? null,
    user_email:    req.user?.email ?? null,
    action:        AuditAction.EXPORT,
    resource_type: resourceType,
    resource_id:   null,
    old_value:     null,
    new_value:     null,
    ip_address:    extractIp(req),
    user_agent:    extractUserAgent(req),
    metadata,
  }).catch(() => {});
}
