/**
 * Shared JSON parsing utility for AI responses.
 *
 * Handles common issues with AI-generated JSON:
 * - Thinking/reasoning tags (e.g. GLM think tags)
 * - Untagged chain-of-thought / preamble text before JSON
 * - Markdown code fences
 * - Preamble text before the JSON
 * - Truncated responses with unclosed brackets/braces
 * - Trailing commas and incomplete key-value pairs
 * - Smart quotes and control characters inside strings
 * - Unicode BOM or other invisible characters
 */

export function parseJsonFromAI(content: string): Record<string, any> | null {
  if (!content || typeof content !== 'string') return null;

  let cleaned = content.trim();

  // Remove Unicode BOM if present
  if (cleaned.charCodeAt(0) === 0xFEFF) {
    cleaned = cleaned.substring(1);
  }

  // Strip thinking/reasoning tags that some models (GLM) include
  // Handle both XML-style tags and GLM-style think blocks
  cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/gi, '');
  cleaned = cleaned.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
  cleaned = cleaned.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '');
  // Handle orphaned closing think tag (GLM sometimes outputs just </think> before the actual answer)
  cleaned = cleaned.replace(/^\s*<\/think>\s*/gi, '');
  // Handle orphaned opening think tag without closing (truncated response)
  cleaned = cleaned.replace(/<think\s*$/gi, '');
  // Strip any remaining think tags that span the entire content with preamble
  cleaned = cleaned.replace(/^[^{[]*?<think>[\s\S]*?<\/think>\s*/gi, '');

  // GLM models sometimes output a ```` tag followed by content
  cleaned = cleaned.replace(/^\s*````\s*/g, '');

  // Strip markdown code fences
  cleaned = cleaned.replace(/^```(?:json)?\s*\n?/gm, '');
  cleaned = cleaned.replace(/\n?```\s*$/gm, '');
  cleaned = cleaned.trim();

  // ── Strip untagged chain-of-thought / preamble text ─────────────────────
  // Some models (especially GLM-5.1) output reasoning text like
  // "The user wants a 30-second storytelling speech in Hindi... Let me craft..."
  // BEFORE the JSON, without any <think> tags. We need to detect and strip this.
  // Strategy: find the first { or [ that starts a valid JSON structure,
  // then strip everything before it.
  const jsonStartCandidate = findJsonStart(cleaned);
  if (jsonStartCandidate > 0) {
    cleaned = cleaned.substring(jsonStartCandidate);
  } else if (jsonStartCandidate === -1) {
    // No JSON structure found at all — try more aggressive strategies
    // Check if the entire content might be wrapped in a code block we missed
    const codeBlockMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
    if (codeBlockMatch) {
      cleaned = codeBlockMatch[1].trim();
    }
  }

  // Remove trailing text after the last } or ]
  const lastBraceIdx = cleaned.lastIndexOf('}');
  const lastBracketIdx = cleaned.lastIndexOf(']');
  const jsonEndIdx = Math.max(lastBraceIdx, lastBracketIdx);
  if (jsonEndIdx > 0 && jsonEndIdx < cleaned.length - 1) {
    const afterLastBrace = cleaned.substring(jsonEndIdx + 1).trim();
    if (afterLastBrace.length > 0 && !afterLastBrace.startsWith('}')) {
      cleaned = cleaned.substring(0, jsonEndIdx + 1);
    }
  }

  // First, try parsing as-is — if the model already produced valid JSON, return immediately
  try {
    return JSON.parse(cleaned);
  } catch {
    // needs fixing — continue below
  }

  // Apply fixes to make the JSON parseable
  let fixed = cleaned;

  // Replace smart/curly quotes with standard double quotes
  fixed = fixed.replace(/[“”]/g, '"');
  fixed = fixed.replace(/[‘’]/g, "'");

  // Escape literal control characters that break JSON string parsing:
  // \r (carriage return), \t (tab)
  fixed = fixed.replace(/\r/g, '\\r');
  fixed = fixed.replace(/\t/g, '\\t');

  // Escape literal newlines that appear inside JSON string values
  // Strategy: walk through the string, track whether we're inside a quoted string,
  // and escape bare newlines inside strings
  let inString = false;
  let escapeNext = false;
  let result = '';
  for (let i = 0; i < fixed.length; i++) {
    const ch = fixed[i];
    if (escapeNext) {
      result += ch;
      escapeNext = false;
      continue;
    }
    if (ch === '\\' && inString) {
      result += ch;
      escapeNext = true;
      continue;
    }
    if (ch === '"') {
      inString = !inString;
      result += ch;
      continue;
    }
    if (ch === '\n' && inString) {
      result += '\\n';
      continue;
    }
    result += ch;
  }
  cleaned = result;

  // Try parsing again after fixes
  try {
    return JSON.parse(cleaned);
  } catch {
    // continue to extraction strategies
  }

  // Try extracting JSON object from response (handles preamble text)
  const objectMatch = cleaned.match(/\{[\s\S]*\}/);
  if (objectMatch) {
    try {
      return JSON.parse(objectMatch[0]);
    } catch {}

    // Try progressively removing trailing content to find valid JSON
    let jsonStr = objectMatch[0];
    // Remove trailing incomplete string values
    jsonStr = jsonStr.replace(/,?\s*"[^"]*"?\s*:\s*"[^"\\]*$/s, '');
    jsonStr = jsonStr.replace(/,?\s*"[^"]*"?\s*:\s*$/s, '');
    // Remove trailing comma before closing brace/bracket
    jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1');
    try {
      return JSON.parse(jsonStr);
    } catch {}
  }

  // Try extracting JSON array
  const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
  if (arrayMatch) {
    try {
      return JSON.parse(arrayMatch[0]);
    } catch {}
  }

  // Attempt bracket repair for truncated responses
  const openBraces = (cleaned.match(/\{/g) || []).length;
  const closeBraces = (cleaned.match(/\}/g) || []).length;
  const openBrackets = (cleaned.match(/\[/g) || []).length;
  const closeBrackets = (cleaned.match(/\]/g) || []).length;

  if (openBraces > closeBraces || openBrackets > closeBrackets) {
    let repaired = cleaned;
    // Remove trailing incomplete key-value pairs (string values)
    repaired = repaired.replace(/,?\s*"[^"]*"?\s*:\s*"[^"\\]*$/s, '');
    repaired = repaired.replace(/,?\s*"[^"]*"?\s*:\s*$/s, '');
    // Remove trailing incomplete objects
    repaired = repaired.replace(/,\s*\{[^}]*$/s, '');
    // Remove trailing commas before closing brackets
    repaired = repaired.replace(/,(\s*[}\]])/g, '$1');

    // Recount after partial repairs
    const newOpenBraces = (repaired.match(/\{/g) || []).length;
    const newCloseBraces = (repaired.match(/\}/g) || []).length;
    const newOpenBrackets = (repaired.match(/\[/g) || []).length;
    const newCloseBrackets = (repaired.match(/\]/g) || []).length;

    // Close unclosed brackets and braces
    for (let i = 0; i < newOpenBrackets - newCloseBrackets; i++) repaired += ']';
    for (let i = 0; i < newOpenBraces - newCloseBraces; i++) repaired += '}';
    try {
      return JSON.parse(repaired);
    } catch {}

    // If still failing, try a more aggressive approach:
    // Find the last complete object in each array and strip the rest
    const aggressiveRepaired = stripIncompleteTrailingItems(cleaned);
    if (aggressiveRepaired) {
      try {
        return JSON.parse(aggressiveRepaired);
      } catch {}
    }
  }

  // Last resort: try to find any valid JSON substring by progressively trimming
  // This handles cases where the model outputs extra text after the JSON
  const jsonCandidates = cleaned.match(/\{[^{}]*\{[\s\S]*?\}[^{}]*\}/g);
  if (jsonCandidates) {
    for (const candidate of jsonCandidates) {
      try {
        return JSON.parse(candidate);
      } catch {}
    }
  }

  // Final last resort: find the first { and the last } and extract everything between them
  const firstBrace = cleaned.indexOf('{');
  const lastBrace = cleaned.lastIndexOf('}');
  if (firstBrace >= 0 && lastBrace > firstBrace) {
    const candidate = cleaned.substring(firstBrace, lastBrace + 1);
    try {
      return JSON.parse(candidate);
    } catch {}
  }

  console.warn('[AI] Failed to parse AI response as JSON');
  return null;
}

/**
 * Find the index of the first character that starts a valid JSON structure.
 * This is smarter than a simple regex search for { or [ because it skips
 * curly braces that appear inside prose/preamble text.
 *
 * Returns:
 *  - The index of the first valid JSON start character
 *  - 0 if the string already starts with valid JSON
 *  - -1 if no JSON structure is found
 */
function findJsonStart(text: string): number {
  // Quick check: if the text starts with { or [, it's likely JSON already
  const trimmed = text.trimStart();
  if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
    return text.length - trimmed.length;
  }

  // Strategy: look for the first occurrence of a pattern that starts a JSON object:
  // {"key": or {" or [ — but NOT patterns inside prose like "the user wants {type}"
  // We look for {" (object start with a key) or [ (array start)

  // Pattern 1: JSON object starting with a quoted key — {"name", {"key":, {"key" :
  const jsonKeyStart = text.search(/\{\s*"/);
  // Pattern 2: JSON object starting with { and newline/whitespace (pretty-printed)
  const jsonObjectStart = text.search(/\{\s*\n\s*"/);
  // Pattern 3: JSON array start
  const jsonArrayStart = text.search(/\[\s*[\{"]/);
  // Pattern 4: Bare { followed by a letter (less strict, but catches most cases)
  const bareBraceStart = text.search(/\{[\n\r\s]*[a-zA-Z]/);

  // Collect all candidate positions, filter out -1
  const candidates = [
    jsonKeyStart,
    jsonObjectStart,
    jsonArrayStart,
    bareBraceStart,
  ].filter(idx => idx >= 0);

  if (candidates.length === 0) {
    // Last resort: find any { character
    const anyBrace = text.indexOf('{');
    const anyBracket = text.indexOf('[');
    if (anyBrace < 0 && anyBracket < 0) return -1;
    if (anyBrace < 0) return anyBracket;
    if (anyBracket < 0) return anyBrace;
    return Math.min(anyBrace, anyBracket);
  }

  return Math.min(...candidates);
}

/**
 * Aggressive truncation repair: for truncated JSON with nested arrays of objects,
 * find the last complete object in each array and discard incomplete trailing items.
 *
 * Example input:  {"templates": [{"id":"1"}, {"id":"2", "name":  (truncated)
 * Example output: {"templates": [{"id":"1"}]}
 */
function stripIncompleteTrailingItems(content: string): string | null {
  // Find the outermost JSON object
  const outerMatch = content.match(/\{[\s\S]*\}/);
  if (!outerMatch) return null;

  let jsonStr = outerMatch[0];

  // For each array in the JSON, find complete items and strip incomplete ones.
  // Find positions of "}," or "}]" which mark the end of complete objects in arrays
  const completeObjectEnds: number[] = [];
  for (let i = 0; i < jsonStr.length; i++) {
    if (jsonStr[i] === '}') {
      // Check if followed by , or ] (end of a complete array item)
      const nextNonSpace = jsonStr.slice(i + 1).search(/\S/);
      if (nextNonSpace >= 0) {
        const nextChar = jsonStr[i + 1 + nextNonSpace];
        if (nextChar === ',' || nextChar === ']') {
          completeObjectEnds.push(i + 1 + nextNonSpace);
        }
      }
    }
  }

  // Try truncating at each complete object boundary from the end
  for (let i = completeObjectEnds.length - 1; i >= 0; i--) {
    let candidate = jsonStr.slice(0, completeObjectEnds[i] + 1);
    // Count and close remaining unclosed brackets/braces
    const oB = (candidate.match(/\{/g) || []).length;
    const cB = (candidate.match(/\}/g) || []).length;
    const oBr = (candidate.match(/\[/g) || []).length;
    const cBr = (candidate.match(/\]/g) || []).length;

    // Remove trailing comma
    candidate = candidate.replace(/,(\s*)$/, '$1');
    for (let j = 0; j < oBr - cBr; j++) candidate += ']';
    for (let j = 0; j < oB - cB; j++) candidate += '}';

    try {
      JSON.parse(candidate);
      return candidate;
    } catch {
      continue;
    }
  }

  return null;
}