import Foundation

@MainActor
final class ProviderRefreshCoordinator<Key: Hashable> {
    enum WaitResult: Equatable {
        case completed
        case retryRequired
        case cancelled
    }

    struct Request {
        let generation: UInt64
        let state: ProviderRefreshTaskState
        let predecessorStates: [ProviderRefreshTaskState]
    }

    private var states: [Key: [ProviderRefreshTaskState]] = [:]
    private var latestGenerations: [Key: UInt64] = [:]
    private var activeCounts: [Key: Int] = [:]
    private var nextGeneration: UInt64 = 0
    private var nextWaiterID: UInt64 = 0

    func coalescingState(for key: Key) -> ProviderRefreshTaskState? {
        guard let latestGeneration = self.latestGenerations[key] else { return nil }
        return self.states[key]?.last { state in
            state.generation == latestGeneration && !state.isCompleted
        }
    }

    func beginReplacingRequest(for key: Key) -> Request {
        self.nextGeneration &+= 1
        let generation = self.nextGeneration
        let predecessorStates = self.states[key] ?? []
        for predecessorState in predecessorStates {
            predecessorState.cancelTask()
        }
        self.latestGenerations[key] = generation
        let state = ProviderRefreshTaskState(generation: generation)
        self.states[key, default: []].append(state)
        return Request(
            generation: generation,
            state: state,
            predecessorStates: predecessorStates)
    }

    /// Invalidates in-flight work without creating a replacement request. Existing states stay
    /// registered until their tasks and waiters drain, but their generations can no longer publish.
    func invalidateRequests(for key: Key) {
        self.nextGeneration &+= 1
        self.latestGenerations[key] = self.nextGeneration
        for state in self.states[key] ?? [] {
            state.cancelTask()
        }
    }

    func wait(for key: Key, state: ProviderRefreshTaskState) async -> WaitResult {
        self.nextWaiterID &+= 1
        let waiterID = self.nextWaiterID
        guard let task = state.addWaiter(waiterID) else { return .completed }
        await withTaskCancellationHandler {
            await task.value
        } onCancel: {
            state.cancelWaiter(waiterID)
        }
        state.finishWaiter(waiterID)
        let result: WaitResult = if Task.isCancelled {
            .cancelled
        } else if state.shouldRetry {
            .retryRequired
        } else {
            .completed
        }
        if state.canRemove {
            self.scheduleRemoval(for: key, state: state)
        }
        return result
    }

    func complete(_ state: ProviderRefreshTaskState, for key: Key, retryRequired: Bool) {
        state.markCompleted(retryRequired: retryRequired)
        self.scheduleRemoval(for: key, state: state)
    }

    func remove(_ state: ProviderRefreshTaskState, for key: Key) {
        guard var keyStates = self.states[key] else { return }
        keyStates.removeAll { $0 === state }
        if keyStates.isEmpty {
            self.states.removeValue(forKey: key)
        } else {
            self.states[key] = keyStates
        }
    }

    func isCurrent(_ generation: UInt64, for key: Key) -> Bool {
        self.latestGenerations[key] == generation
    }

    @discardableResult
    func beginActivity(for key: Key) -> Bool {
        self.activeCounts[key, default: 0] += 1
        return self.activeCounts[key] == 1
    }

    @discardableResult
    func endActivity(for key: Key) -> Bool {
        let remaining = max(0, self.activeCounts[key, default: 1] - 1)
        if remaining == 0 {
            self.activeCounts.removeValue(forKey: key)
            return true
        }
        self.activeCounts[key] = remaining
        return false
    }

    private func scheduleRemoval(for key: Key, state: ProviderRefreshTaskState) {
        Task { @MainActor [weak self] in
            await Task.yield()
            guard let self,
                  self.states[key]?.contains(where: { $0 === state }) == true,
                  state.canRemove
            else {
                return
            }
            self.remove(state, for: key)
        }
    }
}

final class ProviderRefreshTaskState: @unchecked Sendable {
    let generation: UInt64

    private let lock = NSLock()
    private var task: Task<Void, Never>?
    private var waiterIDs: Set<UInt64> = []
    private var completed = false
    private var retryRequired = false

    init(generation: UInt64) {
        self.generation = generation
    }

    func install(task: Task<Void, Never>) {
        self.lock.withLock {
            self.task = task
        }
    }

    func addWaiter(_ waiterID: UInt64) -> Task<Void, Never>? {
        self.lock.withLock {
            self.waiterIDs.insert(waiterID)
            return self.task
        }
    }

    func cancelWaiter(_ waiterID: UInt64) {
        let taskToCancel = self.lock.withLock {
            guard self.waiterIDs.remove(waiterID) != nil else { return nil as Task<Void, Never>? }
            return self.waiterIDs.isEmpty && !self.completed ? self.task : nil
        }
        taskToCancel?.cancel()
    }

    func finishWaiter(_ waiterID: UInt64) {
        _ = self.lock.withLock {
            self.waiterIDs.remove(waiterID)
        }
    }

    func markCompleted(retryRequired: Bool) {
        self.lock.withLock {
            self.completed = true
            self.retryRequired = retryRequired
        }
    }

    func cancelTask() {
        let task = self.lock.withLock {
            self.completed ? nil : self.task
        }
        task?.cancel()
    }

    func waitForTaskCompletion() async {
        let task = self.lock.withLock { self.task }
        await task?.value
    }

    fileprivate var shouldRetry: Bool {
        self.lock.withLock { self.retryRequired }
    }

    fileprivate var isCompleted: Bool {
        self.lock.withLock { self.completed }
    }

    var canRemove: Bool {
        self.lock.withLock { self.completed && self.waiterIDs.isEmpty }
    }
}
