import APIClient
import ComponentLibrary
import Foundation

public class LyricsProcessing {
    // MARK: - Helpers

    /// Remove duplicates while preserving order
    func makeUniqueList(_ list: [String]) -> [String] {
        var seen = Set<String>()
        return list.filter { seen.insert($0).inserted }
    }

    // Pre‑compile regexes
    private let sectionMarkerRegex = try? NSRegularExpression(
        pattern: #"\[(\w+(?:\s*\d*)?)\]\s*"#,
        options: []
    )
    private let speakerFindRegex = try? NSRegularExpression(
        pattern: #"\[([^\]]*?:.*?)\]"#,
        options: []
    )
    private let initialDescRegex = try? NSRegularExpression(
        pattern: #"^\s*\[.{15,}?\]"#,
        options: []
    )
    private let artistNameRegex = try? NSRegularExpression(
        pattern: #"\[[^\]]*?:\s*(.*?)\s*\]"#,
        options: []
    )

    /// 100 anonymized speaker names: "0", "1", … "99"
    private let ANONYMIZED_SPEAKER_NAMES: [String] = (0 ..< 100).map { String($0) }

    // MARK: - Speaker Anonymization

    private func getSpeakerReplMap(_ text: String) -> [String: String] {
        // 1) Find all artist names in “[Intro: Name]”
        let matches = artistNameRegex?.matches(
            in: text,
            options: [],
            range: NSRange(text.startIndex..., in: text)
        ) ?? []

        var artistNames = matches.compactMap { m -> String? in
            guard let r = Range(m.range(at: 1), in: text) else { return nil }
            return String(text[r])
        }
        artistNames = makeUniqueList(artistNames)
        artistNames.sort { $0.count < $1.count }

        // 2) Build anonymization map
        var map = [String: String]()
        for name in artistNames {
            // already handled?
            if map[name] != nil { continue }

            // see if this is a “multi‑speaker” (contains a shorter existing key)
            var newName: String?
            for (k, v) in map where name.contains(k) && !name.replacingOccurrences(of: k, with: "").isEmpty {
                var speakers = [ANONYMIZED_SPEAKER_NAMES.randomElement()!, v]
                speakers.shuffle()
                let joiner = Bool.random() ? " & " : " and "
                newName = speakers.joined(separator: joiner)
                break
            }

            // otherwise pick a fresh random single‑name
            map[name] = newName ?? ANONYMIZED_SPEAKER_NAMES.randomElement()!
        }
        return map
    }

    /// Replace artist names in brackets with anonymized tokens
    func anonymizeSpeakers(_ input: String) -> String {
        var text = input
        let replMap = getSpeakerReplMap(text)

        // Replace “[Role: Name]” using our map
        guard
            let matches = speakerFindRegex?.matches(
                in: text,
                options: [],
                range: NSRange(text.startIndex..., in: text)
            ).reversed() // reverse so replacements don't shift indexes
        else { return text }

        for m in matches {
            guard let fullRange = Range(m.range, in: text),
                  let innerRange = Range(m.range(at: 1), in: text)
            else { continue }

            let parts = text[innerRange].split(separator: ":", maxSplits: 1)
            if parts.count == 2 {
                let role = parts[0].trimmingCharacters(in: .whitespaces)
                let name = parts[1].trimmingCharacters(in: .whitespaces)
                let anon = replMap[name] ?? ""
                text.replaceSubrange(fullRange, with: "[\(role): \(anon)]")
            }
        }

        // Strip any initial “title/artist” bracket of length ≥15
        text = initialDescRegex?.stringByReplacingMatches(
            in: text,
            options: [],
            range: NSRange(text.startIndex..., in: text),
            withTemplate: ""
        ) ?? text

        return text.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    /// Remove speakers entirely, leaving “[Role]”
    func removeSpeakers(_ input: String) -> String {
        var text = input

        // “[Role: anything]” → “[Role]”
        let regex = try? NSRegularExpression(pattern: #"\[([^\]]*?):.*?\]"#, options: [])
        text = regex?.stringByReplacingMatches(
            in: text,
            options: [],
            range: NSRange(text.startIndex..., in: text),
            withTemplate: "[$1]"
        ) ?? text

        // Strip initial long bracket
        text = initialDescRegex?.stringByReplacingMatches(
            in: text,
            options: [],
            range: NSRange(text.startIndex..., in: text),
            withTemplate: ""
        ) ?? text

        return text.trimmingCharacters(in: .whitespacesAndNewlines)
    }

//    private let sectionMarkerRegex =
//        try! NSRegularExpression(pattern: #"\[(\w+(?:\s*\d*)?)\]\s*"#)

    // MARK: ‑‑ Swift port of hoot_lyrics_to_timed_lyrics_json

    // swiftlint:disable:next cyclomatic_complexity function_body_length
    func hootLyricsToTimedLyrics(_ aligned: [AlignedLyric]) -> [LyricsLine] {
        // ─────────────────────────────
        // 1. Tokenise Hoot input
        // ─────────────────────────────
        enum TokType { case word, newline }

        struct Token {
            let type: TokType
            let text: String // exact fragment (may include spaces, apostrophes)
            let start: Double
            let end: Double
            let section: String
        }

        var tokens: [Token] = []
        var currentSection = ""

        for entry in aligned {
            var raw = entry.word
            let start = entry.startsAt
            var end = entry.endsAt
            if end <= start { end = start + 0.05 }

            // pull out [Verse], [Chorus 2], … and remember the latest
            let m = sectionMarkerRegex?.firstMatch(
                in: raw, options: [],
                range: NSRange(raw.startIndex..., in: raw)
            )

            if let m, let r = Range(m.range(at: 1), in: raw) {
                currentSection = String(raw[r])
                raw = sectionMarkerRegex?.stringByReplacingMatches(
                    in: raw, options: [],
                    range: NSRange(raw.startIndex..., in: raw),
                    withTemplate: ""
                ) ?? ""
            }

            guard !raw.isEmpty else { continue }

            // break only on literal newlines – NOT on spaces or apostrophes
            let parts = raw.split(separator: "\n", omittingEmptySubsequences: false)

            for (i, part) in parts.enumerated() {
                if i > 0 {
                    tokens.append(Token(type: .newline,
                                        text: "",
                                        start: start,
                                        end: start + 0.001,
                                        section: currentSection))
                }
                guard !part.isEmpty else { continue }

                tokens.append(Token(type: .word,
                                    text: String(part),
                                    start: start,
                                    end: end,
                                    section: currentSection))
            }
        }

        // ─────────────────────────────
        // 2. Group tokens into lines
        // ─────────────────────────────
        struct LineBuild { var tokens: [Token]; var section: String }
        var lines: [LineBuild] = []

        var buffer: [Token] = []
        func flush() {
            guard !buffer.isEmpty else { return }
            lines.append(LineBuild(tokens: buffer, section: buffer[0].section))
            buffer.removeAll()
        }

        for tok in tokens {
            switch tok.type {
            case .word: buffer.append(tok)
            case .newline: flush()
            }
        }
        flush()

        // ─────────────────────────────
        // 3. Convert each line → LyricsLine
        // ─────────────────────────────
        var result: [LyricsLine] = []

        for line in lines {
            // full visible text = token.text concatenated verbatim
            let lineText = line.tokens.map(\.text).joined()

            // split by whitespace to get display words (this keeps “I'll”, “you're” together)
            let wordsInDisplay = lineText.split(whereSeparator: \.isWhitespace)

            guard !wordsInDisplay.isEmpty else { continue }

            var wordTokens: [WordToken] = []
            var wordSearchCursor = lineText.startIndex

            for displayWord in wordsInDisplay {
                // ➊ Find this word’s range inside lineText starting at current cursor
                guard let wordRange = lineText.range(
                    of: displayWord,
                    range: wordSearchCursor ..< lineText.endIndex
                )
                else { continue }

                // shift cursor so next .range search starts *after* this word
                wordSearchCursor = wordRange.upperBound

                // ➋ Map char positions → token indices
                var wordStart: Double?
                var wordEnd: Double?

                var charPos = lineText.startIndex
                for tok in line.tokens {
                    let tokStart = charPos
                    // this will never go past endIndex — if it would, it just returns endIndex
                    let tokEnd = lineText.index(
                        charPos,
                        offsetBy: tok.text.count,
                        limitedBy: lineText.endIndex
                    ) ?? lineText.endIndex

                    let overlaps =
                        !(wordRange.upperBound <= tokStart ||
                            wordRange.lowerBound >= tokEnd)

                    if overlaps {
                        wordStart = min(wordStart ?? tok.start, tok.start)
                        wordEnd = max(wordEnd ?? tok.end, tok.end)
                    }
                    charPos = tokEnd
                }

                // fallback if something went wrong
                let startTime = wordStart ?? line.tokens.first!.start
                let endTime = wordEnd ?? line.tokens.last!.end

                wordTokens.append(WordToken(
                    text: String(displayWord),
                    startTime: startTime,
                    endTime: endTime
                ))
            }

            // enforce monotonic timings
            for i in 1 ..< wordTokens.count {
                if wordTokens[i].startTime < wordTokens[i - 1].endTime {
                    wordTokens[i].startTime = wordTokens[i - 1].endTime
                }
                if wordTokens[i].endTime <= wordTokens[i].startTime {
                    wordTokens[i].endTime = wordTokens[i].startTime + 0.05
                }
            }

            result.append(LyricsLine(
                text: lineText.trimmingCharacters(in: .whitespaces),
                startTime: wordTokens.first!.startTime,
                endTime: wordTokens.last!.endTime,
                section: line.section,
                words: wordTokens
            ))
        }

        return result
    }
}
