/**
 * Function Call Parser — Consolidated from MCP-SuperAssistant render_prescript
 * Detects both XML (<invoke>) and JSON (function_call_start) formats in AI chat DOM
 */
import type { FunctionInfo } from "./types"
import { CONFIG } from "./config"
import { createLogger } from "./logger"

const logger = createLogger("Parser")

// ─── Language Tag Stripping ───────────────────────────────────────────

const LANG_STRIP_RE =
  /^(javascript|typescript|markdown|csharp|kotlin|python|jsonl|bash|rust|java|scala|swift|shell|json|text|perl|yaml|toml|html|ruby|cpp|php|lua|css|sql|yml|ini|xml|ts|js|py|sh|md|cs|go|rb|c|r)(\s*copy(\s*code)?)?\s*/i

const CODE_FENCE_RE =
  /^```\s*(javascript|typescript|markdown|csharp|kotlin|python|jsonl|bash|rust|java|scala|swift|shell|json|text|perl|yaml|toml|html|ruby|cpp|php|lua|css|sql|yml|ini|xml|ts|js|py|sh|md|cs|go|rb|c|r)?\s*/i

export function stripLanguageTags(line: string): string {
  let cleaned = line.trim()
  cleaned = cleaned.replace(CODE_FENCE_RE, "")
  cleaned = cleaned.replace(LANG_STRIP_RE, "")
  cleaned = cleaned.replace(/^[cC]opy(\s+code)?\s*/i, "")
  return cleaned
}

// ─── JSON Object Extraction ─────────────────────────────────────────

/** Extract JSON objects from content using bracket matching (language-agnostic) */
export function extractJSONObjects(content: string): string[] {
  const objects: string[] = []
  let i = 0
  while (i < content.length) {
    if (content[i] === "{" || content[i] === "[") {
      const startChar = content[i]
      const endChar = startChar === "{" ? "}" : "]"
      const startIndex = i
      let depth = 1
      let inString = false
      let escapeNext = false
      i++
      while (i < content.length && depth > 0) {
        const char = content[i]
        if (escapeNext) {
          escapeNext = false
        } else if (char === "\\" && inString) {
          escapeNext = true
        } else if (char === '"') {
          inString = !inString
        } else if (!inString) {
          if (char === startChar) depth++
          else if (char === endChar) depth--
        }
        i++
      }
      if (depth === 0) {
        const jsonStr = content.substring(startIndex, i)
        try {
          const parsed = JSON.parse(jsonStr)
          if (parsed.type || Array.isArray(parsed)) {
            objects.push(jsonStr)
          }
        } catch {
          /* not valid JSON */
        }
      }
    } else {
      i++
    }
  }
  return objects
}

export function extractCleanContent(content: string): string {
  const objs = extractJSONObjects(content)
  return objs.length > 0 ? objs.join("\n") : content.trim()
}

// ─── JSON Function Call Line Parser ─────────────────────────────────

interface JSONFunctionLine {
  type: "function_call_start" | "description" | "parameter" | "function_call_end"
  name?: string
  call_id?: number | string
  text?: string
  key?: string
  value?: any
}

function parseJSONLine(line: string): JSONFunctionLine | null {
  try {
    let cleaned = stripLanguageTags(line.trim())
    if (!cleaned) return null
    if (!cleaned.startsWith("{") && !cleaned.startsWith("[")) {
      const idx = cleaned.search(/[\[{]/)
      if (idx > 0) cleaned = cleaned.substring(idx)
    }
    if (!cleaned) return null
    const parsed = JSON.parse(cleaned)
    if (!parsed.type || typeof parsed.type !== "string") return null
    return parsed as JSONFunctionLine
  } catch {
    return null
  }
}

/** Reconstruct multi-line pretty-printed JSON into single-line objects */
function reconstructJSONObjects(lines: string[]): string[] {
  const result: string[] = []
  let current = ""
  let depth = 0
  let inObj = false
  for (const line of lines) {
    const t = line.trim()
    if (!t) continue
    for (const ch of t) {
      if (ch === "{") { depth++; inObj = true }
      else if (ch === "}") depth--
    }
    current += (current ? " " : "") + t
    if (inObj && depth === 0) {
      result.push(current)
      current = ""
      inObj = false
    }
  }
  if (current.trim()) result.push(current)
  return result
}

// ─── Main Detection: containsFunctionCalls ──────────────────────────

/** Analyze a DOM element to detect function call blocks (JSON or XML format) */
export function containsFunctionCalls(block: HTMLElement): FunctionInfo {
  const empty: FunctionInfo = {
    hasFunctionCalls: false,
    isComplete: false,
    hasInvoke: false,
    hasParameters: false,
    hasClosingTags: false,
    languageTag: null,
    detectedBlockType: null,
    partialTagDetected: false,
  }

  // Skip if inside already-rendered block
  if (block.closest(".function-block")) return empty

  // Get content — try code child first, then raw text
  let content = ""
  const codeChild = block.querySelector("code:not(.function-block code)")
  if (codeChild?.textContent) {
    content = codeChild.textContent.trim()
  }
  if (!content) {
    const raw = block.textContent?.trim() || ""
    const objs = extractJSONObjects(raw)
    content = objs.length > 0 ? objs.join("\n") : raw
  }

  // ── JSON detection ──
  const jsonResult = detectJSONFunctionCalls(content, block)
  if (jsonResult.hasFunctionCalls) return jsonResult

  // ── XML detection (Claude Opus style) ──
  if (
    content.includes("<function_calls>") ||
    content.includes("<invoke")
  ) {
    const result = { ...empty }
    result.hasFunctionCalls = true
    result.detectedBlockType = "antml"
    result.hasInvoke = content.includes("<invoke")
    result.hasParameters = content.includes("<parameter")
    if (result.hasInvoke) {
      const m = content.match(/<invoke name="([^"]+)"(?:\s+call_id="([^"]+)")?>/)
      if (m?.[1]) result.invokeName = m[1]
    }
    const hasOpen = content.includes("<function_calls>")
    const hasClose = content.includes("</function_calls>")
    result.hasClosingTags = hasOpen && hasClose
    result.isComplete = result.hasClosingTags
    return result
  }

  return empty
}

function detectJSONFunctionCalls(content: string, _block: HTMLElement): FunctionInfo {
  const empty: FunctionInfo = {
    hasFunctionCalls: false, isComplete: false, hasInvoke: false,
    hasParameters: false, hasClosingTags: false, languageTag: null,
    detectedBlockType: null, partialTagDetected: false,
  }

  const hasType = /["']?type["']?\s*:/i.test(content)
  const hasFC = content.includes("function_call") ||
    (hasType && /function_call_\w*/u.test(content)) ||
    (hasType && content.includes("function_ca"))
  const hasParam = content.includes('"parameter"')
  const looksJSON = content.includes('{"type"') || content.includes('{ "type"')

  if (!(hasType && (hasFC || hasParam || looksJSON))) return empty

  // Parse lines
  let lines = content.split(/\r?\n|\u2028|\u2029/)
  const isSingleLine = lines.length === 1 && (content.match(/\{/g) || []).length > 1
  const isPretty = lines.length > 1 && (content.includes("{\n") || lines.some(l => l.trim() === "{"))

  if (isSingleLine) {
    const parts = content.split(/\}\s*\{/)
    lines = parts.map((p, i, a) => {
      if (i === 0) return p + "}"
      if (i === a.length - 1) return "{" + p
      return "{" + p + "}"
    })
  } else if (isPretty) {
    lines = reconstructJSONObjects(lines)
  }

  let hasStart = false, hasEnd = false, paramCount = 0
  let funcName: string | null = null, callId: string | null = null
  let description: string | null = null, hasPartial = false

  for (const line of lines) {
    const parsed = parseJSONLine(line)
    if (!parsed) {
      const t = line.trim()
      if (t.startsWith("{") && !t.endsWith("}")) hasPartial = true
      continue
    }
    switch (parsed.type) {
      case "function_call_start":
        hasStart = true
        funcName = parsed.name || null
        callId = parsed.call_id?.toString() || null
        break
      case "description":
        description = parsed.text || null
        break
      case "parameter":
        paramCount++
        break
      case "function_call_end":
        hasEnd = true
        break
    }
  }

  if (hasStart || (hasPartial && looksJSON)) {
    return {
      hasFunctionCalls: true,
      detectedBlockType: "json",
      hasInvoke: hasStart,
      hasParameters: paramCount > 0,
      hasClosingTags: hasEnd,
      isComplete: hasStart && hasEnd,
      languageTag: null,
      partialTagDetected: hasPartial,
      invokeName: funcName || undefined,
      description: description || undefined,
    }
  }

  return empty
}

// ─── Parameter Extraction ───────────────────────────────────────────

/** Extract function name from raw content (JSON or XML) */
export function extractFunctionName(content: string): string | null {
  // JSON format
  const lines = content.split("\n")
  for (const line of lines) {
    const parsed = parseJSONLine(line)
    if (parsed?.type === "function_call_start" && parsed.name) return parsed.name
  }
  // XML format
  const m = content.match(/<invoke name="([^"]+)"/)
  return m?.[1] || null
}

/** Extract parameters from JSON function call content */
export function extractJSONParameters(content: string): Record<string, any> {
  const params: Record<string, any> = {}
  if (!content) return params

  for (const line of content.split("\n")) {
    const parsed = parseJSONLine(line)
    if (parsed?.type === "parameter" && parsed.key) {
      params[parsed.key] = parsed.value ?? ""
    }
  }

  // Fallback regex for streaming/partial content
  const re = /"type"\s*:\s*"parameter"[^}]*"key"\s*:\s*"([^"]+)"[^}]*"value"\s*:\s*"((?:[^"\\]|\\.)*)"/g
  let m
  while ((m = re.exec(content)) !== null) {
    if (!params.hasOwnProperty(m[1])) {
      params[m[1]] = m[2]
        .replace(/\\n/g, "\n")
        .replace(/\\t/g, "\t")
        .replace(/\\"/g, '"')
        .replace(/\\\\/g, "\\")
    }
  }
  return params
}

/** Extract parameters from XML function call content */
export function extractXMLParameters(content: string): Record<string, any> {
  const params: Record<string, any> = {}
  const re = /<parameter\s+name="([^"]+)"\s*(?:type="([^"]+)")?\s*>(.*?)<\/parameter>/gs
  let m
  while ((m = re.exec(content)) !== null) {
    let value: any = m[3]
    const cdataMatch = value.match(/<!\[CDATA\[([\s\S]*?)\]\]>/)
    if (cdataMatch) value = cdataMatch[1]
    // Type coercion
    if (/^-?\d+(\.\d+)?$/.test(value)) value = Number(value)
    else if (/^(true|false)$/i.test(value)) value = value.toLowerCase() === "true"
    params[m[1]] = value
  }
  return params
}

/** Check if JSON function call is still streaming */
export function isJSONFunctionStreaming(content: string): boolean {
  let hasStart = false, hasEnd = false
  for (const line of content.split("\n")) {
    const p = parseJSONLine(line)
    if (p?.type === "function_call_start") hasStart = true
    if (p?.type === "function_call_end") hasEnd = true
  }
  return hasStart && !hasEnd
}
