import Foundation

// Thinking-level normalization and option resolution. Session entries,
// session defaults, and free-form user aliases all feed the picker; this
// extension owns collapsing them into the canonical option list.

extension OpenClawChatViewModel {
    func applyAdvertisedThinkingLevel(_ level: String) {
        guard level != thinkingLevel else { return }
        thinkingLevel = level
        self.updateCurrentSessionThinkingLevel(level, sessionKey: sessionKey)
    }

    func performSelectThinkingLevel(_ level: String) {
        let clearsOverride = level == Self.inheritedThinkingSelectionID
        let next = clearsOverride
            ? (Self.normalizedThinkingLevel(self.currentSessionEntry()?.thinkingDefault)
                ?? Self.normalizedThinkingLevel(self.sessionDefaults?.thinkingDefault)
                ?? Self.normalizedThinkingLevel(self.thinkingLevel)
                ?? "off")
            : (Self.normalizedThinkingLevel(level) ?? "off")
        if clearsOverride {
            guard !self.thinkingOverrideIsInherited else { return }
        } else {
            guard next != preferredThinkingLevel || self.thinkingOverrideIsInherited else { return }
        }

        let sessionKey = self.sessionKey
        let acceptedBaseline = Self.normalizedThinkingLevel(currentSessionEntry()?.thinkingLevel)
            ?? Self.normalizedThinkingLevel(thinkingLevel)
            ?? "off"
        let target = currentModelPatchTarget()
        if acceptedThinkingLevelsByTarget[target] == nil {
            // Preserve the gateway-confirmed value across the whole queued lane.
            // Later optimistic selections must not become rollback truth.
            acceptedThinkingLevelsByTarget[target] = acceptedBaseline
            acceptedPreferredThinkingLevelsByTarget[target] = preferredThinkingLevel
            let session = currentSessionEntry()
            acceptedSettingsPatchResultsByTarget[target] = OpenClawChatModelPatchResult(
                key: session?.key ?? target.canonicalSessionKey,
                modelProvider: session?.modelProvider,
                model: session?.model,
                thinkingLevel: acceptedBaseline,
                thinkingLevels: session?.thinkingLevels)
        }
        if acceptedExplicitThinkingPreferencesByTarget[target] == nil {
            acceptedExplicitThinkingPreferencesByTarget[target] = prefersExplicitThinkingLevel
            acceptedThinkingOverrideClearedByTarget[target] = currentSessionEntry()?.thinkingLevel == nil
        }
        prefersExplicitThinkingLevel = !clearsOverride
        preferredThinkingLevel = next
        thinkingLevel = next
        self.syncThinkingLevelOptions()
        self.updateCurrentSessionThinkingLevel(clearsOverride ? nil : next, sessionKey: sessionKey)
        let settingsRequestID = reserveSessionSettingsRequest(for: target)
        nextThinkingSelectionRequestID &+= 1
        let requestID = nextThinkingSelectionRequestID
        latestThinkingSelectionRequestIDsByTarget[target] = requestID
        thinkingPreferenceRequests[requestID] = .pending(ThinkingPreferenceState(
            level: next,
            isExplicit: !clearsOverride))
        self.reconcileThinkingPreferenceRequests()
        enqueueSessionSettingsPatch(requestID: settingsRequestID, target: target) { [weak self] routeLease in
            guard let self else { return }
            do {
                guard let routeLease else { throw OpenClawChatTransportSendError.notDispatched }
                let patchResult = try await routeLease.patchSessionSettings(
                    sessionKey: target.canonicalSessionKey,
                    agentID: target.agentID,
                    patch: OpenClawChatSessionSettingsPatch(
                        thinkingLevel: .some(clearsOverride ? nil : next)))
                let acceptedLevel = Self.normalizedThinkingLevel(patchResult?.thinkingLevel) ?? next
                let acceptedResult = self.mergedThinkingPatchSuccess(
                    patchResult,
                    acceptedLevel: acceptedLevel,
                    target: target)
                // Older queued successes remain rollback truth, but never replace
                // a newer optimistic selection in the session row or picker.
                self.lastSuccessfulSettingsPatchResultsByTarget[target] = acceptedResult
                self.acceptedSettingsPatchResultsByTarget[target] = acceptedResult
                self.acceptedThinkingLevelsByTarget[target] = acceptedLevel
                self.acceptedPreferredThinkingLevelsByTarget[target] = acceptedLevel
                self.acceptedExplicitThinkingPreferencesByTarget[target] = !clearsOverride
                self.acceptedThinkingOverrideClearedByTarget[target] = clearsOverride
                self.lastSuccessfulSettingsPatchRequestIDsByTarget[target] = settingsRequestID
                self.lastSuccessfulThinkingOverrideClearedByTarget[target] = clearsOverride
                self.thinkingPreferenceRequests[requestID] = .succeeded(ThinkingPreferenceState(
                    level: acceptedLevel,
                    isExplicit: !clearsOverride))
                self.reconcileThinkingPreferenceRequests()
                guard requestID == self.latestThinkingSelectionRequestIDsByTarget[target] else { return }
                let targetIsCurrent = target == self.currentModelPatchTarget()
                let stateKey: String
                let exactMatchOnly: Bool
                if targetIsCurrent {
                    stateKey = sessionKey
                    exactMatchOnly = false
                } else {
                    guard let inactiveStateKey = self.inactiveSettingsStateKey(for: target) else { return }
                    stateKey = inactiveStateKey
                    exactMatchOnly = true
                }
                self.updateCurrentSessionThinkingLevel(
                    clearsOverride ? nil : acceptedLevel,
                    sessionKey: stateKey,
                    exactMatchOnly: exactMatchOnly)
                if let thinkingLevels = acceptedResult.thinkingLevels {
                    self.updateCurrentSessionThinkingLevels(
                        thinkingLevels,
                        sessionKey: stateKey,
                        exactMatchOnly: exactMatchOnly)
                }
                guard targetIsCurrent else { return }
                self.preferredThinkingLevel = acceptedLevel
                self.thinkingLevel = acceptedLevel
                self.prefersExplicitThinkingLevel = !clearsOverride
                self.syncThinkingLevelOptions()
            } catch {
                self.thinkingPreferenceRequests[requestID] = .failed
                self.reconcileThinkingPreferenceRequests()
                guard requestID == self.latestThinkingSelectionRequestIDsByTarget[target] else { return }
                let rollbackResult = self.acceptedSettingsPatchResultsByTarget[target]
                let rollbackLevel = self.acceptedThinkingLevelsByTarget[target]
                    ?? Self.normalizedThinkingLevel(rollbackResult?.thinkingLevel)
                    ?? acceptedBaseline
                let rollbackPreferredLevel = self.acceptedPreferredThinkingLevelsByTarget[target]
                    ?? rollbackLevel
                let rollbackIsExplicit = self.acceptedExplicitThinkingPreferencesByTarget[target] ?? false
                let targetIsCurrent = target == self.currentModelPatchTarget()
                let stateKey: String
                let exactMatchOnly: Bool
                if targetIsCurrent {
                    stateKey = sessionKey
                    exactMatchOnly = false
                } else {
                    guard let inactiveStateKey = self.inactiveSettingsStateKey(for: target) else { return }
                    stateKey = inactiveStateKey
                    exactMatchOnly = true
                }
                self.updateCurrentSessionThinkingLevel(
                    self.acceptedThinkingOverrideClearedByTarget[target] == true ? nil : rollbackLevel,
                    sessionKey: stateKey,
                    exactMatchOnly: exactMatchOnly)
                if let thinkingLevels = rollbackResult?.thinkingLevels {
                    self.updateCurrentSessionThinkingLevels(
                        thinkingLevels,
                        sessionKey: stateKey,
                        exactMatchOnly: exactMatchOnly)
                }
                guard targetIsCurrent else { return }
                self.prefersExplicitThinkingLevel = rollbackIsExplicit
                self.preferredThinkingLevel = rollbackPreferredLevel
                self.thinkingLevel = rollbackLevel
                self.syncThinkingLevelOptions()
                // Option resolution may project the preferred value when it is
                // supported. A rejection must still leave the applied state at
                // the gateway-confirmed rollback value.
                self.thinkingLevel = rollbackLevel
                self.updateCurrentSessionThinkingLevel(
                    self.acceptedThinkingOverrideClearedByTarget[target] == true ? nil : rollbackLevel,
                    sessionKey: sessionKey)
            }
        }
    }

    private func mergedThinkingPatchSuccess(
        _ patchResult: OpenClawChatModelPatchResult?,
        acceptedLevel: String,
        target: ModelPatchTarget) -> OpenClawChatModelPatchResult
    {
        // Rollback truth can contain newer refreshed model metadata,
        // while the common success snapshot carries fast/verbosity patches.
        let accepted = self.acceptedSettingsPatchResultsByTarget[target]
        let successful = self.lastSuccessfulSettingsPatchResultsByTarget[target]
        return OpenClawChatModelPatchResult(
            key: patchResult?.key ?? accepted?.key ?? successful?.key ?? target.canonicalSessionKey,
            modelProvider: patchResult?.modelProvider ?? accepted?.modelProvider ?? successful?.modelProvider,
            model: patchResult?.model ?? accepted?.model ?? successful?.model,
            thinkingLevel: acceptedLevel,
            thinkingLevels: patchResult?.thinkingLevels ?? accepted?.thinkingLevels ?? successful?.thinkingLevels,
            fastMode: patchResult?.fastMode ?? successful?.fastMode ?? accepted?.fastMode,
            effectiveFastMode: patchResult?.effectiveFastMode ?? successful?.effectiveFastMode
                ?? accepted?.effectiveFastMode,
            verboseLevel: patchResult?.verboseLevel ?? successful?.verboseLevel ?? accepted?.verboseLevel)
    }

    private func reconcileThinkingPreferenceRequests() {
        let requestIDs = self.thinkingPreferenceRequests.keys.sorted(by: >)
        let resolved = requestIDs.compactMap { requestID -> ThinkingPreferenceState? in
            switch self.thinkingPreferenceRequests[requestID] {
            case let .pending(state), let .succeeded(state): state
            case .failed, .none: nil
            }
        }.first ?? self.confirmedThinkingPreference
        if resolved != self.emittedThinkingPreference {
            self.emittedThinkingPreference = resolved
            self.onThinkingLevelChanged?(resolved.level)
            self.onThinkingPreferenceChanged?(resolved.isExplicit ? resolved.level : nil)
        }
        guard !self.thinkingPreferenceRequests.values.contains(where: {
            if case .pending = $0 { return true }
            return false
        }) else { return }
        self.confirmedThinkingPreference = resolved
        self.thinkingPreferenceRequests.removeAll()
    }

    func recordAuthoritativeInheritedThinkingPreference(_ level: String) {
        self.confirmedThinkingPreference = ThinkingPreferenceState(level: level, isExplicit: false)
    }

    func updateCurrentSessionThinkingLevels(
        _ thinkingLevels: [OpenClawChatThinkingLevelOption],
        sessionKey: String,
        exactMatchOnly: Bool = false)
    {
        let index = exactMatchOnly
            ? sessions.firstIndex(where: { $0.key == sessionKey })
            : sessionIndexForModelState(sessionKey: sessionKey)
        guard let index else { return }
        sessions[index].thinkingLevels = thinkingLevels
        sessions[index].thinkingOptions = thinkingLevels.map(\.label)
    }

    /// Agent-qualified keys keep an immutable owner even when their main-session
    /// contract changes. Bare aliases cannot safely identify an inactive row.
    func inactiveSettingsStateKey(for target: ModelPatchTarget) -> String? {
        if target.agentID != nil || target.sessionRoutingContract != nil {
            guard OpenClawChatSessionKey.agentID(from: target.canonicalSessionKey) != nil else { return nil }
        }
        return target.canonicalSessionKey
    }

    func updateCurrentSessionThinkingLevel(
        _ thinkingLevel: String?,
        sessionKey: String,
        exactMatchOnly: Bool = false)
    {
        let index = exactMatchOnly
            ? sessions.firstIndex(where: { $0.key == sessionKey })
            : sessionIndexForModelState(sessionKey: sessionKey)
        guard let index else { return }
        sessions[index].thinkingLevel = thinkingLevel
    }

    func effectiveThinkingLevelForSend(
        _ storedLevel: String,
        sessionKey: String? = nil,
        canonicalSessionKey: String? = nil,
        agentID: String? = nil,
        sessionRoutingContract: String? = nil) -> String
    {
        let usesCurrentSession = sessionKey == nil ||
            (sessionKey == self.sessionKey && canonicalSessionKey == nil && agentID == nil)
        let session: OpenClawChatSessionEntry?
        let showsPicker: Bool
        let target: ModelPatchTarget
        if !usesCurrentSession, let sessionKey {
            target = modelPatchTarget(
                sessionKey: sessionKey,
                canonicalSessionKey: canonicalSessionKey,
                agentID: agentID,
                sessionRoutingContract: sessionRoutingContract)
            // Sessions absent from the loaded list resolve to no metadata and fail
            // open for existing levels. Ultra is new enough that an older or
            // truncated list must use its shipped High meaning until advertised.
            session = sessionEntryForThinking(
                sessionKey: sessionKey,
                canonicalSessionKey: canonicalSessionKey,
                agentID: agentID)
            guard session != nil else {
                let fallback = self.thinkingLevelWithoutGatewayMetadata(
                    storedLevel,
                    target: target,
                    session: nil)
                return fallback == "ultra" ? "high" : (fallback ?? storedLevel)
            }
            showsPicker = self.thinkingPickerIsAvailable(
                for: session,
                modelChoice: self.sessionModelChoice(for: session))
        } else {
            session = currentSessionEntry()
            showsPicker = showsThinkingPicker
            target = currentModelPatchTarget()
        }
        guard showsPicker else { return "off" }
        let resolved = self.resolvedThinkingLevelOptions(for: session)
        guard resolved.isGatewayMetadata else {
            return self.thinkingLevelWithoutGatewayMetadata(
                storedLevel,
                target: target,
                session: session) ?? storedLevel
        }
        return Self.normalizedThinkingLevel(
            storedLevel,
            options: resolved.options,
            fallback: session?.thinkingLevel) ?? storedLevel
    }

    func syncThinkingLevelOptions() {
        let currentSession = currentSessionEntry()
        showsThinkingPicker = self.thinkingPickerIsAvailable(
            for: currentSession,
            modelChoice: self.selectedModelChoice(for: currentSession))

        let resolved = self.resolvedThinkingLevelOptions(for: currentSession)
        var options = resolved.options
        let target = currentModelPatchTarget()
        let preferredLevel = self.prefersExplicitThinkingLevel
            ? self.preferredThinkingLevel
            : Self.normalizedThinkingLevel(currentSession?.thinkingLevel) ?? self.preferredThinkingLevel
        let preferred: String? = if resolved.isGatewayMetadata {
            Self.normalizedThinkingLevel(
                preferredLevel,
                options: options,
                fallback: currentSession?.thinkingLevel)
        } else {
            self.thinkingLevelWithoutGatewayMetadata(
                preferredLevel,
                target: target,
                session: currentSession)
        }
        let current = preferred ?? Self.normalizedThinkingLevel(currentSession?.thinkingLevel)
        if let current {
            self.applyAdvertisedThinkingLevel(current)
            options = Self.withCurrentThinkingOption(options, current: current)
        }
        thinkingLevelOptions = options
    }

    private func thinkingLevelWithoutGatewayMetadata(
        _ level: String,
        target: ModelPatchTarget,
        session: OpenClawChatSessionEntry?) -> String?
    {
        let preferred = Self.normalizedThinkingLevel(level)
        guard preferred == "ultra" else { return preferred }
        if let patched = Self.normalizedThinkingLevel(
            successfulModelPatchResult(for: target, session: session)?.thinkingLevel)
        {
            return patched
        }
        // Older gateways accept the legacy Ultra spelling as High but do not
        // return capability metadata. Never advertise/send more than they run.
        if completedModelPatchTargets.contains(target) {
            return "high"
        }
        return preferred
    }

    private func thinkingPickerIsAvailable(
        for session: OpenClawChatSessionEntry?,
        modelChoice: OpenClawChatModelChoice?) -> Bool
    {
        let resolved = self.resolvedThinkingLevelOptions(for: session)
        let gatewayAllowsOnlyOff = resolved.isGatewayMetadata &&
            resolved.options.allSatisfy { $0.id == "off" }
        return !gatewayAllowsOnlyOff && modelChoice?.reasoning != false
    }

    private struct ThinkingLevelOptionsResolution {
        let options: [OpenClawChatThinkingLevelOption]
        let isGatewayMetadata: Bool
    }

    private func resolvedThinkingLevelOptions(
        for currentSession: OpenClawChatSessionEntry?) -> ThinkingLevelOptionsResolution
    {
        if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty {
            return ThinkingLevelOptionsResolution(options: levels, isGatewayMetadata: true)
        }

        let defaultsMatch = currentSession.map {
            Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults)
        } ?? true

        if defaultsMatch,
           let levels = Self.normalizedThinkingLevelOptions(sessionDefaults?.thinkingLevels),
           !levels.isEmpty
        {
            return ThinkingLevelOptionsResolution(options: levels, isGatewayMetadata: true)
        }

        if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty {
            return ThinkingLevelOptionsResolution(options: options, isGatewayMetadata: true)
        }

        if defaultsMatch,
           let options = Self.thinkingOptions(from: sessionDefaults?.thinkingOptions),
           !options.isEmpty
        {
            return ThinkingLevelOptionsResolution(options: options, isGatewayMetadata: true)
        }

        return ThinkingLevelOptionsResolution(options: Self.baseThinkingLevelOptions, isGatewayMetadata: false)
    }

    private func selectedModelChoice(
        for currentSession: OpenClawChatSessionEntry?) -> OpenClawChatModelChoice?
    {
        if modelSelectionID != Self.defaultModelSelectionID {
            return modelChoices.first(where: { $0.selectionID == self.modelSelectionID })
        }

        return self.sessionModelChoice(for: currentSession)
    }

    private func sessionModelChoice(
        for currentSession: OpenClawChatSessionEntry?) -> OpenClawChatModelChoice?
    {
        if Self.normalizedModelID(currentSession?.model) != nil {
            return self.modelChoice(modelID: currentSession?.model, provider: currentSession?.modelProvider)
        }
        return self.modelChoice(modelID: sessionDefaults?.model, provider: sessionDefaults?.modelProvider)
    }

    private func modelChoice(modelID: String?, provider: String?) -> OpenClawChatModelChoice? {
        guard let modelID = Self.normalizedModelID(modelID) else { return nil }
        let provider = provider?.trimmingCharacters(in: .whitespacesAndNewlines)
        if let provider, !provider.isEmpty {
            let prefix = "\(provider)/"
            let selectionID = modelID.hasPrefix(prefix) ? modelID : "\(prefix)\(modelID)"
            return modelChoices.first(where: {
                $0.selectionID == selectionID ||
                    ($0.modelID == modelID && $0.provider == provider)
            })
        }

        let matches = modelChoices.filter { $0.selectionID == modelID || $0.modelID == modelID }
        return matches.count == 1 ? matches[0] : nil
    }

    private static func normalizedModelID(_ modelID: String?) -> String? {
        let trimmed = modelID?.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let trimmed, !trimmed.isEmpty else { return nil }
        return trimmed
    }

    private static func sessionModelMatchesDefaults(
        _ session: OpenClawChatSessionEntry,
        defaults: OpenClawChatSessionsDefaults?) -> Bool
    {
        let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider
        let modelMatches = session.model == nil || session.model == defaults?.model
        return providerMatches && modelMatches
    }

    private static func normalizedThinkingLevelOptions(
        _ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]?
    {
        guard let levels else { return nil }
        return Self.dedupedThinkingOptions(
            levels.compactMap { level in
                guard let id = Self.normalizedThinkingLevel(level.id) else { return nil }
                let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines)
                return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label)
            })
    }

    private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? {
        guard let labels else { return nil }
        return Self.dedupedThinkingOptions(
            labels.compactMap { label in
                guard let id = Self.normalizedThinkingLevel(label) else { return nil }
                let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
                return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed)
            })
    }

    static func withCurrentThinkingOption(
        _ options: [OpenClawChatThinkingLevelOption],
        current: String) -> [OpenClawChatThinkingLevelOption]
    {
        guard !options.contains(where: { $0.id == current }) else { return options }
        return options + [OpenClawChatThinkingLevelOption(id: current, label: current)]
    }

    private static func dedupedThinkingOptions(
        _ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption]
    {
        var result: [OpenClawChatThinkingLevelOption] = []
        var seen = Set<String>()
        for option in options {
            guard !option.id.isEmpty, !seen.contains(option.id) else { continue }
            seen.insert(option.id)
            result.append(option)
        }
        return result
    }

    static func normalizedThinkingLevel(_ level: String?) -> String? {
        guard let level else { return nil }
        let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        guard !trimmed.isEmpty else { return nil }
        let collapsed = trimmed.replacingOccurrences(
            of: "[\\s_-]+",
            with: "",
            options: .regularExpression)

        switch collapsed {
        case "adaptive", "auto":
            return "adaptive"
        case "max":
            return "max"
        case "ultra":
            return "ultra"
        case "xhigh", "extrahigh":
            return "xhigh"
        case "off", "none":
            return "off"
        case "on", "enable", "enabled":
            return "low"
        case "min", "minimal", "think":
            return "minimal"
        case "low", "thinkhard":
            return "low"
        case "mid", "med", "medium", "thinkharder", "harder":
            return "medium"
        case "high", "ultrathink", "thinkhardest", "highest":
            return "high"
        default:
            return trimmed
        }
    }

    static func normalizedThinkingLevel(
        _ level: String?,
        options: [OpenClawChatThinkingLevelOption],
        fallback: String? = nil) -> String?
    {
        guard let normalized = normalizedThinkingLevel(level) else { return nil }
        guard normalized == "ultra" else { return normalized }
        let advertised = options.compactMap { self.normalizedThinkingLevel($0.id) }
        if advertised.contains("ultra") {
            return "ultra"
        }
        if let fallback = normalizedThinkingLevel(fallback), advertised.contains(fallback) {
            return fallback
        }
        return advertised
            .filter { $0 != "off" }
            .max { self.thinkingLevelRank($0) < self.thinkingLevelRank($1) }
    }

    private static func thinkingLevelRank(_ level: String) -> Int {
        switch level {
        case "off": 0
        case "minimal": 10
        case "low": 20
        case "medium", "adaptive": 30
        case "high": 40
        case "xhigh": 60
        case "max": 70
        case "ultra": 80
        default: -1
        }
    }
}
