import APIClient
import Clerk
import ClerkClient
import ComponentLibrary
import ComposableArchitecture
import Foundation
import Localization
import PhoneNumberKit
import SwiftUI

@Reducer
public struct PhoneCodeEntry {
    static let separator = "-"
    static let codeLength = 6
    static let resendAfterSeconds = 30

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

        public let phoneNumber: String

        public var isWorking = false
        public var isValidating = false
        public var isFocused = true

        public var remainingSeconds = resendAfterSeconds
        public var showResendButton = false

        public var value: String = ""

        public enum Path: Equatable {
            case signIn
            case addNumber(Me, PhoneNumberEnvelope)
            case addNumberInPlatform(PhoneNumberEnvelope)
        }

        public var path: Path

        public init(phone: String, isWorking: Bool = false, value: String = "", path: Path) {
            self.phoneNumber = phone
            self.isWorking = isWorking
            self.value = value
            self.path = path
        }
    }

    public enum Action: BindableAction {
        @CasePathable
        @dynamicMemberLookup
        public enum Internal {
            case resendResponse(Result<Void, Error>)
            case validateResponse(Result<Void, Error>, authenticationInfo: AuthenticationInfo? = nil)
            case addNumberResponse(Result<Void, Error>)
        }

        @CasePathable
        @dynamicMemberLookup
        public enum Delegate {
            case onAppear
            case validationSucceeded(State.Path, authenticationInfo: AuthenticationInfo? = nil)
            case cancelTapped
        }

        @CasePathable
        public enum Alert: Equatable {
            case dismiss
        }

        case onAppear

        case startTimer
        case tick

        case resendTapped
        case validateCode
        case setFocus(Bool)
        case showResendButton(Bool)

        case binding(BindingAction<State>)
        case `internal`(Internal)
        case delegate(Delegate)
        case alert(PresentationAction<Alert>)
    }

    public init() {}

    @Dependency(APIClient.self) var apiClient
    @Dependency(ClerkClient.self) var clerk
    @Dependency(\.continuousClock) var clock

    private struct ResendTimerCancellableId: Hashable {}
    private struct SubmitCancellableId: Hashable {}

    public var body: some ReducerOf<Self> {
        BindingReducer()
            .onChange(of: \.value) { _, newValue in
                Reduce { _, _ in
                    guard newValue.getNumbers().count == PhoneCodeEntry.codeLength else { return .none }

                    return .concatenate([
                        .cancel(id: SubmitCancellableId()),
                        .run { send in
                            try await Task.sleep(for: .seconds(0.3))
                            await send(.validateCode, animation: .default)
                        }.cancellable(id: SubmitCancellableId()),
                    ])
                }
            }

        Reduce { state, action in
            switch action {
            case .onAppear:
                state.value = ""
                return .merge(
                    .send(.delegate(.onAppear)),
                    .send(.startTimer)
                )

            case .startTimer:
                return .run { send in
                    await withTaskCancellation(id: ResendTimerCancellableId(), cancelInFlight: true) {
                        for await _ in clock.timer(interval: .seconds(1)) {
                            await send(.tick)
                        }
                    }
                }

            case .tick:
                if state.remainingSeconds > 0 {
                    state.remainingSeconds -= 1
                } else {
                    state.showResendButton = true
                    return .cancel(id: ResendTimerCancellableId())
                }
                return .none

            case .resendTapped:
                state.isWorking = true
                return .run { [phoneNumber = state.phoneNumber, path = state.path] send in
                    switch path {
                    case .signIn:
                        await send(.internal(.resendResponse(Result(catching: { try await clerk.signInWithPhone(phoneNumber) }))))
                    case .addNumber(_, let phoneNumber), .addNumberInPlatform(let phoneNumber):
                        do {
                            let response = try await clerk.addPhoneNumberToAccount(phoneNumber.e164Formatted)
                            switch response {
                            case .oneTimeCodeSent:
                                await send(.internal(.resendResponse(.success(()))))
                            case .validated:
                                // If somehow the phone number is already validated before resend, we consider
                                // the phone code entry as complete
                                await send(.internal(.validateResponse(.success(()))))
                            }
                        } catch {
                            await send(.internal(.resendResponse(.failure(error))))
                        }
                    }
                }

            case .setFocus(let value):
                state.isFocused = value
                return .none

            case .internal(.resendResponse(let result)):
                state.isWorking = false

                switch result {
                case .success:
                    state.remainingSeconds = PhoneCodeEntry.resendAfterSeconds
                    state.showResendButton = false
                    return .send(.startTimer)

                case .failure(let error):
                    state.alert = .init(
                        title: { TextState(L10n.FeatureSocial.errorTitle) },
                        actions: {
                            ButtonState { TextState(L10n.FeatureSocial.errorButton) }
                        },
                        message: { TextState(error.underlyingError) }
                    )
                    return .none
                }

            case .validateCode:
                if !state.isValidating {
                    state.isValidating = true
                    return .run { [code = state.value.getNumbers(), path = state.path] send in
                        do {
                            let authInfo = try await self.submitCode(code: code, path: path)
                            await send(.internal(.validateResponse(.success(()), authenticationInfo: authInfo)))
                        } catch {
                            await send(.internal(.validateResponse(.failure(error))))
                        }
                    }
                } else {
                    return .none
                }

            case .internal(.validateResponse(let result, let authenticationInfo)):
                state.isValidating = false

                func complete() -> Effect<Action> {
                    switch state.path {
                    case .signIn, .addNumberInPlatform:
                        return .send(.delegate(.validationSucceeded(state.path, authenticationInfo: authenticationInfo)))
                    case .addNumber:
                        return .run { [phoneNumber = state.phoneNumber] send in
                            await send(.internal(.addNumberResponse(Result(catching: { try await apiClient.updatePhoneNumber(phoneNumber: phoneNumber) }))))
                        }
                    }
                }

                switch result {
                case .success:
                    return complete()

                case .failure(let error as ClerkAPIError) where error.code == "verification_already_verified":
                    return complete()

                case .failure(let error):
                    state.alert = .init(
                        title: { TextState(L10n.FeatureSocial.errorTitle) },
                        actions: {
                            ButtonState { TextState(L10n.FeatureSocial.errorButton) }
                        },
                        message: { TextState(error.underlyingError) }
                    )
                    return .none
                }

            case .internal(.addNumberResponse(let result)):
                switch result {
                case .success:
                    return .send(.delegate(.validationSucceeded(state.path)))
                case .failure(let error):
                    state.alert = .init(
                        title: { TextState(L10n.FeatureSocial.errorTitle) },
                        actions: {
                            ButtonState { TextState(L10n.FeatureSocial.errorButton) }
                        },
                        message: { TextState(error.underlyingError) }
                    )
                    return .none
                }

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

            case .binding:
                // catch-all
                return .none

            case .delegate:
                // Handled upstream
                return .none

            case .showResendButton(let show):
                state.showResendButton = show
                return .none
            }
        }
        .ifLet(\.$alert, action: \.alert)

        Analytics()
    }

    private func submitCode(code: String, path: State.Path) async throws -> AuthenticationInfo? {
        switch path {
        case .signIn:
            let authInfo = try await clerk.validatePhoneCode(code)
            return authInfo

        case .addNumber(_, let phoneNumber), .addNumberInPlatform(let phoneNumber):
            _ = try await clerk.verifyAddPhoneNumberToAccount(code: code, phoneNumber: phoneNumber.clerk)
            return nil
        }
    }
}

public struct PhoneCodeEntryScreen: View {
    @Bindable var store: StoreOf<PhoneCodeEntry>
    @FocusState private var focusedState: Bool

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

    public var body: some View {
        VStack {
            Text(L10n.FeatureSocial.sentTo(store.phoneNumber))
                .typographyV1(.body1)
                .foregroundStyle(Color.SemanticV1.textSecondary)
                .lineLimit(2, reservesSpace: true)

            Spacer()

            TextField("XXX\(PhoneCodeEntry.separator)XXX", text: $store.value)
                .typographyV1(.headline2.neueMontrealMedium())
                .textContentType(.oneTimeCode)
                .foregroundStyle(Color.SemanticV1.textPrimary)
                .multilineTextAlignment(.center)
                .keyboardType(.numberPad)
                .lineLimit(1)
                .focused($focusedState)
                .onChange(of: store.value) { _, value in
                    // If the last character is a separator and we're deleting, remove the separator
                    if value.hasSuffix(PhoneCodeEntry.separator) {
                        store.value = String(value.dropLast())
                    } else {
                        store.value = value
                            .prefix(PhoneCodeEntry.codeLength + 1)
                            .replacingOccurrences(of: PhoneCodeEntry.separator, with: "")
                            .separating(every: 3, separator: PhoneCodeEntry.separator)
                    }
                }

            Spacer()

            if store.isValidating {
                HStack(spacing: 8) {
                    ProgressView()
                        .controlSize(.mini)
                    Text(L10n.FeatureSocial.validating)
                        .typographyV1(.monospace)
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                }
            } else {
                if store.showResendButton {
                    resendButton
                } else {
                    Text(L10n.FeatureSocial.resendIn(store.remainingSeconds).uppercased())
                        .typographyV1(.monospace)
                        .foregroundStyle(Color.SemanticV1.textSecondary)
                }
            }
        }
        .padding([.leading, .trailing, .bottom], 24)
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .principal) {
                Text(L10n.FeatureSocial.enterCode)
                    .typographyV1(.headline4)
                    .foregroundStyle(Color.SemanticV1.textPrimary)
            }
        }
        .customBackButton(background: Material.ultraThin) {
            store.send(.delegate(.cancelTapped))
        }
        .bind($store.isFocused.sending(\.setFocus), to: $focusedState)
        .alert($store.scope(state: \.alert, action: \.alert))
        .onAppear { store.send(.onAppear) }
    }

    private var resendButton: some View {
        Button {
            UIImpactFeedbackGenerator(style: .light).impactOccurred()
            store.send(.resendTapped)
        } label: {
            if store.isWorking {
                ProgressView()
                    .controlSize(.mini)
            } else {
                Text(L10n.FeatureSocial.resend)
                    .typographyV1(.body1)
                    .foregroundStyle(Color.SemanticV1.textInvert)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background {
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.SemanticV1.backgroundInvert)
                    }
            }
        }
    }
}

private extension String {
    func separating(every groupSize: Int, separator: String) -> String {
        guard let separatorIndex = index(startIndex, offsetBy: groupSize, limitedBy: endIndex) else {
            return self // The string is too short so no separators are necessary
        }
        return String(self[..<separatorIndex] + Substring(separator) + self[separatorIndex...])
    }

    func getNumbers() -> String {
        components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
    }
}
