interface DataPoint {
  x: number;
  y: number;
}

export interface TrendLineResult {
  // Polynomial coefficients [c, b, a] for y = ax² + bx + c
  // Index 0 = constant term, Index 1 = linear term, Index 2 = quadratic term, etc.
  coeffs: number[];
  rSquared: number;
  degree: number;
  equation: string;
  color: string; // For display purposes
}

export interface ChartStatistics {
  trendLines: TrendLineResult[];
  favsPerDay: number;
}

/**
 * Calculate linear regression for given data points
 */
const calculateLinearRegression = (data: DataPoint[]): TrendLineResult => {
  const n = data.length;
  const sumX = data.reduce((sum, point) => sum + point.x, 0);
  const sumY = data.reduce((sum, point) => sum + point.y, 0);
  const sumXY = data.reduce((sum, point) => sum + point.x * point.y, 0);
  const sumX2 = data.reduce((sum, point) => sum + point.x * point.x, 0);

  const denominator = n * sumX2 - sumX * sumX;

  // Check for singular case (all x values are the same)
  if (Math.abs(denominator) < 1e-10) {
    throw new Error(
      'Cannot calculate linear regression - all x values are identical',
    );
  }

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

  // Validate coefficients
  if (!isFinite(slope) || !isFinite(intercept)) {
    throw new Error('Invalid linear regression coefficients calculated');
  }

  const coeffs = [intercept, slope]; // [c, b] for y = bx + c
  const rSquared = calculateRSquared(data, coeffs);
  const equation = formatPolynomialEquation(coeffs);

  return {
    coeffs,
    rSquared,
    degree: 1,
    equation,
    color: 'rgba(34, 197, 94, 1)', // Green
  };
};

/**
 * Calculate second-order polynomial regression for given data points
 * Fits y = ax² + bx + c using least squares method
 */
const calculateQuadraticRegression = (data: DataPoint[]): TrendLineResult => {
  const n = data.length;

  // Calculate sums needed for polynomial regression
  const sumX = data.reduce((sum, point) => sum + point.x, 0);
  const sumY = data.reduce((sum, point) => sum + point.y, 0);
  const sumX2 = data.reduce((sum, point) => sum + point.x * point.x, 0);
  const sumX3 = data.reduce(
    (sum, point) => sum + point.x * point.x * point.x,
    0,
  );
  const sumX4 = data.reduce((sum, point) => sum + Math.pow(point.x, 4), 0);
  const sumXY = data.reduce((sum, point) => sum + point.x * point.y, 0);
  const sumX2Y = data.reduce(
    (sum, point) => sum + point.x * point.x * point.y,
    0,
  );

  // Set up matrix for normal equations
  // [n    sumX   sumX2 ] [c]   [sumY  ]
  // [sumX sumX2  sumX3 ] [b] = [sumXY ]
  // [sumX2 sumX3 sumX4] [a]   [sumX2Y]

  const matrix = [
    [n, sumX, sumX2],
    [sumX, sumX2, sumX3],
    [sumX2, sumX3, sumX4],
  ];

  const vector = [sumY, sumXY, sumX2Y];

  // Solve using Cramer's rule
  const det = (m: number[][]) =>
    m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -
    m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
    m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);

  const detA = det(matrix);

  // Check for singular matrix (determinant near zero)
  if (Math.abs(detA) < 1e-10) {
    throw new Error('Matrix is singular - cannot solve quadratic regression');
  }

  // Calculate coefficients
  const detC = det([
    [vector[0], sumX, sumX2],
    [vector[1], sumX2, sumX3],
    [vector[2], sumX3, sumX4],
  ]);

  const detB = det([
    [n, vector[0], sumX2],
    [sumX, vector[1], sumX3],
    [sumX2, vector[2], sumX4],
  ]);

  const detA_coeff = det([
    [n, sumX, vector[0]],
    [sumX, sumX2, vector[1]],
    [sumX2, sumX3, vector[2]],
  ]);

  const c = detC / detA;
  const b = detB / detA;
  const a = detA_coeff / detA;

  // Validate coefficients
  if (!isFinite(a) || !isFinite(b) || !isFinite(c)) {
    throw new Error('Invalid coefficients calculated');
  }

  const coeffs = [c, b, a]; // [constant, linear, quadratic]
  const rSquared = calculateRSquared(data, coeffs);
  const equation = formatPolynomialEquation(coeffs);

  return {
    coeffs,
    rSquared,
    degree: 2,
    equation,
    color: 'rgba(239, 68, 68, 1)', // Red
  };
};

/**
 * Calculate R-squared (coefficient of determination) for polynomial fit
 */
export const calculateRSquared = (
  data: DataPoint[],
  coeffs: number[],
): number => {
  if (data.length < 2) {
    return 0; // Cannot calculate meaningful R² with fewer than 2 points
  }

  const meanY = data.reduce((sum, point) => sum + point.y, 0) / data.length;

  let ssRes = 0; // Sum of squares of residuals
  let ssTot = 0; // Total sum of squares

  data.forEach((point) => {
    // Calculate predicted Y using polynomial: y = c₀ + c₁x + c₂x² + ...
    let predictedY = 0;
    for (let i = 0; i < coeffs.length; i++) {
      predictedY += coeffs[i] * Math.pow(point.x, i);
    }
    ssRes += Math.pow(point.y - predictedY, 2);
    ssTot += Math.pow(point.y - meanY, 2);
  });

  // Handle edge case where all y values are the same
  if (Math.abs(ssTot) < 1e-10) {
    return ssRes < 1e-10 ? 1.0 : 0.0; // Perfect fit if residuals are also zero
  }

  const rSquared = 1 - ssRes / ssTot;

  // Validate result
  if (!isFinite(rSquared)) {
    return 0;
  }

  return rSquared;
};

/**
 * Generate polynomial equation string from coefficients
 */
const formatPolynomialEquation = (coeffs: number[]): string => {
  const terms: string[] = [];

  for (let i = coeffs.length - 1; i >= 0; i--) {
    const coeff = coeffs[i];
    if (Math.abs(coeff) < 1e-10) continue; // Skip near-zero coefficients

    let term = '';
    const absCoeff = Math.abs(coeff);

    if (i === 0) {
      // Constant term
      term = absCoeff.toFixed(3);
    } else if (i === 1) {
      // Linear term
      if (absCoeff === 1) {
        term = 'x';
      } else {
        term = `${absCoeff.toFixed(3)}x`;
      }
    } else {
      // Higher order terms
      if (absCoeff === 1) {
        term = `x^${i}`;
      } else {
        term = `${absCoeff.toFixed(3)}x^${i}`;
      }
    }

    if (terms.length === 0) {
      // First term: include negative sign directly, no plus for positive
      terms.push(coeff >= 0 ? term : `-${term}`);
    } else {
      // Subsequent terms: always include sign with proper spacing
      terms.push(coeff >= 0 ? ` + ${term}` : ` - ${term}`);
    }
  }

  return terms.length > 0 ? `y = ${terms.join('')}` : 'y = 0';
};

/**
 * Calculate trend lines for given data points, only including fits with sufficient data
 */
export const calculateTrendLines = (data: DataPoint[]): TrendLineResult[] => {
  const trendLines: TrendLineResult[] = [];

  // Linear regression requires at least 2 points
  if (data.length >= 2) {
    trendLines.push(calculateLinearRegression(data));
  }

  // Quadratic regression requires at least 3 points
  if (data.length >= 3) {
    try {
      trendLines.push(calculateQuadraticRegression(data));
    } catch (e) {
      console.error(e);
    }
  }

  return trendLines;
};

/**
 * Calculate statistics for chart data
 */
export const calculateChartStatistics = (
  data: DataPoint[],
): ChartStatistics => {
  const trendLines = calculateTrendLines(data);

  // Calculate favs per day using linear regression (for interpretability)
  const linearTrendLine = trendLines.find((tl) => tl.degree === 1);
  const linearSlope = linearTrendLine?.coeffs[1] || 0; // coefficient of x in linear equation

  // Only calculate favsPerDay if we have a valid linear slope
  let favsPerDay = 0;
  if (linearSlope !== 0 && isFinite(linearSlope)) {
    favsPerDay = 1 / linearSlope;
    if (!isFinite(favsPerDay)) {
      favsPerDay = 0;
    }
  }

  return {
    trendLines,
    favsPerDay,
  };
};
