import fs from 'fs';

function sigmoid(z) {
  // Cap z to avoid overflow
  if (z > 20) return 1.0;
  if (z < -20) return 0.0;
  return 1 / (1 + Math.exp(-z));
}

function computeLogLoss(features, labels, weights) {
  let loss = 0;
  for (let i = 0; i < features.length; i++) {
    const f = features[i];
    let z = weights[0];
    for (let j = 0; j < f.length; j++) z += weights[j + 1] * f[j];
    const p = sigmoid(z);
    // clip p to avoid log(0)
    const pSafe = Math.max(1e-15, Math.min(1 - 1e-15, p));
    loss += -labels[i] * Math.log(pSafe) - (1 - labels[i]) * Math.log(1 - pSafe);
  }
  return loss / features.length;
}

export class LogisticModel {
  constructor() {
    this.weights = null;
    this.featureKeys = [];
  }

  // Internal full-batch gradient descent
  _train(X, Y, lambda, learningRate, maxSteps, tol) {
    const numFeatures = X[0].length;
    let w = new Array(numFeatures + 1).fill(0.0);
    const N = X.length;

    let prevLoss = Infinity;

    for (let step = 0; step < maxSteps; step++) {
      const grad = new Array(numFeatures + 1).fill(0.0);
      
      for (let i = 0; i < N; i++) {
        let z = w[0];
        for (let j = 0; j < numFeatures; j++) z += w[j + 1] * X[i][j];
        
        const p = sigmoid(z);
        const err = p - Y[i];
        
        grad[0] += err;
        for (let j = 0; j < numFeatures; j++) {
          grad[j + 1] += err * X[i][j];
        }
      }

      // Add L2 penalty (skip intercept grad[0])
      for (let j = 0; j < numFeatures; j++) {
        grad[j + 1] += lambda * w[j + 1];
      }

      // Update weights
      let maxDelta = 0;
      for (let j = 0; j < w.length; j++) {
        const update = (learningRate * grad[j]) / N;
        w[j] -= update;
        if (Math.abs(update) > maxDelta) maxDelta = Math.abs(update);
      }

      if (maxDelta < tol) {
        // Converged
        break;
      }
    }
    return w;
  }

  fit(featuresArray, labels) {
    if (!featuresArray.length) return;
    this.featureKeys = Object.keys(featuresArray[0]);
    
    // Convert to 2D array
    const X = featuresArray.map(f => this.featureKeys.map(k => f[k]));
    const Y = labels;
    const N = X.length;

    // 1. Carve a 80/20 validation slice from TRAIN to tune lambda
    const splitIdx = Math.floor(N * 0.8);
    const X_train = X.slice(0, splitIdx);
    const Y_train = Y.slice(0, splitIdx);
    const X_val = X.slice(splitIdx);
    const Y_val = Y.slice(splitIdx);

    // 2. Tune lambda
    const lambdas = [0.01, 0.1, 1.0, 10.0, 100.0, 500.0, 1000.0];
    let bestLambda = 0;
    let bestLoss = Infinity;
    
    console.log(`\nTuning L2 Regularization (Lambda) on Validation Slice...`);
    for (const l of lambdas) {
      const w = this._train(X_train, Y_train, l, 0.5, 2000, 1e-5);
      const loss = computeLogLoss(X_val, Y_val, w);
      console.log(`  Lambda ${l.toString().padEnd(6)} -> LogLoss: ${loss.toFixed(4)}`);
      if (loss < bestLoss) {
        bestLoss = loss;
        bestLambda = l;
      }
    }
    
    console.log(`Selected Best Lambda: ${bestLambda}`);

    // 3. Refit on FULL TRAIN set using best lambda
    this.weights = this._train(X, Y, bestLambda, 0.5, 5000, 1e-5);

    return this.getCoefficients();
  }

  predict(featuresArray) {
    if (!this.weights) throw new Error("Model not trained");
    
    const probs = [];
    for (let i = 0; i < featuresArray.length; i++) {
      const f = featuresArray[i];
      let z = this.weights[0]; // intercept
      for (let j = 0; j < this.featureKeys.length; j++) {
        z += this.weights[j + 1] * f[this.featureKeys[j]];
      }
      probs.push(sigmoid(z));
    }
    
    return probs;
  }

  getCoefficients() {
    if (!this.weights) return {};
    const coeffs = { intercept: this.weights[0] };
    for (let i = 0; i < this.featureKeys.length; i++) {
      coeffs[this.featureKeys[i]] = this.weights[i + 1];
    }
    return coeffs;
  }

  save(filepath) {
    fs.writeFileSync(filepath, JSON.stringify({
      featureKeys: this.featureKeys,
      weights: this.weights
    }, null, 2));
  }

  load(filepath) {
    if (!fs.existsSync(filepath)) return;
    const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
    this.featureKeys = data.featureKeys;
    this.weights = data.weights;
  }
}
