import Foundation
import SaplingCore

#if os(macOS)
import AppKit
#endif

public enum EditorBenchmarkCategory: String, Hashable, Sendable {
    case documentLoad
    case editorUpdate
    case rendering
    case layout
    case interaction
}

public struct EditorBenchmarkMeasurement: Hashable, Sendable {
    public var name: String
    public var category: EditorBenchmarkCategory
    public var durationMilliseconds: Double
    public var notes: String

    public init(
        name: String,
        category: EditorBenchmarkCategory,
        durationMilliseconds: Double,
        notes: String = ""
    ) {
        self.name = name
        self.category = category
        self.durationMilliseconds = durationMilliseconds
        self.notes = notes
    }
}

public struct EditorBenchmarkDocumentProfile: Hashable, Sendable {
    public var fileName: String
    public var byteCount: Int
    public var characterCount: Int
    public var utf16Count: Int
    public var lineCount: Int
    public var headingCount: Int
    public var unorderedListItemCount: Int
    public var orderedListItemCount: Int
    public var blockquoteCount: Int
    public var fencedCodeFenceCount: Int
    public var inlineCodeLineCount: Int
    public var boldLineCount: Int
    public var italicLineCount: Int
    public var inlineLinkCount: Int
    public var referenceLinkLikeCount: Int
    public var imageCount: Int
    public var maxLineUTF16Length: Int
    public var averageLineUTF16Length: Double

    public var markdownStructureSummary: String {
        [
            "\(headingCount) ATX headings",
            "\(unorderedListItemCount) unordered list items",
            "\(orderedListItemCount) ordered list items",
            "\(blockquoteCount) blockquotes",
            "\(fencedCodeFenceCount) fenced code fences",
            "\(inlineCodeLineCount) lines with inline code",
            "\(boldLineCount) lines with bold markers",
            "\(italicLineCount) lines with italic markers",
            "\(inlineLinkCount) inline links",
            "\(referenceLinkLikeCount) reference-style link markers",
            "\(imageCount) images"
        ].joined(separator: ", ")
    }
}

public struct EditorBenchmarkResult: Sendable {
    public var document: EditorBenchmarkDocumentProfile
    public var measurements: [EditorBenchmarkMeasurement]
    public var renderedModeTrace: EditorRenderedModeTrace

    public var measuredTotalMilliseconds: Double {
        measurements.reduce(0) { $0 + $1.durationMilliseconds }
    }

    public var hottestMeasurements: [EditorBenchmarkMeasurement] {
        measurements.sorted { $0.durationMilliseconds > $1.durationMilliseconds }
    }
}

public struct EditorRenderedModeTrace: Hashable, Sendable {
    public var totalLineCount: Int
    public var sourceLineCount: Int
    public var renderedLineCount: Int
    public var headingPlanCount: Int
    public var paragraphPlanCount: Int
    public var inlineSpanCount: Int
    public var markdownDelimiterSpanCount: Int
    public var unsupportedReferenceLinkLikeCount: Int

    public var renderedModeWasScheduled: Bool {
        renderedLineCount > 0
    }
}

public enum EditorBenchmarkProfiler {
    public static func profileDocument(at url: URL) throws -> EditorBenchmarkResult {
        var measurements: [EditorBenchmarkMeasurement] = []

        let readResult = try measure {
            try String(contentsOf: url, encoding: .utf8)
        }
        let source = readResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "file_read",
            category: .documentLoad,
            durationMilliseconds: readResult.durationMilliseconds,
            notes: "String(contentsOf:encoding:)"
        ))

        let profile = documentProfile(fileName: url.lastPathComponent, source: source)
        let midpoint = source.utf16.count / 2
        let activeLineIndex = EditorActiveLineTracker.lineIndex(containing: midpoint, in: source)

        let documentResult = measure {
            MarkdownDocument(url: url, title: url.deletingPathExtension().lastPathComponent, content: source)
        }
        let markdownDocument = documentResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "document_model_init",
            category: .documentLoad,
            durationMilliseconds: documentResult.durationMilliseconds,
            notes: "MarkdownDocument construction"
        ))

        let stateResult = measure {
            EditorState(
                document: EditorDocument(markdownDocument: markdownDocument),
                activeLineIndex: activeLineIndex
            )
        }
        var state = stateResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "document_parse_editor_state_init",
            category: .documentLoad,
            durationMilliseconds: stateResult.durationMilliseconds,
            notes: "Builds EditorState.lines for the whole document"
        ))

        let lineGenerationResult = measure {
            EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex)
        }
        let lines = lineGenerationResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "line_model_generation",
            category: .editorUpdate,
            durationMilliseconds: lineGenerationResult.durationMilliseconds,
            notes: "EditorActiveLineTracker.lines over full source"
        ))

        let renderPlanResult = measure {
            let renderer = HybridMarkdownLineRenderer()
            return lines.map(renderer.renderPlan(for:))
        }
        let renderPlans = renderPlanResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "render_plan_generation_all_lines",
            category: .rendering,
            durationMilliseconds: renderPlanResult.durationMilliseconds,
            notes: "HybridMarkdownLineRenderer.renderPlan for every line"
        ))

        let renderedModeTrace = renderedTrace(
            lines: lines,
            renderPlans: renderPlans,
            referenceLinkLikeCount: profile.referenceLinkLikeCount
        )

        let changedSource = sourceByInsertingProbeText(in: source, at: midpoint)
        let changedActiveLineIndex = EditorActiveLineTracker.lineIndex(containing: midpoint + 1, in: changedSource)

        let activeLineResult = measure {
            EditorActiveLineTracker.lineIndex(containing: midpoint, in: source)
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "active_line_lookup",
            category: .editorUpdate,
            durationMilliseconds: activeLineResult.durationMilliseconds,
            notes: "Line lookup for midpoint cursor location"
        ))

        let selectionUpdateResult = measure {
            state.updateSelection(EditorSelection(location: midpoint, length: 0))
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "selection_update",
            category: .editorUpdate,
            durationMilliseconds: selectionUpdateResult.durationMilliseconds,
            notes: "EditorState.updateSelection rebuilds line model"
        ))

        let dirtyClickResult = measure {
            EditorDirtyLineInvalidator.plan(
                previousText: source,
                currentText: source,
                previousActiveLineIndex: activeLineIndex,
                currentActiveLineIndex: changedActiveLineIndex
            )
        }
        let dirtyClickPlan = dirtyClickResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "dirty_line_invalidation_click",
            category: .editorUpdate,
            durationMilliseconds: dirtyClickResult.durationMilliseconds,
            notes: "\(dirtyClickPlan.dirtyLineCount) dirty lines"
        ))

        let sourceUpdateResult = measure {
            var updatedState = state
            updatedState.updateSource(changedSource)
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "typing_state_update",
            category: .interaction,
            durationMilliseconds: sourceUpdateResult.durationMilliseconds,
            notes: "EditorState.updateSource after one-character insertion"
        ))

        let dirtyTypingResult = measure {
            EditorDirtyLineInvalidator.plan(
                previousText: source,
                currentText: changedSource,
                previousActiveLineIndex: activeLineIndex,
                currentActiveLineIndex: changedActiveLineIndex
            )
        }
        let dirtyTypingPlan = dirtyTypingResult.value
        measurements.append(EditorBenchmarkMeasurement(
            name: "dirty_line_invalidation_typing",
            category: .editorUpdate,
            durationMilliseconds: dirtyTypingResult.durationMilliseconds,
            notes: "\(dirtyTypingPlan.dirtyLineCount) dirty lines"
        ))

        #if os(macOS)
        measurements.append(contentsOf: profileTextKit(
            source: source,
            changedSource: changedSource,
            activeLineIndex: activeLineIndex,
            changedActiveLineIndex: changedActiveLineIndex,
            dirtyClickPlan: dirtyClickPlan,
            dirtyTypingPlan: dirtyTypingPlan
        ))
        #endif

        return EditorBenchmarkResult(
            document: profile,
            measurements: measurements,
            renderedModeTrace: renderedModeTrace
        )
    }

    public static func documentProfile(fileName: String, source: String) -> EditorBenchmarkDocumentProfile {
        let lines = source.components(separatedBy: "\n")
        let lineLengths = lines.map { $0.utf16.count }
        let lineCount = lines.count
        let maxLineLength = lineLengths.max() ?? 0
        let totalLineLength = lineLengths.reduce(0, +)

        return EditorBenchmarkDocumentProfile(
            fileName: fileName,
            byteCount: source.lengthOfBytes(using: .utf8),
            characterCount: source.count,
            utf16Count: source.utf16.count,
            lineCount: lineCount,
            headingCount: countMatches("(?m)^#{1,6}\\s", in: source),
            unorderedListItemCount: countMatches("(?m)^[-*+]\\s", in: source),
            orderedListItemCount: countMatches("(?m)^\\d+\\.\\s", in: source),
            blockquoteCount: countMatches("(?m)^>\\s", in: source),
            fencedCodeFenceCount: countMatches("(?m)^```", in: source),
            inlineCodeLineCount: countMatchingLines("`[^`\\n]+`", in: lines),
            boldLineCount: countMatchingLines("\\*\\*[^*\\n]+\\*\\*", in: lines),
            italicLineCount: countMatchingLines("(?<!\\*)\\*[^*\\n]+\\*(?!\\*)", in: lines),
            inlineLinkCount: countMatches("(?<!!)\\[[^\\]]+\\]\\([^\\)]+\\)", in: source),
            referenceLinkLikeCount: countMatches("(?<!!)\\[[A-Za-z0-9_-]+\\]", in: source),
            imageCount: countMatches("!\\[", in: source),
            maxLineUTF16Length: maxLineLength,
            averageLineUTF16Length: lineCount == 0 ? 0 : Double(totalLineLength) / Double(lineCount)
        )
    }

    private static func renderedTrace(
        lines: [EditorLine],
        renderPlans: [HybridMarkdownLineRenderPlan],
        referenceLinkLikeCount: Int
    ) -> EditorRenderedModeTrace {
        let headingCount = renderPlans.filter {
            if case .heading = $0.kind {
                return true
            }
            return false
        }.count
        let paragraphCount = renderPlans.count - headingCount
        let spans = renderPlans.flatMap(\.spans)

        return EditorRenderedModeTrace(
            totalLineCount: lines.count,
            sourceLineCount: lines.filter { $0.mode == .source }.count,
            renderedLineCount: lines.filter { $0.mode == .rendered }.count,
            headingPlanCount: headingCount,
            paragraphPlanCount: paragraphCount,
            inlineSpanCount: spans.count,
            markdownDelimiterSpanCount: spans.filter { $0.kind == .markdownDelimiter }.count,
            unsupportedReferenceLinkLikeCount: referenceLinkLikeCount
        )
    }

    private static func sourceByInsertingProbeText(in source: String, at utf16Location: Int) -> String {
        let nsSource = source as NSString
        let location = max(0, min(utf16Location, nsSource.length))
        return nsSource.replacingCharacters(in: NSRange(location: location, length: 0), with: "x")
    }

    private static func countMatches(_ pattern: String, in source: String) -> Int {
        guard let regex = try? NSRegularExpression(pattern: pattern) else { return 0 }
        return regex.numberOfMatches(in: source, range: NSRange(location: 0, length: source.utf16.count))
    }

    private static func countMatchingLines(_ pattern: String, in lines: [String]) -> Int {
        guard let regex = try? NSRegularExpression(pattern: pattern) else { return 0 }
        return lines.reduce(0) { count, line in
            let range = NSRange(location: 0, length: line.utf16.count)
            return count + (regex.firstMatch(in: line, range: range) == nil ? 0 : 1)
        }
    }

    private static func measure<T>(_ operation: () throws -> T) rethrows -> (value: T, durationMilliseconds: Double) {
        let start = DispatchTime.now().uptimeNanoseconds
        let value = try operation()
        let end = DispatchTime.now().uptimeNanoseconds
        return (value, Double(end - start) / 1_000_000)
    }

    #if os(macOS)
    private static func profileTextKit(
        source: String,
        changedSource: String,
        activeLineIndex: Int,
        changedActiveLineIndex: Int,
        dirtyClickPlan: EditorDirtyLineInvalidationPlan,
        dirtyTypingPlan: EditorDirtyLineInvalidationPlan
    ) -> [EditorBenchmarkMeasurement] {
        var measurements: [EditorBenchmarkMeasurement] = []
        let textStorage = NSTextStorage()
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: NSSize(width: 760, height: CGFloat.greatestFiniteMagnitude))
        textContainer.widthTracksTextView = false
        textContainer.heightTracksTextView = false
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        let storageUpdateResult = measure {
            textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: source)
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "text_storage_initial_update",
            category: .layout,
            durationMilliseconds: storageUpdateResult.durationMilliseconds,
            notes: "NSTextStorage.replaceCharacters full source"
        ))

        let fullRenderPlan = EditorDirtyLineInvalidator.plan(
            previousText: nil,
            currentText: source,
            previousActiveLineIndex: nil,
            currentActiveLineIndex: activeLineIndex
        )
        let attributedStringResult = measure {
            MarkdownTextStyler.apply(
                to: textStorage,
                invalidationPlan: fullRenderPlan,
                activeLineIndex: activeLineIndex,
                backgroundColor: .textBackgroundColor,
                activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
                textColor: .labelColor,
                secondaryTextColor: .secondaryLabelColor,
                accentColor: .controlAccentColor
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "attributed_string_generation_initial",
            category: .rendering,
            durationMilliseconds: attributedStringResult.durationMilliseconds,
            notes: "\(attributedStringResult.value.styledLineCount) styled lines"
        ))

        let layoutInvalidationResult = measure {
            var actualRange = NSRange(location: 0, length: 0)
            layoutManager.invalidateLayout(
                forCharacterRange: NSRange(location: 0, length: textStorage.length),
                actualCharacterRange: &actualRange
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "layout_invalidation_full_document",
            category: .layout,
            durationMilliseconds: layoutInvalidationResult.durationMilliseconds,
            notes: "NSLayoutManager.invalidateLayout full character range"
        ))

        let glyphGenerationResult = measure {
            layoutManager.ensureGlyphs(forCharacterRange: NSRange(location: 0, length: textStorage.length))
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "glyph_generation_full_document",
            category: .layout,
            durationMilliseconds: glyphGenerationResult.durationMilliseconds,
            notes: "NSLayoutManager.ensureGlyphs full character range"
        ))

        let layoutGenerationResult = measure {
            layoutManager.ensureLayout(for: textContainer)
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "layout_generation_full_document",
            category: .layout,
            durationMilliseconds: layoutGenerationResult.durationMilliseconds,
            notes: "NSLayoutManager.ensureLayout for text container"
        ))

        let firstViewportResult = measure {
            layoutManager.glyphRange(
                forBoundingRect: NSRect(x: 0, y: 0, width: 760, height: 900),
                in: textContainer
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "first_viewport_glyph_range",
            category: .layout,
            durationMilliseconds: firstViewportResult.durationMilliseconds,
            notes: "Visible glyph range at document top"
        ))

        let clickRenderResult = measure {
            if dirtyClickPlan.requiresStyling {
                return MarkdownTextStyler.apply(
                    to: textStorage,
                    invalidationPlan: dirtyClickPlan,
                    activeLineIndex: changedActiveLineIndex,
                    backgroundColor: .textBackgroundColor,
                    activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
                    textColor: .labelColor,
                    secondaryTextColor: .secondaryLabelColor,
                    accentColor: .controlAccentColor
                )
            }
            return MarkdownTextStylingResult.empty
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "render_update_click_dirty",
            category: .interaction,
            durationMilliseconds: clickRenderResult.durationMilliseconds,
            notes: dirtyClickPlan.requiresStyling
                ? "\(clickRenderResult.value.styledLineCount) styled lines"
                : "skipped; dirty plan did not require styling"
        ))

        let typingStorageResult = measure {
            let midpoint = textStorage.length / 2
            textStorage.replaceCharacters(in: NSRange(location: midpoint, length: 0), with: "x")
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "text_storage_typing_insert",
            category: .interaction,
            durationMilliseconds: typingStorageResult.durationMilliseconds,
            notes: "NSTextStorage single-character insertion"
        ))

        let typingRenderResult = measure {
            MarkdownTextStyler.apply(
                to: textStorage,
                invalidationPlan: dirtyTypingPlan,
                activeLineIndex: changedActiveLineIndex,
                backgroundColor: .textBackgroundColor,
                activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
                textColor: .labelColor,
                secondaryTextColor: .secondaryLabelColor,
                accentColor: .controlAccentColor
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "render_update_typing_dirty",
            category: .interaction,
            durationMilliseconds: typingRenderResult.durationMilliseconds,
            notes: "\(typingRenderResult.value.styledLineCount) styled lines"
        ))

        let layoutAfterTypingResult = measure {
            layoutManager.ensureLayout(forCharacterRange: NSRange(location: textStorage.length / 2, length: 1))
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "layout_after_typing",
            category: .interaction,
            durationMilliseconds: layoutAfterTypingResult.durationMilliseconds,
            notes: "NSLayoutManager.ensureLayout for inserted character"
        ))

        let scrollTopResult = measure {
            layoutManager.glyphRange(
                forBoundingRect: NSRect(x: 0, y: 0, width: 760, height: 900),
                in: textContainer
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "scroll_latency_top_viewport",
            category: .interaction,
            durationMilliseconds: scrollTopResult.durationMilliseconds,
            notes: "glyphRange for top viewport"
        ))

        let scrollMiddleResult = measure {
            layoutManager.glyphRange(
                forBoundingRect: NSRect(x: 0, y: 50_000, width: 760, height: 900),
                in: textContainer
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "scroll_latency_middle_viewport",
            category: .interaction,
            durationMilliseconds: scrollMiddleResult.durationMilliseconds,
            notes: "glyphRange for estimated middle viewport"
        ))

        let scrollDeepResult = measure {
            layoutManager.glyphRange(
                forBoundingRect: NSRect(x: 0, y: 500_000, width: 760, height: 900),
                in: textContainer
            )
        }
        measurements.append(EditorBenchmarkMeasurement(
            name: "scroll_latency_deep_viewport",
            category: .interaction,
            durationMilliseconds: scrollDeepResult.durationMilliseconds,
            notes: "glyphRange for estimated deep viewport"
        ))

        _ = changedSource
        return measurements
    }
    #endif
}
