function mean(arr) {
  const valid = arr.filter(x => x !== null && x !== undefined && !isNaN(x));
  if (valid.length === 0) return null;
  return valid.reduce((a, b) => a + b, 0) / valid.length;
}

export function orientedComponents(r) {
  const inv = x => (x != null && x > 0) ? 1 / x : null;  // guard neg/zero multiples
  const neg = x => (x != null) ? -x : null;
  const div = (a, b) => (a != null && b) ? a / b : null;
  return {
    value:    [ inv(r.trailingPE), div(r.freeCashflow, r.marketCap),
                inv(r.priceToBook), inv(r.priceToSales) ],
    quality:  [ r.returnOnEquity, r.grossMargins, neg(r.debtToEquity) ],
    lowVol:   [ neg(r.realizedVol252) ],
    momentum: [ r.mom_12_1 ],
  };
}

export const FACTORS = {
  value: r => mean(orientedComponents(r).value),
  quality: r => mean(orientedComponents(r).quality),
  lowVol: r => mean(orientedComponents(r).lowVol),
  momentum: r => mean(orientedComponents(r).momentum),
  // composite is an equal-weight average of the standardized scores, computed later
};

function winsorize(arr, trimPercent) {
  if (arr.length < 5) return arr; // Don't winsorize tiny arrays
  const sorted = [...arr].sort((a, b) => a - b);
  const lowerIdx = Math.floor(arr.length * trimPercent);
  let upperIdx = Math.floor(arr.length * (1 - trimPercent)) - 1;
  if (upperIdx < lowerIdx) upperIdx = arr.length - 1;
  
  const lowerBound = sorted[lowerIdx];
  const upperBound = sorted[upperIdx];

  return arr.map(v => {
    if (v < lowerBound) return lowerBound;
    if (v > upperBound) return upperBound;
    return v;
  });
}

function avg(arr) {
  if (!arr.length) return 0;
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

function std(arr, meanVal) {
  if (arr.length <= 1) return 1;
  const variance = arr.reduce((sum, v) => sum + Math.pow(v - meanVal, 2), 0) / (arr.length - 1);
  return Math.sqrt(variance) || 1;
}

export function standardizeCrossSection(rawByTicker, neutralizeSector = false, sectorsByTicker = {}) {
  // Extract non-null values
  const tickers = Object.keys(rawByTicker);
  let validTickers = tickers.filter(t => rawByTicker[t] != null && !isNaN(rawByTicker[t]));
  
  if (validTickers.length === 0) return {};

  const result = {};

  if (neutralizeSector) {
    // Group by sector
    const sectorGroups = {};
    for (const t of validTickers) {
      const sec = sectorsByTicker[t] || 'UNKNOWN';
      if (!sectorGroups[sec]) sectorGroups[sec] = [];
      sectorGroups[sec].push(t);
    }

    for (const sec in sectorGroups) {
      const secTickers = sectorGroups[sec];
      const secVals = secTickers.map(t => rawByTicker[t]);
      
      const w = winsorize(secVals, 0.02);
      const m = avg(w);
      const s = std(w, m);

      for (let i = 0; i < secTickers.length; i++) {
        const t = secTickers[i];
        let val = rawByTicker[t];
        if (val < w[0]) val = w[0]; // approx winsorize clip (since w is same order)
        // Actually, winsorize maps index-to-index.
        const clipped = w[i];
        result[t] = (clipped - m) / s;
      }
    }
  } else {
    // Global standard
    const vals = validTickers.map(t => rawByTicker[t]);
    const w = winsorize(vals, 0.02);
    const m = avg(w);
    const s = std(w, m);

    for (let i = 0; i < validTickers.length; i++) {
      const t = validTickers[i];
      const clipped = w[i];
      result[t] = (clipped - m) / s;
    }
  }

  return result;
}
