/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Diagnostics REST endpoints.
 *
 * POST   /api/diagnostics/sessions              — create session (admin)
 * GET    /api/diagnostics/sessions            — list sessions (admin)
 * GET    /api/diagnostics/sessions/:id        — get session details
 * PATCH  /api/diagnostics/sessions/:id        — update session (admin)
 * DELETE /api/diagnostics/sessions/:id        — cancel session (admin)
 * GET    /api/diagnostics/config               — client polling endpoint
 * POST   /api/diagnostics/sessions/:id/traces — ingest traces
 * POST   /api/diagnostics/sessions/:id/logs    — ingest logs
 * POST   /api/diagnostics/sessions/:id/screenshots — get SAS URL for upload
 * GET    /api/diagnostics/sessions/:id/traces — query traces (admin)
 * GET    /api/diagnostics/sessions/:id/logs   — query logs (admin)
 * GET    /api/diagnostics/sessions/:id/screenshots — list screenshots (admin)
 *
 * @module diagnostics
 */

import type { FastifyInstance, FastifyRequest } from 'fastify';
import { randomUUID } from 'node:crypto';
import { generateId, buildPk } from './types.js';
import { getRequestProductId } from '../../lib/request-context.js';
import { requireRole } from '../../lib/auth.js';
import { BadRequestError, NotFoundError, UnauthorizedError } from '../../lib/errors.js';
import * as repo from './repository.js';
import {
  CreateDebugSessionSchema,
  UpdateDebugSessionSchema,
  ListDebugSessionsQuerySchema,
  IngestTracesSchema,
  IngestLogsSchema,
  CreateScreenshotMetadataSchema,
  QueryTracesSchema,
  QueryLogsSchema,
  type DebugSessionDoc,
  type DebugTraceDoc,
  type DebugLogEntryDoc,
  type DebugScreenshotDoc,
  type CreateDebugSessionInput,
  type UpdateDebugSessionInput,
  type ListDebugSessionsQuery,
  type IngestTracesInput,
  type IngestLogsInput,
  type CreateScreenshotMetadataInput,
  type QueryTracesInput,
  type QueryLogsInput,
} from './types.js';

import { bus } from '../../lib/event-bus.js';
import { generateSasUrl } from '../../lib/blob.js';
import * as auditRepo from '../audit/repository.js';
import type { AuditDoc } from '../audit/types.js';

// Re-export shared helpers from types
export { generateId, buildPk } from './types.js';

// ─── Helpers ───────────────────────────────────────────────────────────────

// PII Redaction patterns — same as telemetry module
const PII_PATTERNS = [
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i, // email
  /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, // US phone
  /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, // credit card
  /\b\d{3}-\d{2}-\d{4}\b/, // SSN
  /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, // IP address
  /(password|token|secret|key)\s*[:=]\s*\S+/i, // credentials
  /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/, // JWT
];

interface RedactionResult {
  redacted: string;
  patterns: string[];
  fieldsRedacted: string[];
}

function redactPii(message: string, context?: Record<string, unknown>): RedactionResult {
  const patterns: string[] = [];
  const fieldsRedacted: string[] = [];
  let redacted = message;

  // Check message
  for (const pattern of PII_PATTERNS) {
    if (pattern.test(redacted)) {
      const patternName = pattern.toString().slice(0, 20);
      patterns.push(patternName);
      redacted = redacted.replace(pattern, '[REDACTED]');
    }
  }

  // Check context fields if provided
  const redactedContext: Record<string, unknown> = {};
  if (context) {
    for (const [key, value] of Object.entries(context)) {
      if (typeof value === 'string') {
        let fieldRedacted = value;
        for (const pattern of PII_PATTERNS) {
          if (pattern.test(fieldRedacted)) {
            if (!fieldsRedacted.includes(key)) {
              fieldsRedacted.push(key);
            }
            fieldRedacted = fieldRedacted.replace(pattern, '[REDACTED]');
          }
        }
        redactedContext[key] = fieldRedacted;
      } else {
        redactedContext[key] = value;
      }
    }
  }

  return { redacted, patterns, fieldsRedacted };
}

/**
 * Require at least authentication (JWT present).
 * Used for client endpoints that need to identify the caller but don't require admin.
 */
function requireAuth(req: FastifyRequest): void {
  if (!req.jwtPayload?.sub) {
    throw new UnauthorizedError('Authentication required');
  }
}

// ─── Routes ─────────────────────────────────────────────────────────────────

export async function diagnosticsRoutes(app: FastifyInstance) {
  // Session management routes (admin only)
  app.post('/diagnostics/sessions', async (req, reply) => {
    await requireRole(req, 'admin');

    const productId = getRequestProductId(req);
    const body = req.body as CreateDebugSessionInput;
    const input = CreateDebugSessionSchema.parse(body);

    // Validate at least one target is specified
    if (!input.targetUserId && !input.targetAnonymousId && !input.targetDeviceId) {
      throw new BadRequestError(
        'At least one target (userId, anonymousId, or deviceId) is required'
      );
    }

    const now = new Date().toISOString();
    const expiresAt = new Date(Date.now() + input.maxDurationMinutes * 60000).toISOString();

    const session: DebugSessionDoc = {
      id: generateId('ds'),
      productId,
      targetUserId: input.targetUserId,
      targetAnonymousId: input.targetAnonymousId,
      targetDeviceId: input.targetDeviceId,
      targetSessionId: input.targetSessionId,
      status: 'pending',
      collectionLevel: input.collectionLevel,
      captureLogs: input.captureLogs,
      captureNetwork: input.captureNetwork,
      captureScreenshots: input.captureScreenshots,
      screenshotOnError: input.screenshotOnError,
      maxDurationMinutes: input.maxDurationMinutes,
      createdAt: now,
      updatedAt: now,
      expiresAt,
      logCount: 0,
      traceCount: 0,
      screenshotCount: 0,
      createdBy: req.jwtPayload?.sub ?? 'system',
    };

    await repo.createSession(session);

    // Emit event bus event
    bus.emit('diagnostics.session.created', {
      sessionId: session.id,
      productId,
      targetUserId: input.targetUserId,
      targetAnonymousId: input.targetAnonymousId,
      targetDeviceId: input.targetDeviceId,
      createdBy: session.createdBy,
    });

    // Audit log
    const auditDoc: AuditDoc = {
      id: `aud_${randomUUID()}`,
      productId,
      userId: session.createdBy,
      action: 'diagnostics.session.created',
      category: 'diagnostics',
      details: {
        sessionId: session.id,
        targetUserId: input.targetUserId,
        targetAnonymousId: input.targetAnonymousId,
        targetDeviceId: input.targetDeviceId,
      },
      createdAt: now,
    };
    auditRepo.create(auditDoc).catch(() => {});

    reply.status(201);
    return session;
  });

  app.get('/diagnostics/sessions', async req => {
    await requireRole(req, 'admin');

    const productId = getRequestProductId(req);
    const query = ListDebugSessionsQuerySchema.parse(req.query);

    const result = await repo.listSessions({ ...query, productId });
    return result;
  });

  app.get('/diagnostics/sessions/:id', async req => {
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    // Allow admin or the target user to view
    const isAdmin = req.jwtPayload?.role === 'admin';
    const isOwner = session.targetUserId === req.jwtPayload?.sub;

    if (!isAdmin && !isOwner) {
      throw new BadRequestError('Access denied');
    }

    return session;
  });

  app.patch('/diagnostics/sessions/:id', async req => {
    await requireRole(req, 'admin');

    const { id } = req.params as { id: string };
    const body = req.body as UpdateDebugSessionInput;
    const input = UpdateDebugSessionSchema.parse(body);

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    // Validate status transitions
    if (input.status) {
      const validTransitions: Record<string, string[]> = {
        pending: ['active', 'cancelled'],
        active: ['paused', 'completed', 'cancelled'],
        paused: ['active', 'cancelled'],
        completed: [],
        cancelled: [],
      };

      const allowed = validTransitions[session.status];
      if (!allowed.includes(input.status)) {
        throw new BadRequestError(
          `Invalid status transition: ${session.status} → ${input.status}. Allowed: ${allowed.join(', ')}`
        );
      }
    }

    const updates: Partial<DebugSessionDoc> = {
      ...input,
      updatedBy: req.jwtPayload?.sub ?? 'system',
    };

    // Set timestamps based on status changes
    if (input.status === 'active' && session.status !== 'active') {
      updates.startedAt = new Date().toISOString();
    }
    if (
      (input.status === 'completed' || input.status === 'cancelled') &&
      session.status !== 'completed' &&
      session.status !== 'cancelled'
    ) {
      updates.endedAt = new Date().toISOString();
    }

    const updated = await repo.updateSession(id, updates);

    // Emit event bus event
    bus.emit('diagnostics.session.updated', {
      sessionId: id,
      productId: session.productId,
      changes: input,
      updatedBy: req.jwtPayload?.sub ?? 'system',
    });

    // Audit log
    const auditDoc: AuditDoc = {
      id: `aud_${randomUUID()}`,
      productId: session.productId,
      userId: req.jwtPayload?.sub ?? 'system',
      action: 'diagnostics.session.updated',
      category: 'diagnostics',
      details: {
        sessionId: id,
        changes: input,
      },
      createdAt: new Date().toISOString(),
    };
    auditRepo.create(auditDoc).catch(() => {});

    return updated;
  });

  app.delete('/diagnostics/sessions/:id', async req => {
    await requireRole(req, 'admin');

    const { id } = req.params as { id: string };

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    // Soft delete (mark as cancelled)
    await repo.deleteSession(id);

    // Emit event bus event
    bus.emit('diagnostics.session.cancelled', {
      sessionId: id,
      productId: session.productId,
      reason: 'admin_cancelled',
      cancelledBy: req.jwtPayload?.sub ?? 'system',
    });

    // Audit log
    const auditDoc: AuditDoc = {
      id: `aud_${randomUUID()}`,
      productId: session.productId,
      userId: req.jwtPayload?.sub ?? 'system',
      action: 'diagnostics.session.cancelled',
      category: 'diagnostics',
      details: {
        sessionId: id,
        reason: 'admin_cancelled',
      },
      createdAt: new Date().toISOString(),
    };
    auditRepo.create(auditDoc).catch(() => {});

    return { success: true, message: 'Session cancelled' };
  });

  // Client polling endpoint (any authenticated user)
  app.get('/diagnostics/config', async (req, reply) => {
    // BUG-3 FIX: Add authentication check
    requireAuth(req);
    const productId = getRequestProductId(req);
    const userId = req.jwtPayload?.sub;
    const deviceId = req.headers['x-device-id'] as string | undefined;

    // Look for active session targeting this user/device
    const session = await repo.getActiveSessionForTarget(productId, {
      userId,
      anonymousId: deviceId,
      deviceId,
    });

    if (!session) {
      // Return empty config with 304 ETag support
      reply.header('ETag', '"empty"');
      return { enabled: false };
    }

    // Build config response
    const config = {
      enabled: true,
      sessionId: session.id,
      collectionLevel: session.collectionLevel,
      captureLogs: session.captureLogs,
      captureNetwork: session.captureNetwork,
      captureScreenshots: session.captureScreenshots,
      screenshotOnError: session.screenshotOnError,
      expiresAt: session.expiresAt,
    };

    // ETag for caching
    const etag = `"${session.id}:${session.updatedAt}"`;
    reply.header('ETag', etag);

    // Check If-None-Match for 304
    const ifNoneMatch = req.headers['if-none-match'];
    if (ifNoneMatch === etag) {
      reply.status(304);
      return null;
    }

    return config;
  });

  // Ingest endpoints (any authenticated user, but validates session ownership)
  app.post('/diagnostics/sessions/:id/traces', async req => {
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const userId = req.jwtPayload?.sub;
    const deviceId = req.headers['x-device-id'] as string | undefined;
    const body = req.body as IngestTracesInput;
    const input = IngestTracesSchema.parse(body);

    // Validate session matches
    if (input.sessionId !== id) {
      throw new BadRequestError('Session ID mismatch');
    }

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    // BUG-8 FIX: Validate session ownership - requester must be the target user/device
    const isTargetUser = session.targetUserId && session.targetUserId === userId;
    const isTargetDevice = session.targetDeviceId && session.targetDeviceId === deviceId;
    const isTargetAnonymous = session.targetAnonymousId && session.targetAnonymousId === deviceId;

    if (!isTargetUser && !isTargetDevice && !isTargetAnonymous) {
      throw new UnauthorizedError('Not authorized to ingest to this session');
    }

    if (session.status !== 'active') {
      throw new BadRequestError(`Session is not active (status: ${session.status})`);
    }

    // Prepare traces with IDs
    const traces: DebugTraceDoc[] = input.traces.map(t => ({
      ...t,
      id: generateId('tr'),
      pk: buildPk(productId, id),
      sessionId: id,
      productId,
    }));

    await repo.ingestTraces(productId, id, traces);

    // Update stats
    await repo.updateSessionStats(id, { traceCount: traces.length });

    return { accepted: traces.length };
  });

  app.post('/diagnostics/sessions/:id/logs', async req => {
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const userId = req.jwtPayload?.sub;
    const deviceId = req.headers['x-device-id'] as string | undefined;
    const body = req.body as IngestLogsInput;
    const input = IngestLogsSchema.parse(body);

    // Validate session matches
    if (input.sessionId !== id) {
      throw new BadRequestError('Session ID mismatch');
    }

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    // BUG-8 FIX: Validate session ownership - requester must be the target user/device
    const isTargetUser = session.targetUserId && session.targetUserId === userId;
    const isTargetDevice = session.targetDeviceId && session.targetDeviceId === deviceId;
    const isTargetAnonymous = session.targetAnonymousId && session.targetAnonymousId === deviceId;

    if (!isTargetUser && !isTargetDevice && !isTargetAnonymous) {
      throw new UnauthorizedError('Not authorized to ingest to this session');
    }

    if (session.status !== 'active') {
      throw new BadRequestError(`Session is not active (status: ${session.status})`);
    }

    // PII Redaction — apply to each log message and context
    const processedLogs = input.logs.map(l => {
      const { redacted, patterns, fieldsRedacted } = redactPii(l.message, l.context);
      return {
        ...l,
        message: redacted,
        context: l.context, // context is already processed in redactPii
        redaction:
          patterns.length > 0
            ? {
                fieldsRedacted,
                patternsMatched: patterns,
              }
            : undefined,
      };
    });

    // Prepare logs with IDs
    const logs: DebugLogEntryDoc[] = processedLogs.map(l => ({
      ...l,
      id: generateId('log'),
      pk: buildPk(productId, id),
      sessionId: id,
      productId,
    }));

    await repo.ingestLogs(productId, id, logs);

    // Update stats
    await repo.updateSessionStats(id, { logCount: logs.length });

    // Check for fatal logs to trigger alerts
    const fatalLog = logs.find(l => l.level === 'fatal');
    if (fatalLog) {
      // Emit fatal log event for alerting
      bus.emit('diagnostics.ingest.fatal', {
        sessionId: id,
        productId,
        logEntry: fatalLog as unknown as Record<string, unknown>,
        timestamp: new Date().toISOString(),
      });
    }

    return { accepted: logs.length };
  });

  // Screenshot upload - get SAS URL
  app.post('/diagnostics/sessions/:id/screenshots', async (req, reply) => {
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const body = req.body as CreateScreenshotMetadataInput;
    const input = CreateScreenshotMetadataSchema.parse(body);

    // Validate session matches
    if (input.sessionId !== id) {
      throw new BadRequestError('Session ID mismatch');
    }

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    if (session.status !== 'active') {
      throw new BadRequestError(`Session is not active (status: ${session.status})`);
    }

    const screenshotId = generateId('scr');
    const blobPath = `screenshots/${productId}/${id}/${screenshotId}.png`;
    const containerName = 'diagnostics-screenshots';

    // Generate SAS URL for blob upload
    let uploadUrl: string | undefined;
    try {
      uploadUrl = await generateSasUrl(containerName, blobPath, 'w', 5);
    } catch (err) {
      req.log.warn({ err }, 'Failed to generate SAS URL for screenshot upload');
    }

    const metadata: DebugScreenshotDoc = {
      ...input,
      id: screenshotId,
      sessionId: id,
      productId,
      blobUrl: uploadUrl ?? '',
      blobPath,
      containerName,
    };

    await repo.createScreenshotMetadata(metadata);

    // Update stats
    await repo.updateSessionStats(id, { screenshotCount: 1 });

    // Emit screenshot captured event
    bus.emit('diagnostics.screenshot.captured', {
      sessionId: id,
      productId,
      screenshotId,
      trigger: input.trigger,
    });

    reply.status(201);
    return {
      screenshotId,
      uploadUrl: uploadUrl ?? '',
      blobPath,
      expiresIn: 300, // 5 minutes
    };
  });

  // Admin query endpoints
  app.get('/diagnostics/sessions/:id/traces', async req => {
    await requireRole(req, 'admin');

    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const query = QueryTracesSchema.parse(req.query);

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    const result = await repo.getTraces(productId, id, query);
    return result;
  });

  app.get('/diagnostics/sessions/:id/logs', async req => {
    await requireRole(req, 'admin');

    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const query = QueryLogsSchema.parse(req.query);

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    const result = await repo.getLogs(productId, id, query);
    return result;
  });

  app.get('/diagnostics/sessions/:id/screenshots', async req => {
    await requireRole(req, 'admin');

    const { id } = req.params as { id: string };

    const session = await repo.getSession(id);
    if (!session) {
      throw new NotFoundError('Debug session not found');
    }

    const screenshots = await repo.getScreenshots(id);
    return { screenshots };
  });
}
