import SwiftUI
import AppKit
import Carbon

extension Notification.Name {
    static let tagLauncherQuickSearchRequested = Notification.Name("TagLauncherQuickSearchRequested")
    static let tagLauncherQuickSearchDismissRequested = Notification.Name("TagLauncherQuickSearchDismissRequested")
    static let tagLauncherQuickSearchVisibilityChanged = Notification.Name("TagLauncherQuickSearchVisibilityChanged")
    static let tagLauncherQuickSearchCompositionChanged = Notification.Name("TagLauncherQuickSearchCompositionChanged")
    static let tagLauncherHotkeyRegistrationChanged = Notification.Name("TagLauncherHotkeyRegistrationChanged")
    static let tagLauncherProEntitlementChanged = Notification.Name("TagLauncherProEntitlementChanged")
}

enum QuickSearchOpenSource {
    static let mainOverlay = "mainOverlay"
    static let globalHidden = "globalHidden"
    static let globalVisible = "globalVisible"
}

enum QuickSearchDismissSource {
    static let keyboard = "keyboard"
    static let mouse = "mouse"
    static let backdrop = "backdrop"
    static let programmatic = "programmatic"
}

// MARK: - Hotkeys

struct LauncherHotkey: Equatable {
    let keyCode: UInt32
    let modifiers: UInt32

    var displayString: String {
        let ordered: [(UInt32, String)] = [
            (UInt32(controlKey), "⌃"),
            (UInt32(optionKey), "⌥"),
            (UInt32(shiftKey), "⇧"),
            (UInt32(cmdKey), "⌘"),
            (UInt32(kEventKeyModifierFnMask), "Fn+")
        ]
        let modifierGlyphs = ordered
            .filter { modifiers & $0.0 != 0 }
            .map(\.1)
            .joined()
        return modifierGlyphs + LauncherHotkey.keyDisplayName(for: keyCode)
    }

    static var main: LauncherHotkey {
        LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(shiftKey | optionKey))
    }

    static var quickSearch: LauncherHotkey {
        LauncherHotkey(keyCode: UInt32(kVK_Space), modifiers: UInt32(kEventKeyModifierFnMask))
    }

    static func keyDisplayName(for keyCode: UInt32) -> String {
        switch Int(keyCode) {
        case kVK_Space: return "Space"
        case kVK_Return: return "Return"
        case kVK_Escape: return "Esc"
        case kVK_Delete: return "Delete"
        case kVK_Tab: return "Tab"
        case kVK_LeftArrow: return "←"
        case kVK_RightArrow: return "→"
        case kVK_UpArrow: return "↑"
        case kVK_DownArrow: return "↓"
        case kVK_F1: return "F1"
        case kVK_F2: return "F2"
        case kVK_F3: return "F3"
        case kVK_F4: return "F4"
        case kVK_F5: return "F5"
        case kVK_F6: return "F6"
        case kVK_F7: return "F7"
        case kVK_F8: return "F8"
        case kVK_F9: return "F9"
        case kVK_F10: return "F10"
        case kVK_F11: return "F11"
        case kVK_F12: return "F12"
        default:
            if let scalar = keyCodeToPrintableScalar[Int(keyCode)] {
                return String(scalar)
            }
            return "Key \(keyCode)"
        }
    }

    static var printableKeyCodes: Set<Int> {
        Set(keyCodeToPrintableScalar.keys)
    }

    private static let keyCodeToPrintableScalar: [Int: Character] = [
        kVK_ANSI_A: "A", kVK_ANSI_B: "B", kVK_ANSI_C: "C", kVK_ANSI_D: "D",
        kVK_ANSI_E: "E", kVK_ANSI_F: "F", kVK_ANSI_G: "G", kVK_ANSI_H: "H",
        kVK_ANSI_I: "I", kVK_ANSI_J: "J", kVK_ANSI_K: "K", kVK_ANSI_L: "L",
        kVK_ANSI_M: "M", kVK_ANSI_N: "N", kVK_ANSI_O: "O", kVK_ANSI_P: "P",
        kVK_ANSI_Q: "Q", kVK_ANSI_R: "R", kVK_ANSI_S: "S", kVK_ANSI_T: "T",
        kVK_ANSI_U: "U", kVK_ANSI_V: "V", kVK_ANSI_W: "W", kVK_ANSI_X: "X",
        kVK_ANSI_Y: "Y", kVK_ANSI_Z: "Z", kVK_ANSI_0: "0", kVK_ANSI_1: "1",
        kVK_ANSI_2: "2", kVK_ANSI_3: "3", kVK_ANSI_4: "4", kVK_ANSI_5: "5",
        kVK_ANSI_6: "6", kVK_ANSI_7: "7", kVK_ANSI_8: "8", kVK_ANSI_9: "9"
    ]
}

enum LauncherHotkeyKind: String {
    case main
    case quickSearch

    var stateKey: String {
        switch self {
        case .main: return LauncherHotkeyRegistrationStore.mainStateKey
        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchStateKey
        }
    }

    var failureCodeKey: String {
        switch self {
        case .main: return LauncherHotkeyRegistrationStore.mainFailureCodeKey
        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchFailureCodeKey
        }
    }

    var attentionKey: String {
        switch self {
        case .main: return LauncherHotkeyRegistrationStore.mainNeedsAttentionKey
        case .quickSearch: return LauncherHotkeyRegistrationStore.quickSearchNeedsAttentionKey
        }
    }

    var hotkey: LauncherHotkey {
        switch self {
        case .main: return .main
        case .quickSearch: return .quickSearch
        }
    }

    var eventID: UInt32 {
        switch self {
        case .main: return 1
        case .quickSearch: return 2
        }
    }
}

enum LauncherHotkeyRegistrationState: String {
    case active
    case failed
}

enum LauncherHotkeyRegistrationStore {
    static let mainStateKey = "mainHotkeyRegistrationState"
    static let quickSearchStateKey = "quickSearchHotkeyRegistrationState"
    static let mainFailureCodeKey = "mainHotkeyRegistrationFailureCode"
    static let quickSearchFailureCodeKey = "quickSearchHotkeyRegistrationFailureCode"
    static let mainNeedsAttentionKey = "mainHotkeyRegistrationNeedsAttention"
    static let quickSearchNeedsAttentionKey = "quickSearchHotkeyRegistrationNeedsAttention"

    static func state(for kind: LauncherHotkeyKind) -> LauncherHotkeyRegistrationState {
        let rawValue = UserDefaults.standard.string(forKey: kind.stateKey)
        return LauncherHotkeyRegistrationState(rawValue: rawValue ?? "") ?? .active
    }

    static func failureCode(for kind: LauncherHotkeyKind) -> Int? {
        let defaults = UserDefaults.standard
        guard defaults.object(forKey: kind.failureCodeKey) != nil else { return nil }
        return defaults.integer(forKey: kind.failureCodeKey)
    }

    static func setActive(for kind: LauncherHotkeyKind) {
        setState(.active, failureCode: nil, for: kind)
    }

    static func setFailed(_ failureCode: OSStatus, for kind: LauncherHotkeyKind) {
        setState(.failed, failureCode: Int(failureCode), for: kind)
    }

    static func consumeNeedsAttention(for kind: LauncherHotkeyKind) -> Bool {
        let defaults = UserDefaults.standard
        let needsAttention = defaults.bool(forKey: kind.attentionKey)
        defaults.set(false, forKey: kind.attentionKey)
        return needsAttention
    }

    private static func setState(
        _ state: LauncherHotkeyRegistrationState,
        failureCode: Int?,
        for kind: LauncherHotkeyKind
    ) {
        let defaults = UserDefaults.standard
        let previousState = defaults.string(forKey: kind.stateKey)
        let previousFailureCode = defaults.object(forKey: kind.failureCodeKey) == nil
            ? nil
            : defaults.integer(forKey: kind.failureCodeKey)
        let stateChanged = previousState != state.rawValue || previousFailureCode != failureCode

        defaults.set(state.rawValue, forKey: kind.stateKey)
        if let failureCode {
            defaults.set(failureCode, forKey: kind.failureCodeKey)
        } else {
            defaults.removeObject(forKey: kind.failureCodeKey)
        }

        if state == .active {
            defaults.set(false, forKey: kind.attentionKey)
        } else if stateChanged {
            defaults.set(true, forKey: kind.attentionKey)
        }

        NotificationCenter.default.post(
            name: .tagLauncherHotkeyRegistrationChanged,
            object: nil,
            userInfo: ["kind": kind.rawValue]
        )
    }
}

// MARK: - Search Documents

private enum QuickSearchFieldKind: Int {
    case name = 0
    case tag = 1
    case note = 2
    case bundleIdentifier = 3
    case internalBundleName = 4

    var weight: Double {
        switch self {
        case .name: return 100
        case .tag: return 70
        case .note: return 45
        case .internalBundleName: return 30
        case .bundleIdentifier: return 20
        }
    }
}

private enum QuickSearchMatchKind {
    case exact
    case prefix
    case substring
    case acronym
    case fuzzy

    var weight: Double {
        switch self {
        case .exact: return 100
        case .prefix: return 80
        case .substring: return 60
        case .acronym: return 55
        case .fuzzy: return 35
        }
    }
}

private struct QuickSearchIndexedField {
    let kind: QuickSearchFieldKind
    let text: String
    let normalized: String
    let acronym: String
    let pinyinCandidates: [String]
    let allowPinyinSubstring: Bool
    let allowPinyinFuzzySubsequence: Bool
}

private struct QuickSearchMatchOptions {
    let allowSubstring: Bool
    let allowFuzzySubsequence: Bool
}

private struct QuickSearchTokenMatch {
    let score: Double
    let fieldRank: Int
    let fieldKind: QuickSearchFieldKind
    let originalText: String
}

struct QuickSearchDocument: Identifiable {
    var id: URL { app.id }
    let app: AppInfo
    let localizedNames: [String]
    let internalBundleNames: [String]
    let tagNames: [String]
    let note: String
    let bundleIdentifier: String
    let lastOpenedAt: Date?
    let openCount: Int
    fileprivate let searchableFields: [QuickSearchIndexedField]
}

struct QuickSearchResult: Identifiable {
    var id: URL { document.id }
    let document: QuickSearchDocument
    let finalScore: Double
    let textScore: Double
    let bestFieldRank: Int
    let matchedTagName: String?
    let noteSnippet: String?

    var app: AppInfo { document.app }
}

struct QuickSearchTagFilterOption: Identifiable, Equatable {
    var id: String { name }
    let name: String
    let colorIndex: Int
    let customColor: TagCustomColor?
}

enum QuickSearchEngine {
    static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
        apps
            .filter { !AppIndexer.isInternalHelperAppPath($0.path) }
            .map { app in
                let localizedNames = uniqueOrdered(AppDisplayNameResolver.searchAliases(for: app))
                let internalBundleNames = internalBundleNames(for: app)
                let note = store.appNotes[app.path.path] ?? app.note ?? ""
                let bundleIdentifier = app.bundleIdentifier ?? ""
                let searchableFields = makeSearchableFields(
                    appName: app.name,
                    localizedNames: localizedNames,
                    internalBundleNames: internalBundleNames,
                    tagNames: app.tags,
                    note: note,
                    bundleIdentifier: bundleIdentifier
                )
                return QuickSearchDocument(
                    app: app,
                    localizedNames: localizedNames,
                    internalBundleNames: internalBundleNames,
                    tagNames: app.tags,
                    note: note,
                    bundleIdentifier: bundleIdentifier,
                    lastOpenedAt: store.appLastOpenedAt[app.path.path],
                    openCount: store.appOpenCounts[app.path.path] ?? 0,
                    searchableFields: searchableFields
                )
            }
    }

    static func search(
        _ query: String,
        documents: [QuickSearchDocument],
        selectedTagName: String? = nil,
        limit: Int = 50
    ) -> [QuickSearchResult] {
        let activeTagName = normalizedSelectedTagName(selectedTagName)
        let scopedDocuments = activeTagName.map { tagName in
            documents.filter { document in
                document.tagNames.contains(tagName)
            }
        } ?? documents
        let normalizedQuery = normalizeQuery(query)
        guard !normalizedQuery.isEmpty else {
            if let activeTagName {
                return tagFilteredEmptyQueryResults(
                    documents: scopedDocuments,
                    selectedTagName: activeTagName,
                    limit: limit
                )
            }
            return emptyQueryResults(documents: scopedDocuments, limit: min(limit, 6))
        }

        let tokens = normalizedQuery.split(separator: " ").map(String.init)
        let results = scopedDocuments.compactMap {
            result(for: $0, tokens: tokens, selectedTagName: activeTagName)
        }
        return results.sorted(by: rank).prefix(limit).map { $0 }
    }

    static func normalizeQuery(_ value: String) -> String {
        value
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .components(separatedBy: .whitespacesAndNewlines)
            .filter { !$0.isEmpty }
            .joined(separator: " ")
            .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
            .lowercased()
    }

    private static func result(
        for document: QuickSearchDocument,
        tokens: [String],
        selectedTagName: String?
    ) -> QuickSearchResult? {
        let fields = document.searchableFields
        var textScore: Double = 0
        var bestFieldRank = Int.max
        var matchedTagName: String?
        var noteSnippet: String?

        for token in tokens {
            guard let tokenMatch = fields
                .compactMap({ match(token: token, field: $0) })
                .max(by: { $0.score < $1.score })
            else {
                return nil
            }
            textScore += tokenMatch.score
            bestFieldRank = min(bestFieldRank, tokenMatch.fieldRank)
            if tokenMatch.fieldKind == .tag {
                matchedTagName = tokenMatch.originalText
            } else if tokenMatch.fieldKind == .note {
                noteSnippet = snippet(from: document.note, token: token)
            }
        }

        let finalScore = textScore + behaviorBoost(for: document)
        return QuickSearchResult(
            document: document,
            finalScore: finalScore,
            textScore: textScore,
            bestFieldRank: bestFieldRank,
            matchedTagName: matchedTagName ?? selectedTagName,
            noteSnippet: noteSnippet
        )
    }

    private static func makeSearchableFields(
        appName: String,
        localizedNames: [String],
        internalBundleNames: [String],
        tagNames: [String],
        note: String,
        bundleIdentifier: String
    ) -> [QuickSearchIndexedField] {
        let names = uniqueOrdered([appName] + localizedNames)
        let nameFields = names.map {
            return QuickSearchIndexedField(
                kind: .name,
                text: $0,
                normalized: normalizeField($0),
                acronym: acronym(for: $0),
                pinyinCandidates: pinyinCandidates(for: $0, includeLatin: true),
                allowPinyinSubstring: true,
                allowPinyinFuzzySubsequence: true
            )
        }
        let tagFields = tagNames.map {
            QuickSearchIndexedField(
                kind: .tag,
                text: $0,
                normalized: normalizeField($0),
                acronym: "",
                pinyinCandidates: pinyinCandidates(for: $0),
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        }
        let noteFields = note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? [] : [
            QuickSearchIndexedField(
                kind: .note,
                text: note,
                normalized: normalizeField(note),
                acronym: "",
                pinyinCandidates: pinyinCandidates(for: note),
                allowPinyinSubstring: true,
                allowPinyinFuzzySubsequence: false
            )
        ]
        let bundleFields = bundleIdentifier.isEmpty ? [] : [
            QuickSearchIndexedField(
                kind: .bundleIdentifier,
                text: bundleIdentifier,
                normalized: normalizeField(bundleIdentifier),
                acronym: "",
                pinyinCandidates: [],
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        ]
        let internalBundleNameFields = internalBundleNames.map {
            QuickSearchIndexedField(
                kind: .internalBundleName,
                text: $0,
                normalized: normalizeField($0),
                acronym: "",
                pinyinCandidates: [],
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        }
        return nameFields + tagFields + noteFields + bundleFields + internalBundleNameFields
    }

    private static func match(token: String, field: QuickSearchIndexedField) -> QuickSearchTokenMatch? {
        guard let candidate = bestMatchCandidate(token: token, field: field) else { return nil }
        let positionBoost = candidate.0 == .exact ? 0 : max(0, 10 - min(candidate.1, 10))
        let score = field.kind.weight + candidate.0.weight + Double(positionBoost)
        return QuickSearchTokenMatch(
            score: score,
            fieldRank: field.kind.rawValue,
            fieldKind: field.kind,
            originalText: field.text
        )
    }

    private static func bestMatchCandidate(token: String, field: QuickSearchIndexedField) -> (QuickSearchMatchKind, Int)? {
        var candidates: [(QuickSearchMatchKind, Int)] = []

        if let textCandidate = matchCandidate(
            token: token,
            normalized: field.normalized,
            options: matchOptions(for: field.kind)
        ) {
            candidates.append(textCandidate)
        }
        if field.kind == .name && !field.acronym.isEmpty && field.acronym.hasPrefix(token) {
            candidates.append((.acronym, 0))
        }
        for pinyin in field.pinyinCandidates {
            if let pinyinCandidate = matchPinyinCandidate(
                token: token,
                normalized: pinyin,
                allowSubstring: field.allowPinyinSubstring,
                allowFuzzySubsequence: field.allowPinyinFuzzySubsequence
            ) {
                candidates.append(pinyinCandidate)
            }
        }

        return candidates.max { lhs, rhs in
            if lhs.0.weight != rhs.0.weight { return lhs.0.weight < rhs.0.weight }
            return lhs.1 > rhs.1
        }
    }

    private static func matchOptions(for fieldKind: QuickSearchFieldKind) -> QuickSearchMatchOptions {
        switch fieldKind {
        case .name:
            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
        case .tag:
            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
        case .note:
            return QuickSearchMatchOptions(allowSubstring: true, allowFuzzySubsequence: true)
        case .bundleIdentifier, .internalBundleName:
            return QuickSearchMatchOptions(allowSubstring: false, allowFuzzySubsequence: false)
        }
    }

    private static func matchCandidate(
        token: String,
        normalized: String,
        options: QuickSearchMatchOptions
    ) -> (QuickSearchMatchKind, Int)? {
        guard !normalized.isEmpty else { return nil }
        if normalized == token {
            return (.exact, 0)
        }
        if normalized.hasPrefix(token) {
            return (.prefix, 0)
        }
        if options.allowSubstring, let range = normalized.range(of: token) {
            return (.substring, normalized.distance(from: normalized.startIndex, to: range.lowerBound))
        }
        if options.allowFuzzySubsequence, token.count >= 4, isSubsequence(token, of: normalized) {
            return (.fuzzy, 10)
        }
        return nil
    }

    private static func matchPinyinCandidate(
        token: String,
        normalized: String,
        allowSubstring: Bool = false,
        allowFuzzySubsequence: Bool = false
    ) -> (QuickSearchMatchKind, Int)? {
        guard !normalized.isEmpty else { return nil }
        if normalized == token {
            return (.exact, 0)
        }
        if normalized.hasPrefix(token) {
            return (.prefix, 0)
        }
        if allowSubstring, let range = normalized.range(of: token) {
            return (.substring, normalized.distance(from: normalized.startIndex, to: range.lowerBound))
        }
        if allowFuzzySubsequence, token.count >= 3, isSubsequence(token, of: normalized) {
            return (.fuzzy, 10)
        }
        return nil
    }

    private static func tagFilteredEmptyQueryResults(
        documents: [QuickSearchDocument],
        selectedTagName: String,
        limit: Int
    ) -> [QuickSearchResult] {
        documents
            .map {
                QuickSearchResult(
                    document: $0,
                    finalScore: behaviorBoost(for: $0),
                    textScore: 0,
                    bestFieldRank: Int.max,
                    matchedTagName: selectedTagName,
                    noteSnippet: nil
                )
            }
            .sorted(by: rank)
            .prefix(limit)
            .map { $0 }
    }

    private static func emptyQueryResults(documents: [QuickSearchDocument], limit: Int) -> [QuickSearchResult] {
        var used = Set<URL>()
        let recent = documents
            .filter { $0.lastOpenedAt != nil }
            .sorted {
                if ($0.lastOpenedAt ?? .distantPast) != ($1.lastOpenedAt ?? .distantPast) {
                    return ($0.lastOpenedAt ?? .distantPast) > ($1.lastOpenedAt ?? .distantPast)
                }
                return $0.app.name.localizedStandardCompare($1.app.name) == .orderedAscending
            }

        let frequent = documents
            .filter { $0.openCount > 0 }
            .sorted {
                if $0.openCount != $1.openCount { return $0.openCount > $1.openCount }
                return $0.app.name.localizedStandardCompare($1.app.name) == .orderedAscending
            }

        let ordered = (recent + frequent).filter { used.insert($0.id).inserted }
        return ordered.prefix(limit).map {
            QuickSearchResult(
                document: $0,
                finalScore: behaviorBoost(for: $0),
                textScore: 0,
                bestFieldRank: Int.max,
                matchedTagName: nil,
                noteSnippet: nil
            )
        }
    }

    private static func rank(_ lhs: QuickSearchResult, _ rhs: QuickSearchResult) -> Bool {
        if lhs.finalScore != rhs.finalScore { return lhs.finalScore > rhs.finalScore }
        if lhs.textScore != rhs.textScore { return lhs.textScore > rhs.textScore }
        if lhs.bestFieldRank != rhs.bestFieldRank { return lhs.bestFieldRank < rhs.bestFieldRank }
        let leftDate = lhs.document.lastOpenedAt ?? .distantPast
        let rightDate = rhs.document.lastOpenedAt ?? .distantPast
        if leftDate != rightDate { return leftDate > rightDate }
        if lhs.document.openCount != rhs.document.openCount {
            return lhs.document.openCount > rhs.document.openCount
        }
        if lhs.app.name.count != rhs.app.name.count {
            return lhs.app.name.count < rhs.app.name.count
        }
        return lhs.app.name.localizedStandardCompare(rhs.app.name) == .orderedAscending
    }

    private static func behaviorBoost(for document: QuickSearchDocument) -> Double {
        min(recentBoost(for: document.lastOpenedAt) + frequencyBoost(for: document.openCount), 20)
    }

    private static func recentBoost(for date: Date?) -> Double {
        guard let date else { return 0 }
        let age = Date().timeIntervalSince(date)
        if age <= 24 * 60 * 60 { return 15 }
        if age <= 7 * 24 * 60 * 60 { return 10 }
        if age <= 30 * 24 * 60 * 60 { return 5 }
        return 2
    }

    private static func frequencyBoost(for openCount: Int) -> Double {
        min(Double(openCount), 10)
    }

    private static func normalizeField(_ value: String) -> String {
        value
            .folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
            .lowercased()
    }

    private static func pinyinCandidates(for value: String, includeLatin: Bool = false) -> [String] {
        guard includeLatin || containsNonLatinLetter(value) else { return [] }
        let mutable = NSMutableString(string: value)
        CFStringTransform(mutable, nil, kCFStringTransformToLatin, false)
        CFStringTransform(mutable, nil, kCFStringTransformStripCombiningMarks, false)

        let spaced = normalizeField(mutable as String)
            .components(separatedBy: CharacterSet.alphanumerics.inverted)
            .filter { !$0.isEmpty }
            .joined(separator: " ")
        guard !spaced.isEmpty else { return [] }

        let compact = spaced.replacingOccurrences(of: " ", with: "")
        let initials = spaced
            .split(separator: " ")
            .compactMap(\.first)
            .map(String.init)
            .joined()
        return uniqueOrdered([spaced, compact, initials].filter { !$0.isEmpty })
    }

    private static func containsNonLatinLetter(_ value: String) -> Bool {
        value.unicodeScalars.contains { scalar in
            CharacterSet.letters.contains(scalar) && !isLatinScriptLetter(scalar)
        }
    }

    private static func isLatinScriptLetter(_ scalar: UnicodeScalar) -> Bool {
        switch scalar.value {
        case 0x0041...0x005A, // Basic Latin uppercase
             0x0061...0x007A, // Basic Latin lowercase
             0x00AA,
             0x00BA,
             0x00C0...0x024F, // Latin-1 Supplement, Extended-A/B
             0x1E00...0x1EFF, // Latin Extended Additional
             0x2C60...0x2C7F, // Latin Extended-C
             0xA720...0xA7FF, // Latin Extended-D
             0xAB30...0xAB6F, // Latin Extended-E
             0xFF21...0xFF3A, // Fullwidth Latin uppercase
             0xFF41...0xFF5A: // Fullwidth Latin lowercase
            return true
        default:
            return false
        }
    }

    private static func isSubsequence(_ token: String, of value: String) -> Bool {
        var searchStart = value.startIndex
        for character in token {
            guard let index = value[searchStart...].firstIndex(of: character) else { return false }
            searchStart = value.index(after: index)
        }
        return true
    }

    private static func acronym(for value: String) -> String {
        var parts: [Character] = []
        var nextStartsWord = true
        var previousWasLowercase = false
        for character in value {
            let current = String(character)
            let isAlphanumeric = current.rangeOfCharacter(from: .alphanumerics) != nil
            guard isAlphanumeric else {
                nextStartsWord = true
                previousWasLowercase = false
                continue
            }

            let isUppercase = current.rangeOfCharacter(from: .uppercaseLetters) != nil
            let isLowercase = current.rangeOfCharacter(from: .lowercaseLetters) != nil
            if nextStartsWord || (isUppercase && previousWasLowercase) {
                parts.append(character)
            }
            nextStartsWord = false
            previousWasLowercase = isLowercase
        }
        return normalizeField(String(parts))
    }

    private static func normalizedSelectedTagName(_ value: String?) -> String? {
        guard let value else { return nil }
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }

    private static func internalBundleNames(for app: AppInfo) -> [String] {
        guard let bundle = Bundle(url: app.path) else { return [] }
        let values = [
            bundle.localizedInfoDictionary?["CFBundleName"] as? String,
            bundle.infoDictionary?["CFBundleName"] as? String
        ]
        return uniqueOrdered(values.compactMap { $0 }.compactMap { value in
            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
            return trimmed.isEmpty || trimmed == app.name ? nil : trimmed
        })
    }

    private static func snippet(from note: String, token: String) -> String {
        let trimmed = note.trimmingCharacters(in: .whitespacesAndNewlines)
        guard trimmed.count > 80 else { return trimmed }
        return String(trimmed.prefix(77)) + "..."
    }

    private static func uniqueOrdered(_ values: [String]) -> [String] {
        var seen = Set<String>()
        return values.filter { seen.insert(normalizeField($0)).inserted }
    }
}

// MARK: - Quick Search UI

enum QuickSearchCommand {
    case moveUp
    case moveDown
    case submit
    case dismiss
}

enum QuickSearchPanelMetrics {
    static let width: CGFloat = 760
    static let shadowOutset: CGFloat = 56
    static let rowHeight: CGFloat = 74
    static let rowSpacing: CGFloat = 2
    static let resultListVerticalInset: CGFloat = 10
    static let headerHeight: CGFloat = 84
    static let tagFilterBarHeight: CGFloat = 40
    static let dividerHeight: CGFloat = 1
    static let messageRowHeight: CGFloat = 86

    static func contentHeight(hasTagFilters: Bool, hasResultList: Bool, visibleRows: Int) -> CGFloat {
        let tagFilterHeight = hasTagFilters ? tagFilterBarHeight : 0
        if hasResultList {
            return headerHeight
                + tagFilterHeight
                + dividerHeight
                + CGFloat(max(1, visibleRows)) * (rowHeight + rowSpacing)
                + resultListVerticalInset * 2
        }
        return headerHeight + tagFilterHeight + dividerHeight + messageRowHeight
    }
}

struct QuickSearchPanelPresentationView: NSViewRepresentable {
    @Binding var query: String
    let results: [QuickSearchResult]
    let tagFilters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let selectedID: URL?
    let focusToken: Int
    let selectionScrollToken: Int
    let isLoading: Bool
    let maxVisibleRows: Int
    let panelTopY: CGFloat
    let panelHeight: CGFloat
    let errorMessage: String?
    let onCommand: (QuickSearchCommand) -> Void
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void
    let onTagFilterChange: (String?) -> Void

    func makeNSView(context: Context) -> QuickSearchPanelAnchorView {
        QuickSearchPanelAnchorView()
    }

    func updateNSView(_ view: QuickSearchPanelAnchorView, context: Context) {
        let contentSize = NSSize(width: QuickSearchPanelMetrics.width, height: panelHeight)
        let windowSize = NSSize(
            width: contentSize.width + QuickSearchPanelMetrics.shadowOutset * 2,
            height: contentSize.height + QuickSearchPanelMetrics.shadowOutset * 2
        )
        let rootView = QuickSearchPanelWindowContent(
            windowSize: windowSize,
            contentSize: contentSize,
            content: AnyView(
                QuickSearchOverlayView(
                    query: $query,
                    results: results,
                    tagFilters: tagFilters,
                    selectedTagName: selectedTagName,
                    selectedID: selectedID,
                    focusToken: focusToken,
                    selectionScrollToken: selectionScrollToken,
                    isLoading: isLoading,
                    maxVisibleRows: maxVisibleRows,
                    errorMessage: errorMessage,
                    onCommand: onCommand,
                    onHover: onHover,
                    onLaunch: onLaunch,
                    onTagFilterChange: onTagFilterChange
                )
            )
        )
        context.coordinator.apply(
            QuickSearchPanelConfiguration(
                panelTopY: panelTopY,
                windowSize: windowSize,
                contentSize: contentSize,
                rootView: AnyView(rootView)
            ),
            anchorView: view
        )
    }

    static func dismantleNSView(_ nsView: QuickSearchPanelAnchorView, coordinator: Coordinator) {
        coordinator.closePanel()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    final class Coordinator {
        private var panel: QuickSearchPanel?
        private var hostingView: NSHostingView<AnyView>?
        private weak var parentWindow: NSWindow?
        private var deferredApplyPending = false

        func apply(_ configuration: QuickSearchPanelConfiguration, anchorView: QuickSearchPanelAnchorView) {
            guard let parentWindow = anchorView.window else {
                scheduleDeferredApply(configuration, anchorView: anchorView)
                return
            }

            deferredApplyPending = false
            let panel = ensurePanel(parentWindow: parentWindow)
            let frame = windowFrame(
                parentWindow: parentWindow,
                panelTopY: configuration.panelTopY,
                contentSize: configuration.contentSize,
                windowSize: configuration.windowSize
            )

            hostingView?.rootView = configuration.rootView
            hostingView?.frame = NSRect(origin: .zero, size: configuration.windowSize)
            panel.contentView?.frame = NSRect(origin: .zero, size: configuration.windowSize)
            panel.collectionBehavior = collectionBehavior(parentWindow: parentWindow)
            panel.level = parentWindow.level

            if panel.parent !== parentWindow {
                if let previousParent = panel.parent {
                    previousParent.removeChildWindow(panel)
                }
                parentWindow.addChildWindow(panel, ordered: .above)
                self.parentWindow = parentWindow
            }

            let shouldOrderForInput = shouldOrderPanelForInput(panel)
            if panel.frame != frame {
                panel.setFrame(frame, display: true)
            }
            if shouldOrderForInput {
                NSApp.activate(ignoringOtherApps: true)
                panel.makeKeyAndOrderFront(nil)
                panel.orderFrontRegardless()
            }
        }

        func closePanel() {
            if let panel {
                if let parent = panel.parent {
                    parent.removeChildWindow(panel)
                }
                panel.orderOut(nil)
            }
            panel = nil
            hostingView = nil
            parentWindow = nil
            deferredApplyPending = false
        }

        private func ensurePanel(parentWindow: NSWindow) -> QuickSearchPanel {
            if let panel {
                return panel
            }

            let panel = QuickSearchPanel(
                contentRect: .zero,
                styleMask: [.borderless, .fullSizeContentView],
                backing: .buffered,
                defer: false
            )
            panel.isFloatingPanel = true
            panel.hidesOnDeactivate = false
            panel.isOpaque = false
            panel.backgroundColor = .clear
            panel.hasShadow = false
            panel.titlebarAppearsTransparent = true
            panel.titleVisibility = .hidden
            panel.isReleasedWhenClosed = false
            panel.collectionBehavior = collectionBehavior(parentWindow: parentWindow)

            let hostingView = NSHostingView(rootView: AnyView(EmptyView()))
            hostingView.wantsLayer = true
            hostingView.layer?.backgroundColor = NSColor.clear.cgColor
            panel.contentView = hostingView

            self.panel = panel
            self.hostingView = hostingView
            return panel
        }

        private func shouldOrderPanelForInput(_ panel: QuickSearchPanel) -> Bool {
            !panel.isVisible || !panel.isKeyWindow || NSApp.keyWindow !== panel
        }

        private func scheduleDeferredApply(
            _ configuration: QuickSearchPanelConfiguration,
            anchorView: QuickSearchPanelAnchorView
        ) {
            guard !deferredApplyPending else { return }
            deferredApplyPending = true
            DispatchQueue.main.async { [weak self, weak anchorView] in
                guard let self, let anchorView else { return }
                self.deferredApplyPending = false
                self.apply(configuration, anchorView: anchorView)
            }
        }

        private func windowFrame(
            parentWindow: NSWindow,
            panelTopY: CGFloat,
            contentSize: NSSize,
            windowSize: NSSize
        ) -> NSRect {
            let parentFrame = parentWindow.frame
            return NSRect(
                x: parentFrame.minX + (parentFrame.width - contentSize.width) / 2 - QuickSearchPanelMetrics.shadowOutset,
                y: parentFrame.maxY - panelTopY - contentSize.height - QuickSearchPanelMetrics.shadowOutset,
                width: windowSize.width,
                height: windowSize.height
            )
        }

        private func collectionBehavior(parentWindow: NSWindow) -> NSWindow.CollectionBehavior {
            var behavior: NSWindow.CollectionBehavior = [
                .fullScreenAuxiliary,
                .stationary,
                .transient,
                .ignoresCycle
            ]
            if parentWindow.collectionBehavior.contains(.canJoinAllSpaces) {
                behavior.insert(.canJoinAllSpaces)
            }
            return behavior
        }
    }
}

private final class QuickSearchPanel: NSPanel {
    override var canBecomeKey: Bool { true }
    override var canBecomeMain: Bool { false }

    override func sendEvent(_ event: NSEvent) {
        if event.type == .leftMouseDown {
            if let tagChip = quickSearchTagFilterClickTarget(at: event.locationInWindow),
               tagChip.activateFromMouseClick() {
                return
            }
            if let row = quickSearchResultRow(at: event.locationInWindow) {
                row.launchFromMouseClick()
                return
            }
        }
        super.sendEvent(event)
    }

    override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
        frameRect
    }

    private func quickSearchResultRow(at windowPoint: NSPoint) -> QuickSearchResultRowView? {
        guard let contentView else { return nil }
        if let row = contentView.quickSearchResultRowFromHitTest(atWindowPoint: windowPoint) {
            return row
        }
        return contentView.quickSearchResultRowDescendant(containingWindowPoint: windowPoint)
    }

    private func quickSearchTagFilterClickTarget(at windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        guard let contentView else { return nil }
        if let chip = contentView.quickSearchTagFilterClickTargetFromHitTest(atWindowPoint: windowPoint) {
            return chip
        }
        return contentView.quickSearchTagFilterClickTargetDescendant(containingWindowPoint: windowPoint)
    }
}

private extension NSView {
    func quickSearchTagFilterClickTargetFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        let localPoint = convert(windowPoint, from: nil)
        var hitView = hitTest(localPoint)
        while let candidate = hitView {
            if let chip = candidate as? QuickSearchTagFilterClickTargetView {
                return chip
            }
            hitView = candidate.superview
        }
        return nil
    }

    func quickSearchTagFilterClickTargetDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        for subview in subviews.reversed() {
            if let chip = subview as? QuickSearchTagFilterClickTargetView,
               chip.containsWindowPointForActivation(windowPoint) {
                return chip
            }
            if let chip = subview.quickSearchTagFilterClickTargetDescendant(containingWindowPoint: windowPoint) {
                return chip
            }
        }
        return nil
    }

    func quickSearchResultRowFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
        let localPoint = convert(windowPoint, from: nil)
        var hitView = hitTest(localPoint)
        while let candidate = hitView {
            if let row = candidate as? QuickSearchResultRowView {
                return row
            }
            hitView = candidate.superview
        }
        return nil
    }

    func quickSearchResultRowDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
        for subview in subviews.reversed() {
            if let row = subview as? QuickSearchResultRowView,
               row.containsWindowPointForLaunch(windowPoint) {
                return row
            }
            if let row = subview.quickSearchResultRowDescendant(containingWindowPoint: windowPoint) {
                return row
            }
        }
        return nil
    }
}

final class QuickSearchPanelAnchorView: NSView {
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    private func setup() {
        wantsLayer = true
        layer?.backgroundColor = NSColor.clear.cgColor
    }
}

struct QuickSearchPanelConfiguration {
    let panelTopY: CGFloat
    let windowSize: NSSize
    let contentSize: NSSize
    let rootView: AnyView
}

private struct QuickSearchPanelWindowContent: View {
    let windowSize: NSSize
    let contentSize: NSSize
    let content: AnyView

    var body: some View {
        VStack(spacing: 0) {
            content
                .frame(width: contentSize.width, height: contentSize.height, alignment: .top)
            Spacer(minLength: 0)
        }
        .padding(.top, QuickSearchPanelMetrics.shadowOutset)
        .padding(.horizontal, QuickSearchPanelMetrics.shadowOutset)
        .padding(.bottom, QuickSearchPanelMetrics.shadowOutset)
        .frame(width: windowSize.width, height: windowSize.height, alignment: .top)
        .background(Color.clear)
    }
}

struct QuickSearchOverlayView: View {
    @Binding var query: String
    let results: [QuickSearchResult]
    let tagFilters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let selectedID: URL?
    let focusToken: Int
    let selectionScrollToken: Int
    let isLoading: Bool
    let maxVisibleRows: Int
    let errorMessage: String?
    let onCommand: (QuickSearchCommand) -> Void
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void
    let onTagFilterChange: (String?) -> Void

    @Environment(\.colorScheme) private var colorScheme

    private let panelWidth: CGFloat = QuickSearchPanelMetrics.width
    private let rowHeight: CGFloat = QuickSearchPanelMetrics.rowHeight

    private var panelBackgroundColor: Color {
        colorScheme == .dark
            ? Color(red: 0.105, green: 0.110, blue: 0.125).opacity(0.97)
            : Color.white.opacity(0.97)
    }

    private var panelShape: RoundedRectangle {
        RoundedRectangle(cornerRadius: 34, style: .continuous)
    }

    private var hasTagFilters: Bool {
        !tagFilters.isEmpty
    }

    private var emptyResultsSystemImage: String {
        if selectedTagName != nil {
            return "tag"
        }
        return query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "keyboard" : "magnifyingglass"
    }

    private var emptyResultsMessage: String {
        if let selectedTagName {
            return tr("quickSearch.tagFilter.noResults", replacements: ["%tagName%": selectedTagName])
        }
        return query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            ? tr("quickSearch.emptyPrompt")
            : tr("quickSearch.noResults")
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(spacing: 18) {
                Image(systemName: "magnifyingglass")
                    .font(.system(size: 29, weight: .regular))
                    .foregroundStyle(Color.primary.opacity(0.48))
                    .frame(width: 34)

                QuickSearchTextField(
                    text: $query,
                    placeholder: tr("quickSearch.placeholder"),
                    focusToken: focusToken,
                    onCommand: onCommand
                )
                .frame(height: 44)
            }
            .padding(.horizontal, 28)
            .padding(.top, 22)
            .padding(.bottom, 18)

            if hasTagFilters {
                QuickSearchTagFilterBar(
                    filters: tagFilters,
                    selectedTagName: selectedTagName,
                    onSelect: onTagFilterChange
                )
                .frame(height: QuickSearchPanelMetrics.tagFilterBarHeight)
            }

            Divider().opacity(0.35)

            if isLoading {
                QuickSearchMessageRow(
                    systemImage: "hourglass",
                    message: tr("quickSearch.loading"),
                    tint: .secondary
                )
            } else if let errorMessage {
                QuickSearchMessageRow(
                    systemImage: "exclamationmark.triangle.fill",
                    message: errorMessage,
                    tint: .orange
                )
            } else if results.isEmpty {
                QuickSearchMessageRow(
                    systemImage: emptyResultsSystemImage,
                    message: emptyResultsMessage,
                    tint: .secondary
                )
            } else {
                QuickSearchResultListView(
                    results: results,
                    selectedID: selectedID,
                    selectionScrollToken: selectionScrollToken,
                    maxVisibleRows: maxVisibleRows,
                    rowHeight: rowHeight,
                    isDarkMode: colorScheme == .dark,
                    onHover: onHover,
                    onLaunch: onLaunch
                )
                .frame(
                    height: CGFloat(min(results.count, maxVisibleRows))
                        * (rowHeight + QuickSearchPanelMetrics.rowSpacing)
                        + QuickSearchPanelMetrics.resultListVerticalInset * 2
                )
            }
        }
        .frame(width: panelWidth)
        .background(
            panelShape.fill(panelBackgroundColor)
        )
        .clipShape(panelShape)
        .overlay(
            panelShape.stroke(Color.primary.opacity(0.10), lineWidth: 1)
        )
        .compositingGroup()
        .shadow(color: .black.opacity(colorScheme == .dark ? 0.46 : 0.26), radius: 30, x: 0, y: 18)
        .shadow(color: .black.opacity(colorScheme == .dark ? 0.24 : 0.12), radius: 8, x: 0, y: 3)
        .accessibilityElement(children: .contain)
        .accessibilityLabel(tr("quickSearch.title"))
    }
}

private struct QuickSearchTagFilterBar: View {
    let filters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let onSelect: (String?) -> Void

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                QuickSearchTagFilterChip(
                    title: tr("quickSearch.tagFilter.all"),
                    colorIndex: nil,
                    customColor: nil,
                    isSelected: selectedTagName == nil,
                    accessibilityLabel: tr("quickSearch.tagFilter.allAccessibility")
                ) {
                    onSelect(nil)
                }

                ForEach(filters) { filter in
                    QuickSearchTagFilterChip(
                        title: filter.name,
                        colorIndex: filter.colorIndex,
                        customColor: filter.customColor,
                        isSelected: selectedTagName == filter.name,
                        accessibilityLabel: tr(
                            "quickSearch.tagFilter.tagAccessibility",
                            replacements: ["%tagName%": filter.name]
                        )
                    ) {
                        onSelect(selectedTagName == filter.name ? nil : filter.name)
                    }
                }
            }
            .padding(.horizontal, 28)
            .padding(.vertical, 5)
        }
        .accessibilityElement(children: .contain)
    }
}

private struct QuickSearchTagFilterChip: View {
    let title: String
    let colorIndex: Int?
    let customColor: TagCustomColor?
    let isSelected: Bool
    let accessibilityLabel: String
    let action: () -> Void

    @Environment(\.colorScheme) private var colorScheme
    @State private var isHovered = false

    private var tagDotColor: Color? {
        guard let colorIndex else { return nil }
        return Color(nsColor: TagColor.nsColor(for: colorIndex, customColor: customColor))
    }

    private var backgroundColor: Color {
        if isSelected {
            return Color.primary.opacity(colorScheme == .dark ? 0.18 : 0.10)
        }
        return Color.primary.opacity(isHovered ? (colorScheme == .dark ? 0.11 : 0.075) : (colorScheme == .dark ? 0.08 : 0.055))
    }

    private var strokeColor: Color {
        Color.primary.opacity(isSelected ? 0.34 : 0.11)
    }

    var body: some View {
        Button(action: action) {
            HStack(spacing: 7) {
                if let tagDotColor {
                    Circle()
                        .fill(tagDotColor)
                        .frame(width: 7, height: 7)
                }

                Text(title)
                    .font(.system(size: 13, weight: .semibold))
                    .lineLimit(1)
                    .truncationMode(.tail)
                    .foregroundStyle(.primary)
                    .layoutPriority(1)
            }
            .padding(.horizontal, 11)
            .frame(maxWidth: 160, minHeight: 30, maxHeight: 30, alignment: .center)
            .background(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .fill(backgroundColor)
            )
            .overlay(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .stroke(strokeColor, lineWidth: isSelected ? 1.2 : 1)
            )
            .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
        }
        .buttonStyle(.plain)
        .help(title)
        .onHover { isHovered = $0 }
        .overlay(
            QuickSearchTagFilterClickTarget(action: action)
                .accessibilityHidden(true)
        )
        .accessibilityLabel(accessibilityLabel)
        .accessibilityAddTraits(isSelected ? .isSelected : [])
    }
}

private struct QuickSearchTagFilterClickTarget: NSViewRepresentable {
    let action: () -> Void

    func makeNSView(context: Context) -> QuickSearchTagFilterClickTargetView {
        let view = QuickSearchTagFilterClickTargetView()
        view.onActivate = action
        return view
    }

    func updateNSView(_ view: QuickSearchTagFilterClickTargetView, context: Context) {
        view.onActivate = action
    }
}

private final class QuickSearchTagFilterClickTargetView: NSView {
    var onActivate: (() -> Void)?

    override var isFlipped: Bool { true }

    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        true
    }

    override func mouseDown(with event: NSEvent) {
        guard activateFromMouseClick() else {
            super.mouseDown(with: event)
            return
        }
    }

    @discardableResult
    func activateFromMouseClick() -> Bool {
        guard let onActivate else { return false }
        DispatchQueue.main.async { [onActivate] in
            onActivate()
        }
        return true
    }

    func containsWindowPointForActivation(_ windowPoint: NSPoint) -> Bool {
        guard !isHidden, window != nil else { return false }
        let localPoint = convert(windowPoint, from: nil)
        return bounds.contains(localPoint) && visibleRect.contains(localPoint)
    }
}

private struct QuickSearchResultListView: NSViewRepresentable {
    let results: [QuickSearchResult]
    let selectedID: URL?
    let selectionScrollToken: Int
    let maxVisibleRows: Int
    let rowHeight: CGFloat
    let isDarkMode: Bool
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void

    func makeNSView(context: Context) -> QuickSearchResultListHostView {
        let view = QuickSearchResultListHostView()
        view.update(
            results: results,
            selectedID: selectedID,
            selectionScrollToken: selectionScrollToken,
            maxVisibleRows: maxVisibleRows,
            rowHeight: rowHeight,
            isDarkMode: isDarkMode,
            onHover: onHover,
            onLaunch: onLaunch
        )
        return view
    }

    func updateNSView(_ view: QuickSearchResultListHostView, context: Context) {
        view.update(
            results: results,
            selectedID: selectedID,
            selectionScrollToken: selectionScrollToken,
            maxVisibleRows: maxVisibleRows,
            rowHeight: rowHeight,
            isDarkMode: isDarkMode,
            onHover: onHover,
            onLaunch: onLaunch
        )
    }
}

final class QuickSearchResultListHostView: NSView {
    private let scrollView = NSScrollView()
    private let documentView = QuickSearchResultListDocumentView()
    private var selectedID: URL?
    private var selectionScrollToken = 0
    private var didInstall = false

    override var isFlipped: Bool { true }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    func update(
        results: [QuickSearchResult],
        selectedID: URL?,
        selectionScrollToken: Int,
        maxVisibleRows: Int,
        rowHeight: CGFloat,
        isDarkMode: Bool,
        onHover: @escaping (QuickSearchResult) -> Void,
        onLaunch: @escaping (QuickSearchResult) -> Void
    ) {
        let shouldScrollSelection = selectionScrollToken != self.selectionScrollToken
        self.selectedID = selectedID
        self.selectionScrollToken = selectionScrollToken
        scrollView.hasVerticalScroller = results.count > maxVisibleRows
        documentView.update(
            results: results,
            selectedID: selectedID,
            rowHeight: rowHeight,
            isDarkMode: isDarkMode,
            onHover: onHover,
            onLaunch: onLaunch
        )
        needsLayout = true
        layoutSubtreeIfNeeded()
        if shouldScrollSelection, let selectedID {
            scrollResultToCenter(selectedID)
        }
    }

    override func layout() {
        super.layout()
        scrollView.frame = bounds
        let visibleSize = scrollView.contentView.bounds.size
        documentView.frame = NSRect(
            origin: .zero,
            size: NSSize(
                width: max(visibleSize.width, bounds.width),
                height: documentView.preferredHeight
            )
        )
        documentView.needsLayout = true
    }

    private func setup() {
        guard !didInstall else { return }
        didInstall = true
        wantsLayer = true
        layer?.backgroundColor = NSColor.clear.cgColor

        scrollView.drawsBackground = false
        scrollView.borderType = .noBorder
        scrollView.autohidesScrollers = true
        scrollView.scrollerStyle = .overlay
        scrollView.hasHorizontalScroller = false
        scrollView.hasVerticalScroller = false
        scrollView.documentView = documentView
        addSubview(scrollView)
    }

    private func scrollResultToCenter(_ id: URL) {
        guard let frame = documentView.frameForResult(id) else { return }
        let visibleBounds = scrollView.contentView.bounds
        let maxY = max(0, documentView.bounds.height - visibleBounds.height)
        let targetY = min(max(0, frame.midY - visibleBounds.height / 2), maxY)
        scrollView.contentView.scroll(to: NSPoint(x: 0, y: targetY))
        scrollView.reflectScrolledClipView(scrollView.contentView)
    }
}

final class QuickSearchResultListDocumentView: NSView {
    private var rowViews: [QuickSearchResultRowView] = []
    private var rowIDs: [URL] = []
    private var rowHeight: CGFloat = 74
    private let rowSpacing: CGFloat = 2
    private let horizontalInset: CGFloat = 10
    private let verticalInset: CGFloat = 10

    override var isFlipped: Bool { true }

    var preferredHeight: CGFloat {
        verticalInset * 2
            + CGFloat(rowViews.count) * rowHeight
            + CGFloat(max(0, rowViews.count - 1)) * rowSpacing
    }

    func update(
        results: [QuickSearchResult],
        selectedID: URL?,
        rowHeight: CGFloat,
        isDarkMode: Bool,
        onHover: @escaping (QuickSearchResult) -> Void,
        onLaunch: @escaping (QuickSearchResult) -> Void
    ) {
        self.rowHeight = rowHeight
        let nextIDs = results.map(\.id)
        if nextIDs != rowIDs {
            rebuildRows(for: results)
            rowIDs = nextIDs
        }

        for (index, row) in rowViews.enumerated() {
            guard index < results.count else { continue }
            row.configure(
                result: results[index],
                isSelected: results[index].id == selectedID,
                isDarkMode: isDarkMode,
                onHover: onHover,
                onLaunch: onLaunch
            )
        }
        needsLayout = true
    }

    func frameForResult(_ id: URL) -> NSRect? {
        guard let index = rowIDs.firstIndex(of: id),
              index < rowViews.count
        else { return nil }
        return rowViews[index].frame
    }

    override func layout() {
        super.layout()
        let width = max(0, bounds.width - horizontalInset * 2)
        var y = verticalInset
        for row in rowViews {
            row.frame = NSRect(x: horizontalInset, y: y, width: width, height: rowHeight)
            y += rowHeight + rowSpacing
        }
    }

    private func rebuildRows(for results: [QuickSearchResult]) {
        rowViews.forEach { $0.removeFromSuperview() }
        rowViews = results.map { _ in
            let row = QuickSearchResultRowView()
            addSubview(row)
            return row
        }
    }
}

final class QuickSearchResultRowView: NSView {
    private enum TagBadgeLayout {
        static let columnWidth: CGFloat = 196
        static let minWidth: CGFloat = 80
        static let maxWidth: CGFloat = 196
        static let height: CGFloat = 32
        static let horizontalInset: CGFloat = 16
        static let labelSafetyWidth: CGFloat = 8
        static let trailingInset: CGFloat = 18
        static let textGap: CGFloat = 18
        static let minimumMainTextWidth: CGFloat = 260
    }

    private let iconView = NSImageView()
    private let titleLabel = NSTextField(labelWithString: "")
    private let detailLabel = NSTextField(labelWithString: "")
    private let tagBackgroundView = NSView()
    private let tagLabel = NSTextField(labelWithString: "")
    private var trackingAreaRef: NSTrackingArea?
    private var result: QuickSearchResult?
    private var isSelected = false
    private var isDarkMode = false
    private var onHover: (QuickSearchResult) -> Void = { _ in }
    private var onLaunch: (QuickSearchResult) -> Void = { _ in }

    override var isFlipped: Bool { true }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    func configure(
        result: QuickSearchResult,
        isSelected: Bool,
        isDarkMode: Bool,
        onHover: @escaping (QuickSearchResult) -> Void,
        onLaunch: @escaping (QuickSearchResult) -> Void
    ) {
        self.result = result
        self.isSelected = isSelected
        self.isDarkMode = isDarkMode
        self.onHover = onHover
        self.onLaunch = onLaunch

        iconView.image = result.app.icon
        titleLabel.stringValue = result.app.displayName
        detailLabel.stringValue = detailText(for: result) ?? ""
        detailLabel.isHidden = detailLabel.stringValue.isEmpty
        tagLabel.stringValue = rightTagName(for: result) ?? ""
        tagBackgroundView.isHidden = tagLabel.stringValue.isEmpty
        setAccessibilityLabel(accessibilityText(for: result))
        updateColors()
        needsLayout = true
    }

    override func layout() {
        super.layout()
        let iconSize: CGFloat = 46
        let iconX: CGFloat = 18
        iconView.frame = NSRect(
            x: iconX,
            y: (bounds.height - iconSize) / 2,
            width: iconSize,
            height: iconSize
        )

        let textX = iconX + iconSize + 16
        let trailingInset = TagBadgeLayout.trailingInset
        let rightLimit = bounds.width - trailingInset
        var tagFrame = NSRect.zero
        let hasTag = !tagLabel.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        let availableBadgeColumnWidth = rightLimit - textX - TagBadgeLayout.minimumMainTextWidth - TagBadgeLayout.textGap
        let tagColumnWidth = min(TagBadgeLayout.columnWidth, max(0, availableBadgeColumnWidth))
        let canShowTag = hasTag && tagColumnWidth >= TagBadgeLayout.minWidth
        tagBackgroundView.isHidden = !canShowTag

        if canShowTag {
            let measuredLabelWidth = measuredTagLabelWidth()
            let measuredBadgeWidth = measuredLabelWidth
                + TagBadgeLayout.horizontalInset * 2
                + TagBadgeLayout.labelSafetyWidth
            let tagWidth = min(
                min(TagBadgeLayout.maxWidth, tagColumnWidth),
                max(TagBadgeLayout.minWidth, measuredBadgeWidth)
            )
            tagFrame = NSRect(
                x: rightLimit - tagWidth,
                y: (bounds.height - TagBadgeLayout.height) / 2,
                width: tagWidth,
                height: TagBadgeLayout.height
            )
            tagBackgroundView.frame = tagFrame
            tagLabel.frame = NSRect(
                x: TagBadgeLayout.horizontalInset,
                y: 6,
                width: tagWidth - TagBadgeLayout.horizontalInset * 2,
                height: 20
            )
        } else {
            tagBackgroundView.frame = .zero
            tagLabel.frame = .zero
        }

        let textRight = tagBackgroundView.isHidden ? bounds.width - trailingInset : tagFrame.minX - TagBadgeLayout.textGap
        let textWidth = max(40, textRight - textX)
        if detailLabel.isHidden {
            titleLabel.frame = NSRect(x: textX, y: (bounds.height - 26) / 2, width: textWidth, height: 26)
            detailLabel.frame = .zero
        } else {
            titleLabel.frame = NSRect(x: textX, y: 14, width: textWidth, height: 25)
            detailLabel.frame = NSRect(x: textX, y: 42, width: textWidth, height: 21)
        }
    }

    private func measuredTagLabelWidth() -> CGFloat {
        let font = tagLabel.font ?? NSFont.systemFont(ofSize: 14, weight: .semibold)
        return ceil((tagLabel.stringValue as NSString).size(withAttributes: [.font: font]).width)
    }

    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if let trackingAreaRef {
            removeTrackingArea(trackingAreaRef)
        }
        let area = NSTrackingArea(
            rect: bounds,
            options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
            owner: self,
            userInfo: nil
        )
        addTrackingArea(area)
        trackingAreaRef = area
    }

    override func hitTest(_ point: NSPoint) -> NSView? {
        guard !isHidden, alphaValue > 0, bounds.contains(point) else { return nil }
        return self
    }

    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        true
    }

    override func mouseEntered(with event: NSEvent) {
        super.mouseEntered(with: event)
        if let result {
            onHover(result)
        }
    }

    override func mouseDown(with event: NSEvent) {
        if !launchFromMouseClick() {
            super.mouseDown(with: event)
        }
    }

    @discardableResult
    func launchFromMouseClick() -> Bool {
        guard let result else { return false }
        DispatchQueue.main.async { [onLaunch, result] in
            onLaunch(result)
        }
        return true
    }

    func containsWindowPointForLaunch(_ windowPoint: NSPoint) -> Bool {
        guard window != nil else { return false }
        var view: NSView? = self
        while let candidate = view {
            if candidate.isHidden || candidate.alphaValue <= 0 {
                return false
            }
            view = candidate.superview
        }
        let localPoint = convert(windowPoint, from: nil)
        return bounds.contains(localPoint) && visibleRect.contains(localPoint)
    }

    private func setup() {
        wantsLayer = true
        layer?.cornerRadius = 18
        layer?.cornerCurve = .continuous

        iconView.imageScaling = .scaleProportionallyUpOrDown
        iconView.wantsLayer = true
        iconView.layer?.cornerRadius = 10
        iconView.layer?.cornerCurve = .continuous
        iconView.layer?.masksToBounds = true
        addSubview(iconView)

        titleLabel.font = NSFont.systemFont(ofSize: 20, weight: .semibold)
        titleLabel.lineBreakMode = .byTruncatingTail
        titleLabel.maximumNumberOfLines = 1
        titleLabel.backgroundColor = .clear
        addSubview(titleLabel)

        detailLabel.font = NSFont.systemFont(ofSize: 16, weight: .medium)
        detailLabel.lineBreakMode = .byTruncatingTail
        detailLabel.maximumNumberOfLines = 1
        detailLabel.backgroundColor = .clear
        addSubview(detailLabel)

        tagBackgroundView.wantsLayer = true
        tagBackgroundView.layer?.cornerRadius = 16
        tagBackgroundView.layer?.cornerCurve = .continuous
        tagBackgroundView.addSubview(tagLabel)
        addSubview(tagBackgroundView)

        tagLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
        tagLabel.alignment = .center
        tagLabel.lineBreakMode = .byTruncatingTail
        tagLabel.maximumNumberOfLines = 1
        tagLabel.backgroundColor = .clear
        tagLabel.allowsDefaultTighteningForTruncation = true
        tagLabel.cell?.usesSingleLineMode = true
        tagLabel.cell?.lineBreakMode = .byTruncatingTail
    }

    private func updateColors() {
        layer?.backgroundColor = isSelected
            ? NSColor.labelColor.withAlphaComponent(isDarkMode ? 0.14 : 0.075).cgColor
            : NSColor.clear.cgColor
        titleLabel.textColor = .labelColor
        detailLabel.textColor = NSColor.labelColor.withAlphaComponent(0.38)
        tagLabel.textColor = NSColor.labelColor.withAlphaComponent(0.46)
        tagBackgroundView.layer?.backgroundColor = NSColor.labelColor
            .withAlphaComponent(isDarkMode ? 0.12 : 0.07)
            .cgColor
    }

    private func detailText(for result: QuickSearchResult) -> String? {
        if let note = result.noteSnippet, !note.isEmpty {
            return note
        }
        let note = result.document.note.trimmingCharacters(in: .whitespacesAndNewlines)
        if !note.isEmpty {
            return note.count > 72 ? String(note.prefix(69)) + "..." : note
        }
        return nil
    }

    private func rightTagName(for result: QuickSearchResult) -> String? {
        let tag = result.matchedTagName ?? result.document.tagNames.first
        guard let tag, !tag.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
        return tag
    }

    private func accessibilityText(for result: QuickSearchResult) -> String {
        [result.app.displayName, detailText(for: result)].compactMap { $0 }.joined(separator: ", ")
    }
}

private struct QuickSearchMessageRow: View {
    let systemImage: String
    let message: String
    let tint: Color

    var body: some View {
        HStack(spacing: 10) {
            Image(systemName: systemImage)
                .font(.system(size: 20, weight: .regular))
                .foregroundStyle(tint)
                .frame(width: 28)
            Text(message)
                .font(.system(size: 15, weight: .medium))
                .foregroundStyle(.secondary)
                .lineLimit(2)
                .fixedSize(horizontal: false, vertical: true)
            Spacer(minLength: 0)
        }
        .padding(.horizontal, 28)
        .padding(.vertical, 24)
        .frame(minHeight: 86)
        .accessibilityLabel(message)
    }
}

private struct QuickSearchTextField: NSViewRepresentable {
    @Binding var text: String
    let placeholder: String
    let focusToken: Int
    let onCommand: (QuickSearchCommand) -> Void

    func makeNSView(context: Context) -> QuickSearchNativeTextField {
        let field = QuickSearchNativeTextField()
        field.isBordered = false
        field.isBezeled = false
        field.drawsBackground = false
        field.focusRingType = .none
        field.font = NSFont.systemFont(ofSize: 28, weight: .regular)
        field.placeholderString = placeholder
        field.delegate = context.coordinator
        field.onCommand = onCommand
        field.target = context.coordinator
        field.action = #selector(Coordinator.submitAction(_:))
        context.coordinator.onCommand = onCommand
        field.setAccessibilityLabel(tr("quickSearch.inputAccessibility"))
        context.coordinator.field = field
        requestFocus(field)
        return field
    }

    func updateNSView(_ field: QuickSearchNativeTextField, context: Context) {
        if !field.isComposingText, field.stringValue != text {
            field.stringValue = text
        }
        field.publishCompositionStateIfNeeded()
        field.placeholderString = placeholder
        field.onCommand = onCommand
        field.target = context.coordinator
        field.action = #selector(Coordinator.submitAction(_:))
        context.coordinator.onCommand = onCommand
        if context.coordinator.lastFocusToken != focusToken {
            context.coordinator.lastFocusToken = focusToken
            requestFocus(field)
        }
    }

    private func requestFocus(_ field: QuickSearchNativeTextField) {
        for delay in [0.0, 0.03, 0.08, 0.16] {
            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak field] in
                guard let field,
                      let window = field.window,
                      window.isVisible,
                      field.currentEditor() == nil
                else { return }
                if !window.isKeyWindow || NSApp.keyWindow !== window {
                    NSApp.activate(ignoringOtherApps: true)
                    window.makeKeyAndOrderFront(nil)
                    window.orderFrontRegardless()
                }
                window.makeFirstResponder(field)
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text)
    }

    final class Coordinator: NSObject, NSTextFieldDelegate {
        var text: Binding<String>
        var lastFocusToken = 0
        var onCommand: ((QuickSearchCommand) -> Void)?
        weak var field: QuickSearchNativeTextField?

        init(text: Binding<String>) {
            self.text = text
        }

        func controlTextDidChange(_ obj: Notification) {
            guard let field = obj.object as? QuickSearchNativeTextField else { return }
            field.publishCompositionStateIfNeeded()
            if !field.isComposingText {
                text.wrappedValue = field.stringValue
            }
        }

        func controlTextDidEndEditing(_ obj: Notification) {
            guard let field = obj.object as? QuickSearchNativeTextField else { return }
            field.publishCompositionState(active: false, force: true)
            text.wrappedValue = field.stringValue
        }

        @objc func submitAction(_ sender: Any?) {
            guard field?.isComposingText != true else { return }
            onCommand?(.submit)
        }

        func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
            if textView.hasMarkedText() {
                (control as? QuickSearchNativeTextField)?.publishCompositionState(active: true)
                return false
            }
            switch commandSelector {
            case #selector(NSResponder.moveUp(_:)):
                onCommand?(.moveUp)
                return true
            case #selector(NSResponder.moveDown(_:)):
                onCommand?(.moveDown)
                return true
            case #selector(NSResponder.insertNewline(_:)):
                onCommand?(.submit)
                return true
            case #selector(NSResponder.insertNewlineIgnoringFieldEditor(_:)):
                onCommand?(.submit)
                return true
            case #selector(NSResponder.cancelOperation(_:)):
                onCommand?(.dismiss)
                return true
            default:
                return false
            }
        }
    }
}

private final class QuickSearchNativeTextField: NSTextField {
    var onCommand: ((QuickSearchCommand) -> Void)?
    private var lastPublishedCompositionActive = false

    override var acceptsFirstResponder: Bool { true }

    var isComposingText: Bool {
        (currentEditor() as? NSTextView)?.hasMarkedText() == true
    }

    func publishCompositionStateIfNeeded() {
        publishCompositionState(active: isComposingText)
    }

    func publishCompositionState(active: Bool, force: Bool = false) {
        guard force || active != lastPublishedCompositionActive else { return }
        lastPublishedCompositionActive = active
        NotificationCenter.default.post(
            name: .tagLauncherQuickSearchCompositionChanged,
            object: self,
            userInfo: ["active": active]
        )
    }

    override func keyDown(with event: NSEvent) {
        guard !isComposingText else {
            super.keyDown(with: event)
            return
        }
        switch Int(event.keyCode) {
        case kVK_UpArrow:
            onCommand?(.moveUp)
        case kVK_DownArrow:
            onCommand?(.moveDown)
        case kVK_Return:
            onCommand?(.submit)
        case kVK_Escape:
            onCommand?(.dismiss)
        default:
            super.keyDown(with: event)
        }
    }
}
