/**
 * Correlation Analysis Module
 *
 * Provides comprehensive correlation and covariance analysis tools for financial time series.
 * Includes correlation matrices, rolling correlations, regime-dependent correlations,
 * and advanced correlation modeling techniques.
 */

export interface CorrelationResult {
  correlation: number;
  pValue: number;
  significance: boolean;
  confidenceInterval?: [number, number];
}

export interface CorrelationMatrix {
  matrix: number[][];
  labels: string[];
  eigenvalues: number[];
  eigenvectors: number[][];
  conditionNumber: number;
}

export interface RollingCorrelationResult {
  correlations: number[];
  timestamps: Date[];
  average: number;
  volatility: number;
  min: number;
  max: number;
}

export interface CovarianceMatrix {
  matrix: number[][];
  labels: string[];
  volatilities: number[];
  correlations: number[][];
  eigenvalues: number[];
  determinant: number;
}

export interface CorrelationBreakdown {
  linear: number;
  nonlinear: number;
  tail: number;
  rank: number;
}

export interface DynamicCorrelationModel {
  parameters: number[];
  correlations: number[];
  logLikelihood: number;
  aic: number;
  bic: number;
}

/**
 * Calculate Pearson correlation coefficient between two time series
 */
export function pearsonCorrelation(x: number[], y: number[]): CorrelationResult {
  if (x.length !== y.length || x.length < 2) {
    throw new Error('Arrays must have same length and at least 2 observations');
  }

  const n = x.length;
  const sumX = x.reduce((a, b) => a + b, 0);
  const sumY = y.reduce((a, b) => a + b, 0);
  const sumXY = x.reduce((sum, xi, i) => {
    const yi = y[i];
    return yi !== undefined ? sum + xi * yi : sum;
  }, 0);
  const sumX2 = x.reduce((sum, xi) => sum + xi * xi, 0);
  const sumY2 = y.reduce((sum, yi) => sum + yi * yi, 0);

  const numerator = n * sumXY - sumX * sumY;
  const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));

  const correlation = denominator === 0 ? 0 : numerator / denominator;

  // Calculate statistical significance (t-test)
  const df = n - 2;
  const tStat = correlation * Math.sqrt(df / (1 - correlation * correlation));
  const pValue = 2 * (1 - studentTCDF(Math.abs(tStat), df));
  const significance = pValue < 0.05;

  // Calculate confidence interval (Fisher transformation)
  const z = 0.5 * Math.log((1 + correlation) / (1 - correlation));
  const seZ = 1 / Math.sqrt(n - 3);
  const zLower = z - 1.96 * seZ;
  const zUpper = z + 1.96 * seZ;
  const confidenceInterval: [number, number] = [
    (Math.exp(2 * zLower) - 1) / (Math.exp(2 * zLower) + 1),
    (Math.exp(2 * zUpper) - 1) / (Math.exp(2 * zUpper) + 1),
  ];

  return {
    correlation,
    pValue,
    significance,
    confidenceInterval,
  };
}

/**
 * Calculate Spearman rank correlation coefficient
 */
export function spearmanCorrelation(x: number[], y: number[]): CorrelationResult {
  if (x.length !== y.length || x.length < 2) {
    throw new Error('Arrays must have same length and at least 2 observations');
  }

  // Convert to ranks
  const xRanks = getRanks(x);
  const yRanks = getRanks(y);

  return pearsonCorrelation(xRanks, yRanks);
}

/**
 * Calculate Kendall's tau correlation coefficient
 */
export function kendallTau(x: number[], y: number[]): CorrelationResult {
  if (x.length !== y.length || x.length < 2) {
    throw new Error('Arrays must have same length and at least 2 observations');
  }

  const n = x.length;
  let concordant = 0;
  let discordant = 0;

  for (let i = 0; i < n - 1; i++) {
    for (let j = i + 1; j < n; j++) {
      const xi = x[i];
      const xj = x[j];
      const yi = y[i];
      const yj = y[j];
      
      if (xi !== undefined && xj !== undefined && yi !== undefined && yj !== undefined) {
        const xDiff = xi - xj;
        const yDiff = yi - yj;

        if (xDiff * yDiff > 0) {
          concordant++;
        } else if (xDiff * yDiff < 0) {
          discordant++;
        }
      }
    }
  }

  const correlation = (concordant - discordant) / ((n * (n - 1)) / 2);

  // Approximate p-value for large samples
  const variance = (2 * (2 * n + 5)) / (9 * n * (n - 1));
  const z = correlation / Math.sqrt(variance);
  const pValue = 2 * (1 - normalCDF(Math.abs(z)));
  const significance = pValue < 0.05;

  return {
    correlation,
    pValue,
    significance,
  };
}

/**
 * Calculate correlation matrix for multiple time series
 */
export function correlationMatrix(
  data: number[][],
  labels: string[] = [],
  method: 'pearson' | 'spearman' | 'kendall' = 'pearson'
): CorrelationMatrix {
  const n = data.length;

  if (labels.length === 0) {
    labels = Array.from({ length: n }, (_, i) => `Series${i + 1}`);
  }

  const matrix: number[][] = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      if (i === j) {
        matrix[i]![j] = 1;
      } else {
        const dataI = data[i];
        const dataJ = data[j];
        if (dataI && dataJ) {
          let corrResult: CorrelationResult;
          switch (method) {
            case 'spearman':
              corrResult = spearmanCorrelation(dataI, dataJ);
              break;
            case 'kendall':
              corrResult = kendallTau(dataI, dataJ);
              break;
            default:
              corrResult = pearsonCorrelation(dataI, dataJ);
          }
          matrix[i]![j] = corrResult.correlation;
        }
      }
    }
  }

  // Calculate eigenvalues and eigenvectors
  const { eigenvalues, eigenvectors } = eigenDecomposition(matrix);

  // Calculate condition number
  const conditionNumber =
    Math.max(...eigenvalues) / Math.min(...eigenvalues.filter(x => x > 1e-10));

  return {
    matrix,
    labels,
    eigenvalues,
    eigenvectors,
    conditionNumber,
  };
}

/**
 * Calculate rolling correlation between two time series
 */
export function rollingCorrelation(
  x: number[],
  y: number[],
  window: number,
  timestamps?: Date[]
): RollingCorrelationResult {
  if (x.length !== y.length || window > x.length) {
    throw new Error('Invalid input parameters');
  }

  const correlations: number[] = [];
  const resultTimestamps: Date[] = [];

  for (let i = window - 1; i < x.length; i++) {
    const xWindow = x.slice(i - window + 1, i + 1);
    const yWindow = y.slice(i - window + 1, i + 1);

    const corr = pearsonCorrelation(xWindow, yWindow).correlation;
    correlations.push(corr);

    if (timestamps) {
      resultTimestamps.push(timestamps[i]);
    } else {
      resultTimestamps.push(new Date(i));
    }
  }

  const average = correlations.reduce((a, b) => a + b, 0) / correlations.length;
  const variance =
    correlations.reduce((sum, corr) => sum + Math.pow(corr - average, 2), 0) / correlations.length;
  const volatility = Math.sqrt(variance);
  const min = Math.min(...correlations);
  const max = Math.max(...correlations);

  return {
    correlations,
    timestamps: resultTimestamps,
    average,
    volatility,
    min,
    max,
  };
}

/**
 * Calculate covariance matrix
 */
export function covarianceMatrix(data: number[][], labels: string[] = []): CovarianceMatrix {
  const n = data.length;

  if (labels.length === 0) {
    labels = Array.from({ length: n }, (_, i) => `Series${i + 1}`);
  }

  // Calculate means
  const means = data.map(series => series.reduce((a, b) => a + b, 0) / series.length);

  // Calculate covariance matrix
  const matrix: number[][] = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));
  const m = data[0].length; // Number of observations

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      let covariance = 0;
      for (let k = 0; k < m; k++) {
        covariance += (data[i][k] - means[i]) * (data[j][k] - means[j]);
      }
      matrix[i][j] = covariance / (m - 1);
    }
  }

  // Calculate volatilities (standard deviations)
  const volatilities = data.map((series, i) => Math.sqrt(matrix[i][i]));

  // Calculate correlation matrix from covariance matrix
  const correlations: number[][] = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      correlations[i][j] = matrix[i][j] / (volatilities[i] * volatilities[j]);
    }
  }

  // Calculate eigenvalues
  const { eigenvalues } = eigenDecomposition(matrix);

  // Calculate determinant
  const determinant = eigenvalues.reduce((prod, val) => prod * val, 1);

  return {
    matrix,
    labels,
    volatilities,
    correlations,
    eigenvalues,
    determinant,
  };
}

/**
 * Calculate partial correlation controlling for other variables
 */
export function partialCorrelation(
  x: number[],
  y: number[],
  controls: number[][]
): CorrelationResult {
  // Use matrix operations to calculate partial correlation
  const n = x.length;
  const k = controls.length;

  // Build design matrix
  const X = Array(n)
    .fill(null)
    .map(() => Array(k + 1).fill(1));
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < k; j++) {
      X[i][j + 1] = controls[j][i];
    }
  }

  // Calculate residuals for x and y after regressing on controls
  const xResiduals = residuals(x, X);
  const yResiduals = residuals(y, X);

  return pearsonCorrelation(xResiduals, yResiduals);
}

/**
 * Test for correlation regime changes
 */
export function correlationRegimeAnalysis(
  x: number[],
  y: number[],
  window: number = 60
): {
  regimes: { start: number; end: number; correlation: number }[];
  breakpoints: number[];
  stability: number;
} {
  const rollingCorr = rollingCorrelation(x, y, window);
  const correlations = rollingCorr.correlations;

  // Detect regime changes using CUSUM test
  const breakpoints: number[] = [];
  const threshold = 2.0; // CUSUM threshold

  let cusum = 0;
  const mean = correlations.reduce((a, b) => a + b, 0) / correlations.length;

  for (let i = 1; i < correlations.length; i++) {
    cusum += correlations[i] - mean;
    if (Math.abs(cusum) > threshold) {
      breakpoints.push(i);
      cusum = 0;
    }
  }

  // Build regimes
  const regimes: { start: number; end: number; correlation: number }[] = [];
  let start = 0;

  for (const breakpoint of breakpoints) {
    const regimeCorr = correlations.slice(start, breakpoint);
    const avgCorr = regimeCorr.reduce((a, b) => a + b, 0) / regimeCorr.length;

    regimes.push({
      start,
      end: breakpoint,
      correlation: avgCorr,
    });
    start = breakpoint;
  }

  // Add final regime
  if (start < correlations.length) {
    const regimeCorr = correlations.slice(start);
    const avgCorr = regimeCorr.reduce((a, b) => a + b, 0) / regimeCorr.length;

    regimes.push({
      start,
      end: correlations.length,
      correlation: avgCorr,
    });
  }

  // Calculate stability measure
  const regimeVariances = regimes.map(regime => {
    const regimeCorr = correlations.slice(regime.start, regime.end);
    const mean = regime.correlation;
    return regimeCorr.reduce((sum, corr) => sum + Math.pow(corr - mean, 2), 0) / regimeCorr.length;
  });

  const stability = 1 / (1 + regimeVariances.reduce((a, b) => a + b, 0) / regimeVariances.length);

  return {
    regimes,
    breakpoints,
    stability,
  };
}

/**
 * Calculate tail correlation using copula methods
 */
export function tailCorrelation(
  x: number[],
  y: number[],
  threshold: number = 0.05
): {
  upperTail: number;
  lowerTail: number;
  symmetric: boolean;
} {
  const n = x.length;
  const upperThreshold = 1 - threshold;
  const lowerThreshold = threshold;

  // Convert to uniform marginals
  const xRanks = getRanks(x).map(rank => rank / n);
  const yRanks = getRanks(y).map(rank => rank / n);

  // Upper tail correlation
  let upperCount = 0;
  let upperTotal = 0;

  for (let i = 0; i < n; i++) {
    if (xRanks[i] > upperThreshold) {
      upperTotal++;
      if (yRanks[i] > upperThreshold) {
        upperCount++;
      }
    }
  }

  const upperTail = upperTotal > 0 ? upperCount / upperTotal : 0;

  // Lower tail correlation
  let lowerCount = 0;
  let lowerTotal = 0;

  for (let i = 0; i < n; i++) {
    if (xRanks[i] < lowerThreshold) {
      lowerTotal++;
      if (yRanks[i] < lowerThreshold) {
        lowerCount++;
      }
    }
  }

  const lowerTail = lowerTotal > 0 ? lowerCount / lowerTotal : 0;

  // Test for symmetry
  const symmetric = Math.abs(upperTail - lowerTail) < 0.1;

  return {
    upperTail,
    lowerTail,
    symmetric,
  };
}

/**
 * Dynamic Conditional Correlation (DCC) model estimation
 */
export function dccModel(
  data: number[][],
  maxIter: number = 100,
  tolerance: number = 1e-6
): DynamicCorrelationModel {
  const n = data.length;
  const T = data[0].length;

  // Initialize parameters [alpha, beta]
  const params = [0.01, 0.95];

  // Standardize data (assume unit variance for simplicity)
  const standardizedData = data.map(series => {
    const mean = series.reduce((a, b) => a + b, 0) / series.length;
    const variance =
      series.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / (series.length - 1);
    const std = Math.sqrt(variance);
    return series.map(x => (x - mean) / std);
  });

  let correlations: number[] = [];
  let logLikelihood = -Infinity;

  for (let iter = 0; iter < maxIter; iter++) {
    const [alpha, beta] = params;

    // Calculate dynamic correlations
    correlations = [];

    // Initialize with unconditional correlation
    const unconditionalCorr = pearsonCorrelation(
      standardizedData[0],
      standardizedData[1]
    ).correlation;
    let Qt = unconditionalCorr;

    let newLogLikelihood = 0;

    for (let t = 1; t < T; t++) {
      // Update correlation
      const prevShock = standardizedData[0][t - 1] * standardizedData[1][t - 1];
      Qt = (1 - alpha - beta) * unconditionalCorr + alpha * prevShock + beta * Qt;

      correlations.push(Qt);

      // Add to log-likelihood
      const det = 1 - Qt * Qt;
      if (det > 0) {
        newLogLikelihood -= 0.5 * Math.log(det);
        newLogLikelihood -=
          (0.5 *
            (Math.pow(standardizedData[0][t], 2) +
              Math.pow(standardizedData[1][t], 2) -
              2 * Qt * standardizedData[0][t] * standardizedData[1][t])) /
          det;
      }
    }

    // Check convergence
    if (Math.abs(newLogLikelihood - logLikelihood) < tolerance) {
      break;
    }

    logLikelihood = newLogLikelihood;

    // Simple gradient update (in practice, use more sophisticated optimization)
    params[0] = Math.max(0.001, Math.min(0.999, params[0] + 0.001));
    params[1] = Math.max(0.001, Math.min(0.999 - params[0], params[1] + 0.001));
  }

  // Calculate information criteria
  const k = 2; // Number of parameters
  const aic = -2 * logLikelihood + 2 * k;
  const bic = -2 * logLikelihood + k * Math.log(T);

  return {
    parameters: params,
    correlations,
    logLikelihood,
    aic,
    bic,
  };
}

/**
 * Test for Granger causality in correlations
 */
export function grangerCausalityTest(
  x: number[],
  y: number[],
  maxLag: number = 5
): {
  xCausesY: { fStatistic: number; pValue: number; significant: boolean };
  yCausesX: { fStatistic: number; pValue: number; significant: boolean };
  optimalLag: number;
} {
  let bestLag = 1;
  let minAIC = Infinity;

  // Find optimal lag
  for (let lag = 1; lag <= maxLag; lag++) {
    const aic = varModel(x, y, lag).aic;
    if (aic < minAIC) {
      minAIC = aic;
      bestLag = lag;
    }
  }

  // Test x -> y causality
  const fullModel = varModel(x, y, bestLag);
  const restrictedModelY = arModel(y, bestLag);

  const fStatX =
    (restrictedModelY.rss - fullModel.rssY) /
    bestLag /
    (fullModel.rssY / (x.length - 2 * bestLag - 1));
  const pValueX = 1 - fCDF(fStatX, bestLag, x.length - 2 * bestLag - 1);

  // Test y -> x causality
  const restrictedModelX = arModel(x, bestLag);

  const fStatY =
    (restrictedModelX.rss - fullModel.rssX) /
    bestLag /
    (fullModel.rssX / (x.length - 2 * bestLag - 1));
  const pValueY = 1 - fCDF(fStatY, bestLag, x.length - 2 * bestLag - 1);

  return {
    xCausesY: {
      fStatistic: fStatX,
      pValue: pValueX,
      significant: pValueX < 0.05,
    },
    yCausesX: {
      fStatistic: fStatY,
      pValue: pValueY,
      significant: pValueY < 0.05,
    },
    optimalLag: bestLag,
  };
}
/**
 * Calculate Distance Correlation
 */
export function distanceCorrelation(x: number[], y: number[]): CorrelationResult {
  if (x.length !== y.length || x.length < 2) {
    throw new Error('Arrays must have same length and at least 2 observations');
  }

  const n = x.length;

  // Calculate distance matrices
  const a = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));
  const b = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      const xi = x[i];
      const xj = x[j];
      const yi = y[i];
      const yj = y[j];
      if (xi !== undefined && xj !== undefined && yi !== undefined && yj !== undefined) {
        a[i]![j] = Math.abs(xi - xj);
        b[i]![j] = Math.abs(yi - yj);
      }
    }
  }

  // Calculate double centered distance matrices
  const aMeanRow = a.map(row => row.reduce((sum, val) => sum + val, 0) / n);
  const bMeanRow = b.map(row => row.reduce((sum, val) => sum + val, 0) / n);
  const aMeanTotal = aMeanRow.reduce((sum, val) => sum + val, 0) / n;
  const bMeanTotal = bMeanRow.reduce((sum, val) => sum + val, 0) / n;

  const A = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));
  const B = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      A[i][j] = a[i][j] - aMeanRow[i] - aMeanRow[j] + aMeanTotal;
      B[i][j] = b[i][j] - bMeanRow[i] - bMeanRow[j] + bMeanTotal;
    }
  }

  // Calculate distance covariance and variances
  let dcov = 0;
  let dvarX = 0;
  let dvarY = 0;

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      dcov += A[i][j] * B[i][j];
      dvarX += A[i][j] * A[i][j];
      dvarY += B[i][j] * B[i][j];
    }
  }

  dcov = Math.sqrt(dcov / (n * n));
  dvarX = Math.sqrt(dvarX / (n * n));
  dvarY = Math.sqrt(dvarY / (n * n));

  const correlation = dvarX * dvarY === 0 ? 0 : dcov / Math.sqrt(dvarX * dvarY);

  // Approximate p-value (permutation test)
  let pValue = 1;
  const numPermutations = 100;

  for (let p = 0; p < numPermutations; p++) {
    const yPermuted = shuffleArray([...y]);
    const bPermuted = Array(n)
      .fill(null)
      .map(() => Array(n).fill(0));

    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        bPermuted[i][j] = Math.abs(yPermuted[i] - yPermuted[j]);
      }
    }

    const bMeanRowPermuted = bPermuted.map(row => row.reduce((sum, val) => sum + val, 0) / n);
    const bMeanTotalPermuted = bMeanRowPermuted.reduce((sum, val) => sum + val, 0) / n;

    const BPermuted = Array(n)
      .fill(null)
      .map(() => Array(n).fill(0));

    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        BPermuted[i][j] =
          bPermuted[i][j] - bMeanRowPermuted[i] - bMeanRowPermuted[j] + bMeanTotalPermuted;
      }
    }

    let dcovPermuted = 0;
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        dcovPermuted += A[i][j] * BPermuted[i][j];
      }
    }
    dcovPermuted = Math.sqrt(dcovPermuted / (n * n));

    if (dcovPermuted >= dcov) {
      pValue++;
    }
  }

  pValue /= numPermutations + 1;
  const significance = pValue < 0.05;

  return {
    correlation,
    pValue,
    significance,
  };
}

/**
 * Calculate Mutual Information
 */
export function mutualInformation(
  x: number[],
  y: number[],
  numBins: number = 10
): CorrelationResult {
  if (x.length !== y.length || x.length < 2) {
    throw new Error('Arrays must have same length and at least 2 observations');
  }

  const n = x.length;

  // Calculate histograms
  const xMin = Math.min(...x);
  const xMax = Math.max(...x);
  const yMin = Math.min(...y);
  const yMax = Math.max(...y);

  const xBinWidth = (xMax - xMin) / numBins;
  const yBinWidth = (yMax - yMin) / numBins;

  const jointHistogram = Array(numBins)
    .fill(null)
    .map(() => Array(numBins).fill(0));
  const xHistogram = Array(numBins).fill(0);
  const yHistogram = Array(numBins).fill(0);

  for (let i = 0; i < n; i++) {
    const xBin = Math.floor((x[i] - xMin) / xBinWidth);
    const yBin = Math.floor((y[i] - yMin) / yBinWidth);

    if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
      jointHistogram[xBin][yBin]++;
      xHistogram[xBin]++;
      yHistogram[yBin]++;
    }
  }

  // Calculate probabilities
  const jointProbabilities = jointHistogram.map(row => row.map(count => count / n));
  const xProbabilities = xHistogram.map(count => count / n);
  const yProbabilities = yHistogram.map(count => count / n);

  // Calculate mutual information
  let mi = 0;
  for (let i = 0; i < numBins; i++) {
    for (let j = 0; j < numBins; j++) {
      if (jointProbabilities[i][j] > 0 && xProbabilities[i] > 0 && yProbabilities[j] > 0) {
        mi +=
          jointProbabilities[i][j] *
          Math.log(jointProbabilities[i][j] / (xProbabilities[i] * yProbabilities[j]));
      }
    }
  }

  const correlation = mi; // Use MI as correlation measure

  // Approximate p-value (permutation test)
  let pValue = 1;
  const numPermutations = 100;

  for (let p = 0; p < numPermutations; p++) {
    const yPermuted = shuffleArray([...y]);
    let miPermuted = 0;

    const jointHistogramPermuted = Array(numBins)
      .fill(null)
      .map(() => Array(numBins).fill(0));

    for (let i = 0; i < n; i++) {
      const xBin = Math.floor((x[i] - xMin) / xBinWidth);
      const yBin = Math.floor((yPermuted[i] - yMin) / yBinWidth);

      if (xBin >= 0 && xBin < numBins && yBin >= 0 && yBin < numBins) {
        jointHistogramPermuted[xBin][yBin]++;
      }
    }

    const jointProbabilitiesPermuted = jointHistogramPermuted.map(row =>
      row.map(count => count / n)
    );

    for (let i = 0; i < numBins; i++) {
      for (let j = 0; j < numBins; j++) {
        if (
          jointProbabilitiesPermuted[i][j] > 0 &&
          xProbabilities[i] > 0 &&
          yProbabilities[j] > 0
        ) {
          miPermuted +=
            jointProbabilitiesPermuted[i][j] *
            Math.log(jointProbabilitiesPermuted[i][j] / (xProbabilities[i] * yProbabilities[j]));
        }
      }
    }

    if (miPermuted >= mi) {
      pValue++;
    }
  }

  pValue /= numPermutations + 1;
  const significance = pValue < 0.05;

  return {
    correlation,
    pValue,
    significance,
  };
}

/**
 * Calculate Cross-Correlation
 */
export function crossCorrelation(x: number[], y: number[], maxLag: number): number[] {
  const n = x.length;
  if (n !== y.length) {
    throw new Error('Arrays must have the same length');
  }

  const correlations: number[] = [];

  for (let lag = -maxLag; lag <= maxLag; lag++) {
    let sum = 0;
    let count = 0;

    for (let i = 0; i < n; i++) {
      const yIndex = i + lag;

      if (yIndex >= 0 && yIndex < n) {
        sum += (x[i] - average(x)) * (y[yIndex] - average(y));
        count++;
      }
    }

    const stdX = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
    const stdY = Math.sqrt(y.reduce((sum, yi) => sum + Math.pow(yi - average(y), 2), 0) / (n - 1));

    const correlation = count > 0 ? sum / ((count - 1) * stdX * stdY) : 0;
    correlations.push(correlation);
  }

  return correlations;
}

/**
 * Calculate Autocorrelation
 */
export function autocorrelation(x: number[], lag: number): number {
  const n = x.length;
  if (lag >= n) {
    throw new Error('Lag must be less than the length of the array');
  }

  let sum = 0;
  for (let i = lag; i < n; i++) {
    sum += (x[i] - average(x)) * (x[i - lag] - average(x));
  }

  const std = Math.sqrt(x.reduce((sum, xi) => sum + Math.pow(xi - average(x), 2), 0) / (n - 1));
  return sum / ((n - lag - 1) * std * std);
}

/**
 * Helper function to shuffle an array (Fisher-Yates shuffle)
 */
function shuffleArray<T>(array: T[]): T[] {
  const newArray = [...array];
  for (let i = newArray.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
  }
  return newArray;
}

/**
 * Helper function to calculate the average of an array of numbers
 */
function average(arr: number[]): number {
  if (arr.length === 0) {
    return 0;
  }
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

function getRanks(arr: number[]): number[] {
  const sorted = arr.map((val, idx) => ({ val, idx })).sort((a, b) => a.val - b.val);
  const ranks = new Array(arr.length);

  for (let i = 0; i < sorted.length; i++) {
    ranks[sorted[i].idx] = i + 1;
  }

  return ranks;
}

function studentTCDF(t: number, df: number): number {
  // Approximation for Student's t CDF
  const x = df / (t * t + df);
  return 1 - 0.5 * betaIncomplete(df / 2, 0.5, x);
}

function normalCDF(z: number): number {
  return 0.5 * (1 + erf(z / Math.sqrt(2)));
}

function erf(x: number): number {
  // Approximation of error function
  const a1 = 0.254829592;
  const a2 = -0.284496736;
  const a3 = 1.421413741;
  const a4 = -1.453152027;
  const a5 = 1.061405429;
  const p = 0.3275911;

  const sign = x >= 0 ? 1 : -1;
  x = Math.abs(x);

  const t = 1.0 / (1.0 + p * x);
  const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);

  return sign * y;
}

function betaIncomplete(a: number, b: number, x: number): number {
  // Better approximation of incomplete beta function
  if (x === 0) {
    return 0;
  }
  if (x === 1) {
    return 1;
  }

  // Use continued fraction approximation (Lentz's algorithm)
  const fpmin = 1e-30;
  const maxIter = 200;
  const eps = 3e-7;

  const bt = Math.exp(
    gammaLn(a + b) - gammaLn(a) - gammaLn(b) + a * Math.log(x) + b * Math.log(1 - x)
  );

  if (x < (a + 1) / (a + b + 2)) {
    return (bt * betaContinuedFraction(a, b, x)) / a;
  } else {
    return 1 - (bt * betaContinuedFraction(b, a, 1 - x)) / b;
  }

  function betaContinuedFraction(a: number, b: number, x: number): number {
    let c = 1;
    let d = 1 - ((a + b) * x) / (a + 1);
    if (Math.abs(d) < fpmin) {
      d = fpmin;
    }
    d = 1 / d;
    let h = d;

    for (let m = 1; m <= maxIter; m++) {
      const m2 = 2 * m;
      const aa = (m * (b - m) * x) / ((a + m2 - 1) * (a + m2));
      d = 1 + aa * d;
      if (Math.abs(d) < fpmin) {
        d = fpmin;
      }
      c = 1 + aa / c;
      if (Math.abs(c) < fpmin) {
        c = fpmin;
      }
      d = 1 / d;
      h *= d * c;

      const bb = (-(a + m) * (a + b + m) * x) / ((a + m2) * (a + m2 + 1));
      d = 1 + bb * d;
      if (Math.abs(d) < fpmin) {
        d = fpmin;
      }
      c = 1 + bb / c;
      if (Math.abs(c) < fpmin) {
        c = fpmin;
      }
      d = 1 / d;
      const del = d * c;
      h *= del;

      if (Math.abs(del - 1) < eps) {
        break;
      }
    }

    return h;
  }

  function gammaLn(xx: number): number {
    const stp = 2.50662827465;
    const coeffs = [
      76.18009172947146, -86.50532032941677, 24.01409824083091, -1.231739572450155,
      0.1208650973866179e-2, -0.5395239384953e-5,
    ];

    let x = xx - 1;
    let tmp = x + 5.5;
    tmp -= (x + 0.5) * Math.log(tmp);
    let ser = 1.000000000190015;

    for (let j = 0; j < 6; j++) {
      x += 1;
      ser += coeffs[j] / x;
    }

    return -tmp + Math.log(stp * ser);
  }
}

function eigenDecomposition(matrix: number[][]): {
  eigenvalues: number[];
  eigenvectors: number[][];
} {
  // Simplified eigenvalue decomposition (for symmetric matrices)
  const n = matrix.length;

  // Power iteration for largest eigenvalue
  const eigenvalues: number[] = [];
  const eigenvectors: number[][] = [];

  for (let k = 0; k < Math.min(n, 3); k++) {
    // Calculate first 3 eigenvalues
    let v = Array(n).fill(1 / Math.sqrt(n));
    let lambda = 0;

    for (let iter = 0; iter < 100; iter++) {
      const Av = matrix.map(row => row.reduce((sum, val, i) => sum + val * v[i], 0));
      const newLambda = Av.reduce((sum, val, i) => sum + val * v[i], 0);
      const norm = Math.sqrt(Av.reduce((sum, val) => sum + val * val, 0));

      if (norm === 0) {
        break;
      }

      v = Av.map(val => val / norm);

      if (Math.abs(newLambda - lambda) < 1e-10) {
        break;
      }
      lambda = newLambda;
    }

    eigenvalues.push(lambda);
    eigenvectors.push([...v]);

    // Deflate matrix for next eigenvalue
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        matrix[i][j] -= lambda * v[i] * v[j];
      }
    }
  }

  return { eigenvalues, eigenvectors };
}

function residuals(y: number[], X: number[][]): number[] {
  // Simple linear regression to calculate residuals
  const n = y.length;
  const k = X[0].length;

  // Calculate (X'X)^-1 X' y
  const XtX = Array(k)
    .fill(null)
    .map(() => Array(k).fill(0));
  const Xty = Array(k).fill(0);

  // X'X
  for (let i = 0; i < k; i++) {
    for (let j = 0; j < k; j++) {
      for (let t = 0; t < n; t++) {
        XtX[i][j] += X[t][i] * X[t][j];
      }
    }
  }

  // X'y
  for (let i = 0; i < k; i++) {
    for (let t = 0; t < n; t++) {
      Xty[i] += X[t][i] * y[t];
    }
  }

  // Solve for beta (simplified - assumes invertible)
  const beta = solveLinearSystem(XtX, Xty);

  // Calculate residuals
  const residuals: number[] = [];
  for (let t = 0; t < n; t++) {
    let fitted = 0;
    for (let i = 0; i < k; i++) {
      fitted += X[t][i] * beta[i];
    }
    residuals.push(y[t] - fitted);
  }

  return residuals;
}

function solveLinearSystem(A: number[][], b: number[]): number[] {
  // Gaussian elimination (simplified)
  const n = A.length;
  const augmented = A.map((row, i) => [...row, b[i]]);

  // Forward elimination
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      const factor = augmented[j][i] / augmented[i][i];
      for (let k = i; k <= n; k++) {
        augmented[j][k] -= factor * augmented[i][k];
      }
    }
  }

  // Back substitution
  const x = Array(n).fill(0);
  for (let i = n - 1; i >= 0; i--) {
    x[i] = augmented[i][n];
    for (let j = i + 1; j < n; j++) {
      x[i] -= augmented[i][j] * x[j];
    }
    x[i] /= augmented[i][i];
  }

  return x;
}

function varModel(
  x: number[],
  y: number[],
  lag: number
): {
  rssX: number;
  rssY: number;
  aic: number;
} {
  // Simplified VAR model calculation
  const n = x.length - lag;

  // Build design matrix
  const X = Array(n)
    .fill(null)
    .map(() => Array(2 * lag + 1).fill(1));
  const yX = Array(n).fill(0);
  const yY = Array(n).fill(0);

  for (let t = 0; t < n; t++) {
    yX[t] = x[t + lag];
    yY[t] = y[t + lag];

    for (let l = 0; l < lag; l++) {
      X[t][1 + l] = x[t + lag - 1 - l];
      X[t][1 + lag + l] = y[t + lag - 1 - l];
    }
  }

  // Calculate residuals for both equations
  const residualsX = residuals(yX, X);
  const residualsY = residuals(yY, X);

  const rssX = residualsX.reduce((sum, r) => sum + r * r, 0);
  const rssY = residualsY.reduce((sum, r) => sum + r * r, 0);

  const k = 2 * lag + 1;
  const aic = n * Math.log(rssX + rssY) + 2 * k;

  return { rssX, rssY, aic };
}

function arModel(y: number[], lag: number): { rss: number } {
  const n = y.length - lag;

  // Build design matrix
  const X = Array(n)
    .fill(null)
    .map(() => Array(lag + 1).fill(1));
  const yVec = Array(n).fill(0);

  for (let t = 0; t < n; t++) {
    yVec[t] = y[t + lag];

    for (let l = 0; l < lag; l++) {
      X[t][1 + l] = y[t + lag - 1 - l];
    }
  }

  const res = residuals(yVec, X);
  const rss = res.reduce((sum, r) => sum + r * r, 0);

  return { rss };
}

function fCDF(f: number, df1: number, df2: number): number {
  // Approximation for F distribution CDF
  if (f <= 0) {
    return 0;
  }
  if (f === Infinity) {
    return 1;
  }

  const x = df2 / (df2 + df1 * f);
  return 1 - betaIncomplete(df2 / 2, df1 / 2, x);
}
