import APIClient
import ComponentLibrary
import ComposableArchitecture
import Localization

private struct UsernameAvailabilityDebounceId: Hashable {}
private struct UsernameAvailabilityAPICancellableId: Hashable {}

@Reducer
public struct UsernameEntry {
    private static let availabilityCacheSize = 10

    @ObservableState
    public struct State: Equatable {
        public var isWorking = false
        public var isFocused = false

        public var value = ""
        public var me: Me
        public var errorMessage: String?

        public var isCheckingAvailability = false
        public var isPendingValidation = false
        public var lastCheckedUsername: String?
        public var isUsernameAvailable: Bool?
        public var wasLastCheckSuccessful = false

        public var isCheckingOrPending: Bool {
            isPendingValidation || isCheckingAvailability
        }

        fileprivate var availabilityCache: [String: Bool] = [:]

        public init(
            me: Me
        ) {
            self.me = me
            self.errorMessage = nil

            // If they set a name for themselves, use that with 3 random characters after that as the default value
            self.value = me.user.displayName.map {
                "\($0.replacingOccurrences(of: " ", with: ""))\(Int.random(in: 100 ..< 1000))"
            } ?? ""
        }
    }

    public enum Action: BindableAction {
        public enum Internal {
            case updateUsernameResult(Result<Me, Error>)
            case validateUsername(String)
            case checkUsernameAvailability(String)
            case usernameAvailabilityResult(Result<Bool, Error>)
        }

        @CasePathable
        @dynamicMemberLookup
        public enum Delegate {
            case onAppear
            case updateUsernameComplete(Me)
            case updateUsernameFailed(Error)
            case backTapped
            case usernameFieldFocused
        }

        case onAppear
        case updateUsernameTapped
        case setFocus(Bool)
        case clearText

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

    @Dependency(APIClient.self) var apiClient
    @Dependency(APIClientV2.self) var api

    public init() {}

    public var body: some ReducerOf<Self> {
        BindingReducer()

        Reduce<State, Action> { state, action in
            switch action {
            case .onAppear:
                return .send(.delegate(.onAppear))

            case .updateUsernameTapped:
                var handle = state.value.removingPrefix("@")
                if handle.isEmpty {
                    handle = state.me.user.handle.removingPrefix("@")
                }

                if let error = handle.usernameContainsProblemChars {
                    state.errorMessage = error
                    state.isFocused = true
                    return .none
                }

                state.isWorking = true
                state.isFocused = false
                return .run(
                    operation: { [user = state.me.user, handle] send in
                        try await apiClient.updateMe(.init(displayName: user.displayName ?? "", handle: handle))
                        let newMe = try await api.getMe()
                        await send(.internal(.updateUsernameResult(.success(newMe))))
                    },
                    catch: { error, send in
                        await send(.internal(.updateUsernameResult(.failure(error))))
                    }
                )

            case let .setFocus(value):
                state.isFocused = value
                if value {
                    // Field was focused, send delegate action
                    return .send(.delegate(.usernameFieldFocused))
                }
                return .none

            case .clearText:
                state.value = ""
                return .merge(
                    .cancel(id: UsernameAvailabilityDebounceId()),
                    .cancel(id: UsernameAvailabilityAPICancellableId())
                )

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

                switch result {
                case let .success(me):
                    return .send(.delegate(.updateUsernameComplete(me)))

                case let .failure(error):
                    state.errorMessage = error.underlyingError
                    state.isFocused = true
                    return .send(.delegate(.updateUsernameFailed(error)))
                }

            case let .internal(.validateUsername(username)):
                let trimmedUsername = username.removingPrefix("@")

                // We're now actually validating, clear pending validation
                state.isPendingValidation = false

                // Reset states
                state.errorMessage = nil
                state.isUsernameAvailable = nil

                // Skip if empty
                guard !trimmedUsername.isEmpty else {
                    state.isCheckingAvailability = false
                    state.isUsernameAvailable = false
                    return .none
                }

                // Client-side validation
                if let validationError = trimmedUsername.usernameContainsProblemChars {
                    state.errorMessage = validationError
                    state.isCheckingAvailability = false
                    state.isPendingValidation = false
                    state.isUsernameAvailable = false
                    return .none
                }

                // Passed validation, check availability
                return .send(.internal(.checkUsernameAvailability(trimmedUsername)))

            case let .internal(.checkUsernameAvailability(username)):
                // Skip API call for usernames that are too short
                guard username.count >= 3 else {
                    state.isCheckingAvailability = false
                    state.isPendingValidation = false
                    state.isUsernameAvailable = false
                    state.errorMessage = L10n.FeatureOnboarding.usernameErrorTooShort
                    return .none
                }

                // Check cache first
                if let cachedAvailability = state.availabilityCache[username] {
                    state.isCheckingAvailability = false
                    state.isPendingValidation = false
                    state.isUsernameAvailable = cachedAvailability
                    state.wasLastCheckSuccessful = cachedAvailability
                    state.lastCheckedUsername = username

                    if cachedAvailability {
                        state.errorMessage = nil
                        return .none
                    } else {
                        state.errorMessage = L10n.APIClient.usernameTaken
                        return .none
                    }
                }

                state.isCheckingAvailability = true
                state.wasLastCheckSuccessful = state.isUsernameAvailable ?? false

                return .run { send in
                    let isAvailable = try await api.checkUsernameAvailability(username)
                    await send(.internal(.usernameAvailabilityResult(.success(isAvailable))))
                } catch: { error, send in
                    await send(.internal(.usernameAvailabilityResult(.failure(error))))
                }
                .cancellable(id: UsernameAvailabilityAPICancellableId(), cancelInFlight: true)

            case let .internal(.usernameAvailabilityResult(result)):
                state.isCheckingAvailability = false
                state.isPendingValidation = false

                let checkedUsername = state.value.removingPrefix("@")
                state.lastCheckedUsername = checkedUsername

                switch result {
                case let .success(isAvailable):
                    state.isUsernameAvailable = isAvailable
                    state.wasLastCheckSuccessful = isAvailable

                    if state.availabilityCache.count > Self.availabilityCacheSize {
                        state.availabilityCache.removeAll()
                    }

                    state.availabilityCache[checkedUsername] = isAvailable

                    if isAvailable {
                        state.errorMessage = nil
                        return .none
                    } else {
                        state.errorMessage = L10n.APIClient.usernameTaken
                        return .none
                    }

                case .failure:
                    state.isUsernameAvailable = false
                    state.wasLastCheckSuccessful = false
                    return .none
                }

            case .binding(\.value):
                state.isCheckingAvailability = false

                let trimmedValue = state.value.removingPrefix("@")
                let notEmpty = !trimmedValue.isEmpty
                let changed = trimmedValue != state.lastCheckedUsername

                // Show pending validation immediately if value changed and not empty
                state.isPendingValidation = notEmpty && changed

                return .merge(
                    .cancel(id: UsernameAvailabilityAPICancellableId()),
                    .run { [username = state.value] send in
                        try await withTaskCancellation(
                            id: UsernameAvailabilityDebounceId(),
                            cancelInFlight: true
                        ) {
                            try await Task.sleep(for: .seconds(username.isEmpty ? 0.0 : 0.6))
                            await send(.internal(.validateUsername(username)))
                        }
                    }
                )

            case .binding:
                return .none

            case .delegate:
                return .none
            }
        }
    }
}

private extension String {
    var usernameContainsProblemChars: String? {
        let failchars = String(self.compactMap { ($0.isLetter || $0.isNumber || $0 == "_" || $0 == "-") ? nil : $0 })

        let allowedSpecials = self.enumerated().compactMap { index, element in
            element == "_" || element == "-" ? (index, element) : nil
        }

        let hasDoubleSpecials = allowedSpecials.enumerated().reduce((false, "")) { result, special in
            guard special.offset > 0 else { return result }
            guard special.element.0 == allowedSpecials[special.offset - 1].0 + 1, special.element.1 == allowedSpecials[special.offset - 1].1 else { return result }
            return (true, String(special.element.1))
        }

        if !failchars.isEmpty {
            return L10n.FeatureOnboarding.usernameErrorSpecialCharacters(failchars)
        } else if self.count < 3 {
            return L10n.FeatureOnboarding.usernameErrorTooShort
        } else if hasDoubleSpecials.0 {
            return L10n.FeatureOnboarding.usernameErrorDoubles(hasDoubleSpecials.1)
        } else if self.count > 40 {
            return L10n.FeatureOnboarding.usernameErrorTooLong
        } else if self.compactMap({ $0.isLetter ? $0 : nil }).isEmpty {
            return L10n.FeatureOnboarding.usernameErrorLetter
        } else {
            return nil
        }
    }
}
