/**
 * recipe 的 payload 與回應處理層（SDD workflow-discovery 3.12 / CP arcrun-usable 步驟 5 缺口②）
 *
 * 為什麼存在（leo 的三層模型，第③層過去是空的）：
 *   ① 零件（http_request）  ② auth recipe（auth_service）  ③ **payload recipe** ← 這層
 *   舊 schema 存不住 body 與「回應怎麼取值」⇒ 帶 body 的 API 只能把整包寫進 workflow code，
 *   回應解析（rag_chat 的 finalize，2786 字元）綁死 Gemini 格式 ⇒ 換源必壞。
 *   有了這層：**換 LLM 供應商＝換 recipe，不必動 workflow**。
 *
 * 相容鐵律：三個欄位全為選填。既有 recipe（沒有這些欄位）行為**完全不變**——
 * renderBodyTemplate(undefined,…) 回 undefined、applyResponseMap(body, undefined) 原樣回傳。
 */

/** 回應正規化規則（隨 recipe 走，故換源＝換 recipe） */
export type ResponseMap = {
  /**
   * 取值路徑（dot path，支援陣列索引）。
   * 例：Gemini `candidates.0.content.parts.0.text`／Claude `content.0.text`／
   *     Workers AI `response`。
   * 搭配 thinking_model 時可指向 parts 陣列本身。
   */
  text_path?: string;
  /**
   * 思考型模型（如 gemma）：parts 內會混入 `thought: true` 的思考過程，
   * 要剔除後取最後一個非 thought 的 part。
   */
  thinking_model?: boolean;
  /** 淨化：要剝掉的前綴（實撞過「Draft:」「*」「Answer:」，且組合順序不定） */
  strip_prefixes?: string[];
  /** 答案標記：出現時只取其後的內容（實撞：模型會把草稿吐在標記前） */
  answer_marker?: string;
};

/** 從物件用 dot path 取值：'a.0.b' → obj.a[0].b */
function getPath(obj: unknown, path: string): unknown {
  let cur: unknown = obj;
  for (const part of path.split('.')) {
    if (cur === null || cur === undefined) return undefined;
    if (typeof cur !== 'object') return undefined;
    cur = (cur as Record<string, unknown>)[part];
  }
  return cur;
}

// ── ③-a body_template：payload 收回 recipe ───────────────────────────────────

/**
 * 把 body_template 內所有 `{{var}}` 用 ctx 填掉（遞迴進巢狀 object / array）。
 *
 * 與 graph-executor 的 interpolateData 同一套語義（刻意一致，避免兩種插值行為）：
 *   - 整個字串就是單一 `{{x}}` → 回**原型別**（陣列/物件/數字不被 stringify）
 *   - 混合文字 → 拼成字串
 *   - 取不到 → **保留原樣** `{{x}}`（看得見才好 debug，不靜默吞掉）
 */
export function renderBodyTemplate(
  template: unknown,
  ctx: Record<string, unknown>,
): unknown {
  if (template === undefined || template === null) return undefined;
  return renderValue(template, ctx);
}

function renderValue(v: unknown, ctx: Record<string, unknown>): unknown {
  if (typeof v === 'string') return renderString(v, ctx);
  if (Array.isArray(v)) return v.map(item => renderValue(item, ctx));
  if (v !== null && typeof v === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
      out[k] = renderValue(val, ctx);
    }
    return out;
  }
  return v;
}

function renderString(s: string, ctx: Record<string, unknown>): unknown {
  const single = s.match(/^\s*\{\{([\w.]+)\}\}\s*$/);
  if (single) {
    const val = getPath(ctx, single[1]);
    return val === undefined ? s : val;
  }
  return s.replace(/\{\{([\w.]+)\}\}/g, (_, key: string) => {
    const val = getPath(ctx, key);
    if (val === undefined) return `{{${key}}}`;
    return typeof val === 'string' ? val : JSON.stringify(val);
  });
}

// ── ③-b response_map：回應正規化 ─────────────────────────────────────────────

export type NormalizedResponse = {
  /** 正規化後的純文字（沒有 response_map 或取不到時 undefined——誠實，不編造） */
  text?: string;
  /** 原始回應永遠保留（除錯與向後相容都靠它） */
  raw: unknown;
};

/**
 * 依 response_map 把各家 API 的回應正規化成 `{ text }`。
 * 沒給 map ⇒ 原樣回傳（既有 recipe 零行為變化）。
 */
export function applyResponseMap(body: unknown, map?: ResponseMap): NormalizedResponse {
  if (!map) return { raw: body };

  let picked: unknown = map.text_path ? getPath(body, map.text_path) : body;

  // 思考型模型：picked 是 parts 陣列 → 剔除 thought=true，取最後一個
  if (map.thinking_model && Array.isArray(picked)) {
    const real = picked.filter(
      p => !(p && typeof p === 'object' && (p as Record<string, unknown>).thought === true),
    );
    const last = real[real.length - 1];
    picked = (last && typeof last === 'object')
      ? (last as Record<string, unknown>).text
      : last;
  }

  if (typeof picked !== 'string') return { text: undefined, raw: body };

  return { text: sanitize(picked, map), raw: body };
}

/**
 * 淨化（知識是實撞出來的，非預想）：
 *   1. 有 answer_marker → 只取標記**最後一次**出現之後的內容
 *      （實撞：模型的自檢清單內文也會提到標記，用 lastIndexOf 才撈得到真的那個）
 *   2. 前綴組合順序不定（「* 【答】」「Draft: 【答】」「Answer: * 【答】」三型都撞過）
 *      ⇒ **循環**剝殼，單趟剝不乾淨
 */
function sanitize(input: string, map: ResponseMap): string {
  let s = input.trim();

  if (map.answer_marker) {
    const idx = s.lastIndexOf(map.answer_marker);
    if (idx >= 0) s = s.slice(idx + map.answer_marker.length);
  }

  const prefixes = map.strip_prefixes ?? [];
  if (prefixes.length > 0) {
    let changed = true;
    while (changed) {
      changed = false;
      s = s.trimStart();
      for (const p of prefixes) {
        if (p && s.startsWith(p)) {
          s = s.slice(p.length);
          changed = true;
        }
      }
    }
  }

  return s.trim();
}
