// lib/json-utils.js — tolerant JSON extraction for LLM responses.
// Local models often wrap JSON in prose or code fences, or leave trailing commas.

export function extractJson(text) {
  if (text == null) throw new Error("Model returned an empty response.");
  let s = String(text).trim();

  // Strip code fences anywhere
  s = s.replace(/```(?:json|javascript|js)?/gi, "```");
  const fenced = s.match(/```([\s\S]*?)```/);
  if (fenced && fenced[1].trim().match(/^[\[{]/)) s = fenced[1].trim();

  // Slice from first brace/bracket to its matching end; try whichever starts first, first.
  const candidates = [];
  const firstObj = s.indexOf("{");
  const firstArr = s.indexOf("[");
  const objSlice = firstObj !== -1 ? s.slice(firstObj, s.lastIndexOf("}") + 1) : null;
  const arrSlice = firstArr !== -1 ? s.slice(firstArr, s.lastIndexOf("]") + 1) : null;
  if (firstArr !== -1 && (firstObj === -1 || firstArr < firstObj)) {
    if (arrSlice) candidates.push(arrSlice);
    if (objSlice) candidates.push(objSlice);
  } else {
    if (objSlice) candidates.push(objSlice);
    if (arrSlice) candidates.push(arrSlice);
  }
  candidates.unshift(s);

  let lastErr = null;
  for (const c of candidates) {
    if (!c) continue;
    for (const attempt of [c, repair(c)]) {
      try {
        return JSON.parse(attempt);
      } catch (e) {
        lastErr = e;
      }
    }
  }
  const preview = s.slice(0, 300).replace(/\s+/g, " ");
  throw new Error(
    "Could not read the model's response as JSON. Try again, lower the temperature, or use a stronger model. Response began: " + preview
  );
}

function repair(s) {
  let out = s;
  // Smart quotes -> straight quotes
  out = out.replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'");
  // Remove trailing commas before } or ]
  out = out.replace(/,\s*([}\]])/g, "$1");
  // Remove JS-style comments
  out = out.replace(/^\s*\/\/.*$/gm, "");
  // Control characters inside strings break JSON.parse; replace bare newlines in strings
  out = sanitizeNewlinesInStrings(out);
  return out;
}

function sanitizeNewlinesInStrings(s) {
  let result = "";
  let inStr = false;
  let escaped = false;
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (inStr) {
      if (escaped) {
        result += ch;
        escaped = false;
        continue;
      }
      if (ch === "\\") {
        result += ch;
        escaped = true;
        continue;
      }
      if (ch === '"') {
        inStr = false;
        result += ch;
        continue;
      }
      if (ch === "\n") { result += "\\n"; continue; }
      if (ch === "\r") { continue; }
      if (ch === "\t") { result += "\\t"; continue; }
      result += ch;
    } else {
      if (ch === '"') inStr = true;
      result += ch;
    }
  }
  return result;
}
