import Foundation

public enum StepFunProviderDescriptor {
    public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
    private static let credentials = ProviderCredentialAdapter(
        tokenResolver: { kind, environment, _ in
            guard kind == .primary, let token = StepFunSettingsReader.token(environment: environment) else {
                return nil
            }
            return ProviderTokenResolution(token: token, source: .environment)
        },
        tokenAccountSupport: TokenAccountSupport(
            title: "Session tokens",
            subtitle: "Store multiple StepFun Oasis-Token values.",
            placeholder: "Oasis-Token=…",
            injection: .cookieHeader,
            requiresManualCookieSource: true,
            cookieName: nil),
        authDetector: { environment, _ in
            StepFunSettingsReader.token(environment: environment) == nil ? [] : ["api"]
        },
        manualTokenPersister: { try Self.persistManualToken($0) })

    private static func persistManualToken(_ token: String) throws {
        let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        let store = CodexBarConfigStore()
        var config = try store.load() ?? .makeDefault()
        var providerConfig = config.providerConfig(for: UsageProvider.stepfun.instanceID)
            ?? ProviderConfig(id: UsageProvider.stepfun.instanceID)
        providerConfig.region = trimmed
        config.setProviderConfig(providerConfig)
        try store.save(config)
    }

    static func makeDescriptor() -> ProviderDescriptor {
        ProviderDescriptor(
            id: .stepfun,
            settingsSection: .init(
                StepFunProviderSettingsKey.self,
                cookieSettings: { settings in
                    CookieProviderSettings(
                        cookieSource: settings.cookieSource,
                        manualCookieHeader: settings.manualToken)
                },
                credentialSettings: { context in
                    let header = context.config?.sanitizedRegion ?? context.config?.sanitizedCookieHeader
                    let settings = context.cookieSettings(for: .stepfun, configuredHeader: header)
                    return StepFunProviderSettings(
                        cookieSource: settings.cookieSource,
                        manualToken: settings.manualCookieHeader ?? "",
                        username: context.config?.sanitizedAPIKey ?? "",
                        password: "")
                }),
            credentials: self.credentials,
            metadata: ProviderMetadata(
                id: .stepfun,
                displayName: "StepFun",
                sessionLabel: "5h Window",
                weeklyLabel: "Weekly Window",
                opusLabel: nil,
                supportsOpus: false,
                supportsCredits: false,
                creditsHint: "",
                toggleTitle: "Show StepFun usage",
                cliName: "stepfun",
                defaultEnabled: false,
                widgetSelectable: false,
                isPrimaryProvider: false,
                usesAccountFallback: false,
                debugLogUnavailableMessage: "StepFun debug log not yet implemented",
                browserCookieOrder: nil,
                dashboardURL: "https://platform.stepfun.com/plan-usage",
                statusPageURL: nil,
                statusLinkURL: nil),
            branding: ProviderBranding(
                iconStyle: .init(provider: .stepfun),
                iconResourceName: "ProviderIcon-stepfun",
                color: ProviderColor(red: 0.13, green: 0.59, blue: 0.95),
                confettiPalette: [
                    ProviderColor(hex: 0x000000),
                    ProviderColor(hex: 0xFFFFFF),
                    ProviderColor(hex: 0x858585),
                ],
                widgetColor: ProviderColor(red: 255 / 255, green: 140 / 255, blue: 0 / 255)),
            tokenCost: ProviderTokenCostConfig(
                supportsTokenCost: false,
                noDataMessage: { "StepFun per-day cost history is not available via API." }),
            pace: .calendarMonthResetWindow,
            fetchPlan: ProviderFetchPlan(
                sourceModes: [.auto, .web],
                pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [StepFunWebFetchStrategy()] })),
            cli: ProviderCLIConfig(
                name: "stepfun",
                aliases: ["step-fun", "sf"],
                versionDetector: nil))
    }
}

struct StepFunWebFetchStrategy: ProviderFetchStrategy {
    let id: String = "stepfun.web"
    let kind: ProviderFetchKind = .web

    func isAvailable(_ context: ProviderFetchContext) async -> Bool {
        context.settings?.stepfun?.cookieSource != .off
    }

    func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
        do {
            let resolved = try await Self.resolveToken(context: context, allowCached: true)
            let usage = try await StepFunUsageFetcher.fetchUsage(token: resolved.token)
            return self.makeResult(
                usage: usage.toUsageSnapshot(),
                sourceLabel: "web")
        } catch let error where Self.isAuthenticationFailure(error) {
            return try await self.recoverFromAuthenticationFailure(context: context, originalError: error)
        }
    }

    func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
        false
    }

    // MARK: - Token Resolution

    private struct ResolvedToken {
        let token: String
        let source: TokenSource
    }

    private enum TokenSource {
        case manual
        case cached
        case settingsLogin
        case environmentToken
        case environmentLogin
    }

    private static func resolveToken(
        context: ProviderFetchContext,
        allowCached: Bool) async throws -> ResolvedToken
    {
        let settings = context.settings?.stepfun

        // 1. Manual mode: use the token directly from settings
        if settings?.cookieSource == .manual {
            let manualToken = settings?.manualToken ?? ""
            guard !manualToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
                throw StepFunUsageError.missingToken
            }
            return ResolvedToken(
                token: StepFunTokenNormalizer.normalize(manualToken),
                source: .manual)
        }

        // 2. Cached token from previous login
        if allowCached, let cached = CookieHeaderCache.load(provider: .stepfun) {
            return ResolvedToken(
                token: StepFunTokenNormalizer.normalize(cached.cookieHeader),
                source: .cached)
        }

        // 3. Username + password from Settings UI → perform full login flow
        //    (register device → sign in by password → get Oasis-Token)
        if let settings, !settings.username.isEmpty, !settings.password.isEmpty {
            let token = try await StepFunUsageFetcher.login(
                username: settings.username,
                password: settings.password)
            CookieHeaderCache.store(provider: .stepfun, cookieHeader: token, sourceLabel: "login")
            return ResolvedToken(token: token, source: .settingsLogin)
        }

        // 4. Direct token from env var
        if let token = StepFunSettingsReader.token(environment: context.env) {
            return ResolvedToken(token: token, source: .environmentToken)
        }

        // 5. Username + password from env vars → perform full login flow
        if let username = StepFunSettingsReader.username(environment: context.env),
           let password = StepFunSettingsReader.password(environment: context.env)
        {
            let token = try await StepFunUsageFetcher.login(username: username, password: password)
            CookieHeaderCache.store(provider: .stepfun, cookieHeader: token, sourceLabel: "login")
            return ResolvedToken(token: token, source: .environmentLogin)
        }

        throw StepFunUsageError.missingCredentials
    }

    private func recoverFromAuthenticationFailure(
        context: ProviderFetchContext,
        originalError: Error) async throws -> ProviderFetchResult
    {
        let resolved = try await Self.resolveToken(context: context, allowCached: true)
        let refreshed: String
        do {
            refreshed = try await StepFunUsageFetcher.refreshToken(token: resolved.token)
        } catch {
            if let fallback = try await Self.resolvedTokenWithoutStaleCache(context: context, source: resolved.source) {
                do {
                    let usage = try await StepFunUsageFetcher.fetchUsage(token: fallback.token)
                    await Self.persistRecoveredToken(fallback.token, source: fallback.source, context: context)
                    return self.makeResult(
                        usage: usage.toUsageSnapshot(),
                        sourceLabel: "web")
                } catch {
                    if !Self.isAuthenticationFailure(error) {
                        throw error
                    }
                }
            }
            if let loginToken = try await Self.loginTokenIfAvailable(context: context, source: resolved.source) {
                let usage = try await StepFunUsageFetcher.fetchUsage(token: loginToken)
                return self.makeResult(
                    usage: usage.toUsageSnapshot(),
                    sourceLabel: "web")
            }
            throw Self.actionableAuthenticationError(for: resolved.source, originalError: originalError)
        }

        await Self.persistRecoveredToken(refreshed, source: resolved.source, context: context)

        do {
            let usage = try await StepFunUsageFetcher.fetchUsage(token: refreshed)
            return self.makeResult(
                usage: usage.toUsageSnapshot(),
                sourceLabel: "web")
        } catch let retryError where Self.isAuthenticationFailure(retryError) {
            if let loginToken = try await Self.loginTokenIfAvailable(context: context, source: resolved.source) {
                let usage = try await StepFunUsageFetcher.fetchUsage(token: loginToken)
                return self.makeResult(
                    usage: usage.toUsageSnapshot(),
                    sourceLabel: "web")
            }
            throw Self.actionableAuthenticationError(for: resolved.source, originalError: originalError)
        }
    }

    private static func resolvedTokenWithoutStaleCache(
        context: ProviderFetchContext,
        source: TokenSource) async throws -> ResolvedToken?
    {
        guard case .cached = source else { return nil }
        CookieHeaderCache.clear(provider: .stepfun)
        do {
            return try await self.resolveToken(context: context, allowCached: false)
        } catch StepFunUsageError.missingCredentials {
            return nil
        } catch StepFunUsageError.missingToken {
            return nil
        }
    }

    private static func loginTokenIfAvailable(
        context: ProviderFetchContext,
        source: TokenSource) async throws -> String?
    {
        if case .manual = source {
            return nil
        }

        let settings = context.settings?.stepfun
        if settings?.cookieSource != .manual,
           let settings,
           !settings.username.isEmpty,
           !settings.password.isEmpty
        {
            CookieHeaderCache.clear(provider: .stepfun)
            let token = try await StepFunUsageFetcher.login(
                username: settings.username,
                password: settings.password)
            CookieHeaderCache.store(provider: .stepfun, cookieHeader: token, sourceLabel: "login")
            return token
        }

        if let username = StepFunSettingsReader.username(environment: context.env),
           let password = StepFunSettingsReader.password(environment: context.env)
        {
            CookieHeaderCache.clear(provider: .stepfun)
            let token = try await StepFunUsageFetcher.login(username: username, password: password)
            CookieHeaderCache.store(provider: .stepfun, cookieHeader: token, sourceLabel: "login")
            return token
        }

        return nil
    }

    private static func persistRecoveredToken(
        _ token: String,
        source: TokenSource,
        context: ProviderFetchContext) async
    {
        switch source {
        case .cached, .settingsLogin, .environmentLogin:
            CookieHeaderCache.store(provider: .stepfun, cookieHeader: token, sourceLabel: "refresh")
        case .manual:
            guard let accountID = context.selectedTokenAccountID,
                  let updater = context.tokenAccountTokenUpdater
            else {
                await context.providerManualTokenUpdater?(.stepfun, token)
                return
            }
            await updater(.stepfun, accountID, token)
        case .environmentToken:
            guard let accountID = context.selectedTokenAccountID,
                  let updater = context.tokenAccountTokenUpdater
            else { return }
            await updater(.stepfun, accountID, token)
        }
    }

    private static func isAuthenticationFailure(_ error: Error) -> Bool {
        guard case let StepFunUsageError.apiError(message) = error else {
            return false
        }
        let lower = message.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        return lower.contains("401") ||
            lower.contains("403") ||
            lower.contains("unauthorized") ||
            lower.contains("unauthenticated") ||
            lower.contains("invalid credentials") ||
            lower.contains("invalid token") ||
            lower.contains("token expired") ||
            lower.contains("expired token")
    }

    private static func actionableAuthenticationError(
        for source: TokenSource,
        originalError: Error) -> StepFunUsageError
    {
        let suffix = switch source {
        case .manual:
            "Refresh the Oasis-Token, or switch StepFun to auto auth with username/password."
        case .environmentToken:
            "Refresh STEPFUN_TOKEN, or configure STEPFUN_USERNAME and STEPFUN_PASSWORD."
        case .cached, .settingsLogin, .environmentLogin:
            "Refresh the StepFun credentials and try again."
        }
        return .apiError("\(Self.authenticationFailureMessage(originalError)). \(suffix)")
    }

    private static func authenticationFailureMessage(_ error: Error) -> String {
        if case let StepFunUsageError.apiError(message) = error {
            return message
        }
        return error.localizedDescription
    }
}

// MARK: - Token Normalizer

public enum StepFunTokenNormalizer {
    /// Normalize a StepFun token value — extracts the Oasis-Token from a cookie header
    /// or returns the raw token value if it's not a cookie header.
    public static func normalize(_ raw: String) -> String {
        let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return "" }

        // If it looks like a cookie header, extract Oasis-Token
        if trimmed.contains("Oasis-Token=") {
            let parts = trimmed.components(separatedBy: "Oasis-Token=")
            if parts.count > 1 {
                let afterToken = parts[1]
                return afterToken.components(separatedBy: ";").first?
                    .trimmingCharacters(in: .whitespaces) ?? afterToken
            }
        }

        return trimmed
    }
}
