import Foundation

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

#if os(macOS)
import Security
#endif

public struct ClaudeOAuthCredentials: Sendable {
    public let accessToken: String
    public let refreshToken: String?
    public let expiresAt: Date?
    public let scopes: [String]
    public let rateLimitTier: String?
    public let subscriptionType: String?

    public init(
        accessToken: String,
        refreshToken: String?,
        expiresAt: Date?,
        scopes: [String],
        rateLimitTier: String?,
        subscriptionType: String? = nil)
    {
        self.accessToken = accessToken
        self.refreshToken = refreshToken
        self.expiresAt = expiresAt
        self.scopes = scopes
        self.rateLimitTier = rateLimitTier
        self.subscriptionType = subscriptionType
    }

    public var isExpired: Bool {
        guard let expiresAt else { return true }
        return Date() >= expiresAt
    }

    public var expiresIn: TimeInterval? {
        guard let expiresAt else { return nil }
        return expiresAt.timeIntervalSinceNow
    }

    /// A one-way discriminator for history owned by this credential.
    ///
    /// Prefer the refresh token because access tokens routinely rotate for the same principal. If a provider
    /// supplies only an access token, rotating that token intentionally starts a new history bucket rather than
    /// risking that two identityless accounts share one. The source secret never leaves this computation.
    var historyOwnerIdentifier: String? {
        let normalizedRefreshToken = self.refreshToken?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let normalizedRefreshToken, !normalizedRefreshToken.isEmpty {
            return Self.makeHistoryOwnerIdentifier(secretKind: "refresh", secret: normalizedRefreshToken)
        }

        let normalizedAccessToken = self.accessToken.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalizedAccessToken.isEmpty else { return nil }
        return Self.makeHistoryOwnerIdentifier(secretKind: "access", secret: normalizedAccessToken)
    }

    static func historyOwnerIdentifier(forRefreshToken refreshToken: String) -> String? {
        let normalized = refreshToken.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !normalized.isEmpty else { return nil }
        return Self.makeHistoryOwnerIdentifier(secretKind: "refresh", secret: normalized)
    }

    private static func makeHistoryOwnerIdentifier(secretKind: String, secret: String) -> String? {
        let material = Data("codexbar:claude-oauth-history-owner:v1\0\(secretKind)\0\(secret)".utf8)
        return SHA256.hash(data: material).map { String(format: "%02x", $0) }.joined()
    }

    static func normalizedHistoryOwnerIdentifier(_ identifier: String?) -> String? {
        guard let identifier else { return nil }
        let normalized = identifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        guard normalized.count == 64,
              normalized.unicodeScalars.allSatisfy({ scalar in
                  switch scalar.value {
                  case 48...57, 97...102:
                      true
                  default:
                      false
                  }
              })
        else { return nil }
        return normalized
    }

    public static func isMcpOAuthOnlyPayload(data: Data) -> Bool {
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return false
        }
        return json["claudeAiOauth"] == nil && json["mcpOAuth"] != nil
    }

    public static func parse(data: Data) throws -> ClaudeOAuthCredentials {
        if ClaudeOAuthCredentials.isMcpOAuthOnlyPayload(data: data) {
            throw ClaudeOAuthCredentialsError.mcpOAuthOnlyKeychain
        }

        let decoder = JSONDecoder()
        guard let root = try? decoder.decode(Root.self, from: data) else {
            throw ClaudeOAuthCredentialsError.decodeFailed
        }
        guard let oauth = root.claudeAiOauth else {
            throw ClaudeOAuthCredentialsError.missingOAuth
        }
        let accessToken = oauth.accessToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !accessToken.isEmpty else {
            throw ClaudeOAuthCredentialsError.missingAccessToken
        }
        let expiresAt = oauth.expiresAt.map { millis in
            Date(timeIntervalSince1970: millis / 1000.0)
        }
        return ClaudeOAuthCredentials(
            accessToken: accessToken,
            refreshToken: oauth.refreshToken,
            expiresAt: expiresAt,
            scopes: oauth.scopes ?? [],
            rateLimitTier: oauth.rateLimitTier,
            subscriptionType: oauth.subscriptionType)
    }

    private struct Root: Decodable {
        let claudeAiOauth: OAuth?
    }

    private struct OAuth: Decodable {
        let accessToken: String?
        let refreshToken: String?
        let expiresAt: Double?
        let scopes: [String]?
        let rateLimitTier: String?
        let subscriptionType: String?

        enum CodingKeys: String, CodingKey {
            case accessToken
            case refreshToken
            case expiresAt
            case scopes
            case rateLimitTier
            case subscriptionType
        }
    }
}

extension ClaudeOAuthCredentials {
    func diagnosticsMetadata(now: Date = Date()) -> [String: String] {
        let hasRefreshToken = !(self.refreshToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
        let hasUserProfileScope = self.scopes.contains("user:profile")

        var metadata: [String: String] = [
            "hasRefreshToken": "\(hasRefreshToken)",
            "scopesCount": "\(self.scopes.count)",
            "hasUserProfileScope": "\(hasUserProfileScope)",
        ]

        if let expiresAt = self.expiresAt {
            let expiresAtMs = Int(expiresAt.timeIntervalSince1970 * 1000.0)
            let expiresInSec = Int(expiresAt.timeIntervalSince(now).rounded())
            metadata["expiresAtMs"] = "\(expiresAtMs)"
            metadata["expiresInSec"] = "\(expiresInSec)"
            metadata["isExpired"] = "\(now >= expiresAt)"
        } else {
            metadata["expiresAtMs"] = "nil"
            metadata["expiresInSec"] = "nil"
            metadata["isExpired"] = "true"
        }

        return metadata
    }
}

public enum ClaudeOAuthCredentialOwner: String, Codable, Sendable {
    case claudeCLI
    case codexbar
    case environment
}

public enum ClaudeOAuthCredentialSource: String, Sendable {
    case environment
    case memoryCache
    case cacheKeychain
    case credentialsFile
    case claudeKeychain
}

enum ClaudeKeychainCredentialMatch: Equatable, Sendable {
    case notApplicable
    case absent
    case unavailable
    case mismatch
    case matched(persistentRefHash: String)

    var persistentRefHash: String? {
        guard case let .matched(persistentRefHash) = self else { return nil }
        return persistentRefHash
    }

    var isMismatch: Bool {
        self == .mismatch
    }

    var isAbsent: Bool {
        self == .absent
    }

    var isUnavailable: Bool {
        self == .unavailable
    }
}

public struct ClaudeOAuthCredentialRecord: Sendable {
    public let credentials: ClaudeOAuthCredentials
    public let owner: ClaudeOAuthCredentialOwner
    public let source: ClaudeOAuthCredentialSource
    private let inheritedHistoryOwnerIdentifier: String?

    /// An opaque, one-way owner identifier that survives a refresh-token rotation proven by a successful refresh.
    /// Records from unrelated credential sources do not inherit this value and derive a fresh identifier instead.
    var historyOwnerIdentifier: String? {
        self.inheritedHistoryOwnerIdentifier ?? self.credentials.historyOwnerIdentifier
    }

    public init(
        credentials: ClaudeOAuthCredentials,
        owner: ClaudeOAuthCredentialOwner,
        source: ClaudeOAuthCredentialSource)
    {
        self.credentials = credentials
        self.owner = owner
        self.source = source
        self.inheritedHistoryOwnerIdentifier = nil
    }

    init(
        credentials: ClaudeOAuthCredentials,
        owner: ClaudeOAuthCredentialOwner,
        source: ClaudeOAuthCredentialSource,
        historyOwnerIdentifier: String?)
    {
        self.credentials = credentials
        self.owner = owner
        self.source = source
        self.inheritedHistoryOwnerIdentifier = ClaudeOAuthCredentials.normalizedHistoryOwnerIdentifier(
            historyOwnerIdentifier)
    }
}

public enum ClaudeOAuthCredentialsError: LocalizedError, Sendable {
    case decodeFailed
    case missingOAuth
    case mcpOAuthOnlyKeychain
    case missingAccessToken
    case notFound
    case keychainError(Int)
    case readFailed(String)
    case refreshFailed(String)
    case noRefreshToken
    case refreshDelegatedToClaudeCLI

    public var errorDescription: String? {
        switch self {
        case .decodeFailed:
            return "Claude OAuth credentials are invalid."
        case .missingOAuth:
            return "Claude OAuth credentials missing. Run `claude` to authenticate."
        case .mcpOAuthOnlyKeychain:
            return "Claude keychain contains MCP OAuth state only (no claudeAiOauth). "
                + "Claude Code may store subscription OAuth elsewhere now. "
                + "Open the CodexBar menu and click Refresh to re-authenticate, "
                + "or switch Claude Usage source to Web/CLI."
        case .missingAccessToken:
            return "Claude OAuth access token missing. Run `claude` to authenticate."
        case .notFound:
            return "Claude OAuth credentials not found. Run `claude` to authenticate."
        case let .keychainError(status):
            #if os(macOS)
            if status == Int(errSecUserCanceled)
                || status == Int(errSecAuthFailed)
                || status == Int(errSecInteractionNotAllowed)
                || status == Int(errSecNoAccessForItem)
            {
                return "Claude Keychain access was denied. CodexBar will back off in the background until you retry "
                    + "via a user action (menu open / manual refresh). "
                    + "Switch Claude Usage source to Web/CLI, or allow access in Keychain Access."
            }
            #endif
            return "Claude OAuth keychain error: \(status)"
        case let .readFailed(message):
            return "Claude OAuth credentials read failed: \(message)"
        case let .refreshFailed(message):
            return "Claude OAuth token refresh failed: \(message)"
        case .noRefreshToken:
            return "Claude OAuth refresh token missing. Run `claude` to authenticate."
        case .refreshDelegatedToClaudeCLI:
            return "Claude OAuth refresh is delegated to Claude CLI."
        }
    }
}
