/* eslint-disable no-useless-escape */
import { createHash } from 'crypto';
import type { ErrorFingerprint, ErrorClusterDoc, ErrorEvent } from './types.js';

// ============================================================================
// Error Normalization Service
// ============================================================================

/**
 * Normalizes error messages by replacing variable parts with placeholders
 * - UUIDs → <UUID>
 * - Numbers → <NUM>
 * - Timestamps/dates → <DATE>
 * - User IDs → <USER_ID>
 * - Object IDs (mongo, cosmos) → <ID>
 * - Email addresses → <EMAIL>
 * - IP addresses → <IP>
 * - URLs → <URL>
 */
export function normalizeErrorMessage(message: string): string {
  let normalized = message;

  // UUIDs (v4 and similar)
  normalized = normalized.replace(
    /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
    '<UUID>'
  );

  // MongoDB ObjectIds (24 hex chars)
  normalized = normalized.replace(/\b[0-9a-f]{24}\b/gi, '<ID>');

  // Email addresses
  normalized = normalized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '<EMAIL>');

  // IP addresses (IPv4 and IPv6)
  normalized = normalized.replace(
    /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
    '<IP>'
  );

  // ISO 8601 timestamps
  normalized = normalized.replace(
    /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})?/g,
    '<DATE>'
  );

  // Simple dates (MM/DD/YYYY or DD/MM/YYYY)
  normalized = normalized.replace(/\b\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b/g, '<DATE>');

  // User IDs (various patterns)
  normalized = normalized.replace(/\buser[_-]?\d+\b/gi, '<USER_ID>');
  normalized = normalized.replace(/\buser[_-]?[0-9a-f]{8,}\b/gi, '<USER_ID>');

  // Long numbers (likely IDs or counts)
  normalized = normalized.replace(/\b\d{10,}\b/g, '<NUM>');

  // Medium numbers (4-9 digits)
  normalized = normalized.replace(/\b\d{4,9}\b/g, '<NUM>');

  // URLs (http/https)
  normalized = normalized.replace(/https?:\/\/[^\s<>"{}|\\^`[]+/g, '<URL>');

  // File paths (keep filename, remove path)
  normalized = normalized.replace(/(?:[/\\][\w.-]+)+\/[\w.-]+\.[\w]+/g, match => {
    const parts = match.split(/[/\\]/);
    return `<PATH>/${parts[parts.length - 1]}`;
  });

  return normalized;
}

// ============================================================================
// Stack Trace Normalization
// ============================================================================

interface ParsedStackFrame {
  function: string;
  file: string;
  line: number;
  column?: number;
}

/**
 * Parses stack traces from various formats:
 * - JavaScript/TypeScript
 * - Python
 * - Swift
 * - Java/Kotlin (Android)
 */
export function parseStackTrace(stackTrace: string): ParsedStackFrame[] {
  const frames: ParsedStackFrame[] = [];
  const lines = stackTrace.split('\n');

  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) continue;

    // JavaScript/TypeScript format:
    // "at functionName (file:line:column)"
    // "at file:line:column"
    // "at async functionName (file:line:column)"
    const jsMatch = trimmed.match(/at\s+(?:async\s+)?(?:([^\s(]+)\s+\()?([^:)]+):(\d+):(\d+)?\)?/);
    if (jsMatch) {
      frames.push({
        function: jsMatch[1] || '<anonymous>',
        file: jsMatch[2],
        line: parseInt(jsMatch[3], 10),
        column: jsMatch[4] ? parseInt(jsMatch[4], 10) : undefined,
      });
      continue;
    }

    // Python format:
    // "File \"path\", line N, in functionName"
    const pyMatch = trimmed.match(/File\s+"([^"]+)"[,\s]+line\s+(\d+)[,\s]+in\s+(\w+)/);
    if (pyMatch) {
      frames.push({
        function: pyMatch[3],
        file: pyMatch[1],
        line: parseInt(pyMatch[2], 10),
      });
      continue;
    }

    // Swift format:
    // "Module function file:line column:col"
    const swiftMatch = trimmed.match(/(\S+)\s+(\S+)\s+(\S+):(\d+)(?:\s+column:(\d+))?/);
    if (swiftMatch && !trimmed.startsWith('Stack')) {
      frames.push({
        function: swiftMatch[2],
        file: swiftMatch[3],
        line: parseInt(swiftMatch[4], 10),
        column: swiftMatch[5] ? parseInt(swiftMatch[5], 10) : undefined,
      });
      continue;
    }

    // Java/Kotlin format:
    // "at com.package.Class.method(File.java:123)"
    const javaMatch = trimmed.match(/at\s+([\w.$]+)\(([^)]+)\.(\w+):(\d+)\)/);
    if (javaMatch) {
      frames.push({
        function: javaMatch[1].split('.').pop() || '<unknown>',
        file: `${javaMatch[2]}.${javaMatch[3]}`,
        line: parseInt(javaMatch[4], 10),
      });
      continue;
    }
  }

  return frames;
}

/**
 * Normalizes stack frames by:
 * - Removing line numbers (but keeping file and function)
 * - Normalizing function names (remove async wrappers)
 * - Truncating to top N frames
 */
export function normalizeStackFrames(frames: ParsedStackFrame[], maxFrames: number = 10): string {
  const normalized = frames.slice(0, maxFrames).map(frame => {
    // Remove line/column numbers, keep just file and function
    const normalizedFile = frame.file
      .replace(/:\d+$/, '') // Remove trailing line numbers
      .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
      .replace(/\b[0-9a-f]{24}\b/gi, '<ID>');

    // Normalize function name
    const normalizedFunction = frame.function
      .replace(/^(async\s+|Generator\.|bound\s+)/, '')
      .replace(/\s*\[.*\]$/, '') // Remove [native code] etc
      .replace(/_\w{8,}/, '_<ID>'); // Minified function suffixes

    return `${normalizedFunction}@${normalizedFile}`;
  });

  return normalized.join('|');
}

// ============================================================================
// Fingerprint Generation
// ============================================================================

export interface FingerprintInput {
  errorType: string;
  message: string;
  stackTrace?: string;
}

export interface FingerprintResult {
  hash: string;
  normalizedType: string;
  normalizedMessage: string;
  stackSignature: string;
  sourceLocation?: {
    file: string;
    function: string;
    line: number;
  };
}

/**
 * Generates a SHA-256 fingerprint from normalized error data
 */
export function generateFingerprint(input: FingerprintInput): FingerprintResult {
  const normalizedType = input.errorType
    .replace(/Error$/, '') // Remove Error suffix
    .replace(/Exception$/, '') // Remove Exception suffix
    .trim();

  const normalizedMessage = normalizeErrorMessage(input.message);

  // Parse and normalize stack
  let stackSignature = '';
  let sourceLocation: FingerprintResult['sourceLocation'] | undefined;

  if (input.stackTrace) {
    const frames = parseStackTrace(input.stackTrace);
    if (frames.length > 0) {
      stackSignature = normalizeStackFrames(frames);

      // Extract source location from first meaningful frame
      const firstFrame = frames[0];
      if (firstFrame.file && !firstFrame.file.includes('node_modules')) {
        sourceLocation = {
          file: firstFrame.file,
          function: firstFrame.function,
          line: firstFrame.line,
        };
      }
    }
  }

  // Generate hash from normalized components
  const hashInput = [normalizedType, normalizedMessage, stackSignature].join('::');

  const hash = createHash('sha256').update(hashInput).digest('hex');

  return {
    hash,
    normalizedType,
    normalizedMessage,
    stackSignature,
    sourceLocation,
  };
}

// ============================================================================
// Similarity Scoring
// ============================================================================

/**
 * Calculates Levenshtein distance between two strings
 */
export function levenshteinDistance(a: string, b: string): number {
  const matrix: number[][] = [];

  for (let i = 0; i <= b.length; i++) {
    matrix[i] = [i];
  }

  for (let j = 0; j <= a.length; j++) {
    matrix[0][j] = j;
  }

  for (let i = 1; i <= b.length; i++) {
    for (let j = 1; j <= a.length; j++) {
      if (b.charAt(i - 1) === a.charAt(j - 1)) {
        matrix[i][j] = matrix[i - 1][j - 1];
      } else {
        matrix[i][j] = Math.min(
          matrix[i - 1][j - 1] + 1, // substitution
          matrix[i][j - 1] + 1, // insertion
          matrix[i - 1][j] + 1 // deletion
        );
      }
    }
  }

  return matrix[b.length][a.length];
}

/**
 * Calculates similarity score (0-1) between two error fingerprints
 */
export function calculateFingerprintSimilarity(a: FingerprintResult, b: FingerprintResult): number {
  let score = 0;
  let weight = 0;

  // Type match (high weight)
  if (a.normalizedType === b.normalizedType) {
    score += 0.4;
  }
  weight += 0.4;

  // Message similarity (medium weight)
  const messageDistance = levenshteinDistance(a.normalizedMessage, b.normalizedMessage);
  const maxLen = Math.max(a.normalizedMessage.length, b.normalizedMessage.length);
  const messageSimilarity = maxLen > 0 ? 1 - messageDistance / maxLen : 1;
  score += 0.3 * messageSimilarity;
  weight += 0.3;

  // Stack signature similarity (medium weight)
  if (a.stackSignature && b.stackSignature) {
    const stackDistance = levenshteinDistance(a.stackSignature, b.stackSignature);
    const maxStackLen = Math.max(a.stackSignature.length, b.stackSignature.length);
    const stackSimilarity = maxStackLen > 0 ? 1 - stackDistance / maxStackLen : 1;
    score += 0.3 * stackSimilarity;
    weight += 0.3;
  }

  return weight > 0 ? score / weight : 0;
}

// ============================================================================
// Error Event Processing
// ============================================================================

export interface ProcessedError {
  fingerprint: FingerprintResult;
  clusterId: string;
  isNewCluster: boolean;
  context: {
    platform?: string;
    osVersion?: string;
    appVersion?: string;
    deviceModel?: string;
    screen?: string;
  };
}

/**
 * Processes an error event into a fingerprint and cluster ID
 */
export function processErrorEvent(
  errorEvent: ErrorEvent,
  existingFingerprints: Map<string, ErrorFingerprint> = new Map()
): ProcessedError {
  const fingerprint = generateFingerprint({
    errorType: errorEvent.errorType,
    message: errorEvent.message,
    stackTrace: errorEvent.stackTrace,
  });

  // Check for near-matches in existing fingerprints
  let bestMatch: ErrorFingerprint | null = null;
  let bestSimilarity = 0;

  for (const existing of existingFingerprints.values()) {
    const existingFingerprint: FingerprintResult = {
      hash: existing.fingerprintHash,
      normalizedType: existing.errorType,
      normalizedMessage: existing.messageTemplate,
      stackSignature: existing.stackSignature,
    };

    const similarity = calculateFingerprintSimilarity(fingerprint, existingFingerprint);
    if (similarity > bestSimilarity && similarity >= 0.85) {
      bestSimilarity = similarity;
      bestMatch = existing;
    }
  }

  const isNewCluster = bestMatch === null;
  const clusterId = bestMatch?.id || `ec_${generateClusterId()}`;

  return {
    fingerprint,
    clusterId,
    isNewCluster,
    context: {
      platform: errorEvent.platform,
      osVersion: errorEvent.osVersion,
      appVersion: errorEvent.appVersion,
      deviceModel: errorEvent.deviceModel,
      screen: errorEvent.screen,
    },
  };
}

function generateClusterId(): string {
  return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 11)}`;
}

// ============================================================================
// Cluster Update Logic
// ============================================================================

/**
 * Updates a cluster document with new error occurrence
 */
export function updateClusterWithError(
  cluster: ErrorClusterDoc,
  errorEvent: ErrorEvent,
  _fingerprint: FingerprintResult
): ErrorClusterDoc {
  const now = new Date().toISOString();
  const isNewUser = !cluster.uniqueUsers || errorEvent.userId !== undefined; // Simplified - real impl would track user set

  return {
    ...cluster,
    lastSeenAt: now,
    occurrenceCount: cluster.occurrenceCount + 1,
    uniqueUsers: isNewUser ? cluster.uniqueUsers + 1 : cluster.uniqueUsers,
    status: cluster.status === 'resolved' ? 'active' : cluster.status,
    updatedAt: now,
    // Update common context aggregations
    commonContext: updateCommonContext(
      cluster.commonContext || {
        osVersions: [],
        appVersions: [],
        deviceModels: [],
        screenContexts: [],
      },
      errorEvent
    ),
  };
}

function updateCommonContext(
  context: NonNullable<ErrorClusterDoc['commonContext']>,
  errorEvent: ErrorEvent
): NonNullable<ErrorClusterDoc['commonContext']> {
  return {
    osVersions: incrementCount(context.osVersions, errorEvent.osVersion || 'unknown', 'version'),
    appVersions: incrementCount(context.appVersions, errorEvent.appVersion || 'unknown', 'version'),
    deviceModels: incrementCount(
      context.deviceModels,
      errorEvent.deviceModel || 'unknown',
      'model'
    ),
    screenContexts: incrementCount(
      context.screenContexts,
      errorEvent.screen || 'unknown',
      'screen'
    ),
  };
}

function incrementCount<T extends { count: number }>(
  items: Array<T>,
  key: string,
  keyField: keyof T & string
): Array<T> {
  const existing = items.find(
    item => (item as unknown as Record<string, string>)[keyField] === key
  );
  if (existing) {
    return items.map(item =>
      (item as unknown as Record<string, string>)[keyField] === key
        ? ({ ...item, count: item.count + 1 } as T)
        : item
    );
  }
  return [...items, { [keyField]: key, count: 1 } as unknown as T];
}
