import Foundation

enum MarkdownSegment: Equatable {
    case text(String)
    case mermaid(String)
    case html(String)
}

enum MermaidParser {
    private static let mermaidRegex: NSRegularExpression? = {
        let pattern = #"(?m)^```mermaid[ \t]*\n([\s\S]*?)^```[ \t]*$"#
        return try? NSRegularExpression(pattern: pattern, options: .anchorsMatchLines)
    }()

    static func parse(_ markdown: String) -> [MarkdownSegment] {
        let mermaidSegments = extractMermaid(markdown)
        return mermaidSegments.flatMap { segment -> [MarkdownSegment] in
            if case let .text(text) = segment {
                return HTMLParser.extractHTML(from: text)
            }
            return [segment]
        }
    }

    private static func extractMermaid(_ markdown: String) -> [MarkdownSegment] {
        guard let regex = mermaidRegex else {
            return [.text(markdown)]
        }

        let nsString = markdown as NSString
        let fullRange = NSRange(location: 0, length: nsString.length)
        let matches = regex.matches(in: markdown, range: fullRange)

        if matches.isEmpty {
            return [.text(markdown)]
        }

        var segments: [MarkdownSegment] = []
        var lastEnd = 0

        for match in matches {
            let matchRange = match.range
            let codeRange = match.range(at: 1)

            if matchRange.location > lastEnd {
                let textRange = NSRange(location: lastEnd, length: matchRange.location - lastEnd)
                let text = nsString.substring(with: textRange)
                if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                    segments.append(.text(text))
                }
            }

            let code = nsString.substring(with: codeRange)
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if !code.isEmpty {
                segments.append(.mermaid(code))
            }

            lastEnd = matchRange.location + matchRange.length
        }

        if lastEnd < nsString.length {
            let text = nsString.substring(from: lastEnd)
            if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                segments.append(.text(text))
            }
        }

        return segments
    }
}

enum HTMLParser {
    private static let blockTags = [
        "details", "table", "div", "center", "picture", "figure", "dl", "section",
    ]

    private static let inlineTags = [
        "kbd", "sup", "sub", "br", "img", "abbr", "mark", "ins", "del", "s",
        "u", "small", "ruby", "rt", "rp", "bdo", "wbr", "q", "cite", "dfn",
        "var", "samp", "data", "time", "span",
    ]

    private static let fencedCodeRegex: NSRegularExpression? = {
        let pattern = #"(?m)^(`{3,}|~{3,}).*\n[\s\S]*?^\1[ \t]*$"#
        return try? NSRegularExpression(pattern: pattern, options: .anchorsMatchLines)
    }()

    private static let inlineCodeRegex: NSRegularExpression? = {
        let pattern = #"`[^`]+`"#
        return try? NSRegularExpression(pattern: pattern, options: [])
    }()

    private static let blockRegex: NSRegularExpression? = {
        let tags = blockTags.joined(separator: "|")
        let pattern = #"(?m)(?:^[ \t]*)?<(\#(tags))(\s[^>]*)?>[\s\S]*?</\1>[ \t]*(?:\n|$)"#
        return try? NSRegularExpression(
            pattern: pattern, options: [.anchorsMatchLines, .caseInsensitive],
        )
    }()

    private static let blankLineRegex: NSRegularExpression? =
        try? NSRegularExpression(pattern: #"\n[ \t]*\n"#, options: [])

    private static let inlineHTMLPattern: NSRegularExpression? = {
        let allTags = (inlineTags + blockTags).joined(separator: "|")
        let pattern = #"</?(\#(allTags))[\s>/]"#
        return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
    }()

    /// Replaces fenced code blocks and inline code with placeholder text (same length)
    /// so HTML inside code is not detected.
    private static func maskCodeBlocks(_ text: String) -> String {
        var result = text

        if let fencedRegex = fencedCodeRegex {
            let nsString = result as NSString
            let matches = fencedRegex.matches(in: result, range: NSRange(location: 0, length: nsString.length))
            for match in matches.reversed() {
                let range = match.range
                let replacement = String(repeating: " ", count: range.length)
                result = (result as NSString).replacingCharacters(in: range, with: replacement)
            }
        }

        if let inlineRegex = inlineCodeRegex {
            let nsString = result as NSString
            let matches = inlineRegex.matches(in: result, range: NSRange(location: 0, length: nsString.length))
            for match in matches.reversed() {
                let range = match.range
                let replacement = String(repeating: " ", count: range.length)
                result = (result as NSString).replacingCharacters(in: range, with: replacement)
            }
        }

        return result
    }

    // swiftlint:disable:next function_body_length cyclomatic_complexity
    static func extractHTML(from text: String) -> [MarkdownSegment] {
        let masked = maskCodeBlocks(text)

        // Collect ranges of HTML blocks (in the original text)
        var htmlRanges: [NSRange] = []

        // 1. Block-level HTML
        if let regex = blockRegex {
            let nsString = masked as NSString
            let matches = regex.matches(in: masked, range: NSRange(location: 0, length: nsString.length))
            for match in matches {
                htmlRanges.append(match.range)
            }
        }

        // 2. Inline HTML, check paragraphs for inline tags
        if let inlinePattern = inlineHTMLPattern {
            let paragraphs = splitIntoParagraphs(masked)
            for para in paragraphs {
                // Skip if this paragraph range already overlaps with a block HTML range
                let overlaps = htmlRanges.contains { overlap($0, para.range) }
                if overlaps { continue }

                let paraText = (masked as NSString).substring(with: para.range)
                let hasTag = inlinePattern.firstMatch(
                    in: paraText,
                    range: NSRange(location: 0, length: (paraText as NSString).length),
                ) != nil

                if hasTag {
                    htmlRanges.append(para.range)
                }
            }
        }

        if htmlRanges.isEmpty {
            return [.text(text)]
        }

        // Sort ranges by location
        htmlRanges.sort { $0.location < $1.location }

        // Merge overlapping ranges
        var merged: [NSRange] = []
        for range in htmlRanges {
            if let last = merged.last, range.location <= last.location + last.length {
                let end = max(last.location + last.length, range.location + range.length)
                merged[merged.count - 1] = NSRange(location: last.location, length: end - last.location)
            } else {
                merged.append(range)
            }
        }

        // Build segments from the original text
        let nsText = text as NSString
        var segments: [MarkdownSegment] = []
        var lastEnd = 0

        for range in merged {
            if range.location > lastEnd {
                let before = nsText.substring(
                    with: NSRange(location: lastEnd, length: range.location - lastEnd),
                )
                if !before.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                    segments.append(.text(before))
                }
            }

            let htmlContent = nsText.substring(with: range)
                .trimmingCharacters(in: .whitespacesAndNewlines)
            if !htmlContent.isEmpty {
                segments.append(.html(htmlContent))
            }

            lastEnd = range.location + range.length
        }

        if lastEnd < nsText.length {
            let after = nsText.substring(from: lastEnd)
            if !after.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                segments.append(.text(after))
            }
        }

        return segments
    }

    private struct Paragraph {
        let range: NSRange
    }

    /// Splits text into paragraphs (separated by blank lines).
    private static func splitIntoParagraphs(_ text: String) -> [Paragraph] {
        let nsString = text as NSString
        var paragraphs: [Paragraph] = []

        guard let regex = blankLineRegex else { return [] }

        let fullRange = NSRange(location: 0, length: nsString.length)
        let splits = regex.matches(in: text, range: fullRange)

        var lastEnd = 0
        for split in splits {
            let paraRange = NSRange(location: lastEnd, length: split.range.location - lastEnd)
            if paraRange.length > 0 {
                paragraphs.append(Paragraph(range: paraRange))
            }
            lastEnd = split.range.location + split.range.length
        }

        if lastEnd < nsString.length {
            let paraRange = NSRange(location: lastEnd, length: nsString.length - lastEnd)
            if paraRange.length > 0 {
                paragraphs.append(Paragraph(range: paraRange))
            }
        }

        return paragraphs
    }

    private static func overlap(_ rangeA: NSRange, _ rangeB: NSRange) -> Bool {
        let endA = rangeA.location + rangeA.length
        let endB = rangeB.location + rangeB.length
        return rangeA.location < endB && rangeB.location < endA
    }
}
