import APIClient
import ComponentLibrary
import ComposableArchitecture
import ContactsClient
import FeatureShare
import Localization
import MessageUI
import SwiftUI
import Utilities

@Reducer
public struct FromYourContacts {
    @Reducer(state: .equatable)
    public enum Destination {
        @Reducer public struct SMSInvite {
            @ObservableState
            public struct State: Equatable {
                let phoneNumber: String
            }
        }

        case smsInviteSheet(SMSInvite)
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var destination: Destination.State?
        var phoneContacts: IdentifiedArrayOf<PhoneContact> = []
        var sunoContacts: IdentifiedArrayOf<SimpleProfileWithContactName> = []
        var followRequestsInFlight: IdentifiedArrayOf<SimpleProfileWithContactName> = []
        var loadState: LoadState = .loading
        var me: Me

        public init(me: Me) {
            self.me = me
        }
    }

    public enum Action {
        public enum Internal {
            // Result primarily used for logging
            case messageComposeResult(MessageComposeResult)
            case fetchPhoneContactsResult(Result<[PhoneContact], Error>)
            case fetchSunoContactsResult(Result<[SimpleProfileWithPhoneNumber], Error>)
            case processAllContactsResult(IdentifiedArrayOf<PhoneContact>, IdentifiedArrayOf<SimpleProfileWithContactName>)
            case followResult(SimpleProfileWithContactName, Result<Void, Error>)
        }

        case onAppear
        case inviteTapped(PhoneContact)
        case followTapped(SimpleProfileWithContactName)
        case dismiss
        case destination(PresentationAction<Destination.Action>)
        case `internal`(Internal)
    }

    public init() {}

    @Dependency(\.apiClientV2) var api
    @Dependency(ContactsClient.self) var contactsClient
    @Dependency(APIClient.self) var apiClient
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(\.dismiss) var dismiss

    public var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .onAppear:
                state.loadState = .loading
                return .run { send in
                    await send(.internal(.fetchPhoneContactsResult(Result(catching: { try await contactsClient.getPhoneContacts() }))))
                }

            case .internal(.fetchPhoneContactsResult(.success(let contacts))):
                state.phoneContacts = .init(uniqueElements: contacts)
                let phoneNumbers = contacts.flatMap { $0.phoneNumbers }
                return .run { send in
                    await send(.internal(.fetchSunoContactsResult(Result(catching: { try await api.findExistingUsersFromPhoneNumbers(phoneNumbers) }))))
                }

            case .internal(.fetchSunoContactsResult(.success(let profilesWithPhoneNumber))):
                return .run { [phoneContacts = state.phoneContacts, profilesWithPhoneNumber] send in
                    var phoneContacts = phoneContacts
                    // Create a phone contact lookup from Phone Number -> PhoneContact
                    // Each PhoneContact can have multiple phone numbers
                    var phoneContactLookup: [String: PhoneContact] = [:]
                    for phoneContact in phoneContacts {
                        for phoneNumber in phoneContact.phoneNumbers {
                            // May have duplicates but we're not too concerned about that,
                            // we're okay with overwriting
                            phoneContactLookup[phoneNumber] = phoneContact
                        }
                    }
                    var sunoContacts: [SimpleProfileWithContactName] = []
                    // Used for error logging
                    var mismatchedPhoneNumbers: [String] = []
                    // Adds the full name from our phone contact to the contact we receive from the backend
                    // We're doing this to avoid sending the full names up to the backend, as it's
                    // only used for this screen
                    for profile in profilesWithPhoneNumber {
                        // Shouldn't be receiving phone numbers that aren't in contacts
                        guard let phoneContact = phoneContactLookup[profile.phoneNumber] else {
                            mismatchedPhoneNumbers.append(profile.phoneNumber)
                            continue
                        }
                        // SimpleProfile + fullname
                        sunoContacts.append(SimpleProfileWithContactName(fullName: phoneContact.fullName, user: profile.user))
                        // Remove any found Suno users from the phone contact list
                        phoneContacts.remove(phoneContact)
                    }
                    // Log any mismatches
                    if !mismatchedPhoneNumbers.isEmpty {
                        log.telemetry.error(FromYourContactsError.sunoContactPhoneNumberMatchingError(mismatchedPhoneNumbers))
                    }

                    let rankedPhoneContacts = phoneContacts.elements.ranked()
                    let rankedSunoContacts = sunoContacts.ranked(filterNegativeNames: false)

                    await send(.internal(.processAllContactsResult(
                        .init(uniqueElements: rankedPhoneContacts),
                        .init(uniqueElements: rankedSunoContacts)
                    )))
                }

            case .internal(.processAllContactsResult(let phoneContacts, let sunoContacts)):
                state.phoneContacts = phoneContacts
                state.sunoContacts = sunoContacts
                // If we didn't find any contacts, show empty screen
                guard !(state.sunoContacts.isEmpty && state.phoneContacts.isEmpty) else {
                    state.loadState = .failed(L10n.FeatureSocial.fromYourContactsEmptyTitle)
                    return .none
                }
                state.loadState = .loaded
                return .none

            case .internal(.messageComposeResult):
                state.destination = nil
                return .none

            case .internal(.fetchPhoneContactsResult(.failure(let error))),
                 .internal(.fetchSunoContactsResult(.failure(let error))):
                state.loadState = .failed(error.underlyingError)
                log.telemetry.error(error)
                return .none

            case .internal(.followResult(let profile, .success)):
                state.followRequestsInFlight.remove(profile)
                var mutableProfile = profile
                mutableProfile.user.isFollowing.toggle()
                state.sunoContacts[id: mutableProfile.id] = mutableProfile
                return .none

            case let .internal(.followResult(profile, .failure(error))):
                state.followRequestsInFlight.remove(profile)
                log.telemetry.error(error)
                return .none

            case .inviteTapped(let contact):
                // If we can't send an SMS, we'll get a runtime error while trying to create `MFMessageComposeViewController`
                guard SMSComposeView.canSendText() else {
                    return .none
                }
                guard let phoneNumber = contact.phoneNumbers.first else { return .none }
                state.destination = .smsInviteSheet(.init(phoneNumber: phoneNumber))
                return .none

            case .followTapped(let profile):
                state.followRequestsInFlight.append(profile)
                return .run { [handle = profile.user.handle, isFollowing = profile.user.isFollowing] send in
                    await send(.internal(.followResult(profile, Result(catching: { try await apiClient.profileFollow(handle, isFollowing) }))))
                }

            case .dismiss:
                return .run { _ in await self.dismiss() }

            case .destination:
                return .none

            case .internal:
                return .none
            }
        }

        Analytics()
    }
}

public struct FromYourContactsScreen: View {
    @Bindable var store: StoreOf<FromYourContacts>

    public init(store: StoreOf<FromYourContacts>) {
        self.store = store
    }

    public var body: some View {
        Group {
            switch store.loadState {
            case .loading:
                loadingView()
            case .loaded:
                loadedView()
            case .failed(let message):
                FailedView(title: L10n.FeatureSocial.errorTitle, message: message, buttonTitle: L10n.FeatureSocial.retry, action: { store.send(.onAppear) })
            }
        }
        .onAppear {
            store.send(.onAppear)
        }
        .sheet(item: $store.scope(state: \.destination?.smsInviteSheet, action: \.destination.smsInviteSheet)) { smsInvite in
            let smsComposeView = try? SMSComposeView(
                recipient: smsInvite.state.phoneNumber,
                messageBody: L10n.FeatureSocial.inviteFriendsSmsMessageBody(store.me.user.shareURL),
                withCompletionHandler: { result in
                    store.send(.internal(.messageComposeResult(result)))
                }
            )

            if let smsComposeView {
                smsComposeView
                    .ignoresSafeArea()
            }
        }
        .contentMargins(.horizontal, 16, for: .scrollContent)
        .navigationTitle(L10n.FeatureSocial.fromYourContactsTitle)
        .navigationBarTitleDisplayMode(.inline)
        .background(Color.SemanticV1.backgroundPrimary)
    }

    @ViewBuilder
    private func loadedView() -> some View {
        List {
            Group {
                if !store.sunoContacts.isEmpty {
                    Section(header: header(title: L10n.FeatureSocial.contactsOnSuno, subtitle: nil)) {
                        ForEach(store.sunoContacts) { sunoContact in
                            VStack(spacing: 0) {
                                sunoContactRow(sunoContact)
                                divider
                            }
                        }
                    }
                    .listSectionSeparator(.hidden)
                }

                if !store.phoneContacts.isEmpty {
                    Section(header: header(title: L10n.FeatureSocial.inviteContacts, subtitle: L10n.FeatureSocial.sendAnSms)) {
                        ForEach(store.phoneContacts) { phoneContact in
                            VStack(spacing: 0) {
                                phoneContactRow(phoneContact)
                                divider
                            }
                        }
                    }
                }
            }
            .listRowInsets(.init())
            .listRowBackground(Color.clear)
            .listRowSeparator(.hidden)
            .alignmentGuide(.listRowSeparatorLeading) { d in
                d[.leading]
            }
            .listSectionSeparator(.hidden)
        }
        .listStyle(.grouped)
        .listRowSpacing(0)
        .contentMargins(.horizontal, 12, for: .scrollContent)
    }

    @ViewBuilder
    private func loadingView() -> some View {
        List {
            Section(header: header(title: L10n.FeatureSocial.findingContacts, subtitle: nil)) {
                // Temp - Replace with skeleton loading
                ProgressView()
                    .progressViewStyle(.circular)
            }
            .listRowInsets(.init())
            .listRowBackground(Color.clear)
            .listSectionSeparator(.hidden)
            .alignmentGuide(.listRowSeparatorLeading) { d in
                d[.leading]
            }
            .listRowSeparator(.hidden)
        }
        .listStyle(.grouped)
        .contentMargins(.horizontal, 12, for: .scrollContent)
    }

    private func header(title: String, subtitle: String? = nil) -> some View {
        return VStack(alignment: .leading, spacing: 0) {
            Text(title)
                .typographyV1(.headline4)
                .foregroundStyle(Color.SemanticV1.textPrimary)

            if let subtitle {
                Text(subtitle)
                    .typographyV1(.subtitleSmall)
                    .foregroundStyle(Color.SemanticV1.textSecondary)
                    .padding(.top, 4)
            }
        }
        .padding(.top, 16)
        .padding(.bottom, 4)
        .textCase(nil)
    }

    private func sunoContactRow(_ profile: SimpleProfileWithContactName) -> some View {
        return AddUserRow(
            displayName: profile.user.displayName,
            subtitle: profile.fullName,
            avatarView: RemoteImage(url: profile.user.avatarImageUrl, fallbackId: profile.id),
            buttonTitle: profile.user.isFollowing ? L10n.FeatureSocial.following : L10n.FeatureSocial.follow,
            isLoading: store.followRequestsInFlight.contains(profile),
            action: { store.send(.followTapped(profile)) }
        )
    }

    private func phoneContactRow(_ contact: PhoneContact) -> some View {
        var uiImage: UIImage = defaultImage(for: contact.id)
        if let thumbnailImageData = contact.thumbnailImageData, let thumbnailImage = UIImage(data: thumbnailImageData) {
            uiImage = thumbnailImage
        }

        return AddUserRow(
            displayName: contact.fullName,
            subtitle: nil,
            avatarView: Image(uiImage: uiImage).resizable(),
            buttonTitle: L10n.FeatureSocial.invite,
            isLoading: false,
            action: { store.send(.inviteTapped(contact)) }
        )
    }

    private var divider: some View {
        Divider()
            .overlay(Color.SemanticV1.borderPrimary)
            .padding(.horizontal, -12) // Negative padding to fill width of screen
    }
}

// Needs to be public for reducer actions
public struct SimpleProfileWithContactName: Identifiable, Equatable {
    let fullName: String
    var user: SimpleProfile
    public var id: String {
        user.id
    }
}

private enum FromYourContactsError: LocalizedError {
    case sunoContactPhoneNumberMatchingError([String])

    var errorDescription: String? {
        switch self {
        case .sunoContactPhoneNumberMatchingError(let phoneNumbers): "Received Suno profiles with phone numbers that don't exist in contacts: \(phoneNumbers)"
        }
    }
}
