import ComposableArchitecture
import Contacts
import Foundation
import PhoneNumberKit

public enum ContactsAuthorizationStatus {
    case notDetermined
    case authorized
    case denied
}

@DependencyClient
public struct ContactsClient {
    public var getAuthorizationStatus: @Sendable () -> ContactsAuthorizationStatus = { .notDetermined }
    public var requestPermission: @Sendable () async -> Bool = { false }
    public var getPhoneContacts: @Sendable () async throws -> [PhoneContact]
    public var getContactPhoneNumbers: @Sendable () async throws -> [String]
}

public enum ContactsClientError: LocalizedError {
    case getPhoneContactsSetupFailed
    case getPhoneContactsFetchError(Error)

    public var errorDescription: String? {
        switch self {
        case .getPhoneContactsSetupFailed: "Exception while setting up phone contact fetch query"
        case .getPhoneContactsFetchError(let error): "Exception while fetching phone contacts: \(error.localizedDescription)"
        }
    }
}

extension ContactsClient: DependencyKey {
    public static var liveValue: ContactsClient = {
        let contactStore = CNContactStore()

        @Sendable
        func getAuthorizationStatus() -> ContactsAuthorizationStatus {
            let status = CNContactStore.authorizationStatus(for: .contacts)
            switch status {
            case .notDetermined:
                return .notDetermined
            case .authorized, .limited:
                return .authorized
            case .denied, .restricted:
                return .denied
            // .limited and future cases to be added
            @unknown default:
                return .denied
            }
        }

        @Sendable
        func getPhoneContacts() async throws -> [PhoneContact] {
            guard let keysToFetch = [
                CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
                CNContactPhoneNumbersKey,
                CNContactImageDataAvailableKey,
                CNContactThumbnailImageDataKey,
            ] as? [CNKeyDescriptor] else {
                throw ContactsClientError.getPhoneContactsSetupFailed
            }
            let fetchRequest = CNContactFetchRequest(keysToFetch: keysToFetch)
            fetchRequest.sortOrder = .userDefault
            let contactStoreId = contactStore.defaultContainerIdentifier()
            let task = Task {
                var result = [PhoneContact]()
                let phoneNumberKit = PhoneNumberUtility()
                do {
                    try contactStore.enumerateContacts(with: fetchRequest) { contact, _ in
                        let name: String? = CNContactFormatter.string(from: contact, style: .fullName)
                        // We use `.e164` as a standardized format of handling phone numbers internally
                        let phoneNumbers = contact.phoneNumbers.compactMap { wrappedNumber -> String? in
                            guard let parsed = try? phoneNumberKit.parse(wrappedNumber.value.stringValue) else { return nil }
                            return phoneNumberKit.format(parsed, toType: .e164)
                        }
                        if let name, !phoneNumbers.isEmpty {
                            result.append(
                                PhoneContact(
                                    id: contact.id.uuidString,
                                    fullName: name,
                                    phoneNumbers: phoneNumbers,
                                    thumbnailImageData: contact.thumbnailImageData
                                )
                            )
                        }
                    }
                } catch {
                    throw ContactsClientError.getPhoneContactsFetchError(error)
                }
                return result
            }
            return try await task.value
        }

        return Self(
            getAuthorizationStatus: {
                getAuthorizationStatus()
            },
            requestPermission: {
                guard getAuthorizationStatus() == .notDetermined else { return false }
                do {
                    return try await contactStore.requestAccess(for: .contacts)
                } catch {
                    // Apple's `requestAccess` returns `false` AND throws when premission is denied
                    // https://developer.apple.com/documentation/contacts/cncontactstore/requestaccess(for:completionhandler:)
                    // We consolidate those two into a falsey return
                    return false
                }
            },
            getPhoneContacts: {
                try await getPhoneContacts()
            },
            getContactPhoneNumbers: {
                try await getPhoneContacts().flatMap { $0.phoneNumbers }
            }
        )
    }()
}

public extension DependencyValues {
    var contactsClient: ContactsClient {
        get { self[ContactsClient.self] }
        set { self[ContactsClient.self] = newValue }
    }
}
