// gdf_06_rebalance.gs — REBALANCE_ENGINE_V1 (GAS)
//
// runRebalanceSheet_(): data_feed + account_snapshot 라이브 데이터 기반
// bucket drift → 레짐 적응 밴드 → 비용효익 게이트 → 3단계 분할 실행 계획
// GatherTradingData.xlsx > rebalance 시트에 4섹션(SUMMARY/BUCKETS/TICKERS/ORDERS) 출력.

// ── 버킷 설정 (gdf_01_price_metrics.gs THRESHOLDS 와 동기화) ─────────────────
const RB_BUCKET_CONFIG = {
  Core:      { target: 66.0, min: 60.0, max: 72.0 },
  Satellite: { target: 17.5, min: 10.0, max: 25.0 },
  Cash:      { target: 16.5, min: 10.0, max: 22.0 },
};

// 코어 주도주 (isCoreLeader 기준, gdc_02_account_satellite.gs 와 일치)
const RB_CORE_TICKERS = new Set(["005930", "000660", "000270"]);

// ── 레짐 적응 밴드 (P3) ──────────────────────────────────────────────────────
const RB_REGIME_BANDS = {
  RISK_ON:             { label: "RISK_ON ±15%p",          expand: 15, contract: 15 },
  SECULAR_LEADER_RISK_ON: { label: "RISK_ON ±15%p",       expand: 15, contract: 15 },
  NEUTRAL:             { label: "NEUTRAL ±5%p",           expand:  5, contract:  5 },
  RISK_OFF_CANDIDATE:  { label: "RISK_OFF_CANDIDATE +2/−10%p", expand: 2, contract: 10 },
  RISK_OFF:            { label: "RISK_OFF +2/−10%p",      expand:  2, contract: 10 },
  EVENT_SHOCK:         { label: "RISK_OFF +2/−10%p",      expand:  2, contract: 10 },
  _DEFAULT:            { label: "NEUTRAL ±5%p",           expand:  5, contract:  5 },
};

// ── 비용효익 게이트 (P4) ─────────────────────────────────────────────────────
const RB_TX_COST_ROUNDTRIP     = 0.0070; // 0.35% × 2
const RB_COST_BENEFIT_THRESHOLD = 0.0050; // 0.50%p
const RB_MIN_DRIFT_PCT          = (RB_TX_COST_ROUNDTRIP + RB_COST_BENEFIT_THRESHOLD) * 100; // 1.20%p
const RB_LIMIT_PRICE_DISCOUNT   = 0.002;  // 매도 지정가 = 종가 × (1 - 0.2%)

// ── 3단계 분할 비율 (P5) ─────────────────────────────────────────────────────
const RB_STAGE_RATIOS = [0.30, 0.30, 0.40];


// ═══════════════════════════════════════════════════════════════════════════════
// Public entry point
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * GatherTradingData.xlsx > rebalance 시트에 4섹션 리밸런싱 계획을 기록한다.
 * 메뉴 또는 runDataFeed 후 자동 호출 가능.
 */
function runRebalanceSheet_() {
  const tag = "runRebalanceSheet_";
  const startMs = Date.now();

  try {
    // 1. 데이터 로드
    const dfRows   = _rbLoadDataFeedRows_();
    const settings = readSettingsTab_();
    const regime   = _rbReadRegime_(settings);
    const band     = RB_REGIME_BANDS[regime] || RB_REGIME_BANDS["_DEFAULT"];

    // 2. 보유 종목 필터링 (Weight_Pct > 0 || Account_Market_Value > 0)
    const holdings = _rbFilterHoldings_(dfRows);

    // 3. 버킷별 현재 비중 집계
    const buckets  = _rbComputeBuckets_(holdings, band);

    // 4. 종목별 분석
    const tickers  = _rbComputeTickers_(holdings, band);

    // 5. ORDERS 생성
    const orders   = _rbComputeOrders_(tickers);

    // 6. SUMMARY 생성
    const summary  = _rbComputeSummary_(holdings, buckets, regime, band, orders.length);

    // 7. 시트 쓰기
    _writeRebalanceSheet_(summary, buckets, tickers, orders);

    const elapsed = Math.round((Date.now() - startMs) / 100) / 10;
    Logger.log(`[${tag}] 완료: holdings=${holdings.length} orders=${orders.length} elapsed=${elapsed}s`);

  } catch (e) {
    Logger.log(`[${tag}][ERROR] 오류: ${e.message}\n${e.stack}`);
    throw e;
  }
}


// ═══════════════════════════════════════════════════════════════════════════════
// 데이터 로드
// ═══════════════════════════════════════════════════════════════════════════════

function _rbLoadDataFeedRows_() {
  const raw = sheetToJson("data_feed");
  if (!Array.isArray(raw) || raw.length === 0) {
    throw new Error("data_feed 시트가 비어 있거나 로드 실패");
  }
  return raw;
}

function _rbReadRegime_(settings) {
  const raw = (settings["REGIME_PRELIM"] || settings["regime_prelim"] || "").trim().toUpperCase();
  return raw in RB_REGIME_BANDS ? raw : "_DEFAULT";
}


// ═══════════════════════════════════════════════════════════════════════════════
// 보유 종목 필터링
// ═══════════════════════════════════════════════════════════════════════════════

function _rbFilterHoldings_(dfRows) {
  return dfRows
    .map(row => {
      const ticker    = String(row["Ticker"] ?? "").trim();
      if (!ticker) return null;
      const weightPct = _rbNum_(row["Weight_Pct"]);
      const acctMv    = _rbNum_(row["Account_Market_Value"]);
      if (weightPct <= 0 && acctMv <= 0) return null;

      return {
        ticker:      ticker,
        name:        String(row["Name"] ?? ""),
        bucket:      _rbAssignBucket_(ticker, row),
        weightPct:   weightPct,
        acctMvKrw:   acctMv,
        holdingQty:  _rbInt_(row["Account_Holding_Qty"]),
        close:       _rbNum_(row["Close"]),
        finalAction: String(row["Final_Action"] ?? ""),
        sellReason:  String(row["Sell_Reason"] ?? ""),
        forceSignal: _rbDetectForce_(row),
      };
    })
    .filter(h => h !== null);
}

function _rbAssignBucket_(ticker, row) {
  const pt = String(row["position_type"] || row["Position_Type"] || "").trim().toLowerCase();
  if (pt === "core")      return "Core";
  if (pt === "satellite") return "Satellite";
  return RB_CORE_TICKERS.has(ticker) ? "Core" : "Satellite";
}

function _rbDetectForce_(row) {
  const combined = [
    row["Sell_Reason"], row["Final_Action"], row["Sell_Action"]
  ].join(" ").toUpperCase();
  if (combined.includes("ABS_FLOOR"))  return "ABS_FLOOR";
  if (combined.includes("TIME_STOP") || combined.includes("TIME_EXIT") || combined.includes("TIME_TRIM"))
    return "TIME_STOP";
  return "";
}


// ═══════════════════════════════════════════════════════════════════════════════
// 버킷 계산
// ═══════════════════════════════════════════════════════════════════════════════

function _rbComputeBuckets_(holdings, band) {
  const corePct = holdings.filter(h => h.bucket === "Core").reduce((s, h) => s + h.weightPct, 0);
  const satPct  = holdings.filter(h => h.bucket === "Satellite").reduce((s, h) => s + h.weightPct, 0);
  const cashPct = Math.max(0, 100 - corePct - satPct);
  const current = { Core: corePct, Satellite: satPct, Cash: cashPct };

  return Object.entries(RB_BUCKET_CONFIG).map(([bname, bcfg]) => {
    const target  = bcfg.target;
    const cur     = _rb2_(current[bname] || 0);
    const drift   = _rb2_(cur - target);
    const bandMin = _rb2_(target - band.contract);
    const bandMax = _rb2_(target + band.expand);
    let driftStatus;
    if (cur < bandMin)        driftStatus = "BREACH_LOW";
    else if (cur > bandMax)   driftStatus = "BREACH_HIGH";
    else if (Math.abs(drift) >= RB_MIN_DRIFT_PCT / 2) driftStatus = "WARN";
    else                      driftStatus = "NORMAL";

    return { bucket: bname, targetPct: target, currentPct: cur, driftPct: drift,
             bandMin, bandMax, regimeBand: band.label, driftStatus };
  });
}


// ═══════════════════════════════════════════════════════════════════════════════
// 종목별 분석
// ═══════════════════════════════════════════════════════════════════════════════

function _rbComputeTickers_(holdings, band) {
  // 버킷별 종목 수 집계
  const countMap = {};
  holdings.forEach(h => { countMap[h.bucket] = (countMap[h.bucket] || 0) + 1; });

  return holdings.map(h => {
    const bcfg       = RB_BUCKET_CONFIG[h.bucket] || RB_BUCKET_CONFIG["Satellite"];
    const nTickers   = countMap[h.bucket] || 1;
    const targetPct  = _rb2_(bcfg.target / nTickers);
    const currentPct = _rb2_(h.weightPct);
    const drift      = _rb2_(currentPct - targetPct);
    const bandMin    = _rb2_(targetPct - band.contract);
    const bandMax    = _rb2_(targetPct + band.expand);
    const force      = h.forceSignal;

    let driftStatus, action, gateStatus;
    if (force) {
      driftStatus = "FORCE_" + force;
      action      = "SELL";
      gateStatus  = "FORCE_OVERRIDE";
    } else if (currentPct > bandMax) {
      driftStatus = "BREACH_HIGH";
      action      = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "SELL" : "WATCH";
      gateStatus  = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "PASS" : "BLOCKED_BY_COST";
    } else if (currentPct < bandMin) {
      driftStatus = "BREACH_LOW";
      action      = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "BUY"  : "WATCH";
      gateStatus  = Math.abs(drift) >= RB_MIN_DRIFT_PCT ? "PASS" : "BLOCKED_BY_COST";
    } else if (Math.abs(drift) >= RB_MIN_DRIFT_PCT / 2) {
      driftStatus = "WARN";
      action      = "WATCH";
      gateStatus  = "BLOCKED_BY_COST";
    } else {
      driftStatus = "NORMAL";
      action      = "HOLD";
      gateStatus  = "BLOCKED_BY_COST";
    }

    // 3단계 수량 분할 (P5)
    let s1q = 0, s1p = 0, s2q = 0, s2p = 0, s3q = 0, s3p = 0;
    let tradeValueKrw = 0, costEstKrw = 0, netBenefitPct = 0;

    if ((action === "SELL" || action === "BUY") && h.holdingQty > 0 && h.close > 0) {
      let adjustQty;
      if (action === "SELL" && currentPct > 0) {
        const adjustRatio = Math.min(Math.abs(drift) / currentPct, 1.0);
        adjustQty = Math.max(1, Math.round(h.holdingQty * adjustRatio));
      } else {
        adjustQty = Math.max(1, Math.round(h.holdingQty * 0.10));
      }

      const stages = _rbStageSplit_(adjustQty);
      const limitP = _rbLimitPrice_(h.close, action);
      [s1q, s2q, s3q] = stages;
      [s1p, s2p, s3p] = [limitP, limitP, limitP];
      tradeValueKrw = _rb2_((s1q + s2q + s3q) * limitP);
      costEstKrw    = _rb2_(tradeValueKrw * RB_TX_COST_ROUNDTRIP);
      netBenefitPct = _rb2_(Math.abs(drift) - RB_TX_COST_ROUNDTRIP * 100);
    }

    return { ticker: h.ticker, name: h.name, bucket: h.bucket,
             targetPct, currentPct, driftPct: drift, bandMin, bandMax,
             regimeBand: band.label, driftStatus, forceSignal: force,
             gateStatus, action,
             stage1Qty: s1q, stage1Price: s1p,
             stage2Qty: s2q, stage2Price: s2p,
             stage3Qty: s3q, stage3Price: s3p,
             tradeValueKrw, costEstKrw, netBenefitPct, close: h.close };
  });
}


// ═══════════════════════════════════════════════════════════════════════════════
// ORDERS 생성
// ═══════════════════════════════════════════════════════════════════════════════

function _rbComputeOrders_(tickers) {
  const active = tickers
    .filter(t => t.gateStatus === "PASS" || t.gateStatus === "FORCE_OVERRIDE")
    .sort((a, b) => {
      const pa = a.gateStatus === "FORCE_OVERRIDE" ? 0 : 1;
      const pb = b.gateStatus === "FORCE_OVERRIDE" ? 0 : 1;
      if (pa !== pb) return pa - pb;
      return Math.abs(b.driftPct) - Math.abs(a.driftPct);
    });

  const orders = [];
  let orderNo = 1;
  active.forEach(t => {
    const stageDefs = [
      { stage: 1, qty: t.stage1Qty, price: t.stage1Price },
      { stage: 2, qty: t.stage2Qty, price: t.stage2Price },
      { stage: 3, qty: t.stage3Qty, price: t.stage3Price },
    ];
    stageDefs.forEach(({ stage, qty, price }) => {
      if (qty <= 0) return;
      const reason = t.forceSignal || t.driftStatus;
      orders.push({
        orderNo, ticker: t.ticker, name: t.name, bucket: t.bucket,
        action: t.action, stage, qty, limitPriceKrw: price,
        tradeValueKrw: qty * price, reason,
      });
      orderNo++;
    });
  });
  return orders;
}


// ═══════════════════════════════════════════════════════════════════════════════
// SUMMARY 생성
// ═══════════════════════════════════════════════════════════════════════════════

function _rbComputeSummary_(holdings, buckets, regime, band, ordersCount) {
  const corePct  = (buckets.find(b => b.bucket === "Core")      || {}).currentPct || 0;
  const satPct   = (buckets.find(b => b.bucket === "Satellite") || {}).currentPct || 0;
  const cashPct  = (buckets.find(b => b.bucket === "Cash")      || {}).currentPct || 0;
  const rebalNeeded = buckets.some(b => b.driftStatus.startsWith("BREACH"));
  const totalKrw = holdings.reduce((s, h) => s + h.acctMvKrw, 0);
  const nowKst   = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss");

  return {
    Run_Date:              nowKst,
    Regime:                regime,
    Regime_Band:           band.label,
    Total_Portfolio_KRW:   totalKrw,
    Core_Pct:              corePct,
    Satellite_Pct:         satPct,
    Cash_Pct:              cashPct,
    Target_Core_Pct:       RB_BUCKET_CONFIG.Core.target,
    Target_Sat_Pct:        RB_BUCKET_CONFIG.Satellite.target,
    Target_Cash_Pct:       RB_BUCKET_CONFIG.Cash.target,
    Rebalance_Needed:      rebalNeeded,
    Holdings_Count:        holdings.length,
    Orders_Count:          ordersCount,
    Min_Actionable_Drift_Pct: RB_MIN_DRIFT_PCT,
  };
}


// ═══════════════════════════════════════════════════════════════════════════════
// 시트 쓰기 — 4섹션 멀티섹션 레이아웃
// ═══════════════════════════════════════════════════════════════════════════════

function _writeRebalanceSheet_(summary, buckets, tickers, orders) {
  const ss    = getSpreadsheet_();
  let sheet   = ss.getSheetByName("rebalance");
  if (!sheet) {
    sheet = ss.insertSheet("rebalance");
  } else {
    sheet.clearContents();
  }

  const rows = [];
  const nowKst = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss");
  rows.push([`updated: ${nowKst} KST`]);

  // ── SUMMARY 섹션 ──────────────────────────────────────────────────────────
  rows.push(["=== SUMMARY ==="]);
  Object.entries(summary).forEach(([k, v]) => rows.push([k, v]));
  rows.push([""]);

  // ── BUCKETS 섹션 ─────────────────────────────────────────────────────────
  rows.push(["=== BUCKETS ==="]);
  rows.push(["Bucket","Target_Pct","Current_Pct","Drift_Pct","Band_Min","Band_Max","Regime_Band","Drift_Status"]);
  buckets.forEach(b => rows.push([
    b.bucket, b.targetPct, b.currentPct, b.driftPct,
    b.bandMin, b.bandMax, b.regimeBand, b.driftStatus,
  ]));
  rows.push([""]);

  // ── TICKERS 섹션 ─────────────────────────────────────────────────────────
  rows.push(["=== TICKERS ==="]);
  rows.push([
    "Ticker","Name","Bucket","Target_Pct","Current_Pct","Drift_Pct",
    "Band_Min","Band_Max","Regime_Band","Drift_Status","Force_Signal","Gate_Status","Action",
    "Stage1_Qty","Stage1_Price","Stage2_Qty","Stage2_Price","Stage3_Qty","Stage3_Price",
    "Trade_Value_KRW","Cost_Est_KRW","Net_Benefit_Pct","Close",
  ]);
  tickers.forEach(t => rows.push([
    t.ticker, t.name, t.bucket, t.targetPct, t.currentPct, t.driftPct,
    t.bandMin, t.bandMax, t.regimeBand, t.driftStatus, t.forceSignal, t.gateStatus, t.action,
    t.stage1Qty, t.stage1Price, t.stage2Qty, t.stage2Price, t.stage3Qty, t.stage3Price,
    t.tradeValueKrw, t.costEstKrw, t.netBenefitPct, t.close,
  ]));
  rows.push([""]);

  // ── ORDERS 섹션 ──────────────────────────────────────────────────────────
  rows.push(["=== ORDERS ==="]);
  rows.push(["Order_No","Ticker","Name","Bucket","Action","Stage","Qty","Limit_Price_KRW","Trade_Value_KRW","Reason"]);
  orders.forEach(o => rows.push([
    o.orderNo, o.ticker, o.name, o.bucket, o.action,
    o.stage, o.qty, o.limitPriceKrw, o.tradeValueKrw, o.reason,
  ]));

  // 한 번에 쓰기
  if (rows.length > 0) {
    const maxCols = Math.max(...rows.map(r => r.length));
    const padded  = rows.map(r => {
      while (r.length < maxCols) r.push("");
      return r;
    });
    sheet.getRange(1, 1, padded.length, maxCols).setValues(padded);
  }
}


// ═══════════════════════════════════════════════════════════════════════════════
// 내부 유틸
// ═══════════════════════════════════════════════════════════════════════════════

function _rbNum_(v) {
  const n = parseFloat(v);
  return isNaN(n) ? 0 : n;
}

function _rbInt_(v) {
  const n = parseInt(v, 10);
  return isNaN(n) ? 0 : n;
}

function _rb2_(v) {
  return Math.round(v * 100) / 100;
}

function _rbStageSplit_(totalQty) {
  if (totalQty <= 0) return [0, 0, 0];
  if (totalQty < 3)  return [totalQty, 0, 0];
  const s1 = Math.max(1, Math.floor(totalQty * RB_STAGE_RATIOS[0]));
  const s2 = Math.max(1, Math.floor(totalQty * RB_STAGE_RATIOS[1]));
  const s3 = Math.max(0, totalQty - s1 - s2);
  return [s1, s2, s3];
}

function _rbLimitPrice_(close, action) {
  if (close <= 0) return 0;
  return action === "SELL" ? Math.round(close * (1 - RB_LIMIT_PRICE_DISCOUNT)) : Math.round(close);
}

// ── WBS-5.3 일일 자율 실행 타이머 트리거 설정 ─────────────────────────────────

/**
 * setupDailyRunAllTrigger()
 * GAS 편집기에서 수동 1회 실행 → 매일 16:30 run_all 타이머 트리거 등록.
 * 중복 트리거 방지: 동일 함수명 트리거가 존재하면 먼저 삭제.
 */
function setupDailyRunAllTrigger() {
  const TARGET_FN = "run_all";
  const TRIGGER_HOUR = 16; // 오후 4시 (장 마감 30분 후)

  // 기존 동일 함수 트리거 삭제 (중복 방지)
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === TARGET_FN) {
      ScriptApp.deleteTrigger(t);
      Logger.log("[WBS-5.3] 기존 트리거 삭제: " + TARGET_FN);
    }
  });

  // 일일 타이머 트리거 등록 (매일 16:00~17:00 사이 실행)
  ScriptApp.newTrigger(TARGET_FN)
    .timeBased()
    .atHour(TRIGGER_HOUR)
    .everyDays(1)
    .inTimezone("Asia/Seoul")
    .create();

  Logger.log("[WBS-5.3] 일일 트리거 등록 완료: " + TARGET_FN + " @ " + TRIGGER_HOUR + ":00 KST");
}

/**
 * listTriggers()
 * 현재 등록된 모든 트리거 목록 출력 (검증용).
 */
function listTriggers() {
  ScriptApp.getProjectTriggers().forEach(t => {
    Logger.log(
      "trigger: fn=" + t.getHandlerFunction() +
      " type=" + t.getEventType() +
      " source=" + t.getTriggerSource()
    );
  });
}
