/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Anomaly Detection with Prophet-style forecasting
 * [3.3] Anomaly Detection
 */

import type { HealthAnomaly } from './types.js';

export interface TimeSeriesPoint {
  timestamp: Date;
  value: number;
}

export interface ForecastResult {
  forecast: TimeSeriesPoint[];
  confidenceIntervals: Array<{ upper: number; lower: number }>;
  anomalies: Array<{
    timestamp: Date;
    actual: number;
    expected: number;
    deviation: number;
  }>;
}

export interface AnomalyDetectionConfig {
  sensitivity: number; // 0-1, higher = more sensitive
  seasonalPeriods: number[]; // e.g., [7] for weekly
  changepointPriorScale: number;
  intervalWidth: number; // confidence interval (0.8 = 80%)
}

const DEFAULT_CONFIG: AnomalyDetectionConfig = {
  sensitivity: 0.8,
  seasonalPeriods: [7], // Weekly seasonality
  changepointPriorScale: 0.05,
  intervalWidth: 0.95,
};

export class AnomalyDetectionEngine {
  private config: AnomalyDetectionConfig;

  constructor(config: Partial<AnomalyDetectionConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Detect anomalies in a time series
   */
  detectAnomalies(
    series: TimeSeriesPoint[],
    metricName: string
  ): Array<{
    timestamp: Date;
    value: number;
    expected: number;
    deviation: number;
    severity: 'critical' | 'warning';
  }> {
    if (series.length < 14) {
      return []; // Need at least 2 weeks of data
    }

    // Simple moving average baseline
    const windowSize = 7;
    const anomalies: Array<{
      timestamp: Date;
      value: number;
      expected: number;
      deviation: number;
      severity: 'critical' | 'warning';
    }> = [];

    for (let i = windowSize; i < series.length; i++) {
      const point = series[i];
      const window = series.slice(i - windowSize, i);

      // Calculate baseline (accounting for seasonality)
      const baseline = this.calculateSeasonalBaseline(window, i, series);
      const stdDev = this.calculateStdDev(window.map(p => p.value));

      // Calculate expected value with trend
      const trend = this.calculateTrend(window);
      const expected = baseline + trend;

      // Calculate deviation
      const deviation = Math.abs(point.value - expected);
      const zScore = stdDev > 0 ? deviation / stdDev : 0;

      // Check if anomaly
      const threshold = this.getZScoreThreshold();
      if (zScore > threshold) {
        anomalies.push({
          timestamp: point.timestamp,
          value: point.value,
          expected,
          deviation: zScore,
          severity: zScore > threshold * 1.5 ? 'critical' : 'warning',
        });
      }
    }

    return anomalies;
  }

  /**
   * Generate forecast for future values
   */
  forecast(series: TimeSeriesPoint[], periods: number): ForecastResult {
    if (series.length < 7) {
      return {
        forecast: [],
        confidenceIntervals: [],
        anomalies: [],
      };
    }

    const lastPoint = series[series.length - 1];
    const values = series.map(p => p.value);

    // Calculate trend
    const trend = this.calculateTrend(series.slice(-14));

    // Calculate seasonal component
    const seasonal = this.extractSeasonality(series, 7);

    // Calculate base level
    const lastWeek = values.slice(-7);
    const baseLevel = lastWeek.reduce((a, b) => a + b, 0) / lastWeek.length;

    // Calculate uncertainty
    const residuals = this.calculateResiduals(series, seasonal);
    const uncertainty = this.calculateStdDev(residuals) * 2;

    // Generate forecast
    const forecast: TimeSeriesPoint[] = [];
    const confidenceIntervals: Array<{ upper: number; lower: number }> = [];

    for (let i = 1; i <= periods; i++) {
      const forecastDate = new Date(lastPoint.timestamp);
      forecastDate.setDate(forecastDate.getDate() + i);

      // Day of week seasonal adjustment
      const dayOfWeek = forecastDate.getDay();
      const seasonalFactor = seasonal[dayOfWeek] || 1;

      const predicted = (baseLevel + trend * i) * seasonalFactor;

      forecast.push({
        timestamp: forecastDate,
        value: predicted,
      });

      confidenceIntervals.push({
        upper: predicted + uncertainty * Math.sqrt(i),
        lower: predicted - uncertainty * Math.sqrt(i),
      });
    }

    return {
      forecast,
      confidenceIntervals,
      anomalies: [],
    };
  }

  /**
   * Multi-dimensional anomaly detection
   */
  detectMultiDimensionalAnomaly(
    metrics: Record<string, TimeSeriesPoint[]>,
    correlationWindow: number = 7
  ): Array<{
    metric: string;
    timestamp: Date;
    deviation: number;
    correlatedMetrics: string[];
    suggestedCause: string;
  }> {
    const anomalies: Array<{
      metric: string;
      timestamp: Date;
      deviation: number;
      correlatedMetrics: string[];
      suggestedCause: string;
    }> = [];

    // Calculate correlations between metrics
    const metricNames = Object.keys(metrics);
    const correlations: Record<string, Record<string, number>> = {};

    for (const m1 of metricNames) {
      correlations[m1] = {};
      for (const m2 of metricNames) {
        if (m1 !== m2) {
          correlations[m1][m2] = this.calculateCorrelation(
            metrics[m1],
            metrics[m2],
            correlationWindow
          );
        }
      }
    }

    // Detect anomalies in each metric
    for (const [metricName, series] of Object.entries(metrics)) {
      const metricAnomalies = this.detectAnomalies(series, metricName);

      for (const anomaly of metricAnomalies) {
        // Find correlated metrics that also show anomalies
        const correlatedMetrics: string[] = [];

        for (const [otherMetric, corr] of Object.entries(correlations[metricName])) {
          if (Math.abs(corr) > 0.7) {
            // Strong correlation
            const otherAnomalies = this.detectAnomalies(metrics[otherMetric], otherMetric);
            const hasAnomalyAtTime = otherAnomalies.some(
              a =>
                Math.abs(a.timestamp.getTime() - anomaly.timestamp.getTime()) < 24 * 60 * 60 * 1000
            );

            if (hasAnomalyAtTime) {
              correlatedMetrics.push(otherMetric);
            }
          }
        }

        anomalies.push({
          metric: metricName,
          timestamp: anomaly.timestamp,
          deviation: anomaly.deviation,
          correlatedMetrics,
          suggestedCause: this.suggestMultiDimensionalCause(
            metricName,
            correlatedMetrics,
            anomaly.severity
          ),
        });
      }
    }

    return anomalies;
  }

  /**
   * Calculate seasonal baseline
   */
  private calculateSeasonalBaseline(
    window: TimeSeriesPoint[],
    index: number,
    fullSeries: TimeSeriesPoint[]
  ): number {
    const values = window.map(p => p.value);
    const avg = values.reduce((a, b) => a + b, 0) / values.length;

    // Adjust for day-of-week seasonality if we have enough data
    if (fullSeries.length >= 14 && index >= 7) {
      const currentDay = index % 7;
      const prevWeekSameDay = fullSeries
        .filter((_, i) => i % 7 === currentDay && i < index)
        .slice(-3);

      if (prevWeekSameDay.length > 0) {
        const seasonalAvg =
          prevWeekSameDay.reduce((a, p) => a + p.value, 0) / prevWeekSameDay.length;
        return seasonalAvg * 0.7 + avg * 0.3;
      }
    }

    return avg;
  }

  /**
   * Calculate trend from series
   */
  private calculateTrend(window: TimeSeriesPoint[]): number {
    if (window.length < 2) return 0;

    const n = window.length;
    const sumX = window.reduce((sum, _, i) => sum + i, 0);
    const sumY = window.reduce((sum, p) => sum + p.value, 0);
    const sumXY = window.reduce((sum, p, i) => sum + i * p.value, 0);
    const sumX2 = window.reduce((sum, _, i) => sum + i * i, 0);

    const denominator = n * sumX2 - sumX * sumX;
    if (denominator === 0) return 0;

    const slope = (n * sumXY - sumX * sumY) / denominator;
    return slope;
  }

  /**
   * Extract weekly seasonality pattern
   */
  private extractSeasonality(series: TimeSeriesPoint[], period: number): number[] {
    const dayAvgs: number[][] = Array.from({ length: period }, () => []);

    for (let i = 0; i < series.length; i++) {
      const dayOfWeek = i % period;
      dayAvgs[dayOfWeek].push(series[i].value);
    }

    const overallAvg = series.reduce((sum, p) => sum + p.value, 0) / series.length;

    return dayAvgs.map(values => {
      if (values.length === 0) return 1;
      const avg = values.reduce((a, b) => a + b, 0) / values.length;
      return overallAvg > 0 ? avg / overallAvg : 1;
    });
  }

  /**
   * Calculate residuals after removing seasonality
   */
  private calculateResiduals(series: TimeSeriesPoint[], seasonal: number[]): number[] {
    return series.map((p, i) => {
      const dayOfWeek = i % 7;
      const seasonalFactor = seasonal[dayOfWeek] || 1;
      return p.value / seasonalFactor;
    });
  }

  /**
   * Calculate correlation between two time series
   */
  private calculateCorrelation(
    series1: TimeSeriesPoint[],
    series2: TimeSeriesPoint[],
    window: number
  ): number {
    const s1 = series1.slice(-window).map(p => p.value);
    const s2 = series2.slice(-window).map(p => p.value);

    if (s1.length !== s2.length || s1.length < 2) return 0;

    const n = s1.length;
    const sum1 = s1.reduce((a, b) => a + b, 0);
    const sum2 = s2.reduce((a, b) => a + b, 0);
    const sum1Sq = s1.reduce((a, b) => a + b * b, 0);
    const sum2Sq = s2.reduce((a, b) => a + b * b, 0);
    const pSum = s1.reduce((sum, v, i) => sum + v * s2[i], 0);

    const numerator = pSum - (sum1 * sum2) / n;
    const denominator = Math.sqrt((sum1Sq - (sum1 * sum1) / n) * (sum2Sq - (sum2 * sum2) / n));

    return denominator === 0 ? 0 : numerator / denominator;
  }

  /**
   * Calculate standard deviation
   */
  private calculateStdDev(values: number[]): number {
    if (values.length < 2) return 0;
    const avg = values.reduce((a, b) => a + b, 0) / values.length;
    const variance = values.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / values.length;
    return Math.sqrt(variance);
  }

  /**
   * Get Z-score threshold based on sensitivity
   */
  private getZScoreThreshold(): number {
    // Higher sensitivity = lower threshold = more anomalies detected
    // Default 3.5 for stricter anomaly detection (99.9% confidence)
    // With sensitivity 0.8: threshold = 3.5 - 0.8 = 2.7
    return 3.5 - this.config.sensitivity;
  }

  /**
   * Suggest cause for multi-dimensional anomaly
   */
  private suggestMultiDimensionalCause(
    metric: string,
    correlatedMetrics: string[],
    severity: string
  ): string {
    const causes: Record<string, string> = {
      dau: 'Daily active users anomaly - check for app crashes or service outages',
      error_rate: 'Error rate spike correlates with other metrics - investigate recent deployment',
      latency: 'Latency increase affecting user experience',
      churn_rate: 'Churn rate anomaly - review recent pricing or policy changes',
    };

    if (correlatedMetrics.length > 0) {
      return `${causes[metric] || 'Multi-metric anomaly detected'}. Correlated with: ${correlatedMetrics.join(', ')}`;
    }

    return causes[metric] || `Anomaly detected in ${metric}`;
  }
}

export const anomalyDetectionEngine = new AnomalyDetectionEngine();
