import Foundation

public struct ClaudeSourcePlanningInput: Equatable, Sendable {
    public let runtime: ProviderRuntime
    public let selectedDataSource: ClaudeUsageDataSource
    public let webExtrasEnabled: Bool
    public let hasWebSession: Bool
    public let hasCLI: Bool
    public let hasOAuthCredentials: Bool

    public init(
        runtime: ProviderRuntime,
        selectedDataSource: ClaudeUsageDataSource,
        webExtrasEnabled: Bool,
        hasWebSession: Bool,
        hasCLI: Bool,
        hasOAuthCredentials: Bool)
    {
        self.runtime = runtime
        self.selectedDataSource = selectedDataSource
        self.webExtrasEnabled = webExtrasEnabled
        self.hasWebSession = hasWebSession
        self.hasCLI = hasCLI
        self.hasOAuthCredentials = hasOAuthCredentials
    }
}

public enum ClaudeSourcePlanReason: String, Equatable, Sendable {
    case explicitSourceSelection = "explicit-source-selection"
    case explicitOAuthOwnerCLIFallback = "explicit-oauth-owner-cli-fallback"
    case appAutoPreferredOAuth = "app-auto-preferred-oauth"
    case appAutoFallbackCLI = "app-auto-fallback-cli"
    case appAutoFallbackWeb = "app-auto-fallback-web"
    case cliAutoPreferredWeb = "cli-auto-preferred-web"
    case cliAutoFallbackCLI = "cli-auto-fallback-cli"
}

public struct ClaudeFetchPlanStep: Equatable, Sendable {
    public let dataSource: ClaudeUsageDataSource
    public let inclusionReason: ClaudeSourcePlanReason
    public let isPlausiblyAvailable: Bool

    public init(
        dataSource: ClaudeUsageDataSource,
        inclusionReason: ClaudeSourcePlanReason,
        isPlausiblyAvailable: Bool)
    {
        self.dataSource = dataSource
        self.inclusionReason = inclusionReason
        self.isPlausiblyAvailable = isPlausiblyAvailable
    }
}

public struct ClaudeFetchPlan: Equatable, Sendable {
    public let input: ClaudeSourcePlanningInput
    public let orderedSteps: [ClaudeFetchPlanStep]

    public init(input: ClaudeSourcePlanningInput, orderedSteps: [ClaudeFetchPlanStep]) {
        self.input = input
        self.orderedSteps = orderedSteps
    }

    public var availableSteps: [ClaudeFetchPlanStep] {
        self.orderedSteps.filter(\.isPlausiblyAvailable)
    }

    public var isNoSourceAvailable: Bool {
        self.availableSteps.isEmpty
    }

    public var preferredStep: ClaudeFetchPlanStep? {
        switch self.input.selectedDataSource {
        case .auto:
            self.availableSteps.first
        case .oauth where self.input.runtime == .app:
            self.availableSteps.first
        case .api, .oauth, .web, .cli:
            self.orderedSteps.first
        }
    }

    public var executionSteps: [ClaudeFetchPlanStep] {
        switch self.input.selectedDataSource {
        case .auto:
            self.availableSteps
        case .api, .oauth, .web, .cli:
            self.orderedSteps
        }
    }

    public var compatibilityStrategy: ClaudeUsageStrategy? {
        guard let preferredStep else { return nil }
        let useWebExtras = self.input.runtime == .app
            && preferredStep.dataSource == .cli
            && self.input.webExtrasEnabled
        return ClaudeUsageStrategy(
            dataSource: preferredStep.dataSource,
            useWebExtras: useWebExtras)
    }

    public var orderLabel: String {
        self.orderedSteps.map(\.dataSource.sourceLabel).joined(separator: "→")
    }

    public func debugLines() -> [String] {
        var lines = ["planner_order=\(self.orderLabel)"]
        lines.append("planner_selected=\(self.preferredStep?.dataSource.rawValue ?? "none")")
        lines.append("planner_no_source=\(self.isNoSourceAvailable)")
        for step in self.orderedSteps {
            let availability = step.isPlausiblyAvailable ? "available" : "unavailable"
            lines.append(
                "planner_step.\(step.dataSource.rawValue)=\(availability) reason=\(step.inclusionReason.rawValue)")
        }
        return lines
    }
}

public enum ClaudeCLIResolver {
    #if DEBUG
    @TaskLocal static var resolvedBinaryPathOverrideForTesting: String?

    public static var currentResolvedBinaryPathOverrideForTesting: String? {
        self.resolvedBinaryPathOverrideForTesting
    }

    public static func withResolvedBinaryPathOverrideForTesting<T>(
        _ path: String?,
        operation: () async throws -> T) async rethrows -> T
    {
        try await self.$resolvedBinaryPathOverrideForTesting.withValue(path) {
            try await operation()
        }
    }
    #endif

    public static func resolvedBinaryPath(
        environment: [String: String] = ProcessInfo.processInfo.environment,
        loginPATH: [String]? = LoginShellPathCache.shared.current)
        -> String?
    {
        #if DEBUG
        if let override = self.resolvedBinaryPathOverrideForTesting {
            return FileManager.default.isExecutableFile(atPath: override) ? override : nil
        }
        #endif

        var normalizedEnvironment = environment
        if let override = environment["CLAUDE_CLI_PATH"]?.trimmingCharacters(in: .whitespacesAndNewlines) {
            if override.isEmpty {
                normalizedEnvironment.removeValue(forKey: "CLAUDE_CLI_PATH")
            } else {
                normalizedEnvironment["CLAUDE_CLI_PATH"] = override
                if FileManager.default.isExecutableFile(atPath: override) {
                    return override
                }
            }
        }

        return BinaryLocator.resolveClaudeBinary(
            env: normalizedEnvironment,
            loginPATH: loginPATH)
    }

    public static func isAvailable(environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool {
        self.resolvedBinaryPath(environment: environment) != nil
    }
}

public enum ClaudeSourcePlanner {
    public static func resolve(input: ClaudeSourcePlanningInput) -> ClaudeFetchPlan {
        ClaudeFetchPlan(input: input, orderedSteps: self.makeSteps(input: input))
    }

    private static func makeSteps(input: ClaudeSourcePlanningInput) -> [ClaudeFetchPlanStep] {
        switch input.selectedDataSource {
        case .auto:
            switch input.runtime {
            case .app:
                [
                    self.step(.oauth, reason: .appAutoPreferredOAuth, input: input),
                    self.step(.cli, reason: .appAutoFallbackCLI, input: input),
                    self.step(.web, reason: .appAutoFallbackWeb, input: input),
                ]
            case .cli:
                [
                    self.step(.web, reason: .cliAutoPreferredWeb, input: input),
                    self.step(.cli, reason: .cliAutoFallbackCLI, input: input),
                ]
            }
        case .api:
            [self.step(.api, reason: .explicitSourceSelection, input: input)]
        case .oauth:
            switch input.runtime {
            case .app:
                [
                    self.step(.oauth, reason: .explicitSourceSelection, input: input),
                    self.step(.cli, reason: .explicitOAuthOwnerCLIFallback, input: input),
                ]
            case .cli:
                [self.step(.oauth, reason: .explicitSourceSelection, input: input)]
            }
        case .web:
            [self.step(.web, reason: .explicitSourceSelection, input: input)]
        case .cli:
            [self.step(.cli, reason: .explicitSourceSelection, input: input)]
        }
    }

    private static func step(
        _ dataSource: ClaudeUsageDataSource,
        reason: ClaudeSourcePlanReason,
        input: ClaudeSourcePlanningInput) -> ClaudeFetchPlanStep
    {
        ClaudeFetchPlanStep(
            dataSource: dataSource,
            inclusionReason: reason,
            isPlausiblyAvailable: self.isPlausiblyAvailable(dataSource, input: input))
    }

    private static func isPlausiblyAvailable(
        _ dataSource: ClaudeUsageDataSource,
        input: ClaudeSourcePlanningInput) -> Bool
    {
        switch dataSource {
        case .auto, .api:
            false
        case .oauth:
            input.hasOAuthCredentials
        case .web:
            input.hasWebSession
        case .cli:
            input.hasCLI
        }
    }
}
