/**
 * synthetic.js
 * Generates synthetic datasets for Known-Answer tests.
 */

export function generateSyntheticData({ numMonths, numStocks, plantedIC = 0.0 }) {
  const universe = [];
  const fundamentals = [];
  const forwardReturnsMap = new Map(); // key: `${symbol}_${date}`, value: 1mo forward return

  // Base date
  const startDate = new Date('2000-01-31');
  
  for (let m = 0; m < numMonths; m++) {
    const d = new Date(startDate);
    d.setMonth(d.getMonth() + m);
    // Approximate end of month
    d.setDate(0);
    const dateStr = d.toISOString().split('T')[0];

    const symbols = [];
    for (let s = 1; s <= numStocks; s++) {
      const sym = `SYM${s}`;
      symbols.push(sym);

      // Generate a base signal (standard normal approx)
      const u1 = Math.random(), u2 = Math.random();
      const zScore = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);

      // We will define a "value" factor that is just this zScore.
      // For the null test, plantedIC = 0.
      // For planted signal, plantedIC = 0.05.
      
      // forwardReturn = plantedIC * zScore + noise
      const noise_u1 = Math.random(), noise_u2 = Math.random();
      const noise = Math.sqrt(-2.0 * Math.log(noise_u1)) * Math.cos(2.0 * Math.PI * noise_u2);
      
      // Adjust noise variance so that correlation between zScore and fwdRet is roughly plantedIC
      // Var(fwd) = IC^2 * Var(Z) + Var(noise)
      // Since Var(Z)=1, if we want Cor(Z, fwd) = IC:
      // Cor = Cov(Z, IC*Z + noise) / sqrt(1 * (IC^2 + Var(noise)))
      // Cor = IC / sqrt(IC^2 + Var(noise))
      // To get Cor = IC, we need sqrt(IC^2 + Var(noise)) = 1 => Var(noise) = 1 - IC^2
      const noiseScale = Math.sqrt(Math.max(0, 1 - plantedIC * plantedIC));
      
      const fwdRet = plantedIC * zScore + noiseScale * noise;

      // Scale fwdRet to realistic monthly return (e.g. mean 0.5%, vol 5%)
      const monthlyReturn = 0.005 + 0.05 * fwdRet;

      forwardReturnsMap.set(`${sym}_${dateStr}`, monthlyReturn);

      // Record fundamental.
      // value factor = mean([ earningsYield, fcfYield, bookToPrice, salesToPrice ])
      // We'll just set earningsYield = zScore, and others to 0 to keep it simple.
      // So mean is zScore / 4. 
      // To make the actual score == zScore, we'll set all 4 to zScore.
      fundamentals.push({
        symbol: sym,
        datekey: dateStr, // assume filed on the exact rebalance date for testing
        fiscalEnd: dateStr,
        metrics: {
          earningsYield: zScore,
          fcfYield: zScore,
          bookToPrice: zScore,
          salesToPrice: zScore,
          
          // quality, lowVol, momentum just random noise
          roe: Math.random(),
          grossMargin: Math.random(),
          accruals: Math.random(),
          debtToEquity: Math.random()
        }
      });
    }

    universe.push({
      date: dateStr,
      symbols: symbols
    });
  }

  return {
    universe,
    fundamentals,
    forwardReturnsMap
  };
}
