/**
 * Volatility Models
 * Advanced volatility modeling and forecasting tools
 */

// Local interface definition to avoid circular dependency
interface OHLCVData {
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
  timestamp: Date;
}

export interface GARCHParameters {
  omega: number; // Constant term
  alpha: number; // ARCH parameter
  beta: number; // GARCH parameter
  logLikelihood: number;
  aic: number;
  bic: number;
}

export interface VolatilityEstimates {
  closeToClose: number;
  parkinson: number;
  garmanKlass: number;
  rogersSatchell: number;
  yangZhang: number;
}

export interface VolatilityRegime {
  regime: number;
  startDate: Date;
  endDate: Date;
  averageVolatility: number;
  observations: number;
}

export interface VolatilityTerm {
  maturity: number; // Days to maturity
  impliedVolatility: number;
  confidence: number;
}

export interface HestonParameters {
  kappa: number; // Mean reversion speed
  theta: number; // Long-term variance
  sigma: number; // Volatility of variance
  rho: number; // Correlation
  v0: number; // Initial variance
  logLikelihood: number;
}

/**
 * Calculate realized volatility using different estimators
 */
export function calculateRealizedVolatility(
  ohlcv: OHLCVData[],
  annualizationFactor: number = 252
): VolatilityEstimates {
  if (ohlcv.length < 2) {
    throw new Error('Need at least 2 observations for volatility calculation');
  }

  const n = ohlcv.length;
  let closeToCloseSum = 0;
  let parkinsonSum = 0;
  let garmanKlassSum = 0;
  let rogersSatchellSum = 0;
  let yangZhangSum = 0;

  // Calculate log returns and volatility estimators
  for (let i = 1; i < n; i++) {
    const prev = ohlcv[i - 1];
    const curr = ohlcv[i];

    // Close-to-close
    const logReturn = Math.log(curr.close / prev.close);
    closeToCloseSum += logReturn * logReturn;

    // Parkinson estimator
    const logHighLow = Math.log(curr.high / curr.low);
    parkinsonSum += logHighLow * logHighLow;

    // Garman-Klass estimator
    const logOpenClose = Math.log(curr.close / curr.open);
    garmanKlassSum +=
      0.5 * logHighLow * logHighLow - (2 * Math.log(2) - 1) * logOpenClose * logOpenClose;

    // Rogers-Satchell estimator
    const logHighOpen = Math.log(curr.high / curr.open);
    const logHighClose = Math.log(curr.high / curr.close);
    const logLowOpen = Math.log(curr.low / curr.open);
    const logLowClose = Math.log(curr.low / curr.close);
    rogersSatchellSum += logHighOpen * logHighClose + logLowOpen * logLowClose;

    // Yang-Zhang estimator components
    const overnight = Math.log(curr.open / prev.close);
    yangZhangSum += overnight * overnight + rogersSatchellSum / i; // Simplified for brevity
  }

  return {
    closeToClose: Math.sqrt((closeToCloseSum / (n - 1)) * annualizationFactor),
    parkinson: Math.sqrt((parkinsonSum / (n - 1) / (4 * Math.log(2))) * annualizationFactor),
    garmanKlass: Math.sqrt((garmanKlassSum / (n - 1)) * annualizationFactor),
    rogersSatchell: Math.sqrt((rogersSatchellSum / (n - 1)) * annualizationFactor),
    yangZhang: Math.sqrt((yangZhangSum / (n - 1)) * annualizationFactor),
  };
}

/**
 * Estimate GARCH(1,1) model parameters
 */
export function estimateGARCH(
  returns: number[],
  maxIterations: number = 100,
  tolerance: number = 1e-6
): GARCHParameters {
  const n = returns.length;

  // Initial parameter estimates
  let omega = 0.01;
  let alpha = 0.05;
  let beta = 0.9;

  // Calculate unconditional variance
  const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
  const unconditionalVar =
    returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);

  let logLikelihood = -Infinity;

  for (let iter = 0; iter < maxIterations; iter++) {
    const variances: number[] = [unconditionalVar];
    let newLogLikelihood = 0;

    // Calculate conditional variances
    for (let t = 1; t < n; t++) {
      const prevVar = variances[t - 1];
      const prevReturn = returns[t - 1] - meanReturn;
      const currentVar = omega + alpha * prevReturn * prevReturn + beta * prevVar;
      variances.push(Math.max(currentVar, 1e-8)); // Ensure positive variance

      // Add to log-likelihood
      const currentReturn = returns[t] - meanReturn;
      newLogLikelihood -=
        0.5 *
        (Math.log(2 * Math.PI) +
          Math.log(currentVar) +
          (currentReturn * currentReturn) / currentVar);
    }

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

    logLikelihood = newLogLikelihood;

    // Simple gradient update (in practice, use more sophisticated optimization)
    const gradientStep = 0.001;
    omega = Math.max(0.001, omega + gradientStep);
    alpha = Math.max(0.001, Math.min(0.999, alpha + gradientStep));
    beta = Math.max(0.001, Math.min(0.999 - alpha, beta + gradientStep));
  }

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

  return {
    omega,
    alpha,
    beta,
    logLikelihood,
    aic,
    bic,
  };
}

/**
 * Calculate EWMA volatility
 */
export function calculateEWMAVolatility(
  returns: number[],
  lambda: number = 0.94,
  annualizationFactor: number = 252
): number[] {
  const n = returns.length;
  const volatilities: number[] = [];

  // Initialize with sample variance
  const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
  let variance = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);

  for (let t = 0; t < n; t++) {
    if (t > 0) {
      const prevReturn = returns[t - 1] - meanReturn;
      variance = lambda * variance + (1 - lambda) * prevReturn * prevReturn;
    }
    volatilities.push(Math.sqrt(variance * annualizationFactor));
  }

  return volatilities;
}

/**
 * Identify volatility regimes
 */
export function identifyVolatilityRegimes(
  returns: number[],
  numRegimes: number = 3,
  windowSize: number = 60
): VolatilityRegime[] {
  // Calculate rolling volatility
  const rollingVol: number[] = [];
  const timestamps: Date[] = [];

  for (let i = windowSize - 1; i < returns.length; i++) {
    const window = returns.slice(i - windowSize + 1, i + 1);
    const mean = window.reduce((sum, r) => sum + r, 0) / window.length;
    const variance =
      window.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (window.length - 1);
    rollingVol.push(Math.sqrt(variance * 252)); // Annualized
    timestamps.push(new Date(Date.now() + i * 24 * 60 * 60 * 1000)); // Mock timestamps
  }

  // Simple k-means clustering on absolute returns
  const absReturns = returns.map(ret => Math.abs(ret));
  const sortedReturns = [...absReturns].sort((a, b) => a - b);

  // Define regime thresholds
  const thresholds: number[] = [];
  for (let i = 1; i < numRegimes; i++) {
    const index = Math.floor((i / numRegimes) * sortedReturns.length);
    thresholds.push(sortedReturns[index]);
  }

  // Classify returns into regimes
  const regimeSequence = absReturns.map(absRet => {
    for (let i = 0; i < thresholds.length; i++) {
      if (absRet <= thresholds[i]) {
        return i;
      }
    }
    return numRegimes - 1;
  });

  // Calculate regime statistics
  const regimes: VolatilityRegime[] = [];
  for (let regime = 0; regime < numRegimes; regime++) {
    const regimeIndices = regimeSequence
      .map((r, idx) => (r === regime ? idx : -1))
      .filter(idx => idx !== -1);

    if (regimeIndices.length > 0) {
      const regimeVolatilities = regimeIndices.map(idx =>
        idx < rollingVol.length ? rollingVol[idx] : 0
      );
      const avgVol =
        regimeVolatilities.reduce((sum, vol) => sum + vol, 0) / regimeVolatilities.length;

      regimes.push({
        regime,
        startDate: new Date(Date.now()),
        endDate: new Date(Date.now() + regimeIndices.length * 24 * 60 * 60 * 1000),
        averageVolatility: avgVol,
        observations: regimeIndices.length,
      });
    }
  }

  return regimes;
}

/**
 * Calculate volatility term structure
 */
export function calculateVolatilityTermStructure(
  spotVol: number,
  maturities: number[],
  meanReversion: number = 0.5
): VolatilityTerm[] {
  return maturities.map(maturity => {
    // Simple mean reversion model for term structure
    const timeToMaturity = maturity / 365; // Convert to years
    const termVolatility = spotVol * Math.exp(-meanReversion * timeToMaturity);

    return {
      maturity,
      impliedVolatility: Math.max(termVolatility, 0.01), // Floor at 1%
      confidence: Math.exp(-timeToMaturity), // Confidence decreases with maturity
    };
  });
}

/**
 * Calculate volatility smile/skew parameters
 */
export function calculateVolatilitySmile(
  strikes: number[],
  spotPrice: number,
  impliedVols: number[]
): {
  atmVolatility: number;
  skew: number;
  convexity: number;
  riskReversal: number;
} {
  if (strikes.length !== impliedVols.length || strikes.length < 3) {
    throw new Error('Need at least 3 strikes with corresponding implied volatilities');
  }

  // Find ATM volatility
  const atmIndex = strikes.reduce(
    (closest, strike, idx) =>
      Math.abs(strike - spotPrice) < Math.abs(strikes[closest] - spotPrice) ? idx : closest,
    0
  );
  const atmVolatility = impliedVols[atmIndex];

  // Calculate skew (derivative at ATM)
  let skew = 0;
  if (atmIndex > 0 && atmIndex < strikes.length - 1) {
    const deltaStrike = strikes[atmIndex + 1] - strikes[atmIndex - 1];
    const deltaVol = impliedVols[atmIndex + 1] - impliedVols[atmIndex - 1];
    skew = deltaVol / deltaStrike;
  }

  // Calculate convexity (second derivative)
  let convexity = 0;
  if (atmIndex > 0 && atmIndex < strikes.length - 1) {
    const h = strikes[atmIndex + 1] - strikes[atmIndex];
    convexity =
      (impliedVols[atmIndex + 1] - 2 * impliedVols[atmIndex] + impliedVols[atmIndex - 1]) / (h * h);
  }

  // Risk reversal (put-call vol difference)
  const otmPutIndex = strikes.findIndex(strike => strike < spotPrice * 0.9);
  const otmCallIndex = strikes.findIndex(strike => strike > spotPrice * 1.1);
  let riskReversal = 0;

  if (otmPutIndex !== -1 && otmCallIndex !== -1) {
    riskReversal = impliedVols[otmCallIndex] - impliedVols[otmPutIndex];
  }

  return {
    atmVolatility,
    skew,
    convexity,
    riskReversal,
  };
}

/**
 * Estimate Heston stochastic volatility model parameters
 */
export function estimateHestonParameters(
  returns: number[],
  maxIterations: number = 100
): HestonParameters {
  const n = returns.length;

  if (n < 10) {
    throw new Error('Need at least 10 observations for Heston parameter estimation');
  }

  // Initial parameter estimates
  let kappa = 2.0; // Mean reversion speed
  let theta = 0.04; // Long-term variance
  let sigma = 0.3; // Volatility of variance
  let rho = -0.5; // Correlation
  let v0 = 0.04; // Initial variance

  // Calculate sample statistics for initialization
  const meanReturn = returns.reduce((sum, r) => sum + r, 0) / n;
  const sampleVariance = returns.reduce((sum, r) => sum + Math.pow(r - meanReturn, 2), 0) / (n - 1);

  theta = sampleVariance;
  v0 = sampleVariance;

  let logLikelihood = -Infinity;

  for (let iter = 0; iter < maxIterations; iter++) {
    let newLogLikelihood = 0;
    let currentVariance = v0;

    for (let t = 1; t < n; t++) {
      const dt = 1.0; // Assuming daily data
      const prevReturn = returns[t - 1];

      // Euler discretization of variance process
      const dW1 = Math.random() - 0.5; // Simplified random shock
      const dW2 = rho * dW1 + Math.sqrt(1 - rho * rho) * (Math.random() - 0.5);

      const varianceChange =
        kappa * (theta - currentVariance) * dt +
        sigma * Math.sqrt(Math.max(currentVariance, 0)) * dW2;

      currentVariance = Math.max(currentVariance + varianceChange, 0.001);

      // Log-likelihood contribution (simplified)
      const expectedReturn = meanReturn;
      const variance = currentVariance;

      if (variance > 0) {
        newLogLikelihood -= 0.5 * Math.log(2 * Math.PI * variance);
        newLogLikelihood -= (0.5 * Math.pow(returns[t] - expectedReturn, 2)) / variance;
      }
    }

    // Check for convergence
    if (Math.abs(newLogLikelihood - logLikelihood) < 1e-6) {
      break;
    }

    logLikelihood = newLogLikelihood;

    // Simple parameter updates (in practice, use maximum likelihood estimation)
    const learningRate = 0.001;
    kappa = Math.max(0.1, Math.min(10, kappa + learningRate));
    theta = Math.max(0.001, Math.min(1, theta + learningRate));
    sigma = Math.max(0.01, Math.min(2, sigma + learningRate));
    rho = Math.max(-0.99, Math.min(0.99, rho + learningRate * 0.1));
    v0 = Math.max(0.001, Math.min(1, v0 + learningRate));
  }

  return {
    kappa,
    theta,
    sigma,
    rho,
    v0,
    logLikelihood,
  };
}

/**
 * Calculate volatility risk metrics
 */
export function calculateVolatilityRisk(
  returns: number[],
  confidenceLevel: number = 0.05
): {
  volatilityVaR: number;
  expectedShortfall: number;
  maxVolatility: number;
  volatilityVolatility: number;
} {
  // Calculate rolling volatilities
  const windowSize = 30;
  const volatilities: number[] = [];

  for (let i = windowSize - 1; i < returns.length; i++) {
    const window = returns.slice(i - windowSize + 1, i + 1);
    const mean = window.reduce((sum, r) => sum + r, 0) / window.length;
    const variance =
      window.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / (window.length - 1);
    volatilities.push(Math.sqrt(variance * 252)); // Annualized
  }

  // Sort volatilities for VaR calculation
  const sortedVols = [...volatilities].sort((a, b) => b - a); // Descending order
  const varIndex = Math.floor(confidenceLevel * sortedVols.length);
  const volatilityVaR = sortedVols[varIndex];

  // Expected shortfall (average of worst volatilities)
  const esVols = sortedVols.slice(0, varIndex + 1);
  const expectedShortfall = esVols.reduce((sum, vol) => sum + vol, 0) / esVols.length;

  // Maximum volatility
  const maxVolatility = Math.max(...volatilities);

  // Volatility of volatility
  const meanVol = volatilities.reduce((sum, vol) => sum + vol, 0) / volatilities.length;
  const volVariance =
    volatilities.reduce((sum, vol) => sum + Math.pow(vol - meanVol, 2), 0) /
    (volatilities.length - 1);
  const volatilityVolatility = Math.sqrt(volVariance);

  return {
    volatilityVaR,
    expectedShortfall,
    maxVolatility,
    volatilityVolatility,
  };
}

/**
 * Fix Yang-Zhang volatility calculation
 */
export function calculateYangZhangVolatility(
  ohlcv: OHLCVData[],
  annualizationFactor: number = 252
): number {
  if (ohlcv.length < 2) {
    throw new Error('Need at least 2 observations for Yang-Zhang volatility calculation');
  }

  const n = ohlcv.length;
  let overnightSum = 0;
  let openToCloseSum = 0;
  let rogersSatchellSum = 0;

  for (let i = 1; i < n; i++) {
    const prev = ohlcv[i - 1];
    const curr = ohlcv[i];

    // Overnight return (close to open)
    const overnight = Math.log(curr.open / prev.close);
    overnightSum += overnight * overnight;

    // Open to close return
    const openToClose = Math.log(curr.close / curr.open);
    openToCloseSum += openToClose * openToClose;

    // Rogers-Satchell component
    const logHighOpen = Math.log(curr.high / curr.open);
    const logHighClose = Math.log(curr.high / curr.close);
    const logLowOpen = Math.log(curr.low / curr.open);
    const logLowClose = Math.log(curr.low / curr.close);
    rogersSatchellSum += logHighOpen * logHighClose + logLowOpen * logLowClose;
  }

  // Yang-Zhang estimator
  const k = 0.34 / (1.34 + (n + 1) / (n - 1)); // Drift adjustment factor
  const yangZhangVariance =
    overnightSum / (n - 1) +
    (k * openToCloseSum) / (n - 1) +
    ((1 - k) * rogersSatchellSum) / (n - 1);

  return Math.sqrt(yangZhangVariance * annualizationFactor);
}

/**
 * Parkinson volatility estimator
 */
export function parkinsonVolatility(ohlcv: OHLCVData[], annualizationFactor: number = 252): number {
  if (ohlcv.length < 2) {
    return 0;
  }
  const sum = ohlcv.slice(1).reduce((acc, curr) => {
    const range = Math.log(curr.high / curr.low);
    return acc + range * range;
  }, 0);
  return Math.sqrt((sum / (ohlcv.length - 1)) * annualizationFactor);
}

/**
 * Black-Scholes option pricing model
 */
function blackScholes(
  spotPrice: number,
  strikePrice: number,
  timeToExpiry: number,
  volatility: number,
  riskFreeRate: number,
  optionType: 'call' | 'put'
): number {
  const d1 =
    (Math.log(spotPrice / strikePrice) +
      (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
    (volatility * Math.sqrt(timeToExpiry));
  const d2 = d1 - volatility * Math.sqrt(timeToExpiry);

  if (optionType === 'call') {
    return (
      spotPrice * normalCDF(d1) -
      strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(d2)
    );
  } else {
    return (
      strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2) -
      spotPrice * normalCDF(-d1)
    );
  }
}

/**
 * Normal cumulative distribution function
 */
function normalCDF(x: number): number {
  const a1 = 0.254829592;
  const a2 = -0.284496736;
  const a3 = 1.421060743;
  const a4 = -1.453152027;
  const a5 = 1.061405429;
  const p = 0.3275911;

  const sign = x < 0 ? -1 : 1;
  const absX = Math.abs(x);
  const t = 1 / (1 + p * absX);
  const y =
    1 -
    (a1 * t + a2 * t * t + a3 * t * t * t + a4 * t * t * t * t + a5 * t * t * t * t * t) *
      Math.exp((-absX * absX) / 2);

  return 0.5 * (1 + sign * y);
}

/**
 * Forecast volatility using EWMA
 */
export function forecastVolatilityEWMA(
  volatilities: number[],
  lambda: number = 0.94,
  forecastHorizon: number = 1
): number {
  if (volatilities.length === 0) {
    return 0;
  }

  let forecast = volatilities[volatilities.length - 1];
  for (let i = 0; i < forecastHorizon; i++) {
    forecast = lambda * forecast + (1 - lambda) * forecast; // Using the last value as the long-term average
  }
  return forecast;
}
