import Foundation

#if os(macOS)
import os.lock

public enum ClaudeOAuthKeychainAccessGate {
    private struct State {
        var loaded = false
        var deniedUntil: Date?
        var promptAttemptGeneration: UInt64 = 0
    }

    private static let lock = OSAllocatedUnfairLock<State>(initialState: State())
    private static let defaultsKey = "claudeOAuthKeychainDeniedUntil"
    private static let cooldownInterval: TimeInterval = 60 * 60 * 6
    @TaskLocal private static var taskOverrideShouldAllowPromptForTesting: Bool?
    #if DEBUG
    public final class DeniedUntilStore: @unchecked Sendable {
        public var deniedUntil: Date?

        public init() {}
    }

    @TaskLocal private static var taskDeniedUntilStoreOverrideForTesting: DeniedUntilStore?
    #endif

    public static func shouldAllowPrompt(now: Date = Date()) -> Bool {
        guard !KeychainAccessGate.isDisabled else { return false }
        if let override = self.taskOverrideShouldAllowPromptForTesting { return override }
        #if DEBUG
        if let store = self.taskDeniedUntilStoreOverrideForTesting {
            if let deniedUntil = store.deniedUntil, deniedUntil > now {
                return false
            }
            store.deniedUntil = nil
            return true
        }
        #endif
        return self.lock.withLock { state in
            self.loadIfNeeded(&state)
            if let deniedUntil = state.deniedUntil {
                if deniedUntil > now {
                    return false
                }
                state.deniedUntil = nil
                self.persist(state)
            }
            return true
        }
    }

    static func promptAttemptGeneration() -> UInt64 {
        self.lock.withLock { state in
            self.loadIfNeeded(&state)
            return state.promptAttemptGeneration
        }
    }

    /// Advances the generation after an interactive attempt so callers waiting on the prompt lock reuse its outcome.
    static func recordPromptAttemptCompleted() -> UInt64 {
        self.lock.withLock { state in
            self.loadIfNeeded(&state)
            state.promptAttemptGeneration &+= 1
            return state.promptAttemptGeneration
        }
    }

    public static func recordDenied(now: Date = Date()) {
        let deniedUntil = now.addingTimeInterval(self.cooldownInterval)
        #if DEBUG
        if let store = self.taskDeniedUntilStoreOverrideForTesting {
            store.deniedUntil = deniedUntil
            return
        }
        #endif
        self.lock.withLock { state in
            self.loadIfNeeded(&state)
            state.deniedUntil = deniedUntil
            self.persist(state)
        }
    }

    /// Clears the cooldown so the next attempt can proceed. Intended for user-initiated repairs.
    /// - Returns: true if a cooldown was present and cleared.
    public static func clearDenied(now: Date = Date()) -> Bool {
        #if DEBUG
        if let store = self.taskDeniedUntilStoreOverrideForTesting {
            guard let deniedUntil = store.deniedUntil, deniedUntil > now else {
                store.deniedUntil = nil
                return false
            }
            store.deniedUntil = nil
            return true
        }
        #endif
        return self.lock.withLock { state in
            self.loadIfNeeded(&state)
            guard let deniedUntil = state.deniedUntil, deniedUntil > now else {
                state.deniedUntil = nil
                self.persist(state)
                return false
            }
            state.deniedUntil = nil
            self.persist(state)
            return true
        }
    }

    #if DEBUG
    static func withShouldAllowPromptOverrideForTesting<T>(
        _ value: Bool?,
        operation: () throws -> T) rethrows -> T
    {
        try self.$taskOverrideShouldAllowPromptForTesting.withValue(value) {
            try operation()
        }
    }

    static func withShouldAllowPromptOverrideForTesting<T>(
        _ value: Bool?,
        operation: () async throws -> T) async rethrows -> T
    {
        try await self.$taskOverrideShouldAllowPromptForTesting.withValue(value) {
            try await operation()
        }
    }

    public static func withDeniedUntilStoreOverrideForTesting<T>(
        _ store: DeniedUntilStore?,
        operation: () throws -> T) rethrows -> T
    {
        try self.$taskDeniedUntilStoreOverrideForTesting.withValue(store) {
            try operation()
        }
    }

    public static func withDeniedUntilStoreOverrideForTesting<T>(
        _ store: DeniedUntilStore?,
        operation: () async throws -> T) async rethrows -> T
    {
        try await self.$taskDeniedUntilStoreOverrideForTesting.withValue(store) {
            try await operation()
        }
    }

    public static var currentDeniedUntilStoreOverrideForTesting: DeniedUntilStore? {
        self.taskDeniedUntilStoreOverrideForTesting
    }

    public static func resetForTesting() {
        self.lock.withLock { state in
            // Keep deterministic during tests: avoid re-loading UserDefaults written by unrelated code paths.
            state.loaded = true
            state.deniedUntil = nil
            state.promptAttemptGeneration = 0
            UserDefaults.standard.removeObject(forKey: self.defaultsKey)
        }
    }

    public static func resetInMemoryForTesting() {
        self.lock.withLock { state in
            state.loaded = false
            state.deniedUntil = nil
            state.promptAttemptGeneration = 0
        }
    }
    #endif

    private static func loadIfNeeded(_ state: inout State) {
        guard !state.loaded else { return }
        state.loaded = true
        if let raw = UserDefaults.standard.object(forKey: self.defaultsKey) as? Double {
            state.deniedUntil = Date(timeIntervalSince1970: raw)
        }
    }

    private static func persist(_ state: State) {
        if let deniedUntil = state.deniedUntil {
            UserDefaults.standard.set(deniedUntil.timeIntervalSince1970, forKey: self.defaultsKey)
        } else {
            UserDefaults.standard.removeObject(forKey: self.defaultsKey)
        }
    }
}
#else
public enum ClaudeOAuthKeychainAccessGate {
    public static func shouldAllowPrompt(now _: Date = Date()) -> Bool {
        true
    }

    public static func recordDenied(now _: Date = Date()) {}

    static func promptAttemptGeneration() -> UInt64 {
        0
    }

    static func recordPromptAttemptCompleted() -> UInt64 {
        0
    }

    public static func clearDenied(now _: Date = Date()) -> Bool {
        false
    }

    #if DEBUG
    public static func resetForTesting() {}

    public static func resetInMemoryForTesting() {}
    #endif
}
#endif
