import AsyncAlgorithms
import Combine
import ComposableArchitecture

public extension Effect {
    static func `if`(_ predicate: Bool, _ effect: @autoclosure () -> Effect) -> Effect {
        guard predicate else { return .none }
        return effect()
    }

    /// !! Make sure to call in `View.task` !!
    /// https://github.com/pointfreeco/swift-composable-architecture/discussions/1415
    static func stream<T>(
        _ stream: AsyncStream<T>,
        send action: @escaping (T) -> Action,
        cancellableId: some Hashable & Sendable
    ) -> Effect {
        .run { send in
            for await value in stream {
                await send(action(value))
            }
        }
        .cancellable(id: cancellableId, cancelInFlight: true)
    }

    private struct SubscribeCancellable: Hashable {}
    /// !! Make sure to call in `View.task` !!
    /// https://github.com/pointfreeco/swift-composable-architecture/discussions/1415
    static func subscribe<T>(
        _ publisher: AnyPublisher<T, Never>,
        send action: @escaping (T) -> Action,
        cancellableId: (any Hashable & Sendable)? = nil,
        label _: String? = nil
    ) -> Effect {
        let id: any Hashable & Sendable = cancellableId ?? SubscribeCancellable()
        return .run { send in
            // AsyncPublisher is *not* threadsafe and drops values
            // https://www.cleevio.com/blog/the-not-so-equivalent-code-demystifying-asyncpublisher
            // Look into converting this to a stream
            for await value in publisher.values {
                // We need to return to the `MainActor` boundary in order to prevent data loss
                // This might be fixed once we're able to migrate our Actions to Swift 6 concurrency
                // https://gist.github.com/lukeredpath/a04051224bedffad3fdac3aeb1c6a124#reducer-actions
                // There is still a chance of data loss with this, as safety isn't guaranteed without Swift 6
                // concurrency enforcement, in case you notice that your value types are dropping fields
                Task { @MainActor in
                    send(action(value))
                }
            }
        }
        .cancellable(id: id, cancelInFlight: true)
    }

    /// Helper function to subscribe to an `AsyncChannel` as an effect
    /// ...
    /// struct ClipChannelCancellationId: Hashable {}
    /// ...
    /// case .onAppear:
    ///     return .merge(
    ///         .....
    ///         .channel(eventBusClient. getClipChannel(), send: Action.clipChannel, cancellableId: ClipChannelCancellationId())
    ///     )
    static func channel<T>(
        _ channel: AsyncChannel<T>,
        send action: @escaping (T) -> Action,
        cancellableId: some Hashable & Sendable
    ) -> Effect {
        .run { send in
            for await value in channel {
                await send(action(value))
            }
        }
        .cancellable(id: cancellableId, cancelInFlight: true)
    }
}
