import Foundation
import OpenClawKit

enum ChatToolDiffLineKind: Equatable, Sendable {
    case add
    case del
    case ctx
    case file
    case skip
}

struct ChatToolDiffLine: Equatable, Sendable {
    let kind: ChatToolDiffLineKind
    let lineNo: Int?
    let text: String

    init(kind: ChatToolDiffLineKind, lineNo: Int? = nil, text: String) {
        self.kind = kind
        self.lineNo = lineNo
        self.text = text
    }
}

struct ChatToolDiffStat: Equatable, Sendable {
    let added: Int
    let removed: Int
}

enum ChatToolDiff {
    private struct EditPair {
        let oldText: String
        let newText: String
    }

    private struct ParsedDetailsDiff {
        let lines: [ChatToolDiffLine]
        let truncated: Bool
    }

    private enum PatchOperation {
        case add
        case delete
        case update
    }

    private struct PatchSection {
        var operation: PatchOperation
        let sourcePath: String
        var path: String
        var lines: [ChatToolDiffLine] = []
        var added = 0
        var removed = 0
    }

    private struct PatchHunk {
        var oldLine: Int?
        var newLine: Int?
    }

    private static let maxInputLines = 600
    private static let maxRenderLines = 400
    private static let maxLocalPairs = 8
    private static let maxWritePreviewLines = 80
    private static let maxLocalInputCharacters = 120_000

    private static let editToolNames: Set<String> = [
        "edit",
        "edit_file",
        "multiedit",
        "multi_edit",
        "notebookedit",
        "notebook_edit",
    ]
    private static let textEditorToolNames: Set<String> = [
        "str_replace_editor",
        "str_replace_based_edit_tool",
    ]
    private static let writeToolNames: Set<String> = ["write", "write_file", "create_file"]
    private static let patchToolNames: Set<String> = ["apply_patch", "applypatch", "patch"]

    private static func parseDetailsDiffResult(_ diff: String) -> ParsedDetailsDiff? {
        guard !diff.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }

        var lines: [ChatToolDiffLine] = []
        var truncated = diff.components(separatedBy: "\n").contains { raw in
            raw.trimmingCharacters(in: .whitespacesAndNewlines) == "...(truncated)..."
        }
        for raw in diff.components(separatedBy: "\n") {
            guard !raw.isEmpty else { continue }
            let marker = raw.trimmingCharacters(in: .whitespacesAndNewlines)
            let line: ChatToolDiffLine
            if marker == "..." || marker == "...(truncated)..." {
                line = ChatToolDiffLine(kind: .skip, text: "")
            } else {
                guard let numberedLine = self.parseNumberedLine(raw) else { return nil }
                line = numberedLine
            }
            if lines.count >= self.maxRenderLines {
                truncated = true
                if lines.last?.kind != .skip {
                    lines.append(ChatToolDiffLine(kind: .skip, text: ""))
                }
                break
            }
            lines.append(line)
        }

        guard lines.contains(where: { $0.kind == .add || $0.kind == .del }) else { return nil }
        return ParsedDetailsDiff(lines: lines, truncated: truncated)
    }

    static func computeLineDiff(old: String, new: String) -> [ChatToolDiffLine] {
        let allOldLines = self.splitLines(old)
        let allNewLines = self.splitLines(new)
        let inputTruncated = allOldLines.count > self.maxInputLines || allNewLines.count > self.maxInputLines
        let oldLines = Array(allOldLines.prefix(self.maxInputLines))
        let newLines = Array(allNewLines.prefix(self.maxInputLines))
        let oldCount = oldLines.count
        let newCount = newLines.count
        let width = newCount + 1
        var lcs = Array(repeating: 0, count: (oldCount + 1) * width)

        if oldCount > 0, newCount > 0 {
            for oldIndex in stride(from: oldCount - 1, through: 0, by: -1) {
                for newIndex in stride(from: newCount - 1, through: 0, by: -1) {
                    let index = oldIndex * width + newIndex
                    if oldLines[oldIndex] == newLines[newIndex] {
                        lcs[index] = lcs[(oldIndex + 1) * width + newIndex + 1] + 1
                    } else {
                        lcs[index] = max(
                            lcs[(oldIndex + 1) * width + newIndex],
                            lcs[oldIndex * width + newIndex + 1])
                    }
                }
            }
        }

        var lines: [ChatToolDiffLine] = []
        var oldIndex = 0
        var newIndex = 0
        while oldIndex < oldCount, newIndex < newCount {
            if oldLines[oldIndex] == newLines[newIndex] {
                lines.append(ChatToolDiffLine(kind: .ctx, text: oldLines[oldIndex]))
                oldIndex += 1
                newIndex += 1
            } else if lcs[(oldIndex + 1) * width + newIndex] >= lcs[oldIndex * width + newIndex + 1] {
                lines.append(ChatToolDiffLine(kind: .del, text: oldLines[oldIndex]))
                oldIndex += 1
            } else {
                lines.append(ChatToolDiffLine(kind: .add, text: newLines[newIndex]))
                newIndex += 1
            }
        }
        while oldIndex < oldCount {
            lines.append(ChatToolDiffLine(kind: .del, text: oldLines[oldIndex]))
            oldIndex += 1
        }
        while newIndex < newCount {
            lines.append(ChatToolDiffLine(kind: .add, text: newLines[newIndex]))
            newIndex += 1
        }
        return self.compact(lines, inputTruncated: inputTruncated)
    }

    static func resolveDiff(
        name: String?,
        arguments: AnyCodable?,
        details: AnyCodable?,
        isError: Bool = false) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        let normalizedName = name?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
        let argumentsRecord = arguments?.dictionaryValue
        if self.textEditorToolNames.contains(normalizedName) {
            if let detailsDiff = self.resolveDetailsDiff(details) {
                // Applied diff details stay authoritative even when the result reports an error.
                return detailsDiff
            }
            // Failed args describe a proposal, not a mutation known to have been applied.
            guard !isError else { return nil }
            switch self.string(in: argumentsRecord, keys: ["command"])?
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .lowercased()
            {
            case "create":
                return self.resolveWriteDiff(
                    argumentsRecord,
                    keys: ["file_text", "content"],
                    details: details)
            case "insert":
                return self.resolveInsertionDiff(argumentsRecord, details: details)
            default:
                return self.resolveEditDiff(argumentsRecord, details: details)
            }
        }

        // Plugin tools own their details schema; only edit-family tools may
        // interpret details.diff as a filesystem diff (mirrors the web guard).
        if self.editToolNames.contains(normalizedName) {
            if let detailsDiff = self.resolveDetailsDiff(details) {
                return detailsDiff
            }
            guard !isError else { return nil }
            return self.resolveEditDiff(argumentsRecord, details: nil)
        }
        if self.writeToolNames.contains(normalizedName) {
            if let detailsDiff = self.resolveDetailsDiff(details) {
                return detailsDiff
            }
            guard !isError else { return nil }
            return self.resolveWriteDiff(
                argumentsRecord,
                keys: ["content", "text", "file_text"],
                details: details)
        }
        if self.patchToolNames.contains(normalizedName) {
            if let detailsDiff = self.resolveDetailsDiff(details) {
                return detailsDiff
            }
            guard !isError else { return nil }
            return self.resolvePatchDiff(argumentsRecord)
        }
        return nil
    }

    private static func parseNumberedLine(_ raw: String) -> ChatToolDiffLine? {
        guard let sign = raw.first, sign == "+" || sign == "-" || sign == " " else { return nil }
        var remainder = raw.dropFirst()
        while remainder.first?.isWhitespace == true {
            remainder = remainder.dropFirst()
        }
        let digits = remainder.prefix { character in
            character >= "0" && character <= "9"
        }
        guard !digits.isEmpty, let lineNo = Int(digits) else { return nil }
        remainder = remainder.dropFirst(digits.count)
        if remainder.first == " " {
            remainder = remainder.dropFirst()
        }
        let kind: ChatToolDiffLineKind = switch sign {
        case "+": .add
        case "-": .del
        default: .ctx
        }
        return ChatToolDiffLine(kind: kind, lineNo: lineNo, text: String(remainder))
    }

    private static func resolvePatchDiff(
        _ arguments: [String: AnyCodable]?) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        // First NON-BLANK alias, matching the transcript cache's selection so a
        // cold-cache reload renders the same envelope as the live row.
        guard let patch = self.firstNonBlankString(in: arguments, keys: ["input", "patch", "diff"])
        else { return nil }

        let inputClipped = patch.utf16.count > self.maxLocalInputCharacters
        let text = inputClipped
            ? String(decoding: patch.utf16.prefix(self.maxLocalInputCharacters), as: UTF16.self)
            : patch
        var sections: [PatchSection] = []
        var current: PatchSection?
        var hunk = PatchHunk()
        var storedRows = 0
        var clipped = inputClipped

        for raw in self.splitLines(text) {
            let structural = current?.operation == .update
                ? raw.replacingOccurrences(of: #"[ \t]+$"#, with: "", options: .regularExpression)
                : raw.trimmingCharacters(in: .whitespaces)
            if structural == "...(truncated)..." {
                // Cached envelopes use this marker to make an unknown patch tail explicit.
                clipped = true
                continue
            }
            if let header = self.patchFileHeader(structural) {
                if let current { sections.append(current) }
                current = PatchSection(
                    operation: header.operation,
                    sourcePath: header.path,
                    path: header.path)
                hunk = PatchHunk()
                continue
            }
            if current?.operation == .update,
               structural.hasPrefix("*** Move to: "),
               case let path = String(structural.dropFirst("*** Move to: ".count))
                   .trimmingCharacters(in: .whitespacesAndNewlines),
                   !path.isEmpty
            {
                current?.path = path
                continue
            }
            if structural == "*** Begin Patch" || structural == "*** End Patch" ||
                structural == "*** End of File" || structural.hasPrefix("*** Environment ID:")
            {
                continue
            }
            guard var section = current else { continue }

            if section.operation == .update, raw.hasPrefix("@@") {
                if !section.lines.isEmpty, section.lines.last?.kind != .skip {
                    self.pushPatchLine(
                        ChatToolDiffLine(kind: .skip, text: ""),
                        section: &section,
                        storedRows: &storedRows,
                        clipped: &clipped)
                }
                hunk = self.parsePatchHunk(raw)
            } else if section.operation == .add, raw.hasPrefix("+") {
                self.pushPatchLine(
                    ChatToolDiffLine(kind: .add, lineNo: section.added + 1, text: String(raw.dropFirst())),
                    section: &section,
                    storedRows: &storedRows,
                    clipped: &clipped)
            } else if section.operation == .delete, raw.hasPrefix("-") {
                self.pushPatchLine(
                    ChatToolDiffLine(kind: .del, lineNo: section.removed + 1, text: String(raw.dropFirst())),
                    section: &section,
                    storedRows: &storedRows,
                    clipped: &clipped)
            } else if section.operation == .update,
                      raw.isEmpty || raw.hasPrefix("+") || raw.hasPrefix("-") || raw.hasPrefix(" ")
            {
                self.pushPatchHunkLine(
                    raw,
                    hunk: &hunk,
                    section: &section,
                    storedRows: &storedRows,
                    clipped: &clipped)
            }
            current = section
        }
        if let current { sections.append(current) }
        return self.finishPatch(sections, clipped: clipped)
    }

    private static func patchFileHeader(
        _ raw: String) -> (operation: PatchOperation, path: String)?
    {
        let headers: [(String, PatchOperation)] = [
            ("*** Update File: ", .update),
            ("*** Add File: ", .add),
            ("*** Delete File: ", .delete),
        ]
        for (prefix, operation) in headers where raw.hasPrefix(prefix) {
            let path = raw.dropFirst(prefix.count).trimmingCharacters(in: .whitespacesAndNewlines)
            guard !path.isEmpty else { return nil }
            return (operation, path)
        }
        return nil
    }

    private static func pushPatchLine(
        _ line: ChatToolDiffLine,
        section: inout PatchSection,
        storedRows: inout Int,
        clipped: inout Bool)
    {
        switch line.kind {
        case .add:
            section.added += 1
        case .del:
            section.removed += 1
        case .ctx, .file, .skip:
            break
        }
        if storedRows < self.maxRenderLines {
            section.lines.append(line)
            storedRows += 1
        } else {
            clipped = true
        }
    }

    private static func parsePatchHunk(_ raw: String) -> PatchHunk {
        guard raw.hasPrefix("@@ -") else { return PatchHunk() }
        let body = raw.dropFirst(4)
        guard let plus = body.range(of: " +"),
              let end = body[plus.upperBound...].range(of: " @@")
        else { return PatchHunk() }
        let old = body[..<plus.lowerBound]
        let new = body[plus.upperBound..<end.lowerBound]

        func line(_ rangeText: Substring) -> Int? {
            Int(rangeText.split(separator: ",", maxSplits: 1).first ?? "")
        }
        guard let oldLine = line(old), let newLine = line(new) else { return PatchHunk() }
        return PatchHunk(oldLine: oldLine, newLine: newLine)
    }

    private static func pushPatchHunkLine(
        _ raw: String,
        hunk: inout PatchHunk,
        section: inout PatchSection,
        storedRows: inout Int,
        clipped: inout Bool)
    {
        let kind: ChatToolDiffLineKind
        let lineNo: Int?
        if raw.hasPrefix("+") {
            kind = .add
            lineNo = hunk.newLine
            if let newLine = hunk.newLine {
                hunk.newLine = newLine + 1
            }
        } else if raw.hasPrefix("-") {
            kind = .del
            lineNo = hunk.oldLine
            if let oldLine = hunk.oldLine {
                hunk.oldLine = oldLine + 1
            }
        } else {
            kind = .ctx
            lineNo = hunk.newLine
            if let oldLine = hunk.oldLine, let newLine = hunk.newLine {
                hunk.oldLine = oldLine + 1
                hunk.newLine = newLine + 1
            }
        }
        self.pushPatchLine(
            ChatToolDiffLine(kind: kind, lineNo: lineNo, text: raw.isEmpty ? "" : String(raw.dropFirst())),
            section: &section,
            storedRows: &storedRows,
            clipped: &clipped)
    }

    private static func finishPatch(
        _ sections: [PatchSection],
        clipped initialClipped: Bool) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        guard !sections.isEmpty else { return nil }

        var lines: [ChatToolDiffLine] = []
        var clipped = initialClipped
        let hasHeaderOnlyDelete = sections.contains { section in
            section.operation == .delete && !section.lines.contains(where: { $0.kind == .del })
        }
        func append(_ line: ChatToolDiffLine) {
            if lines.count < self.maxRenderLines {
                lines.append(line)
            } else {
                clipped = true
            }
        }
        for section in sections {
            // Header rows also carry move-only and header-only sections that
            // would otherwise render nothing.
            let isHeaderOnly = section.lines.isEmpty &&
                (section.operation == .delete || section.path != section.sourcePath)
            if sections.count > 1 || isHeaderOnly {
                if !lines.isEmpty, lines.last?.kind != .skip {
                    append(ChatToolDiffLine(kind: .skip, text: ""))
                }
                append(ChatToolDiffLine(kind: .file, text: self.patchSectionLabel(section)))
            }
            section.lines.forEach(append)
        }
        if clipped, lines.last?.kind != .skip {
            lines.append(ChatToolDiffLine(kind: .skip, text: ""))
        }
        guard lines.contains(where: { $0.kind == .add || $0.kind == .del || $0.kind == .file }) else { return nil }
        let stat = self.stat(for: lines)
        // Zero/zero (move-only) and unknowable header-only-delete stats read as noise or lies.
        let statMeaningful = !clipped && !hasHeaderOnlyDelete && (stat.added > 0 || stat.removed > 0)
        return (lines, statMeaningful ? stat : nil)
    }

    private static func patchSectionLabel(_ section: PatchSection) -> String {
        if section.operation == .update, section.path != section.sourcePath {
            return "Move \(section.sourcePath) → \(section.path)"
        }
        let verb = switch section.operation {
        case .add: "Add"
        case .delete: "Delete"
        case .update: "Update"
        }
        return "\(verb) \(section.path)"
    }

    private static func splitLines(_ text: String) -> [String] {
        let normalized = text
            .replacingOccurrences(of: "\r\n", with: "\n")
            .replacingOccurrences(of: "\r", with: "\n")
        guard !normalized.isEmpty else { return [] }
        var lines = normalized.components(separatedBy: "\n")
        if lines.count > 1, lines.last?.isEmpty == true {
            lines.removeLast()
        }
        return lines
    }

    private static func compact(
        _ lines: [ChatToolDiffLine],
        inputTruncated: Bool) -> [ChatToolDiffLine]
    {
        if lines.count <= self.maxRenderLines, !inputTruncated {
            return lines
        }
        let hasChange = lines.contains { $0.kind == .add || $0.kind == .del }
        guard hasChange else {
            return inputTruncated
                ? [ChatToolDiffLine(kind: .skip, text: "")]
                : Array(lines.prefix(self.maxRenderLines)) + [ChatToolDiffLine(kind: .skip, text: "")]
        }

        var keep = Array(repeating: false, count: lines.count)
        for index in lines.indices where lines[index].kind == .add || lines[index].kind == .del {
            let start = max(0, index - 3)
            let end = min(lines.count, index + 4)
            for contextIndex in start..<end {
                keep[contextIndex] = true
            }
        }

        var preview: [ChatToolDiffLine] = []
        var gap = false
        var clipped = inputTruncated
        for index in lines.indices {
            guard keep[index] else {
                gap = true
                clipped = true
                continue
            }
            if gap, preview.last?.kind != .skip {
                preview.append(ChatToolDiffLine(kind: .skip, text: ""))
            }
            gap = false
            if preview.count >= self.maxRenderLines {
                clipped = true
                break
            }
            preview.append(lines[index])
        }
        if clipped, preview.last?.kind != .skip {
            preview.append(ChatToolDiffLine(kind: .skip, text: ""))
        }
        return preview
    }

    private static func resolveDetailsDiff(
        _ details: AnyCodable?) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        guard let diff = details?.dictionaryValue?["diff"]?.stringValue,
              !diff.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
              let parsed = self.parseDetailsDiffResult(diff)
        else {
            return nil
        }
        // Only explicit truncation or the render cap makes a persisted stat incomplete.
        return (parsed.lines, parsed.truncated ? nil : self.stat(for: parsed.lines))
    }

    private static func resolveInsertionDiff(
        _ arguments: [String: AnyCodable]?,
        details: AnyCodable?) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        if let detailsDiff = self.resolveDetailsDiff(details) {
            return detailsDiff
        }
        guard let insertText = self.string(in: arguments, keys: ["insert_text"]) else { return nil }
        let lines = self.computeLineDiff(old: "", new: insertText)
        // The inserted text is known, but its final placement is not, so keep the stat absent.
        return lines.isEmpty ? nil : (lines, nil)
    }

    private static func resolveWriteDiff(
        _ arguments: [String: AnyCodable]?,
        keys: [String],
        details: AnyCodable?) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        let detailValues = details?.dictionaryValue
        guard detailValues?["changed"]?.boolValue != false else { return nil }
        guard let content = self.string(in: arguments, keys: keys) else { return nil }
        let allLines = self.splitLines(content)
        guard !allLines.isEmpty else { return nil }
        // Written content is fully known, so number additions from line 1 and
        // keep the exact stat even when the rendered preview is clipped.
        var lines: [ChatToolDiffLine] = []
        var remainingCharacters = self.maxLocalInputCharacters
        for (index, text) in allLines.prefix(self.maxWritePreviewLines).enumerated() {
            let lineCharacters = text.utf16.count
            guard lineCharacters <= remainingCharacters else { break }
            remainingCharacters -= lineCharacters
            lines.append(ChatToolDiffLine(kind: .add, lineNo: index + 1, text: text))
        }
        if lines.count < allLines.count {
            lines.append(ChatToolDiffLine(kind: .skip, text: ""))
        }
        // Present details need created=true before zero removals are authoritative.
        let stat = detailValues != nil && detailValues?["created"]?.boolValue != true
            ? nil
            : ChatToolDiffStat(added: allLines.count, removed: 0)
        return (lines, stat)
    }

    private static func resolveEditDiff(
        _ arguments: [String: AnyCodable]?,
        details: AnyCodable?) -> (lines: [ChatToolDiffLine], stat: ChatToolDiffStat?)?
    {
        // Persisted details are authoritative; args are a local fallback for live/foreign harnesses.
        if let detailsDiff = self.resolveDetailsDiff(details) {
            return detailsDiff
        }
        guard let arguments else { return nil }
        let resolved = self.readEditPairs(arguments)
        guard !resolved.pairs.isEmpty else {
            return resolved.truncated
                ? ([ChatToolDiffLine(kind: .skip, text: "")], nil)
                : nil
        }

        let sections = resolved.pairs.map { pair in
            self.computeLineDiff(old: pair.oldText, new: pair.newText)
        }
        let sectionTruncated = sections.contains { section in
            section.contains(where: { $0.kind == .skip })
        }
        let joined = self.join(sections, truncated: resolved.truncated)
        guard !joined.lines.isEmpty else { return nil }
        let truncated = resolved.truncated || sectionTruncated || joined.truncated
        let stat = truncated ? nil : sections.reduce(ChatToolDiffStat(added: 0, removed: 0)) { sum, section in
            let sectionStat = self.stat(for: section)
            return ChatToolDiffStat(
                added: sum.added + sectionStat.added,
                removed: sum.removed + sectionStat.removed)
        }
        return (joined.lines, stat)
    }

    private static func readEditPairs(
        _ arguments: [String: AnyCodable]) -> (pairs: [EditPair], truncated: Bool)
    {
        var pairs: [EditPair] = []
        var inputCharacters = 0
        var truncated = false

        func appendPair(oldValue: AnyCodable?, newValue: AnyCodable?) {
            guard let oldText = oldValue?.stringValue, let newText = newValue?.stringValue else { return }
            let pairCharacters = oldText.utf16.count + newText.utf16.count
            guard pairCharacters <= self.maxLocalInputCharacters - inputCharacters else {
                truncated = true
                return
            }
            inputCharacters += pairCharacters
            pairs.append(EditPair(oldText: oldText, newText: newText))
        }

        if let edits = arguments["edits"]?.arrayValue {
            for (index, edit) in edits.enumerated() {
                guard index < self.maxLocalPairs else {
                    truncated = true
                    break
                }
                guard let record = edit.dictionaryValue else { continue }
                appendPair(
                    oldValue: self.firstValue(in: record, keys: ["oldText", "old_string", "oldString", "old_str"]),
                    newValue: self.firstValue(in: record, keys: ["newText", "new_string", "newString", "new_str"]))
                if truncated { break }
            }
        } else {
            appendPair(
                oldValue: self.firstValue(
                    in: arguments,
                    keys: ["oldText", "old_string", "oldString", "old_str"]),
                newValue: self.firstValue(
                    in: arguments,
                    keys: ["newText", "new_string", "newString", "new_str"]))
        }
        return (pairs, truncated)
    }

    private static func join(
        _ sections: [[ChatToolDiffLine]],
        truncated initialTruncated: Bool) -> (lines: [ChatToolDiffLine], truncated: Bool)
    {
        var joined: [ChatToolDiffLine] = []
        var truncated = initialTruncated
        for section in sections where !section.isEmpty {
            if !joined.isEmpty {
                guard joined.count < self.maxRenderLines else {
                    truncated = true
                    break
                }
                joined.append(ChatToolDiffLine(kind: .skip, text: ""))
            }
            let remaining = self.maxRenderLines - joined.count
            guard section.count <= remaining else {
                joined.append(contentsOf: section.prefix(remaining))
                truncated = true
                break
            }
            joined.append(contentsOf: section)
        }
        if truncated, joined.last?.kind != .skip {
            joined.append(ChatToolDiffLine(kind: .skip, text: ""))
        }
        return (joined, truncated)
    }

    private static func stat(for lines: [ChatToolDiffLine]) -> ChatToolDiffStat {
        lines.reduce(ChatToolDiffStat(added: 0, removed: 0)) { stat, line in
            switch line.kind {
            case .add:
                ChatToolDiffStat(added: stat.added + 1, removed: stat.removed)
            case .del:
                ChatToolDiffStat(added: stat.added, removed: stat.removed + 1)
            case .ctx, .file, .skip:
                stat
            }
        }
    }

    private static func firstNonBlankString(
        in record: [String: AnyCodable]?,
        keys: [String]) -> String?
    {
        guard let record else { return nil }
        for key in keys {
            if let value = record[key]?.stringValue,
               !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            {
                return value
            }
        }
        return nil
    }

    private static func string(in record: [String: AnyCodable]?, keys: [String]) -> String? {
        guard let record else { return nil }
        return self.firstValue(in: record, keys: keys)?.stringValue
    }

    private static func firstValue(in record: [String: AnyCodable], keys: [String]) -> AnyCodable? {
        for key in keys {
            if let value = record[key] {
                return value
            }
        }
        return nil
    }
}
