import ContactsClient
import Foundation

// MARK: - ContactRankingDataProtocol

protocol ContactRankingDataProtocol {
    var givenName: String { get }
    var familyName: String { get }
    var imageDataAvailable: Bool { get }
}

extension PhoneContact: ContactRankingDataProtocol {
    var givenName: String {
        let components = fullName.components(separatedBy: " ")
        return components.first ?? ""
    }

    var familyName: String {
        let components = fullName.components(separatedBy: " ")
        return components.count > 1 ? components.dropFirst().joined(separator: " ") : ""
    }

    var imageDataAvailable: Bool {
        thumbnailImageData != nil
    }
}

extension SimpleProfileWithContactName: ContactRankingDataProtocol {
    var givenName: String {
        let components = fullName.components(separatedBy: " ")
        return components.first ?? ""
    }

    var familyName: String {
        let components = fullName.components(separatedBy: " ")
        return components.count > 1 ? components.dropFirst().joined(separator: " ") : ""
    }

    var imageDataAvailable: Bool {
        user.avatarImageUrl != nil
    }
}

extension [PhoneContact] {
    func ranked(
        filterNegativeNames: Bool = true
    ) -> Self {
        ContactRanker.sorted(
            from: self,
            filterNegativeNames: filterNegativeNames
        )
    }
}

extension [SimpleProfileWithContactName] {
    func ranked(
        filterNegativeNames: Bool = true
    ) -> Self {
        ContactRanker.sorted(
            from: self,
            filterNegativeNames: filterNegativeNames
        )
    }
}

// MARK: - ContactRanker

enum ContactRanker {
    private enum Constants {
        static let hostileEmojis: Set<Character> = ["👿", "😡", "😠", "🤬", "🖕", "🤢", "🤮", "😾"]
        static let negativeTokens: Set<String> = [
            "spam", "scam", "junk", "blocked", "creep", "do", "not", "answer", "unknown",
        ]
    }

    enum Bucket {
        case emoji
        case sameLastName
        case kin
        case other

        var order: Int {
            switch self {
            case .emoji: return 0
            case .sameLastName: return 1
            case .kin: return 2
            case .other: return 3
            }
        }
    }

    static func sorted<T: ContactRankingDataProtocol>(
        from contacts: [T],
        currentUserLastName: String? = nil,
        locale: Locale = .current,
        filterNegativeNames: Bool = true
    ) -> [T] {
        let kinSemantic = KinSemantic(locale: locale)

        let cleansed = filterNegativeNames
            ? contacts.filter { !containsNegativeName($0) }
            : contacts

        return cleansed.sorted { lhs, rhs in
            // Bucket ordering
            let lBucket = bucket(for: lhs, currentUserLastName: currentUserLastName, kinSemantic: kinSemantic)
            let rBucket = bucket(for: rhs, currentUserLastName: currentUserLastName, kinSemantic: kinSemantic)
            if lBucket != rBucket {
                return lBucket.order < rBucket.order
            }

            // Prefer contacts with photos
            if lhs.imageDataAvailable != rhs.imageDataAvailable {
                return lhs.imageDataAvailable
            }

            // More emojis wins
            if lBucket == Bucket.emoji {
                let lEmoji = emojiCount(in: displayName(lhs))
                let rEmoji = emojiCount(in: displayName(rhs))
                if lEmoji != rEmoji {
                    return lEmoji > rEmoji
                }
            }

            // Alphabetically, kind of
            let result = displayName(lhs).localizedCaseInsensitiveCompare(displayName(rhs))
            return result == .orderedAscending
        }
    }

    private static func displayName<T: ContactRankingDataProtocol>(
        _ contact: T
    ) -> String {
        ([contact.givenName, contact.familyName].filter { !$0.isEmpty }).joined(separator: " ")
    }

    private static func emojiCount(
        in string: String
    ) -> Int {
        string.reduce(0) { $0 + ($1.isEmoji ? 1 : 0) }
    }

    private static func containsNegativeName<T: ContactRankingDataProtocol>(
        _ contact: T
    ) -> Bool {
        let name = displayName(contact)
        if name.contains(where: Constants.hostileEmojis.contains) {
            return true
        }

        return name
            .lowercased()
            .components(separatedBy: .whitespacesAndNewlines.union(.punctuationCharacters))
            .contains(where: Constants.negativeTokens.contains)
    }

    private static func bucket<T: ContactRankingDataProtocol>(
        for contact: T,
        currentUserLastName: String?,
        kinSemantic: KinSemantic
    ) -> Bucket {
        let name = displayName(contact)

        if emojiCount(in: name) > 0 {
            return .emoji
        }

        if let currentUserLastName,
           contact.familyName.compare(currentUserLastName, options: .caseInsensitive) == .orderedSame
        {
            return .sameLastName
        }

        if kinSemantic.containsKinWord(in: name) {
            return .kin
        }

        return .other
    }
}

// MARK: - KinSemantic

struct KinSemantic {
    private let staticKin: Set<String>

    init(
        locale: Locale
    ) {
        staticKin = Set(
            KinSemantic.kinSeedTerms(for: locale).map { $0.lowercased() }
        )
    }

    func containsKinWord(
        in name: String
    ) -> Bool {
        name
            .components(separatedBy: .whitespacesAndNewlines.union(.punctuationCharacters))
            .contains { staticKin.contains($0.lowercased()) }
    }

    private static func kinSeedTerms(
        for locale: Locale
    ) -> [String] {
        switch locale.language.languageCode?.identifier {
        case "es": return ["mamá", "papá", "hermano", "hermana", "abuela", "abuelo", "tío", "tía", "primo", "prima"]
        case "fr": return ["maman", "papa", "frère", "soeur", "tante", "oncle", "grand-mère", "grand-père", "cousin", "cousine"]
        case "de": return ["mama", "papa", "bruder", "schwester", "onkel", "tante", "oma", "opa", "cousin", "cousine"]
        case "it": return ["mamma", "papà", "fratello", "sorella", "zia", "zio", "nonna", "nonno", "cugino", "cugina"]
        case "pt": return ["mãe", "pai", "irmão", "irmã", "tia", "tio", "avó", "avô", "primo", "prima"]
        case "nl": return ["mama", "papa", "broer", "zus", "oom", "tante", "oma", "opa", "neef", "nicht"]
        case "ru": return ["мама", "папа", "брат", "сестра", "тётя", "дядя", "бабушка", "дедушка", "двоюродный", "сестра"]
        case "tr": return ["anne", "baba", "abi", "abla", "kardeş", "teyze", "amca", "hala", "dede", "nine", "kuzen"]
        case "ar": return ["أم", "أبي", "أب", "أخي", "أخت", "عم", "عمة", "خال", "خالة", "جدة", "جد"]
        case "ja": return ["ママ", "お母さん", "パパ", "お父さん", "兄", "姉", "妹", "弟", "おば", "おじ", "祖母", "祖父", "いとこ"]
        case "ko": return ["엄마", "아빠", "형", "누나", "오빠", "언니", "동생", "이모", "삼촌", "할머니", "할아버지", "사촌"]
        case "zh": return ["妈妈", "爸", "哥哥", "姐姐", "弟弟", "妹妹", "叔叔", "阿姨", "爷爷", "奶奶", "表哥", "表姐", "表弟", "表妹"]
        default: return ["mom", "mother", "dad", "father", "brother", "sister", "grandma", "grandpa", "aunt", "uncle", "cousin"]
        }
    }
}
