import type { MessageTurn } from "./types"
import { normalizeToolName } from "./tool-call-normalization"

export type FileOperation = "read" | "edit" | "write" | "apply_patch"

export interface SessionFileChange {
  path: string
  operations: FileOperation[]
}

export interface FileChangeStat {
  id: string
  path: string
  additions: number
  deletions: number
  diff: string | null
}

export interface UserMessageGroup {
  userTurnId: string
  userMessage: string
  timestamp: string
  files: FileChangeStat[]
}

interface DiffStat {
  additions: number
  deletions: number
}

interface EditChangePreview {
  oldText: string
  newText: string
  unifiedDiff: string | null
}

interface DiffSection extends DiffStat {
  chunk: string
}

const WRITE_OPS = new Set<string>(["edit", "write", "apply_patch"])
const FILE_OPS = new Set<string>(["read", "edit", "write", "apply_patch"])

const NESTED_PAYLOAD_KEYS = ["input", "arguments", "params", "payload"]

const PATH_KEYS = [
  "file_path",
  "filePath",
  "path",
  "target_file",
  "targetFile",
  "filename",
  "notebook_path",
] as const

const PATCH_TEXT_KEYS = [
  "patch",
  "content",
  "diff",
  "unified_diff",
  "unifiedDiff",
  "input",
  "command",
  "cmd",
  "script",
] as const

const EDIT_CHANGE_OLD_KEYS = [
  "old_string",
  "oldString",
  "old_text",
  "oldText",
  "old",
  "before",
  "source",
  "original",
]

const EDIT_CHANGE_NEW_KEYS = [
  "new_string",
  "newString",
  "new_text",
  "newText",
  "new_content",
  "newContent",
  "new",
  "replacement",
  "after",
  "after_text",
  "afterText",
  "updated",
  "updated_text",
  "updatedText",
  "content",
  "new_source",
  "newSource",
  "text",
]

const EDIT_CHANGE_DIFF_KEYS = ["diff", "patch", "unified_diff", "unifiedDiff"]

function normalizePath(path: string): string {
  return path.replace(/\\/g, "/")
}

function asObjectLike(value: unknown): Record<string, unknown> | null {
  if (value && typeof value === "object" && !Array.isArray(value)) {
    return value as Record<string, unknown>
  }

  if (typeof value !== "string") return null
  const trimmed = value.trim()
  if (!trimmed.startsWith("{")) return null

  try {
    const parsed = JSON.parse(trimmed)
    return typeof parsed === "object" &&
      parsed !== null &&
      !Array.isArray(parsed)
      ? (parsed as Record<string, unknown>)
      : null
  } catch {
    return null
  }
}

function parseInputObject(
  inputPreview: string | null
): Record<string, unknown> | null {
  if (!inputPreview) return null
  return asObjectLike(inputPreview)
}

function findStringFieldDeep(
  value: unknown,
  keys: readonly string[],
  depth: number = 0
): string | null {
  if (depth > 4) return null

  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findStringFieldDeep(item, keys, depth + 1)
      if (found) return found
    }
    return null
  }

  const obj = asObjectLike(value)
  if (!obj) return null

  for (const key of keys) {
    const direct = obj[key]
    if (typeof direct === "string" && direct.trim().length > 0) {
      return direct
    }
  }

  for (const nestedKey of NESTED_PAYLOAD_KEYS) {
    const found = findStringFieldDeep(obj[nestedKey], keys, depth + 1)
    if (found) return found
  }

  for (const nestedValue of Object.values(obj)) {
    const found = findStringFieldDeep(nestedValue, keys, depth + 1)
    if (found) return found
  }

  return null
}

function findObjectFieldDeep(
  value: unknown,
  key: string,
  depth: number = 0
): Record<string, unknown> | null {
  if (depth > 4) return null

  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findObjectFieldDeep(item, key, depth + 1)
      if (found) return found
    }
    return null
  }

  const obj = asObjectLike(value)
  if (!obj) return null

  const direct = asObjectLike(obj[key])
  if (direct) return direct

  for (const nestedKey of NESTED_PAYLOAD_KEYS) {
    const found = findObjectFieldDeep(obj[nestedKey], key, depth + 1)
    if (found) return found
  }

  for (const nestedValue of Object.values(obj)) {
    const found = findObjectFieldDeep(nestedValue, key, depth + 1)
    if (found) return found
  }

  return null
}

function firstStringField(
  value: Record<string, unknown>,
  keys: readonly string[]
): string | null {
  for (const key of keys) {
    const field = value[key]
    if (typeof field === "string") {
      return field
    }
  }

  return null
}

function unescapeInlineEscapes(text: string): string {
  return text
    .replace(/\\r\\n/g, "\n")
    .replace(/\\n/g, "\n")
    .replace(/\\t/g, "\t")
}

function decodeJsonEscapedString(value: string): string {
  return value.replace(/\\"/g, '"').replace(/\\\//g, "/").replace(/\\\\/g, "\\")
}

function normalizeDiffPath(rawPath: string | null): string | null {
  if (!rawPath) return null

  const trimmed = rawPath.trim()
  if (!trimmed || trimmed === "/dev/null") return null

  if (trimmed.startsWith("a/") || trimmed.startsWith("b/")) {
    return normalizePath(trimmed.slice(2))
  }

  return normalizePath(trimmed)
}

function isDiffAddedLine(line: string): boolean {
  return line.startsWith("+") && !line.startsWith("+++")
}

function isDiffDeletedLine(line: string): boolean {
  return line.startsWith("-") && !line.startsWith("---")
}

function countLines(s: string): number {
  if (!s) return 0
  return s.split("\n").length
}

function countDiffLines(text: string): DiffStat {
  let additions = 0
  let deletions = 0

  for (const line of text.split("\n")) {
    if (isDiffAddedLine(line)) additions += 1
    if (isDiffDeletedLine(line)) deletions += 1
  }

  return { additions, deletions }
}

function createHunkHeader(oldLineCount: number, newLineCount: number): string {
  const oldStart = oldLineCount === 0 ? 0 : 1
  const newStart = newLineCount === 0 ? 0 : 1
  return `@@ -${oldStart},${oldLineCount} +${newStart},${newLineCount} @@`
}

function buildUnifiedDiff(
  filePath: string,
  oldText: string,
  newText: string
): string | null {
  if (!oldText && !newText) return null

  const oldLines = oldText ? oldText.split("\n") : []
  const newLines = newText ? newText.split("\n") : []

  const lines: string[] = [
    `--- a/${filePath}`,
    `+++ b/${filePath}`,
    createHunkHeader(oldLines.length, newLines.length),
  ]

  for (const line of oldLines) lines.push(`-${line}`)
  for (const line of newLines) lines.push(`+${line}`)

  return lines.join("\n")
}

function parseEditChangeValue(value: unknown): EditChangePreview | null {
  if (typeof value === "string") {
    return {
      oldText: "",
      newText: value,
      unifiedDiff: null,
    }
  }

  const record = asObjectLike(value)
  if (!record) return null

  const oldText =
    firstStringField(record, EDIT_CHANGE_OLD_KEYS) ??
    findStringFieldDeep(record, [
      "old_string",
      "old_text",
      "before_text",
      "old",
    ]) ??
    ""

  const newText =
    firstStringField(record, EDIT_CHANGE_NEW_KEYS) ??
    findStringFieldDeep(record, [
      "new_string",
      "new_text",
      "after_text",
      "new",
    ]) ??
    ""

  const unifiedDiff =
    firstStringField(record, EDIT_CHANGE_DIFF_KEYS) ??
    findStringFieldDeep(record, ["diff"]) ??
    null

  if (!oldText && !newText && !unifiedDiff) {
    return null
  }

  return {
    oldText,
    newText,
    unifiedDiff:
      unifiedDiff && unifiedDiff.trim().length > 0 ? unifiedDiff : null,
  }
}

function collectEditChangeValues(value: unknown): EditChangePreview[] {
  if (value === null || value === undefined) return []

  if (Array.isArray(value)) {
    return value.flatMap((item) => collectEditChangeValues(item))
  }

  const single = parseEditChangeValue(value)
  if (single) return [single]

  const record = asObjectLike(value)
  if (!record) return []

  return Object.values(record).flatMap((item) => collectEditChangeValues(item))
}

function pickValueByNormalizedPath(
  record: Record<string, unknown>,
  filePath: string
): unknown {
  if (filePath in record) return record[filePath]

  const escapedPath = filePath.replace(/\//g, "\\/")
  if (escapedPath in record) return record[escapedPath]

  for (const [key, value] of Object.entries(record)) {
    const normalizedKey = normalizePath(decodeJsonEscapedString(key.trim()))
    if (normalizedKey === filePath) {
      return value
    }
  }

  return undefined
}

function buildChunkFromEditChange(
  filePath: string,
  change: EditChangePreview
): string | null {
  if (change.unifiedDiff) {
    return change.unifiedDiff.trim()
  }

  return buildUnifiedDiff(filePath, change.oldText, change.newText)
}

function extractPatchTextFromInputPreview(
  inputPreview: string | null
): string | null {
  if (!inputPreview) return null

  const parsed = parseInputObject(inputPreview)
  const parsedPatchText = parsed
    ? findStringFieldDeep(parsed, PATCH_TEXT_KEYS)
    : null

  const candidates = [parsedPatchText, inputPreview]

  for (const candidate of candidates) {
    if (!candidate) continue

    const normalized = unescapeInlineEscapes(candidate.trim())
    if (!normalized) continue

    const block = normalized.match(
      /(\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch(?:\n|$))/m
    )?.[1]
    if (block) return block.trim()

    if (normalized.includes("*** Update File:")) return normalized
    if (/^diff --git /m.test(normalized)) return normalized
    if (/^--- .+/m.test(normalized) && /^\+\+\+ .+/m.test(normalized)) {
      return normalized
    }
  }

  return null
}

function parseApplyPatchSections(patchText: string): Map<string, DiffSection> {
  const sections = new Map<string, DiffSection>()

  let currentPath: string | null = null
  let currentLines: string[] = []
  let additions = 0
  let deletions = 0

  const flush = () => {
    if (!currentPath || currentLines.length === 0) return

    const chunk = currentLines.join("\n").trim()
    if (!chunk) return

    const existing = sections.get(currentPath)
    if (!existing) {
      sections.set(currentPath, { chunk, additions, deletions })
      return
    }

    sections.set(currentPath, {
      chunk: `${existing.chunk}\n${chunk}`,
      additions: existing.additions + additions,
      deletions: existing.deletions + deletions,
    })
  }

  const beginMarkers = [
    "*** Update File: ",
    "*** Add File: ",
    "*** Delete File: ",
  ]

  for (const line of patchText.split("\n")) {
    const marker = beginMarkers.find((prefix) => line.startsWith(prefix))
    if (marker) {
      flush()

      currentPath = normalizeDiffPath(line.slice(marker.length))
      currentLines = [line]
      additions = 0
      deletions = 0
      continue
    }

    if (!currentPath) continue

    currentLines.push(line)
    if (line.startsWith("*** Move to: ")) {
      const movedPath = normalizeDiffPath(line.slice(13))
      if (movedPath) currentPath = movedPath
      continue
    }
    if (isDiffAddedLine(line)) additions += 1
    if (isDiffDeletedLine(line)) deletions += 1
  }

  flush()
  return sections
}

function parseUnifiedDiffSections(diffText: string): Map<string, DiffSection> {
  const sections = new Map<string, DiffSection>()

  let currentPath: string | null = null
  let currentLines: string[] = []
  let additions = 0
  let deletions = 0

  const flush = () => {
    if (!currentPath || currentLines.length === 0) return

    const chunk = currentLines.join("\n").trim()
    if (!chunk) return

    const existing = sections.get(currentPath)
    if (!existing) {
      sections.set(currentPath, { chunk, additions, deletions })
      return
    }

    sections.set(currentPath, {
      chunk: `${existing.chunk}\n${chunk}`,
      additions: existing.additions + additions,
      deletions: existing.deletions + deletions,
    })
  }

  for (const line of diffText.split("\n")) {
    if (line.startsWith("diff --git ")) {
      flush()

      const match = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/)
      currentPath = normalizeDiffPath(match?.[2] ?? null)
      currentLines = [line]
      additions = 0
      deletions = 0
      continue
    }

    if (line.startsWith("--- ")) {
      const maybePath = normalizeDiffPath(line.slice(4))
      if (!currentPath && maybePath) {
        flush()
        currentPath = maybePath
        currentLines = []
        additions = 0
        deletions = 0
      }

      if (currentPath) currentLines.push(line)
      continue
    }

    if (line.startsWith("+++ ")) {
      const maybePath = normalizeDiffPath(line.slice(4))
      if (maybePath && maybePath !== currentPath) {
        flush()
        currentPath = maybePath
        currentLines = []
        additions = 0
        deletions = 0
      }

      if (currentPath) currentLines.push(line)
      continue
    }

    if (!currentPath) continue

    currentLines.push(line)
    if (isDiffAddedLine(line)) additions += 1
    if (isDiffDeletedLine(line)) deletions += 1
  }

  flush()
  return sections
}

function extractPathsFromPatchText(patchText: string): string[] {
  const applyPatchSections = parseApplyPatchSections(patchText)
  if (applyPatchSections.size > 0) {
    return Array.from(applyPatchSections.keys())
  }

  const unifiedDiffSections = parseUnifiedDiffSections(patchText)
  if (unifiedDiffSections.size > 0) {
    return Array.from(unifiedDiffSections.keys())
  }

  return []
}

function extractFilePaths(inputPreview: string | null): string[] {
  if (!inputPreview) return []

  const paths = new Set<string>()
  const parsed = parseInputObject(inputPreview)

  if (parsed) {
    const directPath = findStringFieldDeep(parsed, PATH_KEYS)
    if (directPath) {
      paths.add(normalizePath(directPath))
    }

    const editChanges = findObjectFieldDeep(parsed, "changes")
    if (editChanges) {
      for (const path of Object.keys(editChanges)) {
        const normalized = normalizePath(path.trim())
        if (normalized) paths.add(normalized)
      }
    }
  }

  const patchText = extractPatchTextFromInputPreview(inputPreview)
  if (patchText) {
    for (const path of extractPathsFromPatchText(patchText)) {
      paths.add(path)
    }
  }

  if (paths.size === 0) {
    const match = inputPreview.match(
      /"(?:file_path|filePath|path|target_file|targetFile|notebook_path)"\s*:\s*"((?:[^"\\]|\\.)+)"/
    )
    if (match?.[1]) {
      paths.add(normalizePath(decodeJsonEscapedString(match[1])))
    }
  }

  return Array.from(paths)
}

function computeLineDiff(
  op: string,
  inputPreview: string | null,
  filePath?: string
): DiffStat | null {
  if (!inputPreview) return null

  const normalizedPath = filePath ? normalizePath(filePath) : null
  const parsed = parseInputObject(inputPreview)

  if (op === "edit") {
    if (parsed && normalizedPath) {
      const changes = findObjectFieldDeep(parsed, "changes")
      const changeValue = changes
        ? pickValueByNormalizedPath(changes, normalizedPath)
        : undefined
      const changeValues = collectEditChangeValues(changeValue)

      if (changeValues.length > 0) {
        let additions = 0
        let deletions = 0

        for (const change of changeValues) {
          if (change.unifiedDiff) {
            const diff = countDiffLines(change.unifiedDiff)
            additions += diff.additions
            deletions += diff.deletions
            continue
          }

          additions += countLines(change.newText)
          deletions += countLines(change.oldText)
        }

        return { additions, deletions }
      }
    }

    if (!parsed) return null

    const oldStr =
      typeof parsed.old_string === "string" ? parsed.old_string : ""
    const newStr =
      typeof parsed.new_string === "string" ? parsed.new_string : ""

    if (!oldStr && !newStr) return null

    return {
      additions: countLines(newStr),
      deletions: countLines(oldStr),
    }
  }

  if (op === "write") {
    if (!parsed) return null

    const content = typeof parsed.content === "string" ? parsed.content : ""
    if (!content) return null

    return {
      additions: countLines(content),
      deletions: 0,
    }
  }

  if (op === "apply_patch") {
    const patchText = extractPatchTextFromInputPreview(inputPreview)
    if (!patchText) return null

    const applyPatchSections = parseApplyPatchSections(patchText)
    if (normalizedPath && applyPatchSections.has(normalizedPath)) {
      const section = applyPatchSections.get(normalizedPath)
      if (section) {
        return {
          additions: section.additions,
          deletions: section.deletions,
        }
      }
    }

    const unifiedDiffSections = parseUnifiedDiffSections(patchText)
    if (normalizedPath && unifiedDiffSections.has(normalizedPath)) {
      const section = unifiedDiffSections.get(normalizedPath)
      if (section) {
        return {
          additions: section.additions,
          deletions: section.deletions,
        }
      }
    }

    const total = countDiffLines(patchText)
    if (total.additions === 0 && total.deletions === 0) {
      return null
    }

    return total
  }

  return null
}

function extractUserMessage(turn: MessageTurn): string {
  for (const block of turn.blocks) {
    if (block.type === "text" && block.text) {
      const text = block.text.trim()
      if (text.length > 80) return `${text.slice(0, 77)}...`
      return text
    }
  }

  return "User message"
}

export function extractSessionFilesGrouped(
  turns: MessageTurn[]
): UserMessageGroup[] {
  const groups: UserMessageGroup[] = []
  let currentUserTurn: MessageTurn | null = null
  let currentFiles: FileChangeStat[] = []

  const flushGroup = () => {
    if (!currentUserTurn) return
    if (currentFiles.length === 0) return

    groups.push({
      userTurnId: currentUserTurn.id,
      userMessage: extractUserMessage(currentUserTurn),
      timestamp: currentUserTurn.timestamp,
      files: currentFiles,
    })
  }

  for (const turn of turns) {
    if (turn.role === "user") {
      flushGroup()
      currentUserTurn = turn
      currentFiles = []
      continue
    }

    if (turn.role !== "assistant") continue

    for (const block of turn.blocks) {
      if (block.type !== "tool_use") continue

      const normalized = normalizeToolName(block.tool_name)
      if (!WRITE_OPS.has(normalized)) continue

      const filePaths = extractFilePaths(block.input_preview)
      if (filePaths.length === 0) continue

      for (const [fileIndex, filePath] of filePaths.entries()) {
        const normalizedPath = normalizePath(filePath)
        const diff = computeLineDiff(
          normalized,
          block.input_preview,
          normalizedPath
        )
        const diffChunk = buildDiffChunk(
          normalized,
          block.input_preview,
          normalizedPath
        )

        currentFiles.push({
          id: `${turn.id}:${block.tool_use_id ?? "tool"}:${fileIndex}:${currentFiles.length}`,
          path: normalizedPath,
          additions: diff?.additions ?? 0,
          deletions: diff?.deletions ?? 0,
          diff: diffChunk?.trim() ? diffChunk.trim() : null,
        })
      }
    }
  }

  flushGroup()
  return groups
}

/**
 * Build a unified-diff string for a specific file within a user-message group.
 * Scans from `userTurnId` forward through assistant turns until the next user
 * turn, collecting all edit/write/apply_patch operations on `filePath`.
 */
export function buildSessionFileDiff(
  turns: MessageTurn[],
  userTurnId: string,
  filePath: string
): string {
  let inGroup = false
  const chunks: string[] = []
  const normalizedTargetPath = normalizePath(filePath)

  for (const turn of turns) {
    if (turn.role === "user") {
      if (turn.id === userTurnId) {
        inGroup = true
        continue
      }

      if (inGroup) {
        break
      }

      continue
    }

    if (!inGroup || turn.role !== "assistant") continue

    for (const block of turn.blocks) {
      if (block.type !== "tool_use") continue

      const normalized = normalizeToolName(block.tool_name)
      if (!WRITE_OPS.has(normalized)) continue

      const blockPaths = extractFilePaths(block.input_preview).map(
        normalizePath
      )
      if (!blockPaths.includes(normalizedTargetPath)) continue

      const chunk = buildDiffChunk(
        normalized,
        block.input_preview,
        normalizedTargetPath
      )
      if (chunk && chunk.trim().length > 0) chunks.push(chunk.trim())
    }
  }

  if (chunks.length === 0) {
    return `No diff data available for ${filePath}`
  }

  return chunks.join("\n\n")
}

function buildDiffChunk(
  op: string,
  inputPreview: string | null,
  filePath: string
): string | null {
  if (!inputPreview) return null

  const parsed = parseInputObject(inputPreview)

  if (op === "edit") {
    if (parsed) {
      const changes = findObjectFieldDeep(parsed, "changes")
      const changeValue = changes
        ? pickValueByNormalizedPath(changes, filePath)
        : undefined
      const changeValues = collectEditChangeValues(changeValue)

      if (changeValues.length > 0) {
        const chunks = changeValues
          .map((change) => buildChunkFromEditChange(filePath, change))
          .filter((chunk): chunk is string => Boolean(chunk?.trim()))
        if (chunks.length > 0) return chunks.join("\n\n")
      }
    }

    if (!parsed) return null

    const oldStr =
      typeof parsed.old_string === "string" ? parsed.old_string : ""
    const newStr =
      typeof parsed.new_string === "string" ? parsed.new_string : ""

    return buildUnifiedDiff(filePath, oldStr, newStr)
  }

  if (op === "write") {
    if (!parsed) return null

    const content = typeof parsed.content === "string" ? parsed.content : ""
    if (!content) return null

    const newLines = content.split("\n")
    const lines: string[] = [
      "--- /dev/null",
      `+++ b/${filePath}`,
      `@@ -0,0 +1,${newLines.length} @@`,
    ]

    for (const line of newLines) lines.push(`+${line}`)

    return lines.join("\n")
  }

  if (op === "apply_patch") {
    const patchText = extractPatchTextFromInputPreview(inputPreview)
    if (!patchText) return null

    const applyPatchSections = parseApplyPatchSections(patchText)
    if (applyPatchSections.has(filePath)) {
      return applyPatchSections.get(filePath)?.chunk ?? null
    }

    const unifiedDiffSections = parseUnifiedDiffSections(patchText)
    if (unifiedDiffSections.has(filePath)) {
      return unifiedDiffSections.get(filePath)?.chunk ?? null
    }

    return patchText
  }

  return null
}

export function extractSessionFiles(turns: MessageTurn[]): SessionFileChange[] {
  const fileMap = new Map<string, Set<FileOperation>>()

  for (const turn of turns) {
    for (const block of turn.blocks) {
      if (block.type !== "tool_use") continue

      const normalized = normalizeToolName(block.tool_name)
      if (!FILE_OPS.has(normalized)) continue

      const filePaths = extractFilePaths(block.input_preview)
      if (filePaths.length === 0) continue

      for (const filePath of filePaths) {
        if (!fileMap.has(filePath)) {
          fileMap.set(filePath, new Set())
        }

        fileMap.get(filePath)?.add(normalized as FileOperation)
      }
    }
  }

  return Array.from(fileMap.entries()).map(([path, operations]) => ({
    path,
    operations: Array.from(operations),
  }))
}
