/**
 * Portfolio Analytics
 * Advanced portfolio analysis and optimization tools
 */

import { OHLCVData, PriceData } from './index';

export interface PortfolioPosition {
  symbol: string;
  shares: number;
  price: number;
  value: number;
  weight: number;
}

export interface PortfolioAnalysis {
  totalValue: number;
  totalReturn: number;
  volatility: number;
  sharpeRatio: number;
  maxDrawdown: number;
  var95: number;
  beta: number;
  alpha: number;
  treynorRatio: number;
  informationRatio: number;
  trackingError: number;
}

export interface AssetAllocation {
  symbol: string;
  targetWeight: number;
  currentWeight: number;
  difference: number;
  rebalanceAmount: number;
}

export interface PortfolioOptimizationResult {
  weights: number[];
  expectedReturn: number;
  volatility: number;
  sharpeRatio: number;
  symbols: string[];
}

/**
 * Calculate portfolio value and weights
 */
export function calculatePortfolioMetrics(positions: PortfolioPosition[]): {
  totalValue: number;
  weights: number[];
  concentrationRisk: number;
} {
  const totalValue = positions.reduce((sum, pos) => sum + pos.value, 0);
  const weights = positions.map(pos => pos.value / totalValue);

  // Calculate Herfindahl-Hirschman Index for concentration risk
  const concentrationRisk = weights.reduce((sum, weight) => sum + weight * weight, 0);

  return {
    totalValue,
    weights,
    concentrationRisk,
  };
}

/**
 * Calculate portfolio returns from position returns
 */
export function calculatePortfolioReturns(assetReturns: number[][], weights: number[]): number[] {
  if (assetReturns.length === 0 || weights.length !== assetReturns[0].length) {
    return [];
  }

  const portfolioReturns: number[] = [];

  for (let i = 0; i < assetReturns.length; i++) {
    let portfolioReturn = 0;
    for (let j = 0; j < weights.length; j++) {
      portfolioReturn += weights[j] * assetReturns[i][j];
    }
    portfolioReturns.push(portfolioReturn);
  }

  return portfolioReturns;
}

/**
 * Mean-Variance Optimization (Markowitz)
 */
export function markowitzOptimization(
  expectedReturns: number[],
  covarianceMatrix: number[][],
  riskFreeRate: number = 0.02,
  riskAversion: number = 1
): PortfolioOptimizationResult {
  const n = expectedReturns.length;

  // Simplified optimization using equal weights as baseline
  // In production, use proper quadratic programming solver
  const weights = new Array(n).fill(1 / n);

  const expectedReturn = weights.reduce((sum, weight, i) => sum + weight * expectedReturns[i], 0);

  // Calculate portfolio variance
  let portfolioVariance = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      portfolioVariance += weights[i] * weights[j] * covarianceMatrix[i][j];
    }
  }

  const volatility = Math.sqrt(portfolioVariance);
  const sharpeRatio = volatility > 0 ? (expectedReturn - riskFreeRate) / volatility : 0;

  return {
    weights,
    expectedReturn,
    volatility,
    sharpeRatio,
    symbols: [], // Would be filled with actual symbols
  };
}

/**
 * Black-Litterman Model
 */
export function blackLittermanOptimization(
  marketCaps: number[],
  covarianceMatrix: number[][],
  views: Array<{ assets: number[]; expectedReturn: number; confidence: number }>,
  riskAversion: number = 3,
  riskFreeRate: number = 0.02
): PortfolioOptimizationResult {
  const n = marketCaps.length;

  // Calculate market weights
  const totalMarketCap = marketCaps.reduce((sum, cap) => sum + cap, 0);
  const marketWeights = marketCaps.map(cap => cap / totalMarketCap);

  // Implied equilibrium returns
  const equilibriumReturns: number[] = [];
  for (let i = 0; i < n; i++) {
    let equilibriumReturn = 0;
    for (let j = 0; j < n; j++) {
      equilibriumReturn += riskAversion * covarianceMatrix[i][j] * marketWeights[j];
    }
    equilibriumReturns.push(equilibriumReturn);
  }

  // Simplified BL implementation - in production use proper matrix operations
  const weights = [...marketWeights]; // Start with market weights

  const expectedReturn = weights.reduce(
    (sum, weight, i) => sum + weight * equilibriumReturns[i],
    0
  );

  let portfolioVariance = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      portfolioVariance += weights[i] * weights[j] * covarianceMatrix[i][j];
    }
  }

  const volatility = Math.sqrt(portfolioVariance);
  const sharpeRatio = volatility > 0 ? (expectedReturn - riskFreeRate) / volatility : 0;

  return {
    weights,
    expectedReturn,
    volatility,
    sharpeRatio,
    symbols: [],
  };
}

/**
 * Risk Parity Portfolio
 */
export function riskParityOptimization(covarianceMatrix: number[][]): PortfolioOptimizationResult {
  const n = covarianceMatrix.length;

  // Start with equal weights
  let weights = new Array(n).fill(1 / n);

  // Iterative optimization for equal risk contribution
  const maxIterations = 100;
  const tolerance = 1e-8;

  for (let iter = 0; iter < maxIterations; iter++) {
    const riskContributions = calculateRiskContributions(weights, covarianceMatrix);
    const totalRisk = Math.sqrt(calculatePortfolioVariance(weights, covarianceMatrix));
    const targetRiskContribution = totalRisk / n;

    let converged = true;
    const newWeights = [...weights];

    for (let i = 0; i < n; i++) {
      const diff = riskContributions[i] - targetRiskContribution;
      if (Math.abs(diff) > tolerance) {
        converged = false;
        // Simple adjustment - in production use proper optimization
        newWeights[i] *= 1 - (diff / totalRisk) * 0.1;
      }
    }

    // Normalize weights
    const sum = newWeights.reduce((s, w) => s + w, 0);
    weights = newWeights.map(w => w / sum);

    if (converged) {
      break;
    }
  }

  const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix);
  const volatility = Math.sqrt(portfolioVariance);

  return {
    weights,
    expectedReturn: 0, // Not calculated for risk parity
    volatility,
    sharpeRatio: 0,
    symbols: [],
  };
}

/**
 * Calculate risk contributions for each asset
 */
export function calculateRiskContributions(
  weights: number[],
  covarianceMatrix: number[][]
): number[] {
  const n = weights.length;
  const riskContributions: number[] = [];

  const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix);
  const portfolioVolatility = Math.sqrt(portfolioVariance);

  for (let i = 0; i < n; i++) {
    let marginalContribution = 0;
    for (let j = 0; j < n; j++) {
      marginalContribution += weights[j] * covarianceMatrix[i][j];
    }

    const riskContribution = (weights[i] * marginalContribution) / portfolioVolatility;
    riskContributions.push(riskContribution);
  }

  return riskContributions;
}

/**
 * Calculate portfolio variance
 */
export function calculatePortfolioVariance(
  weights: number[],
  covarianceMatrix: number[][]
): number {
  const n = weights.length;
  let variance = 0;

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      variance += weights[i] * weights[j] * covarianceMatrix[i][j];
    }
  }

  return variance;
}

/**
 * Portfolio rebalancing analysis
 */
export function calculateRebalancing(
  currentPositions: PortfolioPosition[],
  targetWeights: number[],
  totalValue: number
): AssetAllocation[] {
  if (currentPositions.length !== targetWeights.length) {
    throw new Error('Number of positions must match number of target weights');
  }

  return currentPositions.map((position, index) => {
    const currentWeight = position.value / totalValue;
    const targetWeight = targetWeights[index];
    const difference = targetWeight - currentWeight;
    const rebalanceAmount = difference * totalValue;

    return {
      symbol: position.symbol,
      targetWeight,
      currentWeight,
      difference,
      rebalanceAmount,
    };
  });
}

/**
 * Factor model analysis (Fama-French)
 */
export function famaFrenchAnalysis(
  portfolioReturns: number[],
  marketReturns: number[],
  smbReturns: number[], // Small minus Big
  hmlReturns: number[], // High minus Low
  riskFreeRate: number = 0.02
): {
  alpha: number;
  marketBeta: number;
  sizeBeta: number;
  valueBeta: number;
  rSquared: number;
} {
  const n = portfolioReturns.length;

  // Excess returns
  const excessPortfolioReturns = portfolioReturns.map(r => r - riskFreeRate);
  const excessMarketReturns = marketReturns.map(r => r - riskFreeRate);

  // Simple linear regression (in production, use proper multiple regression)
  const meanExcessPortfolio = excessPortfolioReturns.reduce((sum, r) => sum + r, 0) / n;
  const meanExcessMarket = excessMarketReturns.reduce((sum, r) => sum + r, 0) / n;
  const meanSMB = smbReturns.reduce((sum, r) => sum + r, 0) / n;
  const meanHML = hmlReturns.reduce((sum, r) => sum + r, 0) / n;

  // Calculate market beta
  let covariance = 0;
  let marketVariance = 0;

  for (let i = 0; i < n; i++) {
    const portfolioDiff = excessPortfolioReturns[i] - meanExcessPortfolio;
    const marketDiff = excessMarketReturns[i] - meanExcessMarket;

    covariance += portfolioDiff * marketDiff;
    marketVariance += marketDiff * marketDiff;
  }

  const marketBeta = marketVariance > 0 ? covariance / marketVariance : 0;
  const alpha = meanExcessPortfolio - marketBeta * meanExcessMarket;

  return {
    alpha,
    marketBeta,
    sizeBeta: 0, // Simplified - would need proper regression
    valueBeta: 0, // Simplified - would need proper regression
    rSquared: 0, // Simplified - would need proper regression
  };
}

/**
 * Portfolio performance attribution
 */
export function performanceAttribution(
  portfolioReturns: number[],
  benchmarkReturns: number[],
  sectorWeights: number[][],
  sectorReturns: number[][]
): {
  totalActiveReturn: number;
  allocationEffect: number;
  selectionEffect: number;
  interactionEffect: number;
} {
  const n = portfolioReturns.length;

  const portfolioReturn = portfolioReturns.reduce((sum, r) => sum + r, 0) / n;
  const benchmarkReturn = benchmarkReturns.reduce((sum, r) => sum + r, 0) / n;
  const totalActiveReturn = portfolioReturn - benchmarkReturn;

  // Simplified attribution analysis
  let allocationEffect = 0;
  let selectionEffect = 0;
  let interactionEffect = 0;

  // This would require proper implementation with sector-level analysis
  // For now, return the total active return distributed equally
  allocationEffect = totalActiveReturn * 0.4;
  selectionEffect = totalActiveReturn * 0.4;
  interactionEffect = totalActiveReturn * 0.2;

  return {
    totalActiveReturn,
    allocationEffect,
    selectionEffect,
    interactionEffect,
  };
}

/**
 * Calculate Efficient Frontier points
 */
export function calculateEfficientFrontier(
  returns: number[][], // Array of return series for each asset
  symbols: string[],
  riskFreeRate: number = 0.02,
  numPoints: number = 50
): Array<{
  weights: number[];
  expectedReturn: number;
  volatility: number;
  sharpeRatio: number;
}> {
  if (returns.length !== symbols.length || returns.length < 2) {
    return [];
  }

  const n = returns.length;
  const results: Array<{
    weights: number[];
    expectedReturn: number;
    volatility: number;
    sharpeRatio: number;
  }> = [];

  // Calculate expected returns and covariance matrix
  const expectedReturns = returns.map(
    assetReturns => assetReturns.reduce((sum, ret) => sum + ret, 0) / assetReturns.length
  );

  const covarianceMatrix = calculateCovarianceMatrix(returns);

  // Generate target returns from min to max expected return
  const minReturn = Math.min(...expectedReturns);
  const maxReturn = Math.max(...expectedReturns);
  const returnStep = (maxReturn - minReturn) / (numPoints - 1);

  for (let i = 0; i < numPoints; i++) {
    const targetReturn = minReturn + i * returnStep;

    // Find minimum variance portfolio for target return using quadratic programming (simplified)
    const weights = findMinimumVarianceWeights(expectedReturns, covarianceMatrix, targetReturn);

    if (weights && weights.length === n) {
      const portfolioReturn = weights.reduce((sum, w, j) => sum + w * expectedReturns[j], 0);
      const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix);
      const portfolioVolatility = Math.sqrt(portfolioVariance);
      const sharpeRatio =
        portfolioVolatility > 0 ? (portfolioReturn - riskFreeRate) / portfolioVolatility : 0;

      results.push({
        weights,
        expectedReturn: portfolioReturn,
        volatility: portfolioVolatility,
        sharpeRatio,
      });
    }
  }

  return results.sort((a, b) => a.volatility - b.volatility);
}

/**
 * Find Minimum Variance Portfolio
 */
export function findMinimumVariancePortfolio(
  returns: number[][],
  symbols: string[]
): PortfolioOptimizationResult | null {
  if (returns.length !== symbols.length || returns.length < 2) {
    return null;
  }

  const covarianceMatrix = calculateCovarianceMatrix(returns);
  const n = returns.length;

  // For minimum variance portfolio: w = (Σ^-1 * 1) / (1' * Σ^-1 * 1)
  // Simplified implementation using equal weights as starting point
  const weights = new Array(n).fill(1 / n);

  // Iterative optimization (simplified)
  for (let iter = 0; iter < 100; iter++) {
    const gradient = calculateVarianceGradient(weights, covarianceMatrix);
    const stepSize = 0.01;

    // Update weights
    for (let i = 0; i < n; i++) {
      weights[i] -= stepSize * gradient[i];
    }

    // Normalize weights to sum to 1
    const weightSum = weights.reduce((sum, w) => sum + w, 0);
    for (let i = 0; i < n; i++) {
      weights[i] = Math.max(0, weights[i] / weightSum);
    }
  }

  const expectedReturns = returns.map(
    assetReturns => assetReturns.reduce((sum, ret) => sum + ret, 0) / assetReturns.length
  );

  const portfolioReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0);
  const portfolioVariance = calculatePortfolioVariance(weights, covarianceMatrix);
  const portfolioVolatility = Math.sqrt(portfolioVariance);
  const sharpeRatio = portfolioVolatility > 0 ? portfolioReturn / portfolioVolatility : 0;

  return {
    weights,
    expectedReturn: portfolioReturn,
    volatility: portfolioVolatility,
    sharpeRatio,
    symbols,
  };
}

// Helper functions for portfolio optimization

function calculateCovarianceMatrix(returns: number[][]): number[][] {
  const n = returns.length;
  const matrix: number[][] = [];

  for (let i = 0; i < n; i++) {
    matrix[i] = [];
    for (let j = 0; j < n; j++) {
      matrix[i][j] = calculateCovariance(returns[i], returns[j]);
    }
  }

  return matrix;
}

function calculateCovariance(x: number[], y: number[]): number {
  if (x.length !== y.length || x.length < 2) {
    return 0;
  }

  const n = x.length;
  const meanX = x.reduce((sum, val) => sum + val, 0) / n;
  const meanY = y.reduce((sum, val) => sum + val, 0) / n;

  return x.reduce((sum, val, i) => sum + (val - meanX) * (y[i] - meanY), 0) / (n - 1);
}

// calculatePortfolioVariance is already exported above

function calculateVarianceGradient(weights: number[], covarianceMatrix: number[][]): number[] {
  const n = weights.length;
  const gradient: number[] = [];

  for (let i = 0; i < n; i++) {
    let grad = 0;
    for (let j = 0; j < n; j++) {
      grad += 2 * weights[j] * covarianceMatrix[i][j];
    }
    gradient[i] = grad;
  }

  return gradient;
}

function findMinimumVarianceWeights(
  expectedReturns: number[],
  covarianceMatrix: number[][],
  targetReturn: number
): number[] | null {
  const n = expectedReturns.length;

  // Simplified implementation - in practice would use quadratic programming solver
  // Start with equal weights and adjust
  const weights = new Array(n).fill(1 / n);

  // Iterative adjustment to meet target return constraint
  for (let iter = 0; iter < 50; iter++) {
    const currentReturn = weights.reduce((sum, w, i) => sum + w * expectedReturns[i], 0);
    const returnDiff = targetReturn - currentReturn;

    if (Math.abs(returnDiff) < 0.001) {
      break;
    }

    // Adjust weights proportionally to expected returns
    const totalExpectedReturn = expectedReturns.reduce((sum, r) => sum + Math.abs(r), 0);

    for (let i = 0; i < n; i++) {
      const adjustment = (returnDiff * Math.abs(expectedReturns[i])) / totalExpectedReturn;
      weights[i] = Math.max(0, weights[i] + adjustment * 0.1);
    }

    // Normalize weights
    const weightSum = weights.reduce((sum, w) => sum + w, 0);
    if (weightSum > 0) {
      for (let i = 0; i < n; i++) {
        weights[i] /= weightSum;
      }
    }
  }

  return weights;
}
