/**
 * Pattern Detection Engine
 *
 * Detects recurring themes in rejections/criticisms:
 * - Theme clustering from free text and annotations
 * - Rejection reason frequency analysis
 * - Candidate rule extraction from strong patterns
 * - Anomaly detection (sudden changes in feedback patterns)
 */

import type {
  EnrichedFeedbackEvent,
  ThemeCluster,
  CandidateRule,
  Anomaly,
  QualityDimension,
} from "../types.js";

// ═══════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════

export interface PatternResults {
  themes: ThemeCluster[];
  candidateRules: CandidateRule[];
  anomalies: Anomaly[];
}

/**
 * Detect patterns from accumulated feedback events.
 */
export async function detectPatterns(
  events: EnrichedFeedbackEvent[],
  minDataPoints: number = 5
): Promise<PatternResults> {
  const themes = extractThemeClusters(events);
  const candidateRules = extractCandidateRules(events, themes, minDataPoints);
  const anomalies = detectAnomalies(events);

  return { themes, candidateRules, anomalies };
}

// ═══════════════════════════════════════════════════════════════
// THEME CLUSTERING
// ═══════════════════════════════════════════════════════════════

function extractThemeClusters(
  events: EnrichedFeedbackEvent[]
): ThemeCluster[] {
  const themeCounts = new Map<
    string,
    {
      count: number;
      severities: number[];
      lastSeen: string;
      dimensions: Set<QualityDimension>;
      examples: string[];
    }
  >();

  for (const event of events) {
    const themes = event._themes ?? [];
    const severity = getSeverityScore(event);

    for (const theme of themes) {
      if (!themeCounts.has(theme)) {
        themeCounts.set(theme, {
          count: 0,
          severities: [],
          lastSeen: event.timestamp,
          dimensions: new Set(),
          examples: [],
        });
      }

      const entry = themeCounts.get(theme)!;
      entry.count++;
      entry.severities.push(severity);
      if (event.timestamp > entry.lastSeen) {
        entry.lastSeen = event.timestamp;
      }

      // Associate themes with dimensions from scores
      for (const score of event.feedback.scores ?? []) {
        entry.dimensions.add(score.dimension);
      }

      // Collect example feedback text
      const exampleText = getExampleText(event);
      if (exampleText && entry.examples.length < 5) {
        entry.examples.push(exampleText);
      }
    }
  }

  // Convert to ThemeCluster array, sorted by occurrence
  return [...themeCounts.entries()]
    .map(([theme, data]) => ({
      theme,
      occurrences: data.count,
      avgSeverity:
        data.severities.length > 0
          ? data.severities.reduce((a, b) => a + b, 0) / data.severities.length
          : 0,
      lastSeen: data.lastSeen,
      relatedDimensions: [...data.dimensions],
      exampleFeedback: data.examples,
    }))
    .sort((a, b) => b.occurrences - a.occurrences);
}

// ═══════════════════════════════════════════════════════════════
// CANDIDATE RULE EXTRACTION
// ═══════════════════════════════════════════════════════════════

/** Theme-to-rule mapping — how themes become actionable rules */
const THEME_RULE_MAP: Record<string, { rule: string; prompt: string; domain: QualityDimension }> = {
  "error-handling": {
    rule: "Always use typed error classes with context, never throw raw strings",
    prompt: "ALWAYS use typed error classes (e.g., `class ApiError extends Error`) with descriptive messages and context. NEVER throw raw strings or use generic catch-all handlers.",
    domain: "error_handling",
  },
  "input-validation": {
    rule: "Validate all inputs at function boundaries using Zod schemas",
    prompt: "ALWAYS validate external inputs at function boundaries using Zod schemas. Every public function that accepts external data must validate it before processing.",
    domain: "code_quality",
  },
  "naming-clarity": {
    rule: "Use descriptive function and variable names, no single-letter variables outside loops",
    prompt: "Use descriptive verb-noun function names (e.g., `fetchUser`, `validateInput`). No single-letter variables outside of loop iterators. Prefer clarity over brevity.",
    domain: "naming",
  },
  "test-coverage": {
    rule: "Test edge cases: empty arrays, null values, Unicode, rate limits, timeouts",
    prompt: "ALWAYS test edge cases including: empty inputs, null/undefined, Unicode strings, rate limit responses, network timeouts, and boundary values.",
    domain: "test_coverage",
  },
  "documentation": {
    rule: "Every public function needs JSDoc with @param, @returns, @throws",
    prompt: "Add JSDoc comments to all exported functions with @param, @returns, and @throws tags. Include brief description of purpose and any side effects.",
    domain: "documentation",
  },
  "code-structure": {
    rule: "Maintain clear separation of concerns — small focused functions",
    prompt: "Keep functions small and focused on a single responsibility. Extract complex logic into well-named helper functions. Separate I/O from business logic.",
    domain: "architecture",
  },
  "performance": {
    rule: "Consider caching for expensive operations and avoid unnecessary re-computations",
    prompt: "Cache results of expensive API calls and computations. Use Map/Set for O(1) lookups instead of array .find()/.includes() in loops.",
    domain: "performance",
  },
  "security": {
    rule: "Validate auth tokens, sanitize inputs, never expose secrets in logs",
    prompt: "ALWAYS validate authentication tokens before processing requests. Sanitize all user inputs. NEVER log secrets, tokens, or PII.",
    domain: "security",
  },
  "type-safety": {
    rule: "No TypeScript `any` — use `unknown` with type guards",
    prompt: "NEVER use TypeScript `any`. Use `unknown` with type guards, or define proper interfaces. Enable strict mode in tsconfig.",
    domain: "code_quality",
  },
  "rate-limiting": {
    rule: "Include retry logic with exponential backoff on all external API calls",
    prompt: "ALWAYS include retry logic with exponential backoff (base 1s, max 30s, 3 attempts) on all external API calls. Handle 429 responses gracefully.",
    domain: "error_handling",
  },
  "builder-pattern": {
    rule: "Use builder pattern for complex configurations",
    prompt: "Use the builder pattern for objects with many optional configuration fields. Provides a fluent, readable API.",
    domain: "architecture",
  },
  "clean-code": {
    rule: "Prefer functional composition over class hierarchies",
    prompt: "Prefer functional composition and plain objects over deep class hierarchies. Use pure functions where possible. Favor explicit over implicit behavior.",
    domain: "code_quality",
  },
};

function extractCandidateRules(
  events: EnrichedFeedbackEvent[],
  themes: ThemeCluster[],
  minDataPoints: number
): CandidateRule[] {
  const rules: CandidateRule[] = [];

  for (const theme of themes) {
    // Only create rules from themes with enough occurrences
    if (theme.occurrences < minDataPoints) continue;

    const ruleMapping = THEME_RULE_MAP[theme.theme];
    if (!ruleMapping) continue;

    // Calculate confidence based on occurrence frequency and severity
    const confidence = Math.min(
      1.0,
      (theme.occurrences / (minDataPoints * 3)) * (0.5 + theme.avgSeverity * 0.5)
    );

    // Determine which server types this applies to
    const serverTypes = new Set<string>();
    for (const event of events) {
      if (event._themes?.includes(theme.theme) && event.mcpServerType) {
        serverTypes.add(event.mcpServerType);
      }
    }

    rules.push({
      rule: ruleMapping.rule,
      promptInstruction: ruleMapping.prompt,
      confidence: Math.round(confidence * 100) / 100,
      occurrences: theme.occurrences,
      source: "feedback_pattern",
      domain: ruleMapping.domain,
      appliesTo: serverTypes.size > 0 ? [...serverTypes] : ["all"],
    });
  }

  // Also extract rules from annotation patterns
  const annotationRules = extractAnnotationRules(events, minDataPoints);
  rules.push(...annotationRules);

  return rules.sort((a, b) => b.confidence - a.confidence);
}

function extractAnnotationRules(
  events: EnrichedFeedbackEvent[],
  minDataPoints: number
): CandidateRule[] {
  // Group annotations by category
  const categoryCount = new Map<string, { count: number; texts: string[] }>();

  for (const event of events) {
    for (const annotation of event.feedback.annotations ?? []) {
      if (annotation.type === "criticism" && annotation.category) {
        if (!categoryCount.has(annotation.category)) {
          categoryCount.set(annotation.category, { count: 0, texts: [] });
        }
        const entry = categoryCount.get(annotation.category)!;
        entry.count++;
        if (entry.texts.length < 5) {
          entry.texts.push(annotation.text);
        }
      }
    }
  }

  const rules: CandidateRule[] = [];
  for (const [category, data] of categoryCount) {
    if (data.count >= minDataPoints) {
      rules.push({
        rule: `Address recurring "${category}" issues in annotations`,
        promptInstruction: `Pay attention to "${category}" — this has been flagged ${data.count} times in code annotations.`,
        confidence: Math.min(1.0, data.count / (minDataPoints * 3)),
        occurrences: data.count,
        source: "annotation",
        domain: "code_quality",
        appliesTo: ["all"],
      });
    }
  }

  return rules;
}

// ═══════════════════════════════════════════════════════════════
// ANOMALY DETECTION
// ═══════════════════════════════════════════════════════════════

function detectAnomalies(events: EnrichedFeedbackEvent[]): Anomaly[] {
  const anomalies: Anomaly[] = [];

  if (events.length < 10) return anomalies; // Not enough data

  // Split into two halves and compare
  const mid = Math.floor(events.length / 2);
  const firstHalf = events.slice(0, mid);
  const secondHalf = events.slice(mid);

  // Check for approval rate drop
  const firstApprovalRate = computeApprovalRate(firstHalf);
  const secondApprovalRate = computeApprovalRate(secondHalf);

  if (
    firstApprovalRate > 0 &&
    secondApprovalRate < firstApprovalRate * 0.8
  ) {
    anomalies.push({
      type: "approval_rate_drop",
      description: `Approval rate dropped from ${(firstApprovalRate * 100).toFixed(0)}% to ${(secondApprovalRate * 100).toFixed(0)}%`,
      severity: secondApprovalRate < firstApprovalRate * 0.6 ? "critical" : "warning",
      data: { firstHalf: firstApprovalRate, secondHalf: secondApprovalRate },
      detectedAt: new Date().toISOString(),
    });
  }

  // Check for new rejection patterns
  const firstRejectionReasons = collectRejectionReasons(firstHalf);
  const secondRejectionReasons = collectRejectionReasons(secondHalf);

  for (const [reason, count] of secondRejectionReasons) {
    if (!firstRejectionReasons.has(reason) && count >= 3) {
      anomalies.push({
        type: "new_rejection_pattern",
        description: `New rejection reason appearing: "${reason}" (${count} times)`,
        severity: "warning",
        data: { reason, count },
        detectedAt: new Date().toISOString(),
      });
    }
  }

  return anomalies;
}

// ═══════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════

function getSeverityScore(event: EnrichedFeedbackEvent): number {
  const severity = event.feedback.decision?.severity;
  switch (severity) {
    case "critical": return 1.0;
    case "major": return 0.75;
    case "moderate": return 0.5;
    case "minor": return 0.25;
    default: return 0.5;
  }
}

function getExampleText(event: EnrichedFeedbackEvent): string | undefined {
  return (
    event.feedback.freeText?.disliked ??
    event.feedback.freeText?.generalNotes ??
    event.feedback.decision?.reason ??
    event.feedback.annotations?.[0]?.text
  );
}

function computeApprovalRate(events: EnrichedFeedbackEvent[]): number {
  const decisions = events.filter((e) => e.feedback.decision);
  if (decisions.length === 0) return 0;
  const approved = decisions.filter(
    (e) => e.feedback.decision?.decision === "approved"
  ).length;
  return approved / decisions.length;
}

function collectRejectionReasons(
  events: EnrichedFeedbackEvent[]
): Map<string, number> {
  const reasons = new Map<string, number>();
  for (const event of events) {
    if (
      event.feedback.decision?.decision === "rejected" &&
      event.feedback.decision.reason
    ) {
      const reason = event.feedback.decision.reason.toLowerCase().trim();
      reasons.set(reason, (reasons.get(reason) ?? 0) + 1);
    }
  }
  return reasons;
}
