/**
 * Parse LLM output into JSON, handling common LLM quirks:
 * 1. Markdown code fences (```json ... ``` or ``` ... ```)
 * 2. Leading/trailing text around the JSON
 * 3. Nested objects where strings were expected (flatten them)
 */

/** Strip markdown code fences and extract the JSON object */
export function extractJson(raw: string): string | null {
  // Remove markdown code fences: ```json ... ``` or ``` ... ```
  let cleaned = raw.trim();

  // Try to extract from code fence
  const fenceMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
  if (fenceMatch) {
    cleaned = fenceMatch[1].trim();
  }

  // Find the outermost { ... } brace pair
  const start = cleaned.indexOf("{");
  const end = cleaned.lastIndexOf("}");
  if (start !== -1 && end > start) {
    return cleaned.substring(start, end + 1);
  }

  return null;
}

/** Parse JSON from LLM output, stripping fences and handling errors */
export function parseLlmJson<T>(raw: string): T | null {
  const jsonStr = extractJson(raw);
  if (!jsonStr) return null;

  try {
    return JSON.parse(jsonStr) as T;
  } catch (e) {
    // ponytail: LLMs often emit *almost*-valid JSON. Strict parse runs first so
    // valid output is never altered; only on failure do we try a conservative
    // repair pass (surface fixes → merge stray array literals → balance brackets).
    const repaired = repairJson(jsonStr);
    if (repaired !== jsonStr) {
      try {
        const parsed = JSON.parse(repaired) as T;
        if (parsed) return parsed;
      } catch (e2) {
        console.error("JSON parse error (after repair):", e2, "Input:", jsonStr);
      }
    }
    console.error("JSON parse error:", e, "Input:", jsonStr);
    return null;
  }
}

/** Conservative repairs for common LLM JSON mistakes. String-aware (never
 *  touches text inside double-quoted strings — that was the bug that turned
 *  "Coop's" / "crows'" into garbage). Only applied on parse failure. */
function repairJson(input: string): string {
  return balanceJson(mergeSeparateArrays(surfaceFixes(input)));
}

/** Drop trailing commas before closers and convert smart quotes → straight,
 *  only outside double-quoted strings. */
function surfaceFixes(s: string): string {
  let out = "";
  let i = 0;
  let inStr = false;
  while (i < s.length) {
    const ch = s[i];
    if (inStr) {
      out += ch;
      if (ch === "\\") { out += s[i + 1] ?? ""; i += 2; continue; }
      if (ch === '"') inStr = false;
      i++;
      continue;
    }
    if (ch === '"') { inStr = true; out += ch; i++; continue; }
    if (ch === "“" || ch === "”" || ch === "„") { out += '"'; i++; continue; }
    if (ch === ",") {
      let j = i + 1;
      while (j < s.length && /\s/.test(s[j])) j++;
      if (s[j] === "}" || s[j] === "]") { i++; continue; } // drop trailing comma
    }
    out += ch;
    i++;
  }
  return out;
}

/** Collapse the common "array of strings written as separate array literals"
 *  mistake: ["a"], ["b"], ["c"] → "a", "b", "c" (inside one array). */
function mergeSeparateArrays(s: string): string {
  return s.replace(/"\s*\]\s*,\s*\[\s*"/g, '", "');
}

/** Stack-based bracket balance. On a closer that doesn't match the top, close
 *  the open intermediate openers first (inserts the `}` the LLM forgot before a
 *  `]`), then match. Closers with no matching opener anywhere are stray → skip.
 *  Any still-open openers at the end get their closers appended. */
function balanceJson(s: string): string {
  let out = "";
  const st: string[] = [];
  let inStr = false;
  let esc = false;
  const closeOf = (o: string) => (o === "{" ? "}" : "]");
  for (const ch of s) {
    if (inStr) {
      out += ch;
      if (esc) esc = false;
      else if (ch === "\\") esc = true;
      else if (ch === '"') inStr = false;
      continue;
    }
    if (ch === '"') { inStr = true; out += ch; continue; }
    if (ch === "{" || ch === "[") { st.push(ch); out += ch; continue; }
    if (ch === "}" || ch === "]") {
      const want = ch === "}" ? "{" : "[";
      if (st.includes(want)) {
        while (st.length) {
          const top = st.pop()!;
          out += top === want ? ch : closeOf(top);
          if (top === want) break;
        }
      }
      // else: no matching opener anywhere → stray closer, skip
      continue;
    }
    out += ch;
  }
  while (st.length) out += closeOf(st.pop()!);
  return out;
}

/**
 * Flatten nested objects into display-friendly strings.
 * If a value is an object, stringify it nicely.
 * If a value is an array of objects, join them.
 */
export function flattenValue(val: unknown): string {
  if (val === null || val === undefined) return "";
  if (typeof val === "string") return val;
  if (typeof val === "number") return String(val);
  if (typeof val === "boolean") return String(val);
  if (Array.isArray(val)) {
    return val.map(flattenValue).join("\n");
  }
  if (typeof val === "object") {
    // Pretty-format the object as key: value pairs
    return Object.entries(val as Record<string, unknown>)
      .map(([k, v]) => {
        const formatted = typeof v === "string" ? v : JSON.stringify(v);
        return `**${formatKey(k)}**: ${formatted}`;
      })
      .join("\n");
  }
  return String(val);
}

/** Convert camelCase or snake_case keys to Title Case */
function formatKey(key: string): string {
  return key
    .replace(/_/g, " ")
    .replace(/([a-z])([A-Z])/g, "$1 $2")
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

/** Safely get an array from a field that might be a string or array */
export function ensureArray(val: unknown): string[] {
  if (Array.isArray(val)) {
    return val.map((v) => (typeof v === "string" ? v : JSON.stringify(v)));
  }
  if (typeof val === "string") {
    // Try to parse as JSON array
    try {
      const parsed = JSON.parse(val);
      if (Array.isArray(parsed)) return parsed.map((v: unknown) => String(v));
    } catch {
      // Not JSON, return as single-element array
    }
    return [val];
  }
  return [];
}