/**
 * Options Pricing Models
 * Implementation of various options pricing models and Greeks calculations
 */

export interface OptionParameters {
  spotPrice: number;
  strikePrice: number;
  timeToExpiry: number; // in years
  riskFreeRate: number;
  volatility: number;
  dividendYield?: number;
}

export interface OptionPricing {
  callPrice: number;
  putPrice: number;
  intrinsicValueCall: number;
  intrinsicValuePut: number;
  timeValueCall: number;
  timeValuePut: number;
}

export interface GreeksCalculation {
  delta: number;
  gamma: number;
  theta: number;
  vega: number;
  rho: number;
}

export interface ImpliedVolatilityResult {
  impliedVolatility: number;
  iterations: number;
  converged: boolean;
}

/**
 * Black-Scholes option pricing model
 */
export function blackScholes(params: OptionParameters): OptionPricing {
  const {
    spotPrice,
    strikePrice,
    timeToExpiry,
    riskFreeRate,
    volatility,
    dividendYield = 0,
  } = params;

  if (timeToExpiry <= 0) {
    const intrinsicValueCall = Math.max(spotPrice - strikePrice, 0);
    const intrinsicValuePut = Math.max(strikePrice - spotPrice, 0);

    return {
      callPrice: intrinsicValueCall,
      putPrice: intrinsicValuePut,
      intrinsicValueCall,
      intrinsicValuePut,
      timeValueCall: 0,
      timeValuePut: 0,
    };
  }

  const d1 =
    (Math.log(spotPrice / strikePrice) +
      (riskFreeRate - dividendYield + 0.5 * volatility * volatility) * timeToExpiry) /
    (volatility * Math.sqrt(timeToExpiry));
  const d2 = d1 - volatility * Math.sqrt(timeToExpiry);

  const nd1 = normalCDF(d1);
  const nd2 = normalCDF(d2);
  const nMinusd1 = normalCDF(-d1);
  const nMinusd2 = normalCDF(-d2);

  const callPrice =
    spotPrice * Math.exp(-dividendYield * timeToExpiry) * nd1 -
    strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nd2;

  const putPrice =
    strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nMinusd2 -
    spotPrice * Math.exp(-dividendYield * timeToExpiry) * nMinusd1;

  const intrinsicValueCall = Math.max(spotPrice - strikePrice, 0);
  const intrinsicValuePut = Math.max(strikePrice - spotPrice, 0);

  const timeValueCall = callPrice - intrinsicValueCall;
  const timeValuePut = putPrice - intrinsicValuePut;

  return {
    callPrice,
    putPrice,
    intrinsicValueCall,
    intrinsicValuePut,
    timeValueCall,
    timeValuePut,
  };
}

export function impliedVolatility(
  price: number,
  S: number,
  K: number,
  T: number,
  r: number,
  isCall = true
): number {
  // …Newton–Raphson on σ to match blackScholesPrice
  let sigma = 0.2; // Initial guess for volatility
  const tolerance = 1e-6;
  const maxIterations = 100;
  let iteration = 0;
  let priceDiff = 1; // Initialize to a non-zero value
  while (Math.abs(priceDiff) > tolerance && iteration < maxIterations) {
    const params: OptionParameters = {
      spotPrice: S,
      strikePrice: K,
      timeToExpiry: T,
      riskFreeRate: r,
      volatility: sigma,
    };

    const calculatedPrice = isCall ? blackScholes(params).callPrice : blackScholes(params).putPrice;
    priceDiff = calculatedPrice - price;

    // Calculate Vega
    const greeks = calculateGreeks(params, isCall ? 'call' : 'put');
    const vega = greeks.vega * 100; // Convert from percentage to absolute

    if (vega === 0) {
      break; // Avoid division by zero
    }

    sigma -= priceDiff / vega; // Update volatility estimate
    iteration++;
  }
  if (iteration === maxIterations) {
    console.warn('Implied volatility calculation did not converge');
  }

  if (sigma < 0) {
    console.warn('Calculated implied volatility is negative, returning 0');
    return 0;
  }

  if (sigma > 10) {
    console.warn('Calculated implied volatility is too high, returning 10');
    return 10; // Cap at a reasonable maximum
  }
  if (isNaN(sigma)) {
    console.warn('Calculated implied volatility is NaN, returning 0');
    return 0;
  }
  return sigma;
}

/**
 * Calculate option Greeks using Black-Scholes model
 */
export function calculateGreeks(
  params: OptionParameters,
  optionType: 'call' | 'put' = 'call'
): GreeksCalculation {
  const {
    spotPrice,
    strikePrice,
    timeToExpiry,
    riskFreeRate,
    volatility,
    dividendYield = 0,
  } = params;

  if (timeToExpiry <= 0) {
    return {
      delta:
        optionType === 'call'
          ? spotPrice > strikePrice
            ? 1
            : 0
          : spotPrice < strikePrice
            ? -1
            : 0,
      gamma: 0,
      theta: 0,
      vega: 0,
      rho: 0,
    };
  }

  const d1 =
    (Math.log(spotPrice / strikePrice) +
      (riskFreeRate - dividendYield + 0.5 * volatility * volatility) * timeToExpiry) /
    (volatility * Math.sqrt(timeToExpiry));
  const d2 = d1 - volatility * Math.sqrt(timeToExpiry);

  const nd1 = normalCDF(d1);
  const nd2 = normalCDF(d2);
  const npd1 = normalPDF(d1);

  // Delta
  const callDelta = Math.exp(-dividendYield * timeToExpiry) * nd1;
  const putDelta = Math.exp(-dividendYield * timeToExpiry) * (nd1 - 1);
  const delta = optionType === 'call' ? callDelta : putDelta;

  // Gamma (same for calls and puts)
  const gamma =
    (Math.exp(-dividendYield * timeToExpiry) * npd1) /
    (spotPrice * volatility * Math.sqrt(timeToExpiry));

  // Theta
  const term1 =
    -(spotPrice * npd1 * volatility * Math.exp(-dividendYield * timeToExpiry)) /
    (2 * Math.sqrt(timeToExpiry));
  const term2Call = riskFreeRate * strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * nd2;
  const term2Put =
    -riskFreeRate * strikePrice * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2);
  const term3 =
    dividendYield *
    spotPrice *
    Math.exp(-dividendYield * timeToExpiry) *
    (optionType === 'call' ? nd1 : normalCDF(-d1));

  const theta =
    optionType === 'call' ? (term1 - term2Call + term3) / 365 : (term1 + term2Put + term3) / 365;

  // Vega (same for calls and puts)
  const vega =
    (spotPrice * Math.exp(-dividendYield * timeToExpiry) * npd1 * Math.sqrt(timeToExpiry)) / 100;

  // Rho
  const callRho = (strikePrice * timeToExpiry * Math.exp(-riskFreeRate * timeToExpiry) * nd2) / 100;
  const putRho =
    (-strikePrice * timeToExpiry * Math.exp(-riskFreeRate * timeToExpiry) * normalCDF(-d2)) / 100;
  const rho = optionType === 'call' ? callRho : putRho;

  return {
    delta,
    gamma,
    theta,
    vega,
    rho,
  };
}

/**
 * Calculate implied volatility using Newton-Raphson method
 */
export function calculateImpliedVolatility(
  marketPrice: number,
  spotPrice: number,
  strikePrice: number,
  timeToExpiry: number,
  riskFreeRate: number,
  optionType: 'call' | 'put' = 'call',
  dividendYield: number = 0,
  initialGuess: number = 0.2,
  tolerance: number = 1e-6,
  maxIterations: number = 100
): ImpliedVolatilityResult {
  let volatility = initialGuess;
  let iterations = 0;
  let converged = false;

  for (let i = 0; i < maxIterations; i++) {
    iterations = i + 1;

    const params: OptionParameters = {
      spotPrice,
      strikePrice,
      timeToExpiry,
      riskFreeRate,
      volatility,
      dividendYield,
    };

    const pricing = blackScholes(params);
    const theoreticalPrice = optionType === 'call' ? pricing.callPrice : pricing.putPrice;

    const priceDiff = theoreticalPrice - marketPrice;

    if (Math.abs(priceDiff) < tolerance) {
      converged = true;
      break;
    }

    // Calculate vega for Newton-Raphson
    const greeks = calculateGreeks(params, optionType);
    const vega = greeks.vega * 100; // Convert back from percentage

    if (Math.abs(vega) < 1e-10) {
      break; // Avoid division by zero
    }

    volatility = volatility - priceDiff / vega;

    // Keep volatility within reasonable bounds
    volatility = Math.max(0.001, Math.min(volatility, 10));
  }

  return {
    impliedVolatility: volatility,
    iterations,
    converged,
  };
}

/**
 * Binomial option pricing model
 */
export function binomialOptionPricing(
  params: OptionParameters,
  optionType: 'call' | 'put' = 'call',
  americanStyle: boolean = false,
  steps: number = 100
): OptionPricing {
  const {
    spotPrice,
    strikePrice,
    timeToExpiry,
    riskFreeRate,
    volatility,
    dividendYield = 0,
  } = params;

  const dt = timeToExpiry / steps;
  const u = Math.exp(volatility * Math.sqrt(dt));
  const d = 1 / u;
  const p = (Math.exp((riskFreeRate - dividendYield) * dt) - d) / (u - d);
  const discount = Math.exp(-riskFreeRate * dt);

  // Create price tree
  const stockPrices: number[][] = [];
  for (let i = 0; i <= steps; i++) {
    stockPrices[i] = [];
    for (let j = 0; j <= i; j++) {
      stockPrices[i][j] = spotPrice * Math.pow(u, i - j) * Math.pow(d, j);
    }
  }

  // Calculate option values at expiration
  const optionValues: number[][] = [];
  for (let i = 0; i <= steps; i++) {
    optionValues[i] = [];
  }

  for (let j = 0; j <= steps; j++) {
    if (optionType === 'call') {
      optionValues[steps][j] = Math.max(stockPrices[steps][j] - strikePrice, 0);
    } else {
      optionValues[steps][j] = Math.max(strikePrice - stockPrices[steps][j], 0);
    }
  }

  // Work backwards through the tree
  for (let i = steps - 1; i >= 0; i--) {
    for (let j = 0; j <= i; j++) {
      // European option value
      const holdValue =
        discount * (p * optionValues[i + 1][j] + (1 - p) * optionValues[i + 1][j + 1]);

      if (americanStyle) {
        // American option - can exercise early
        const exerciseValue =
          optionType === 'call'
            ? Math.max(stockPrices[i][j] - strikePrice, 0)
            : Math.max(strikePrice - stockPrices[i][j], 0);

        optionValues[i][j] = Math.max(holdValue, exerciseValue);
      } else {
        optionValues[i][j] = holdValue;
      }
    }
  }

  const price = optionValues[0][0];
  const intrinsicValue =
    optionType === 'call'
      ? Math.max(spotPrice - strikePrice, 0)
      : Math.max(strikePrice - spotPrice, 0);
  const timeValue = price - intrinsicValue;

  if (optionType === 'call') {
    return {
      callPrice: price,
      putPrice: 0, // Not calculated
      intrinsicValueCall: intrinsicValue,
      intrinsicValuePut: 0,
      timeValueCall: timeValue,
      timeValuePut: 0,
    };
  } else {
    return {
      callPrice: 0, // Not calculated
      putPrice: price,
      intrinsicValueCall: 0,
      intrinsicValuePut: intrinsicValue,
      timeValueCall: 0,
      timeValuePut: timeValue,
    };
  }
}

/**
 * Monte Carlo option pricing
 */
export function monteCarloOptionPricing(
  params: OptionParameters,
  optionType: 'call' | 'put' = 'call',
  numSimulations: number = 100000
): OptionPricing {
  const {
    spotPrice,
    strikePrice,
    timeToExpiry,
    riskFreeRate,
    volatility,
    dividendYield = 0,
  } = params;

  let totalPayoff = 0;

  for (let i = 0; i < numSimulations; i++) {
    // Generate random price path
    const z = boxMullerTransform();
    const finalPrice =
      spotPrice *
      Math.exp(
        (riskFreeRate - dividendYield - 0.5 * volatility * volatility) * timeToExpiry +
          volatility * Math.sqrt(timeToExpiry) * z
      );

    // Calculate payoff
    const payoff =
      optionType === 'call'
        ? Math.max(finalPrice - strikePrice, 0)
        : Math.max(strikePrice - finalPrice, 0);

    totalPayoff += payoff;
  }

  const averagePayoff = totalPayoff / numSimulations;
  const price = averagePayoff * Math.exp(-riskFreeRate * timeToExpiry);

  const intrinsicValue =
    optionType === 'call'
      ? Math.max(spotPrice - strikePrice, 0)
      : Math.max(strikePrice - spotPrice, 0);
  const timeValue = price - intrinsicValue;

  if (optionType === 'call') {
    return {
      callPrice: price,
      putPrice: 0,
      intrinsicValueCall: intrinsicValue,
      intrinsicValuePut: 0,
      timeValueCall: timeValue,
      timeValuePut: 0,
    };
  } else {
    return {
      callPrice: 0,
      putPrice: price,
      intrinsicValueCall: 0,
      intrinsicValuePut: intrinsicValue,
      timeValueCall: 0,
      timeValuePut: timeValue,
    };
  }
}

/**
 * Calculate option portfolio risk metrics
 */
export function calculateOptionPortfolioRisk(
  positions: Array<{
    optionType: 'call' | 'put';
    quantity: number;
    params: OptionParameters;
  }>
): {
  totalDelta: number;
  totalGamma: number;
  totalTheta: number;
  totalVega: number;
  totalRho: number;
  portfolioValue: number;
} {
  let totalDelta = 0;
  let totalGamma = 0;
  let totalTheta = 0;
  let totalVega = 0;
  let totalRho = 0;
  let portfolioValue = 0;

  for (const position of positions) {
    const greeks = calculateGreeks(position.params, position.optionType);
    const pricing = blackScholes(position.params);
    const optionPrice = position.optionType === 'call' ? pricing.callPrice : pricing.putPrice;

    totalDelta += greeks.delta * position.quantity;
    totalGamma += greeks.gamma * position.quantity;
    totalTheta += greeks.theta * position.quantity;
    totalVega += greeks.vega * position.quantity;
    totalRho += greeks.rho * position.quantity;
    portfolioValue += optionPrice * position.quantity;
  }

  return {
    totalDelta,
    totalGamma,
    totalTheta,
    totalVega,
    totalRho,
    portfolioValue,
  };
}

/**
 * Volatility surface interpolation
 */
export function interpolateVolatilitySurface(
  strikes: number[],
  expiries: number[],
  volatilities: number[][],
  targetStrike: number,
  targetExpiry: number
): number {
  // Simplified bilinear interpolation
  // In production, use more sophisticated interpolation methods

  // Find surrounding points
  let strikeIndex = 0;
  let expiryIndex = 0;

  for (let i = 0; i < strikes.length - 1; i++) {
    if (targetStrike >= strikes[i] && targetStrike <= strikes[i + 1]) {
      strikeIndex = i;
      break;
    }
  }

  for (let i = 0; i < expiries.length - 1; i++) {
    if (targetExpiry >= expiries[i] && targetExpiry <= expiries[i + 1]) {
      expiryIndex = i;
      break;
    }
  }

  // Bilinear interpolation
  const x1 = strikes[strikeIndex];
  const x2 = strikes[strikeIndex + 1];
  const y1 = expiries[expiryIndex];
  const y2 = expiries[expiryIndex + 1];

  const q11 = volatilities[expiryIndex][strikeIndex];
  const q12 = volatilities[expiryIndex + 1][strikeIndex];
  const q21 = volatilities[expiryIndex][strikeIndex + 1];
  const q22 = volatilities[expiryIndex + 1][strikeIndex + 1];

  const wx = (targetStrike - x1) / (x2 - x1);
  const wy = (targetExpiry - y1) / (y2 - y1);

  return q11 * (1 - wx) * (1 - wy) + q21 * wx * (1 - wy) + q12 * (1 - wx) * wy + q22 * wx * wy;
}

// Helper functions

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

/**
 * Normal probability density function
 */
function normalPDF(x: number): number {
  return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
}

/**
 * Error function approximation
 */
function erf(x: number): number {
  // Abramowitz and Stegun approximation
  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;
}

/**
 * Box-Muller transformation for normal random numbers
 */
function boxMullerTransform(): number {
  let u1 = Math.random();
  const u2 = Math.random();

  // Ensure u1 is not zero
  while (u1 === 0) {
    u1 = Math.random();
  }

  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

/**
 * Prices a straddle option strategy
 */
export function straddle(params: OptionParameters): {
  callPrice: number;
  putPrice: number;
  strategyCost: number;
} {
  const callOption = blackScholes(params);
  const putOption = blackScholes(params);
  const strategyCost = callOption.callPrice + putOption.putPrice;

  return {
    callPrice: callOption.callPrice,
    putPrice: putOption.putPrice,
    strategyCost: strategyCost,
  };
}

/**
 * Prices a strangle option strategy
 */
export function strangle(
  callParams: OptionParameters,
  putParams: OptionParameters
): { callPrice: number; putPrice: number; strategyCost: number } {
  const callOption = blackScholes(callParams);
  const putOption = blackScholes(putParams);
  const strategyCost = callOption.callPrice + putOption.putPrice;

  return {
    callPrice: callOption.callPrice,
    putPrice: putOption.putPrice,
    strategyCost: strategyCost,
  };
}

/**
 * Prices a butterfly option strategy
 */
export function butterfly(
  lowerStrikeParams: OptionParameters,
  middleStrikeParams: OptionParameters,
  upperStrikeParams: OptionParameters
): {
  lowerCallPrice: number;
  middleCallPrice: number;
  upperCallPrice: number;
  strategyCost: number;
} {
  const lowerCall = blackScholes(lowerStrikeParams);
  const middleCall = blackScholes(middleStrikeParams);
  const upperCall = blackScholes(upperStrikeParams);

  const strategyCost = lowerCall.callPrice - 2 * middleCall.callPrice + upperCall.callPrice;

  return {
    lowerCallPrice: lowerCall.callPrice,
    middleCallPrice: middleCall.callPrice,
    upperCallPrice: upperCall.callPrice,
    strategyCost: strategyCost,
  };
}

/**
 * Prices a condor option strategy
 */
export function condor(
  lowerStrikeParams: OptionParameters,
  middleLowerStrikeParams: OptionParameters,
  middleUpperStrikeParams: OptionParameters,
  upperStrikeParams: OptionParameters
): {
  lowerCallPrice: number;
  middleLowerCallPrice: number;
  middleUpperCallPrice: number;
  upperCallPrice: number;
  strategyCost: number;
} {
  const lowerCall = blackScholes(lowerStrikeParams);
  const middleLowerCall = blackScholes(middleLowerStrikeParams);
  const middleUpperCall = blackScholes(middleUpperStrikeParams);
  const upperCall = blackScholes(upperStrikeParams);

  const strategyCost =
    lowerCall.callPrice -
    middleLowerCall.callPrice -
    middleUpperCall.callPrice +
    upperCall.callPrice;

  return {
    lowerCallPrice: lowerCall.callPrice,
    middleLowerCallPrice: middleLowerCall.callPrice,
    middleUpperCallPrice: middleUpperCall.callPrice,
    upperCallPrice: upperCall.callPrice,
    strategyCost: strategyCost,
  };
}

/**
 * Calculates combined Greeks for an option strategy
 */
export function calculateStrategyGreeks(
  positions: Array<{
    optionType: 'call' | 'put';
    quantity: number;
    params: OptionParameters;
  }>
): GreeksCalculation {
  let totalDelta = 0;
  let totalGamma = 0;
  let totalTheta = 0;
  let totalVega = 0;
  let totalRho = 0;

  for (const position of positions) {
    const greeks = calculateGreeks(position.params, position.optionType);

    totalDelta += greeks.delta * position.quantity;
    totalGamma += greeks.gamma * position.quantity;
    totalTheta += greeks.theta * position.quantity;
    totalVega += greeks.vega * position.quantity;
    totalRho += greeks.rho * position.quantity;
  }

  return {
    delta: totalDelta,
    gamma: totalGamma,
    theta: totalTheta,
    vega: totalVega,
    rho: totalRho,
  };
}

/**
 * Black-Scholes option pricing model with greeks
 */
export function blackScholesWithGreeks(
  params: OptionParameters,
  optionType: 'call' | 'put' = 'call'
): { pricing: OptionPricing; greeks: GreeksCalculation } {
  const pricing = blackScholes(params);
  const greeks = calculateGreeks(params, optionType);
  return { pricing, greeks };
}

/**
 * Calculates the breakeven point for a call option at expiration
 */
export function callBreakeven(strikePrice: number, callPrice: number): number {
  return strikePrice + callPrice;
}

/**
 * Calculates the breakeven point for a put option at expiration
 */
export function putBreakeven(strikePrice: number, putPrice: number): number {
  return strikePrice - putPrice;
}

/**
 * Estimates the probability of profit for a call option at expiration
 */
export function callProbabilityOfProfit(
  spotPrice: number,
  strikePrice: number,
  timeToExpiry: number,
  riskFreeRate: number,
  volatility: number
): number {
  const d1 =
    (Math.log(spotPrice / strikePrice) +
      (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
    (volatility * Math.sqrt(timeToExpiry));
  return normalCDF(d1);
}

/**
 * Estimates the probability of profit for a put option at expiration
 */
export function putProbabilityOfProfit(
  spotPrice: number,
  strikePrice: number,
  timeToExpiry: number,
  riskFreeRate: number,
  volatility: number
): number {
  const d1 =
    (Math.log(spotPrice / strikePrice) +
      (riskFreeRate + 0.5 * volatility * volatility) * timeToExpiry) /
    (volatility * Math.sqrt(timeToExpiry));
  return 1 - normalCDF(d1);
}
