/* ============================================================
   twin-engine.js — 모낭 디지털 트윈 ODE 엔진 (브라우저 포팅)
   digital_twin/follicle_model.py 의 정확한 포팅. RK4 적분.
   라이브 'what-if' 모드용. 정밀 시나리오는 twin_scenarios.json 사용.
   ============================================================ */
(function (global) {
  'use strict';

  const STATE_NAMES = ["Wnt", "BMP", "SHH", "DP", "HFSC", "APO", "Hair"];

  // Python Params 와 1:1 동일
  const P = {
    kWp: 0.45, kWd: 0.32, kBp: 0.30, kBd: 0.38, kSp: 0.50, kSd: 0.40,
    kDp: 0.50, kDd: 0.30, kHp: 0.58, kHd: 0.24, kAd: 0.50,
    kHairP: 0.80, kHairD: 0.16, Wbasal: 1.0, Bbasal: 0.4, Dbasal: 1.0,
    kDKK: 0.65, Kd: 1.0, Kb: 1.2, kBMPand: 0.30, Kw: 0.7, Ksw: 0.5, Kb2: 0.9,
    kIGFand: 0.55, kMiniAND: 0.40, hpow: 0.6, Hcap: 3.5,
    kDinf: 0.45, kDapo: 0.5, Kh: 0.35, Ks: 0.35, Kb3: 1.5,
    kHinf: 0.55, kHapo: 0.6, kApAND: 0.30, kApINF: 0.85, kSurv: 1.6,
    Ksh: 0.4, kHairApo: 0.55,
  };

  const DISEASE_PRESETS = {
    "Healthy": { AND: 0.10, INF: 0.05, chemo_amp: 0.0 },
    "Androgenetic Alopecia": { AND: 0.62, INF: 0.08, chemo_amp: 0.0 },
    "Alopecia Areata": { AND: 0.12, INF: 0.82, chemo_amp: 0.0 },
    "Chemotherapy-induced Alopecia": { AND: 0.10, INF: 0.06, chemo_amp: 1.7 },
  };
  const CHEMO_TIMES = [10, 31, 52, 73];

  // 개입 → drive 수정자
  const INTERVENTIONS = {
    none: d => d,
    finasteride: d => (d.AND *= 0.40, d),
    dutasteride: d => (d.AND *= 0.25, d),
    AR_antagonist: d => (d.AND *= 0.45, d),
    minoxidil: d => (d.uDP += 0.80, d.uWnt += 0.15, d),
    anti_DKK1: d => (d.uWnt += 0.45, d),
    wnt_agonist: d => (d.uWnt += 0.50, d),
    JAK_inhibitor: d => (d.INF *= 0.18, d),
    corticosteroid: d => (d.INF *= 0.55, d),
    CDK46_inhibitor: d => (d.chemo_protect = 0.70, d),
    scalp_cooling: d => (d.chemo_protect = Math.max(d.chemo_protect, 0.45), d),
    PTH_CBD: d => (d.uDP += 0.35, d),
    exosome_MSC: d => (d.uDP += 0.35, d.uWnt += 0.25, d),
  };

  function buildDrive(disease, interventions, overrides) {
    const pre = DISEASE_PRESETS[disease] || DISEASE_PRESETS["Healthy"];
    let d = { AND: pre.AND, INF: pre.INF, chemo_amp: pre.chemo_amp || 0,
              uWnt: 0, uDP: 0, uNog: 0, chemo_protect: 0 };
    (interventions || []).forEach(iv => { if (INTERVENTIONS[iv]) d = INTERVENTIONS[iv](d); });
    if (overrides) Object.assign(d, overrides);   // 라이브 슬라이더
    d.AND = Math.max(0, d.AND); d.INF = Math.max(0, d.INF);
    return d;
  }

  function chemo(d, t) {
    if (!d.chemo_amp) return 0;
    let s = 0;
    for (const tc of CHEMO_TIMES) s += Math.exp(-((t - tc) ** 2) / (2 * 1.5 * 1.5));
    return d.chemo_amp * s * (1 - (d.chemo_protect || 0));
  }

  function rhs(t, y, d) {
    let [Wnt, BMP, SHH, DP, HFSC, APO, Hair] = y.map(v => Math.max(0, v));
    const AND = d.AND, INF = d.INF, ch = chemo(d, t);
    const DKK = P.kDKK * AND;
    const dWnt = P.kWp * (P.Wbasal + d.uWnt) / (1 + DKK / P.Kd) / (1 + BMP / P.Kb) - P.kWd * Wnt;
    const dBMP = P.kBp * (P.Bbasal + P.kBMPand * AND) / (1 + d.uNog) / (1 + Wnt / P.Kw) - P.kBd * BMP;
    const dSHH = P.kSp * (Wnt / (P.Ksw + Wnt)) / (1 + BMP / P.Kb2) - P.kSd * SHH;
    const IGF = 1 / (1 + P.kIGFand * AND);
    const dDP = P.kDp * (P.Dbasal + d.uDP) * IGF - P.kDd * DP - DP * (P.kDinf * INF + P.kDapo * APO);
    const dHFSC = P.kHp * (Wnt / (P.Kh + Wnt)) * (SHH / (P.Ks + SHH)) / (1 + BMP / P.Kb3)
                  - P.kHd * HFSC - HFSC * (P.kHinf * INF + P.kHapo * APO);
    const dAPO = (P.kApAND * AND + P.kApINF * INF + ch) / (1 + P.kSurv * DP) - P.kAd * APO;
    // 분수 지수(hpow=0.6)의 음수 밑 → Math.pow(neg,0.6)=NaN 방지: 밑을 명시적으로 0 하한.
    // (위 y.map(Math.max(0,·)) 클램프와 중복이나, 적분기 단계 값이 음수를 흘려도 NaN 전파 차단)
    const HFSCp = Math.pow(Math.max(0, HFSC), P.hpow);
    const DPp = Math.pow(Math.max(0, DP), P.hpow);
    const dHair = P.kHairP * HFSCp * DPp * (SHH / (P.Ksh + SHH))
                  / (1 + P.kMiniAND * AND) * Math.max(0, 1 - Hair / P.Hcap)
                  - P.kHairD * Hair - P.kHairApo * APO * Hair;
    return [dWnt, dBMP, dSHH, dDP, dHFSC, dAPO, dHair];
  }

  function rk4(y0, d, days, dt) {
    dt = dt || 0.5;
    const steps = Math.round(days / dt);
    const out = { t: [], y: y0.map(() => []) };
    let y = y0.slice();
    for (let i = 0; i <= steps; i++) {
      const t = i * dt;
      if (Math.abs(t - Math.round(t)) < 1e-9) {       // 일 단위 샘플만 저장
        out.t.push(Math.round(t));
        y.forEach((v, j) => out.y[j].push(v));
      }
      const k1 = rhs(t, y, d);
      const k2 = rhs(t + dt / 2, y.map((v, j) => v + dt / 2 * k1[j]), d);
      const k3 = rhs(t + dt / 2, y.map((v, j) => v + dt / 2 * k2[j]), d);
      const k4 = rhs(t + dt, y.map((v, j) => v + dt * k3[j]), d);
      y = y.map((v, j) => Math.max(0, v + dt / 6 * (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j])));
    }
    return out;
  }

  let _healthy = null;
  function healthyState() {
    if (!_healthy) {
      const d = buildDrive("Healthy", []);
      const sol = rk4([1.0, 0.45, 0.7, 1.0, 0.8, 0.1, 1.0], d, 400);
      _healthy = sol.y.map(arr => arr[arr.length - 1]);
    }
    return _healthy;
  }

  function proteinReadouts(states, d) {
    const n = states.Wnt.length, ones = new Array(n);
    const AND = d.AND, INF = d.INF;
    return {
      "AR/DHT (AR)": ones.fill(AND).slice(),
      "DKK1": new Array(n).fill(P.kDKK * AND),
      "β-catenin (CTNNB1)": states.Wnt.slice(),
      "BMP4": states.BMP.slice(),
      "SHH": states.SHH.slice(),
      "IGF1": new Array(n).fill(1 / (1 + P.kIGFand * AND)),
      "VEGFA (DP)": states.DP.slice(),
      "JAK-STAT (STAT1)": new Array(n).fill(INF),
      "p53/apoptosis (TP53)": states.APO.slice(),
    };
  }

  // 질환의 '무처치 평형상태' = 환자가 이미 탈모를 가진 출발점(치료 전).
  // 건강 상태에서 질환 구동을 충분히 오래 적분해 정착시킨 종단 상태.
  const _diseq = {};
  function diseaseEquilibrium(disease) {
    if (!_diseq[disease]) {
      const d = buildDrive(disease, []);
      const sol = rk4(healthyState(), d, 600);
      _diseq[disease] = sol.y.map(arr => arr[arr.length - 1]);
    }
    return _diseq[disease].slice();
  }

  // 메인 API: 시뮬레이션 실행. opts.y0 = 커스텀 초기상태(미지정 시 건강 평형).
  function run(disease, interventions, opts) {
    opts = opts || {};
    const d = buildDrive(disease, interventions, opts.overrides);
    const y0 = opts.y0 || healthyState();
    const sol = rk4(y0, d, opts.days || 240);
    const states = {};
    STATE_NAMES.forEach((nm, i) => states[nm] = sol.y[i]);
    const hss = healthyState()[6];
    const rel = states.Hair.map(h => Math.min(100, 100 * h / hss));
    states.HairDensity = rel;
    const proteins = proteinReadouts(states, d);
    const metrics = {
      final_hair_density_pct: +rel[rel.length - 1].toFixed(1),
      min_hair_density_pct: +Math.min(...rel).toFixed(1),
      anagen_fraction: +(rel.filter(v => v > 70).length / rel.length).toFixed(3),
      AND_load: +d.AND.toFixed(3), INF_load: +d.INF.toFixed(3),
    };
    let tracked = [];
    (interventions || []).forEach(iv => {
      // genes 는 scenarios JSON 에서 보강; 엔진은 비움
    });
    return { disease, interventions, drive: d, t: sol.t, states, proteins, metrics };
  }

  global.TwinEngine = { run, diseaseEquilibrium, STATE_NAMES, DISEASE_PRESETS, INTERVENTIONS, buildDrive };
  if (typeof module !== 'undefined' && module.exports) module.exports = global.TwinEngine;
})(typeof window !== 'undefined' ? window : globalThis);
