import APIClient
import AttributionAndDeeplinkingClient
import ComposableArchitecture
import ContactsClient
import FeatureOnboardingModels
import FeatureOnboardingScreens
import FeatureSocial
import Foundation
import Localization
import StatsigClient
import SwiftUI
import UserNotificationsClient
import Utilities

// swiftlint:disable file_length

@Reducer
public struct OnboardingCoordinator {
    @Reducer(state: .equatable)
    public enum Path {
        case signUp(SignUp)
        case notificationQuestion(OnboardingNotificationsQuestion)
        case nameEntry(NameEntry)
        case usernameEntry(UsernameEntry)
        case birthday(BirthdayEntry)
        case phoneSignIn(PhoneSignIn)
        case phoneCodeEntry(PhoneCodeEntry)
        case contactsAuthorization(ContactsAuthorization)
        case contactSync(ContactSync)
    }

    @ObservableState
    public struct State: Equatable {
        @Presents public var alert: AlertState<Action.Alert>?

        public var initialMethod: SignUp.Method
        public var signUp: SignUp.State
        public var path: StackState<Path.State> = .init()

        // Used to store and overwrite the Full Name if it comes back from
        // ClerkClient's signInWithApple method. This makes sure
        // we don't show the .nameEntry screen for new users if we have it already.
        public var fullNameFromClerk: String?

        @Shared(.appStorage(.contactSyncSeen)) var contactSyncSeen: Bool = false

        public mutating func setupFirstStep(me: Me) {
            @Dependency(StatsigClient.self) var statsigClient
            @Dependency(\.telemetryClient) var telemetry

            statsigClient.trackEvent(.onboardingStarted(stableID: statsigClient.getStableID(), sunoUserID: me.user.id))
            self.path = .init([.nameEntry(.init(me: me, initialValue: me.user.displayName, errorRepresentation: .systemAlert))])
        }

        public init(initialMethod: SignUp.Method, error: Error?) {
            self.initialMethod = initialMethod
            self.signUp = SignUp.State(method: initialMethod)
            self.alert = error.map { error in
                // TODO: Create a way to make these more meaningful
                AlertState(
                    title: { TextState(L10n.FeatureOnboarding.errorTitle) },
                    actions: {
                        ButtonState { TextState(L10n.FeatureOnboarding.errorButton) }
                    },
                    message: { TextState(error.underlyingError) }
                )
            }
        }
    }

    public enum Action {
        @CasePathable
        @dynamicMemberLookup
        public enum Internal {
            case finishSignIn(Me)
            case goToContactSync(Me)
        }

        @CasePathable
        @dynamicMemberLookup
        public enum Delegate {
            case registerForRemoteNotifications
            case signUpResult(Result<Me, Error>)
            case signOut
            case backTappedOnSignUp
        }

        @CasePathable
        public enum Alert: Equatable {
            case dismiss
        }

        public enum Analytics {
            case onboardingFlowStarted
        }

        case signUp(SignUp.Action)
        case path(StackAction<Path.State, Path.Action>)
        case analytics(Analytics)
        case completedNotificationRegistration(_ granted: Bool)
        case hasAlreadyCompletedNotificationRegistration
        case delegate(Delegate)
        case `internal`(Internal)
        case alert(PresentationAction<Alert>)
        case getMe(isFirstTimeUser: Bool)
        case getMeResponse(Result<Me, Error>)
    }

    @Dependency(\.apiClientV2) var api
    @Dependency(StatsigClient.self) var statsigClient
    @Dependency(ContactsClient.self) var contactsClient
    @Dependency(\.telemetryClient) var telemetry
    @Dependency(AttributionAndDeeplinkingClient.self) var attributionAndDeeplinkingClient

    public init() {}

    private let analyticsReducer = Analytics()

    public var body: some ReducerOf<Self> {
        Scope(state: \.signUp, action: \.signUp) {
            SignUp()
        }

        // Current screen order:
        //  - Social > Name > Username > [Complete]
        // or
        //  - Phone > Code > Name > Username
        Reduce { state, action in
            switch action {
            case .getMeResponse(.success(let me)):
                var updatedMe: Me = me
                // Set any full name properties we get from sign-in providers
                // for free (like Full Name from Sign in With Apple)
                if let displayName = state.fullNameFromClerk {
                    updatedMe.user.displayName = displayName
                }

                // Show contact sync reonboarding if needed
                if shouldShowContactSyncOnboarding(for: updatedMe, contactSyncSeen: state.contactSyncSeen) {
                    state.$contactSyncSeen.withLock { $0 = true }
                    return .send(.internal(.goToContactSync(updatedMe)))
                }
                return .send(.internal(.finishSignIn(updatedMe)))

            case .getMeResponse(.failure(let error)):
                return .send(.delegate(.signUpResult(.failure(error))))

            case .internal(.finishSignIn(let me)):
                /// Let the AppCoordinator decide how to proceed
                /// This allows us to block on `userSessionChanged`, so we can setup analytics and other SDKs with the newly signed in user's `user_id`
                return .send(.delegate(.signUpResult(.success(me))))

            case .signUp(.delegate(let delegateAction)):
                return handleSignUpDelegateAction(delegateAction, state: &state)

            case .signUp(.signIn(.phone)):
                state.path.append(.phoneSignIn(.init()))
                return .none

            case .path(.element(_, .signUp(.delegate(let delegateAction)))):
                return handleSignUpDelegateAction(delegateAction, state: &state)

            case .path(.element(_, .signUp(.signIn(.phone)))):
                state.path.append(.phoneSignIn(.init()))
                return .none

            case .path(.element(_, .nameEntry(.delegate(.updateNameComplete(let me))))):
                state.path.append(.usernameEntry(.init(me: me)))
                return .none

            case .path(.element(_, .usernameEntry(.delegate(.updateUsernameComplete(let me))))):
                if FeatureFlag.onboarding.showBirthdayScreen {
                    state.path.append(.birthday(.init(me: me)))
                    return .none
                } else {
                    /* Last Step */
                    return .send(.delegate(.signUpResult(.success(me))))
                }

            case .path(.element(_, .birthday(.delegate(.updateBirthdayComplete(let me))))):
                /* Last Step */
                return .send(.delegate(.signUpResult(.success(me))))

            case .path(.element(_, .usernameEntry(.delegate(.backTapped)))), // Back on username screen
                 .path(.element(_, .phoneSignIn(.delegate(.cancelTapped)))), // Back on phone number screen
                 .path(.element(_, .phoneCodeEntry(.delegate(.cancelTapped)))), // Back on phone code screen
                 .path(.element(_, .birthday(.delegate(.backTapped)))):
                return handleRouteBackToAuth(state: &state)

            case .completedNotificationRegistration,
                 .hasAlreadyCompletedNotificationRegistration:
                guard case .notificationQuestion(let store) = state.path.last else { return .none }
                state.path.append(.nameEntry(.init(me: store.me, errorRepresentation: .systemAlert)))
                return .none

            case .path(.element(_, .nameEntry(.delegate(.backTapped)))):
                return .send(.delegate(.signOut))

            case .path(.element(_, .phoneCodeEntry(.delegate(.validationSucceeded(.signIn, authenticationInfo: let info))))):
                // Phone sign up completed, try to load user
                return .send(.getMe(isFirstTimeUser: info?.isFirstTimeUser ?? false))

            case .internal(.goToContactSync(let me)):
                // Edge case: contacts permissioning could already be authorized from outside the app
                guard contactsClient.getAuthorizationStatus() != .authorized else {
                    state.path.append(.contactSync(.init(me: me)))
                    return .none
                }
                state.path.append(.contactsAuthorization(.init(me: me)))
                return .none

            case let .path(.element(_, .phoneSignIn(.delegate(.phoneCodeSent(phone))))):
                state.path.append(.phoneCodeEntry(PhoneCodeEntry.State(phone: phone, path: .signIn)))
                return .none

            case .path(.element(_, .contactsAuthorization(.delegate(.authorized(let me))))):
                state.path.append(.contactSync(.init(me: me)))
                return .none

            case .path(.element(_, .contactsAuthorization(.delegate(.denied(let me))))),
                 .path(.element(_, .contactSync(.delegate(.doneButtonTapped(let me))))):
                return .send(.internal(.finishSignIn(me)))

            case .alert(.dismiss), .alert(.presented(.dismiss)):
                state.alert = nil
                return .none

            case .signUp, .path:
                return .none

            case .getMe:
                // JWT auth was successful, now we need a `Me`
                return .run { send in
                    do {
                        let me = try await api.getMe()
                        // Try to set up statsig with Me
                        // This is best placed here as it's important that we run it in the same async context as `getMe`
                        // as we want to make sure we try to finish this before returning `.success`
                        do {
                            try await statsigClient.setUser(.authenticated(
                                userId: me.user.id,
                                email: me.user.email,
                                custom: me.statsigCustomProperties?.custom,
                                customIDs: me.statsigCustomProperties?.customIDs
                            ))
                        } catch {
                            // Unsuccessful Statsig is still a successful signin
                            log.telemetry.error(error)
                        }
                        await send(.getMeResponse(.success(me)))
                    } catch {
                        await send(.getMeResponse(.failure(error)))
                    }
                }

            case .delegate, .analytics:
                // Handled upstream
                return .none
            }
        }
        .forEach(\.path, action: \.path)
        .ifLet(\.$alert, action: \.alert)

        analyticsReducer
    }

    private func shouldShowContactSyncOnboarding(for _: Me, contactSyncSeen: Bool) -> Bool {
        // If we have and existing number we don't need to add a new one /
        // go down the validation pipeline
        let contactSyncEnabled = FeatureFlag.legacy.contactSync
        return contactSyncEnabled && !contactSyncSeenWithOverride(contactSyncSeen: contactSyncSeen)
    }

    private func contactSyncSeenWithOverride(contactSyncSeen: Bool) -> Bool {
        // Bypass `contactSyncSeen` during dogfooding
        if FeatureFlag.legacy.dogfoodingOverride {
            return false
        }
        return contactSyncSeen
    }
}

private extension OnboardingCoordinator {
    func handleSignUpDelegateAction(
        _ delegateAction: SignUp.Action.Delegate,
        state: inout State
    ) -> Effect<Action> {
        switch delegateAction {
        case .flowCompleted(let result):
            switch result {
            case .authenticated(let isFirstTimeUser):
                // TODO: - use isFirstTimeUser in onboarding revamp (IOS-1194)
                // https://linear.app/sunomusic/issue/IOS-1194/onboarding-groundwork-need-a-coordination-system-and-way-to-clean-up
                return .send(.getMe(isFirstTimeUser: isFirstTimeUser))

            case .authenticatedWithFullName(let isFirstTimeUser, let fullName):
                // TODO: - use isFirstTimeUser in onboarding revamp (IOS-1194)
                // https://linear.app/sunomusic/issue/IOS-1194/onboarding-groundwork-need-a-coordination-system-and-way-to-clean-up
                state.fullNameFromClerk = fullName
                return .send(.getMe(isFirstTimeUser: isFirstTimeUser))

            case .cancelled:
                return handleRouteBackToAuth(state: &state)

            case .phoneSignInSelected:
                state.path.append(.phoneSignIn(PhoneSignIn.State()))
                return .none
            }

        case .authenticationError:
            // TODO: - show error alert to user
            return .none

        case .onAppear:
            return .none

        case .authMethodSelected:
            return .none
        }
    }

    func handleRouteBackToAuth(
        state: inout State
    ) -> Effect<Action> {
        UIImpactFeedbackGenerator(style: .light).impactOccurred()
        let pathCount = state.path.count

        if pathCount == 0 {
            return .send(.delegate(.backTappedOnSignUp))
        } else if pathCount == 1 {
            return .send(.delegate(.signOut))
        } else if case .nameEntry = state.path.last {
            /*
                Should always return to signed out on having reach .nameEntryCase
                Avoiding ability to navigate back to things:
                - Simple Song create
                - Notification allow disallow
             */
            return .send(.delegate(.signOut))
        } else if pathCount > 0 {
            state.path.removeLast()
        } else {
            state.path = .init()
        }
        return .none
    }
}

public struct OnboardingCoordinatorScreen: View {
    @Bindable var store: StoreOf<OnboardingCoordinator>

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

    public var body: some View {
        NavigationStack(
            path: $store.scope(state: \.path, action: \.path),
            root: {
                SignUpScreen(store: store.scope(state: \.signUp, action: \.signUp))
            },
            destination: { path in
                switch path.case {
                case .signUp(let store):
                    SignUpScreen(store: store)
                case .nameEntry(let store):
                    NameEntryScreen(store: store)
                case .usernameEntry(let store):
                    UsernameEntryScreen(store: store)
                case .notificationQuestion(let store):
                    OnboardingNotificationsQuestionScreen(store: store)
                case .birthday(let store):
                    BirthdayEntryScreen(store: store)
                case .phoneSignIn(let store):
                    PhoneSignInScreen(store: store)
                case .phoneCodeEntry(let store):
                    PhoneCodeEntryScreen(store: store)
                case .contactsAuthorization(let store):
                    ContactsAuthorizationScreen(store: store)
                case .contactSync(let store):
                    ContactSyncScreen(store: store)
                }
            }
        )
        .alert($store.scope(state: \.alert, action: \.alert))
    }
}

extension OnboardingCoordinator.Action.Delegate: Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.registerForRemoteNotifications, .registerForRemoteNotifications):
            return true

        case let (.signUpResult(lhsResult), .signUpResult(rhsResult)):
            switch (lhsResult, rhsResult) {
            case let (.success(lhsMe), .success(rhsMe)):
                return lhsMe == rhsMe
            case let (.failure(lhsError), .failure(rhsError)):
                // Compare errors by their localized description
                return lhsError.localizedDescription == rhsError.localizedDescription
            default:
                return false
            }

        case (.signOut, .signOut):
            return true

        default:
            return false
        }
    }
}
