import Foundation
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif

public struct KiroUsageSnapshot: Sendable {
    public let planName: String
    public let displayPlanName: String
    public let accountEmail: String?
    public let authMethod: String?
    public let creditsUsed: Double
    public let creditsTotal: Double
    public let creditsPercent: Double
    public let bonusCreditsUsed: Double?
    public let bonusCreditsTotal: Double?
    public let bonusExpiryDays: Int?
    public let overagesStatus: String?
    public let overageCreditsUsed: Double?
    public let estimatedOverageCostUSD: Double?
    public let manageURL: String?
    public let contextUsage: KiroContextUsageSnapshot?
    public let resetsAt: Date?
    public let updatedAt: Date

    public init(
        planName: String,
        displayPlanName: String? = nil,
        accountEmail: String? = nil,
        authMethod: String? = nil,
        creditsUsed: Double,
        creditsTotal: Double,
        creditsPercent: Double,
        bonusCreditsUsed: Double?,
        bonusCreditsTotal: Double?,
        bonusExpiryDays: Int?,
        overagesStatus: String? = nil,
        overageCreditsUsed: Double? = nil,
        estimatedOverageCostUSD: Double? = nil,
        manageURL: String? = nil,
        contextUsage: KiroContextUsageSnapshot? = nil,
        resetsAt: Date?,
        updatedAt: Date)
    {
        self.planName = planName
        self.displayPlanName = displayPlanName ?? KiroStatusProbe.displayPlanName(planName)
        self.accountEmail = accountEmail
        self.authMethod = authMethod
        self.creditsUsed = creditsUsed
        self.creditsTotal = creditsTotal
        self.creditsPercent = creditsPercent
        self.bonusCreditsUsed = bonusCreditsUsed
        self.bonusCreditsTotal = bonusCreditsTotal
        self.bonusExpiryDays = bonusExpiryDays
        self.overagesStatus = overagesStatus
        self.overageCreditsUsed = overageCreditsUsed
        self.estimatedOverageCostUSD = estimatedOverageCostUSD
        self.manageURL = manageURL
        self.contextUsage = contextUsage
        self.resetsAt = resetsAt
        self.updatedAt = updatedAt
    }

    public func toUsageSnapshot() -> UsageSnapshot {
        let primary = RateWindow(
            usedPercent: self.creditsPercent,
            windowMinutes: nil,
            resetsAt: self.resetsAt,
            resetDescription: nil)

        var secondary: RateWindow?
        if let bonusUsed = self.bonusCreditsUsed,
           let bonusTotal = self.bonusCreditsTotal,
           bonusTotal > 0
        {
            let bonusPercent = (bonusUsed / bonusTotal) * 100.0
            var expiryDate: Date?
            if let days = self.bonusExpiryDays {
                expiryDate = Calendar.current.date(byAdding: .day, value: days, to: Date())
            }
            secondary = RateWindow(
                usedPercent: bonusPercent,
                windowMinutes: nil,
                resetsAt: expiryDate,
                resetDescription: self.bonusExpiryDays.map { "expires in \($0)d" })
        }

        let identity = ProviderIdentitySnapshot(
            providerID: .kiro,
            accountEmail: self.accountEmail,
            accountOrganization: nil,
            loginMethod: self.authMethod)

        var detailRows: [ProviderDetailSection.Row] = [
            .makeRow(label: "Plan", value: self.displayPlanName),
            .makeRow(label: "Credits left", value: UsageFormatter.kiroCreditNumber(self.creditsRemaining)),
            .makeRow(label: "Credits used", value: UsageFormatter.kiroCreditNumber(self.creditsUsed)),
            .makeRow(label: "Credits total", value: UsageFormatter.kiroCreditNumber(self.creditsTotal)),
        ]
        if let remaining = self.bonusCreditsRemaining, let total = self.bonusCreditsTotal {
            detailRows.append(.makeRow(
                label: "Bonus credits left",
                value: UsageFormatter.kiroCreditNumber(remaining),
                secondaryValue: [
                    "of \(UsageFormatter.kiroCreditNumber(total))",
                    self.bonusExpiryDays.map { "expires in \($0)d" },
                ].compactMap(\.self).joined(separator: " · ")))
        }
        if let overagesStatus = self.overagesStatus {
            detailRows.append(.makeRow(label: "Overages", value: overagesStatus))
        }
        let overagesEnabled = self.overagesStatus?
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .lowercased()
            .hasPrefix("enabled") == true
        if overagesEnabled, let overageCreditsUsed = self.overageCreditsUsed {
            detailRows.append(.makeRow(
                label: "Overage usage",
                value: "\(UsageFormatter.kiroCreditNumber(overageCreditsUsed)) credits"))
        }
        if overagesEnabled, let estimatedOverageCostUSD = self.estimatedOverageCostUSD {
            detailRows.append(.makeRow(
                label: "Overage cost",
                value: UsageFormatter.usdString(estimatedOverageCostUSD)))
        }
        if let contextUsage = self.contextUsage {
            detailRows.append(.makeRow(
                label: "Context used",
                value: String(format: "%.1f%%", contextUsage.totalPercentUsed)))
            let contextParts: [(String, Double?)] = [
                ("Context files", contextUsage.contextFilesPercent),
                ("Tools", contextUsage.toolsPercent),
                ("Kiro responses", contextUsage.kiroResponsesPercent),
                ("Prompts", contextUsage.promptsPercent),
            ]
            detailRows.append(contentsOf: contextParts.compactMap { label, value in
                value.map { .makeRow(label: label, value: String(format: "%.1f%%", $0)) }
            })
        }
        if let manageURL = self.manageURL {
            detailRows.append(.makeRow(label: "Manage", value: manageURL))
        }

        return UsageSnapshot(
            primary: primary,
            secondary: secondary,
            tertiary: nil,
            providerCost: nil,
            details: [.makeSection(title: "Usage", rows: detailRows)],
            updatedAt: self.updatedAt,
            identity: identity)
    }

    public var creditsRemaining: Double {
        max(0, self.creditsTotal - self.creditsUsed)
    }

    public var bonusCreditsRemaining: Double? {
        guard let bonusCreditsUsed, let bonusCreditsTotal else { return nil }
        return max(0, bonusCreditsTotal - bonusCreditsUsed)
    }
}

public struct KiroContextUsageSnapshot: Codable, Equatable, Sendable {
    public let totalPercentUsed: Double
    public let contextFilesPercent: Double?
    public let toolsPercent: Double?
    public let kiroResponsesPercent: Double?
    public let promptsPercent: Double?

    public init(
        totalPercentUsed: Double,
        contextFilesPercent: Double?,
        toolsPercent: Double?,
        kiroResponsesPercent: Double?,
        promptsPercent: Double?)
    {
        self.totalPercentUsed = totalPercentUsed
        self.contextFilesPercent = contextFilesPercent
        self.toolsPercent = toolsPercent
        self.kiroResponsesPercent = kiroResponsesPercent
        self.promptsPercent = promptsPercent
    }
}

public enum KiroStatusProbeError: LocalizedError, Sendable {
    case cliNotFound
    case notLoggedIn
    case cliFailed(String)
    case parseError(String)
    case timeout

    public var errorDescription: String? {
        switch self {
        case .cliNotFound:
            "kiro-cli not found. Install it from https://kiro.dev"
        case .notLoggedIn:
            "Not logged in to Kiro. Run 'kiro-cli login' first."
        case let .cliFailed(message):
            message
        case let .parseError(msg):
            "Failed to parse Kiro usage: \(msg)"
        case .timeout:
            "Kiro CLI timed out."
        }
    }
}

public struct KiroStatusProbe: Sendable {
    struct PipeProcessRegistry: Sendable {
        let beginLaunch: @Sendable () -> Bool
        let endLaunch: @Sendable () -> Void
        let register: @Sendable (pid_t, String) -> Bool
        let updateProcessGroup: @Sendable (pid_t, pid_t?) -> Void
        let unregister: @Sendable (pid_t) -> Void

        static let live = Self(
            beginLaunch: { TTYCommandRunner.beginActiveProcessLaunchForAppShutdown() },
            endLaunch: { TTYCommandRunner.endActiveProcessLaunchForAppShutdown() },
            register: { pid, binary in
                TTYCommandRunner.registerActiveProcessForAppShutdown(pid: pid, binary: binary)
            },
            updateProcessGroup: { pid, processGroup in
                TTYCommandRunner.updateActiveProcessGroupForAppShutdown(pid: pid, processGroup: processGroup)
            },
            unregister: { pid in
                TTYCommandRunner.unregisterActiveProcessForAppShutdown(pid: pid)
            })
    }

    private let cliBinaryResolver: @Sendable () -> String?
    private let accountProbeTimeout: TimeInterval
    private let usageProbeTimeout: TimeInterval
    private let contextProbeTimeout: TimeInterval
    private let pipeTimeoutCap: TimeInterval
    private let pipeProcessRegistry: PipeProcessRegistry

    public init() {
        self.cliBinaryResolver = { TTYCommandRunner.which("kiro-cli") }
        self.accountProbeTimeout = 3.0
        self.usageProbeTimeout = 20.0
        self.contextProbeTimeout = 8.0
        self.pipeTimeoutCap = 5.0
        self.pipeProcessRegistry = .live
    }

    init(
        cliBinaryResolver: @escaping @Sendable () -> String?,
        accountProbeTimeout: TimeInterval = 3.0,
        usageProbeTimeout: TimeInterval = 20.0,
        contextProbeTimeout: TimeInterval = 8.0,
        pipeTimeoutCap: TimeInterval = 5.0,
        pipeProcessRegistry: PipeProcessRegistry = .live)
    {
        self.cliBinaryResolver = cliBinaryResolver
        self.accountProbeTimeout = accountProbeTimeout
        self.usageProbeTimeout = usageProbeTimeout
        self.contextProbeTimeout = contextProbeTimeout
        self.pipeTimeoutCap = pipeTimeoutCap
        self.pipeProcessRegistry = pipeProcessRegistry
    }

    private static let logger = CodexBarLog.logger(LogCategories.provider(.kiro))

    public static func detectVersion() -> String? {
        guard let path = TTYCommandRunner.which("kiro-cli"),
              let output = ProviderVersionDetector.run(
                  path: path,
                  args: ["--version"],
                  mergeStandardError: true)
        else {
            self.logger.debug("kiro-cli version detection failed")
            return nil
        }
        // Output is like "kiro-cli 1.23.1"
        if output.hasPrefix("kiro-cli ") {
            return String(output.dropFirst("kiro-cli ".count))
        }
        return output
    }

    public func fetch() async throws -> KiroUsageSnapshot {
        let accountTask = Task { await self.fetchAccountStatus() }

        let output: String
        do {
            output = try await self.runUsageCommand()
        } catch is CancellationError {
            accountTask.cancel()
            _ = await accountTask.value
            throw CancellationError()
        } catch {
            if try await self.awaitAccountStatus(accountTask) == .notLoggedIn {
                throw KiroStatusProbeError.notLoggedIn
            }
            throw error
        }

        var contextUsage: KiroContextUsageSnapshot?
        do {
            contextUsage = try await self.fetchContextUsage()
        } catch is CancellationError {
            accountTask.cancel()
            _ = await accountTask.value
            throw CancellationError()
        } catch {
            Self.logger.debug("Kiro context usage probe failed: \(error.localizedDescription)")
        }

        let accountStatus = try await self.awaitAccountStatus(accountTask)
        let accountInfo = accountStatus.account
        do {
            return try self.parse(
                output: output,
                accountEmail: accountInfo?.email,
                authMethod: accountInfo?.authMethod,
                contextUsage: contextUsage)
        } catch KiroStatusProbeError.parseError where accountStatus == .notLoggedIn {
            throw KiroStatusProbeError.notLoggedIn
        }
    }

    struct KiroAccountInfo: Equatable {
        let authMethod: String?
        let email: String?
    }

    struct KiroCLIResult: Sendable {
        let stdout: String
        let stderr: String
        let terminationStatus: Int32
        let stoppedAfterOutput: Bool

        var output: String {
            let stdout = self.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
            let stderr = self.stderr.trimmingCharacters(in: .whitespacesAndNewlines)
            return [stdout, stderr]
                .filter { !$0.isEmpty }
                .joined(separator: "\n")
        }
    }

    private enum KiroCommandKind: String, Sendable {
        case whoAmI = "whoami"
        case usage
        case context
    }

    private enum KiroTransportOutcome: Sendable {
        case result(KiroCLIResult)
        case failure(KiroStatusProbeError)
        case cancelled
    }

    private enum KiroTransportEvent: Sendable {
        case pipe(KiroTransportOutcome)
        case pty(KiroTransportOutcome)
        case fallbackReady
    }

    private final class KiroPipeActivityState: @unchecked Sendable {
        private let lock = NSLock()
        private var lastActivity = ContinuousClock.now
        private var receivedOutput = false

        var lastActivityAt: ContinuousClock.Instant {
            self.lock.withLock { self.lastActivity }
        }

        var hasReceivedOutput: Bool {
            self.lock.withLock { self.receivedOutput }
        }

        func markActivity() {
            self.lock.withLock {
                self.lastActivity = .now
                self.receivedOutput = true
            }
        }
    }

    private final class KiroTransportCancellationState: @unchecked Sendable {
        private let lock = NSLock()
        private var cancelled = false

        var isCancelled: Bool {
            self.lock.withLock { self.cancelled }
        }

        func cancel() {
            self.lock.withLock { self.cancelled = true }
        }
    }

    private enum KiroAccountProbeStatus: Equatable {
        case account(KiroAccountInfo)
        case notLoggedIn
        case unavailable

        var account: KiroAccountInfo? {
            guard case let .account(info) = self else { return nil }
            return info
        }
    }

    private func fetchAccountStatus() async -> KiroAccountProbeStatus {
        do {
            return try await .account(self.ensureLoggedIn())
        } catch KiroStatusProbeError.notLoggedIn {
            return .notLoggedIn
        } catch {
            Self.logger.debug("Kiro account probe failed: \(error.localizedDescription)")
            return .unavailable
        }
    }

    private func awaitAccountStatus(
        _ task: Task<KiroAccountProbeStatus, Never>) async throws -> KiroAccountProbeStatus
    {
        let status = await withTaskCancellationHandler {
            await task.value
        } onCancel: {
            task.cancel()
        }
        try Task.checkCancellation()
        return status
    }

    private func ensureLoggedIn() async throws -> KiroAccountInfo {
        let result = try await self.runCommand(
            arguments: ["whoami"],
            timeout: self.accountProbeTimeout,
            idleTimeout: 1.5,
            kind: .whoAmI)
        if result.stoppedAfterOutput {
            if Self.isLoginRequired(result.output) {
                throw KiroStatusProbeError.notLoggedIn
            }
            throw KiroStatusProbeError.timeout
        }
        return try self.validateWhoAmIOutput(
            stdout: result.stdout,
            stderr: result.stderr,
            terminationStatus: result.terminationStatus)
    }

    func validateWhoAmIOutput(stdout: String, stderr: String, terminationStatus: Int32) throws -> KiroAccountInfo {
        let combined = KiroCLIResult(
            stdout: stdout,
            stderr: stderr,
            terminationStatus: terminationStatus,
            stoppedAfterOutput: false).output

        if Self.isLoginRequired(combined) {
            throw KiroStatusProbeError.notLoggedIn
        }

        if terminationStatus != 0 {
            let message = combined.isEmpty
                ? "Kiro CLI failed with status \(terminationStatus)."
                : combined
            throw KiroStatusProbeError.cliFailed(message)
        }

        if combined.isEmpty {
            throw KiroStatusProbeError.cliFailed("Kiro CLI whoami returned no output.")
        }

        return self.parseWhoAmIOutput(combined)
    }

    private static func isLoginRequired(_ output: String) -> Bool {
        let lowered = Self.stripANSI(output).lowercased()
        return lowered.contains("not logged in")
            || lowered.contains("login required")
            || lowered.contains("failed to initialize auth portal")
            || lowered.contains("kiro-cli login")
            || lowered.contains("oauth error")
    }

    func parseWhoAmIOutput(_ output: String) -> KiroAccountInfo {
        let stripped = Self.stripANSI(output)
        var authMethod: String?
        var email: String?
        for rawLine in stripped.components(separatedBy: .newlines) {
            let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !line.isEmpty else { continue }
            if line.localizedCaseInsensitiveContains("logged in with") {
                authMethod = line.replacingOccurrences(
                    of: #"(?i)^\s*logged in with\s+"#,
                    with: "",
                    options: [.regularExpression])
                    .trimmingCharacters(in: .whitespacesAndNewlines)
            } else if line.localizedCaseInsensitiveContains("email:") {
                email = line.replacingOccurrences(
                    of: #"(?i)^\s*email:\s*"#,
                    with: "",
                    options: [.regularExpression])
                    .trimmingCharacters(in: .whitespacesAndNewlines)
            } else if email == nil,
                      !line.contains(" "),
                      line.contains("@")
            {
                email = line
            }
        }
        return KiroAccountInfo(
            authMethod: authMethod?.nilIfEmpty,
            email: email?.nilIfEmpty)
    }

    private func runUsageCommand() async throws -> String {
        let result = try await self.runCommand(
            arguments: ["chat", "--no-interactive", "/usage"],
            timeout: self.usageProbeTimeout,
            idleTimeout: 4.0,
            kind: .usage)
        let output = result.output
        if Self.isLoginRequired(output) {
            throw KiroStatusProbeError.notLoggedIn
        }

        try Self.validateCommandCompletion(
            result,
            command: "usage",
            allowIdleOutput: (try? self.parse(output: output)) != nil)
        return output
    }

    private func fetchContextUsage() async throws -> KiroContextUsageSnapshot? {
        let result = try await self.runCommand(
            arguments: ["chat", "--no-interactive", "/context"],
            timeout: self.contextProbeTimeout,
            idleTimeout: 3.0,
            kind: .context)
        let contextUsage = self.parseContextUsage(output: result.output)
        try Self.validateCommandCompletion(
            result,
            command: "context",
            allowIdleOutput: contextUsage != nil)
        return contextUsage
    }

    private func runViaPipe(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        activityState state: KiroPipeActivityState) async throws -> KiroCLIResult
    {
        try Task.checkCancellation()
        guard let binary = self.cliBinaryResolver() else {
            throw KiroStatusProbeError.cliNotFound
        }

        let stdoutPipe = Pipe()
        let stderrPipe = Pipe()

        var env = TTYCommandRunner.enrichedEnvironment()
        env["TERM"] = "xterm-256color"

        guard self.pipeProcessRegistry.beginLaunch() else {
            throw KiroStatusProbeError.cliFailed("App shutdown in progress")
        }
        var launchReservationHeld = true
        defer {
            if launchReservationHeld {
                self.pipeProcessRegistry.endLaunch()
            }
        }

        let stdoutCapture = ProcessPipeCapture(pipe: stdoutPipe, onData: { state.markActivity() })
        let stderrCapture = ProcessPipeCapture(pipe: stderrPipe, onData: { state.markActivity() })
        stdoutCapture.start()
        stderrCapture.start()

        let process: SpawnedProcessGroup
        do {
            try Task.checkCancellation()
            process = try SpawnedProcessGroup.launch(
                binary: binary,
                arguments: arguments,
                environment: env,
                stdoutPipe: stdoutPipe,
                stderrPipe: stderrPipe)
        } catch {
            stdoutCapture.stop()
            stderrCapture.stop()
            throw error
        }

        guard self.pipeProcessRegistry.register(
            process.pid,
            URL(fileURLWithPath: binary).lastPathComponent)
        else {
            await Self.terminateCancelledPipeProcess(
                process,
                stdoutCapture: stdoutCapture,
                stderrCapture: stderrCapture)
            throw KiroStatusProbeError.cliFailed("App shutdown in progress")
        }
        self.pipeProcessRegistry.updateProcessGroup(process.pid, process.processGroup)
        self.pipeProcessRegistry.endLaunch()
        launchReservationHeld = false
        defer { self.pipeProcessRegistry.unregister(process.pid) }

        let clock = ContinuousClock()
        let deadline = clock.now.advanced(by: .seconds(max(0, timeout)))
        var didHitDeadline = false
        var didTerminateForIdle = false

        do {
            while process.isRunning {
                try Task.checkCancellation()
                let now = clock.now
                if now >= deadline {
                    didHitDeadline = true
                    break
                }
                if state.hasReceivedOutput,
                   state.lastActivityAt.duration(to: now) >= .seconds(max(0, idleTimeout))
                {
                    didTerminateForIdle = true
                    break
                }
                try await Task.sleep(for: .milliseconds(100))
            }
        } catch {
            await Self.terminateCancelledPipeProcess(
                process,
                stdoutCapture: stdoutCapture,
                stderrCapture: stderrCapture)
            throw error
        }

        if process.isRunning {
            await process.terminate()
            guard !process.isRunning else {
                stdoutCapture.stop()
                stderrCapture.stop()
                throw KiroStatusProbeError.timeout
            }
            if !state.hasReceivedOutput {
                stdoutCapture.stop()
                stderrCapture.stop()
                throw KiroStatusProbeError.timeout
            }
        }
        await process.terminateResidualProcesses()

        async let stdoutDataTask = stdoutCapture.finish(timeout: .seconds(1))
        async let stderrDataTask = stderrCapture.finish(timeout: .seconds(1))
        let (stdoutData, stderrData) = await (stdoutDataTask, stderrDataTask)
        if !stdoutCapture.reachedEOF || !stderrCapture.reachedEOF {
            await process.terminateResidualProcesses()
        }
        await process.finish()
        guard let terminationStatus = process.terminationStatus else {
            throw KiroStatusProbeError.timeout
        }
        return KiroCLIResult(
            stdout: ProcessPipeCapture.decodeUTF8(stdoutData),
            stderr: ProcessPipeCapture.decodeUTF8(stderrData),
            terminationStatus: terminationStatus,
            stoppedAfterOutput: didTerminateForIdle || didHitDeadline)
    }

    private static func terminateCancelledPipeProcess(
        _ process: SpawnedProcessGroup,
        stdoutCapture: ProcessPipeCapture,
        stderrCapture: ProcessPipeCapture) async
    {
        let cleanupTask = Task.detached(priority: .userInitiated) {
            await process.terminate()
            stdoutCapture.stop()
            stderrCapture.stop()
        }
        await cleanupTask.value
    }

    private func runViaPTY(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        cancellationState: KiroTransportCancellationState) throws -> KiroCLIResult
    {
        guard let binary = self.cliBinaryResolver() else {
            throw KiroStatusProbeError.cliNotFound
        }
        do {
            let result = try TTYCommandRunner().run(
                binary: binary,
                send: "",
                options: TTYCommandRunner.Options(
                    rows: 50,
                    cols: 200,
                    timeout: timeout,
                    idleTimeout: idleTimeout,
                    extraArgs: arguments,
                    returnOnEmptyProcessExit: true,
                    cancellationCheck: {
                        cancellationState.isCancelled || Task<Never, Never>.isCancelled
                    }))
            switch result.completion {
            case let .processExited(status):
                return KiroCLIResult(
                    stdout: result.text,
                    stderr: "",
                    terminationStatus: status,
                    stoppedAfterOutput: false)
            case .idleTimeout:
                return KiroCLIResult(
                    stdout: result.text,
                    stderr: "",
                    terminationStatus: 0,
                    stoppedAfterOutput: true)
            case .outputCondition, .deadlineExceeded:
                throw KiroStatusProbeError.timeout
            }
        } catch TTYCommandRunner.Error.binaryNotFound {
            throw KiroStatusProbeError.cliNotFound
        } catch TTYCommandRunner.Error.timedOut {
            throw KiroStatusProbeError.timeout
        } catch let TTYCommandRunner.Error.launchFailed(message) {
            throw KiroStatusProbeError.cliFailed(message)
        }
    }

    private func runViaPTYAsync(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        cancellationState: KiroTransportCancellationState) async throws -> KiroCLIResult
    {
        let task = Task.detached(priority: .userInitiated) {
            try self.runViaPTY(
                arguments: arguments,
                timeout: timeout,
                idleTimeout: idleTimeout,
                cancellationState: cancellationState)
        }
        return try await withTaskCancellationHandler {
            try await task.value
        } onCancel: {
            cancellationState.cancel()
        }
    }

    private static func validateCommandCompletion(
        _ result: KiroCLIResult,
        command: String,
        allowIdleOutput: Bool) throws
    {
        if result.stoppedAfterOutput {
            guard allowIdleOutput else { throw KiroStatusProbeError.timeout }
            return
        }
        guard result.terminationStatus == 0 else {
            let message = Self.stripANSI(result.output).trimmingCharacters(in: .whitespacesAndNewlines)
            throw KiroStatusProbeError.cliFailed(
                message.isEmpty
                    ? "Kiro CLI \(command) failed with status \(result.terminationStatus)."
                    : message)
        }
    }

    func parse(
        output: String,
        accountEmail: String? = nil,
        authMethod: String? = nil,
        contextUsage: KiroContextUsageSnapshot? = nil) throws -> KiroUsageSnapshot
    {
        let stripped = Self.stripANSI(output)

        let trimmed = stripped.trimmingCharacters(in: .whitespacesAndNewlines)
        if trimmed.isEmpty {
            throw KiroStatusProbeError.parseError("Empty output from kiro-cli.")
        }

        let lowered = stripped.lowercased()
        if lowered.contains("could not retrieve usage information") {
            throw KiroStatusProbeError.parseError("Kiro CLI could not retrieve usage information.")
        }

        // Check for not logged in
        if lowered.contains("not logged in")
            || lowered.contains("login required")
            || lowered.contains("failed to initialize auth portal")
            || lowered.contains("kiro-cli login")
            || lowered.contains("oauth error")
        {
            throw KiroStatusProbeError.notLoggedIn
        }

        // Track which key patterns matched to detect format changes
        var matchedPercent = false
        var matchedCredits = false
        var matchedNewFormat = false

        let parsedPlan = Self.parsePlanName(from: stripped)
        let planName = parsedPlan.name
        matchedNewFormat = parsedPlan.matchedNewFormat

        // Check if this is a managed plan with no usage data
        let isManagedPlan = lowered.contains("managed by admin")
            || lowered.contains("managed by organization")

        let resetsAt = Self.parseResetDate(in: stripped)

        // Parse credits percentage from "████...█ X%"
        var creditsPercent: Double = 0
        if let percentMatch = stripped.range(of: #"█+\s*(\d+)%"#, options: .regularExpression) {
            let percentStr = String(stripped[percentMatch])
            if let numMatch = percentStr.range(of: #"\d+"#, options: .regularExpression) {
                creditsPercent = Double(String(percentStr[numMatch])) ?? 0
                matchedPercent = true
            }
        }

        // Parse credits used/total from "(X.XX of Y covered in plan)"
        var creditsUsed: Double = 0
        var creditsTotal: Double = 50 // default free tier
        let creditsPattern = #"\((\d+\.?\d*)\s+of\s+(\d+)\s+covered"#
        if let creditsMatch = stripped.range(of: creditsPattern, options: .regularExpression) {
            let creditsStr = String(stripped[creditsMatch])
            let numbers = creditsStr.matches(of: /(\d+\.?\d*)/)
            if numbers.count >= 2 {
                creditsUsed = Double(String(numbers[0].output.1)) ?? 0
                creditsTotal = Double(String(numbers[1].output.1)) ?? 50
                matchedCredits = true
            }
        }
        if !matchedPercent, matchedCredits, creditsTotal > 0 {
            creditsPercent = (creditsUsed / creditsTotal) * 100.0
        }

        let bonusCredits = Self.parseBonusCredits(in: stripped)

        let overagesStatus = Self.firstCapture(
            in: stripped,
            pattern: #"(?i)Overages:\s*([^\n]+)"#)
            .map(Self.cleanInlineValue)
            .flatMap(\.nilIfEmpty)
        let overageCreditsUsed = Self.firstCapture(
            in: stripped,
            pattern: #"(?i)Credits used:\s*(\d+\.?\d*)"#)
            .flatMap(Double.init)
        let estimatedOverageCostUSD = Self.firstCapture(
            in: stripped,
            pattern: #"(?i)Est\.\s*cost:\s*\$?(\d+\.?\d*)\s*USD"#)
            .flatMap(Double.init)
        let manageURL = Self.firstCapture(
            in: stripped,
            pattern: #"https://app\.kiro\.dev/account/usage"#)

        // Managed plans in new format may omit usage metrics. Only fall back to zeros when
        // we did not parse any usage values, so we do not mask real metrics.
        if matchedNewFormat, isManagedPlan, !matchedPercent, !matchedCredits {
            // Managed plans don't expose credits; return snapshot with plan name only
            return KiroUsageSnapshot(
                planName: planName,
                displayPlanName: Self.displayPlanName(planName),
                accountEmail: accountEmail?.nilIfEmpty,
                authMethod: authMethod?.nilIfEmpty,
                creditsUsed: 0,
                creditsTotal: 0,
                creditsPercent: 0,
                bonusCreditsUsed: bonusCredits.used,
                bonusCreditsTotal: bonusCredits.total,
                bonusExpiryDays: bonusCredits.expiryDays,
                overagesStatus: overagesStatus,
                overageCreditsUsed: overageCreditsUsed,
                estimatedOverageCostUSD: estimatedOverageCostUSD,
                manageURL: manageURL,
                contextUsage: contextUsage,
                resetsAt: nil,
                updatedAt: Date())
        }

        // Require at least one key pattern to match to avoid silent failures.
        // Managed plans without usage data return early above.
        if !matchedPercent, !matchedCredits {
            throw KiroStatusProbeError.parseError(
                "No recognizable usage patterns found. Kiro CLI output format may have changed.")
        }

        return KiroUsageSnapshot(
            planName: planName,
            displayPlanName: Self.displayPlanName(planName),
            accountEmail: accountEmail?.nilIfEmpty,
            authMethod: authMethod?.nilIfEmpty,
            creditsUsed: creditsUsed,
            creditsTotal: creditsTotal,
            creditsPercent: creditsPercent,
            bonusCreditsUsed: bonusCredits.used,
            bonusCreditsTotal: bonusCredits.total,
            bonusExpiryDays: bonusCredits.expiryDays,
            overagesStatus: overagesStatus,
            overageCreditsUsed: overageCreditsUsed,
            estimatedOverageCostUSD: estimatedOverageCostUSD,
            manageURL: manageURL,
            contextUsage: contextUsage,
            resetsAt: resetsAt,
            updatedAt: Date())
    }

    func parseContextUsage(output: String) -> KiroContextUsageSnapshot? {
        let stripped = Self.stripANSI(output)
        guard let total = Self.firstCapture(
            in: stripped,
            pattern: #"(?i)Context window:\s*(\d+\.?\d*)%\s+used"#)
            .flatMap(Double.init)
        else {
            return nil
        }
        return KiroContextUsageSnapshot(
            totalPercentUsed: total,
            contextFilesPercent: Self.percent(after: "Context files", in: stripped),
            toolsPercent: Self.percent(after: "Tools", in: stripped),
            kiroResponsesPercent: Self.percent(after: "Kiro responses", in: stripped),
            promptsPercent: Self.percent(after: "Your prompts", in: stripped))
    }

    private static func stripANSI(_ text: String) -> String {
        // Remove ANSI escape sequences
        let pattern = #"\x1B\[[0-9;?]*[A-Za-z]|\x1B\].*?\x07"#
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
            return text
        }
        let range = NSRange(text.startIndex..., in: text)
        return regex.stringByReplacingMatches(in: text, options: [], range: range, withTemplate: "")
    }

    private static func parsePlanName(from text: String) -> (name: String, matchedNewFormat: Bool) {
        var planName = "Kiro"
        var matchedNewFormat = false

        // Parse plan name from "| KIRO FREE" or similar (legacy format)
        // Horizontal whitespace only ([ \t]) so the match cannot bridge a newline into the next line.
        if let planMatch = text.range(of: #"\|[ \t]*(KIRO[ \t]+\w+)"#, options: .regularExpression) {
            let raw = String(text[planMatch]).replacingOccurrences(of: "|", with: "")
            planName = raw.trimmingCharacters(in: .whitespaces)
        }

        // Parse plan name from "Estimated Usage | resets on 2026-06-01 | KIRO FREE" (kiro-cli 2.x)
        if let estimatedMatch = text.range(
            of: #"Estimated Usage[ \t]*\|[^\n|]*\|[ \t]*([A-Z][A-Z0-9 ]+)"#,
            options: .regularExpression)
        {
            let line = String(text[estimatedMatch])
            if let plan = line.split(separator: "|").last?.trimmingCharacters(in: .whitespacesAndNewlines),
               !plan.isEmpty
            {
                planName = plan
            }
        }

        // Parse plan name from "Plan: Q Developer Pro" (new format, kiro-cli 1.24+)
        if let newPlanMatch = text.range(of: #"Plan:[ \t]*(.+)"#, options: .regularExpression) {
            let line = String(text[newPlanMatch])
            let planLine = line.replacingOccurrences(of: "Plan:", with: "").trimmingCharacters(in: .whitespaces)
            if let firstLine = planLine.split(separator: "\n").first {
                planName = String(firstLine).trimmingCharacters(in: .whitespaces)
                matchedNewFormat = true
            }
        }

        return (planName, matchedNewFormat)
    }

    private static func parseResetDate(in text: String) -> Date? {
        guard let resetMatch = text.range(
            of: #"resets on (\d{4}-\d{2}-\d{2}|\d{2}/\d{2})"#,
            options: .regularExpression)
        else { return nil }

        let resetStr = String(text[resetMatch])
        guard let dateRange = resetStr.range(
            of: #"\d{4}-\d{2}-\d{2}|\d{2}/\d{2}"#,
            options: .regularExpression)
        else { return nil }

        return Self.parseResetDate(String(resetStr[dateRange]))
    }

    private static func parseBonusCredits(in text: String) -> (used: Double?, total: Double?, expiryDays: Int?) {
        var used: Double?
        var total: Double?
        var expiryDays: Int?
        if let bonusMatch = text.range(of: #"Bonus credits:\s*(\d+\.?\d*)/(\d+)"#, options: .regularExpression) {
            let bonusStr = String(text[bonusMatch])
            let numbers = bonusStr.matches(of: /(\d+\.?\d*)/)
            if numbers.count >= 2 {
                used = Double(String(numbers[0].output.1))
                total = Double(String(numbers[1].output.1))
            }
        }
        if let expiryMatch = text.range(of: #"expires in (\d+) days?"#, options: .regularExpression) {
            let expiryStr = String(text[expiryMatch])
            if let numMatch = expiryStr.range(of: #"\d+"#, options: .regularExpression) {
                expiryDays = Int(String(expiryStr[numMatch]))
            }
        }
        return (used, total, expiryDays)
    }

    private static func parseResetDate(_ dateStr: String) -> Date? {
        if dateStr.contains("-") {
            let formatter = DateFormatter()
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = Calendar.current.timeZone
            formatter.dateFormat = "yyyy-MM-dd"
            return formatter.date(from: dateStr)
        }

        // Format: MM/DD - assume current or next year
        let parts = dateStr.split(separator: "/")
        guard parts.count == 2,
              let month = Int(parts[0]),
              let day = Int(parts[1])
        else { return nil }

        let calendar = Calendar.current
        let now = Date()
        let currentYear = calendar.component(.year, from: now)

        var components = DateComponents()
        components.month = month
        components.day = day
        components.year = currentYear

        if let date = calendar.date(from: components), date > now {
            return date
        }

        // If the date is in the past, it's next year
        components.year = currentYear + 1
        return calendar.date(from: components)
    }

    public static func displayPlanName(_ planName: String) -> String {
        let cleaned = Self.cleanInlineValue(planName)
            .replacingOccurrences(of: #"\s+"#, with: " ", options: [.regularExpression])
            .trimmingCharacters(in: .whitespacesAndNewlines)
        guard cleaned.localizedCaseInsensitiveContains("KIRO") else {
            return cleaned.isEmpty ? planName : cleaned
        }
        return cleaned
            .split(separator: " ")
            .map { word in
                if word.caseInsensitiveCompare("KIRO") == .orderedSame {
                    return "Kiro"
                }
                return word.prefix(1).uppercased() + word.dropFirst().lowercased()
            }
            .joined(separator: " ")
    }

    private static func percent(after label: String, in text: String) -> Double? {
        let escaped = NSRegularExpression.escapedPattern(for: label)
        return self.firstCapture(
            in: text,
            pattern: #"(?i)"# + escaped + #"\s+(\d+\.?\d*)%"#)
            .flatMap(Double.init)
    }

    private static func firstCapture(in text: String, pattern: String) -> String? {
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return nil }
        let nsRange = NSRange(text.startIndex..<text.endIndex, in: text)
        guard let match = regex.firstMatch(in: text, options: [], range: nsRange) else { return nil }
        let captureIndex = match.numberOfRanges > 1 ? 1 : 0
        guard let range = Range(match.range(at: captureIndex), in: text) else { return nil }
        return String(text[range]).trimmingCharacters(in: .whitespacesAndNewlines)
    }

    private static func cleanInlineValue(_ text: String) -> String {
        self.stripANSI(text)
            .replacingOccurrences(of: #"\x1B|\[[0-9;?]*[A-Za-z]"#, with: "", options: [.regularExpression])
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

extension KiroStatusProbe {
    /// Recent Kiro CLIs can keep their TUI alive indefinitely under a PTY even with `--no-interactive`,
    /// while older releases emit no output through pipes. Prefer pipes and retain PTY as a bounded fallback.
    private func runCommand(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        kind: KiroCommandKind) async throws -> KiroCLIResult
    {
        let clock = ContinuousClock()
        let deadline = clock.now.advanced(by: .seconds(max(0, timeout)))
        let fallbackDelay = min(max(0, self.pipeTimeoutCap), max(0, timeout / 2))
        let pipeActivity = KiroPipeActivityState()
        let cancellationState = KiroTransportCancellationState()

        return try await withThrowingTaskGroup(of: KiroTransportEvent.self) { group in
            defer {
                cancellationState.cancel()
                group.cancelAll()
            }
            group.addTask {
                await .pipe(self.pipeOutcome(
                    arguments: arguments,
                    timeout: timeout,
                    idleTimeout: idleTimeout,
                    activityState: pipeActivity))
            }
            group.addTask {
                try? await Task.sleep(for: .seconds(fallbackDelay))
                return .fallbackReady
            }

            var ptyStarted = false
            var pipeFinishedWithoutAcceptedResult = false
            var pendingPTYResult: KiroCLIResult?
            var pendingPTYFailure: KiroStatusProbeError?
            while let event = try await group.next() {
                try Task.checkCancellation()
                switch event {
                case .fallbackReady:
                    guard !ptyStarted, !pipeActivity.hasReceivedOutput else { continue }
                    let remaining = Self.timeInterval(from: clock.now.duration(to: deadline))
                    guard remaining > 0 else { continue }
                    ptyStarted = true
                    group.addTask {
                        await .pty(self.ptyOutcome(
                            arguments: arguments,
                            timeout: remaining,
                            idleTimeout: min(idleTimeout, remaining),
                            cancellationState: cancellationState))
                    }

                case let .pipe(.result(result)):
                    if try self.shouldReturnPipeResult(
                        result,
                        for: kind,
                        before: deadline,
                        now: clock.now)
                    {
                        cancellationState.cancel()
                        group.cancelAll()
                        return result
                    }
                    pipeFinishedWithoutAcceptedResult = true
                    Self.logger.debug("Kiro pipe \(kind.rawValue) output was incomplete; awaiting PTY fallback")
                    if let pending = try Self.resolvePendingPTY(
                        result: pendingPTYResult,
                        failure: pendingPTYFailure,
                        before: deadline,
                        now: clock.now)
                    {
                        return pending
                    }
                    if !ptyStarted {
                        let remaining = Self.timeInterval(from: clock.now.duration(to: deadline))
                        guard remaining > 0 else { throw KiroStatusProbeError.timeout }
                        ptyStarted = true
                        group.addTask {
                            await .pty(self.ptyOutcome(
                                arguments: arguments,
                                timeout: remaining,
                                idleTimeout: min(idleTimeout, remaining),
                                cancellationState: cancellationState))
                        }
                    }

                case .pipe(.failure(.timeout)):
                    pipeFinishedWithoutAcceptedResult = true
                    Self.logger.debug("Kiro pipe \(kind.rawValue) probe timed out; awaiting PTY fallback")
                    if let pending = try Self.resolvePendingPTY(
                        result: pendingPTYResult,
                        failure: pendingPTYFailure,
                        before: deadline,
                        now: clock.now)
                    {
                        return pending
                    }

                case let .pipe(.failure(error)):
                    cancellationState.cancel()
                    group.cancelAll()
                    throw error

                case .pipe(.cancelled), .pty(.cancelled):
                    throw CancellationError()

                case let .pty(.result(result)):
                    guard self.shouldAcceptPTYResult(result, for: kind) else {
                        if pipeFinishedWithoutAcceptedResult {
                            try Self.ensureBeforeDeadline(clock.now, deadline: deadline)
                            return result
                        }
                        pendingPTYResult = result
                        continue
                    }
                    guard clock.now <= deadline else { throw KiroStatusProbeError.timeout }
                    cancellationState.cancel()
                    group.cancelAll()
                    return result

                case let .pty(.failure(error)):
                    if pipeFinishedWithoutAcceptedResult {
                        throw error
                    }
                    pendingPTYFailure = error
                }
            }
            if let pending = try Self.resolvePendingPTY(
                result: pendingPTYResult,
                failure: pendingPTYFailure,
                before: deadline,
                now: clock.now)
            {
                return pending
            }
            throw KiroStatusProbeError.timeout
        }
    }

    private func pipeOutcome(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        activityState: KiroPipeActivityState) async -> KiroTransportOutcome
    {
        do {
            return try await .result(self.runViaPipe(
                arguments: arguments,
                timeout: timeout,
                idleTimeout: idleTimeout,
                activityState: activityState))
        } catch is CancellationError {
            return .cancelled
        } catch let error as KiroStatusProbeError {
            return .failure(error)
        } catch {
            return .failure(.cliFailed(error.localizedDescription))
        }
    }

    private func ptyOutcome(
        arguments: [String],
        timeout: TimeInterval,
        idleTimeout: TimeInterval,
        cancellationState: KiroTransportCancellationState) async -> KiroTransportOutcome
    {
        do {
            return try await .result(self.runViaPTYAsync(
                arguments: arguments,
                timeout: timeout,
                idleTimeout: idleTimeout,
                cancellationState: cancellationState))
        } catch is CancellationError {
            return .cancelled
        } catch let error as KiroStatusProbeError {
            return .failure(error)
        } catch {
            return .failure(.cliFailed(error.localizedDescription))
        }
    }

    fileprivate static func resolvePendingPTY(
        result: KiroCLIResult?,
        failure: KiroStatusProbeError?,
        before deadline: ContinuousClock.Instant,
        now: ContinuousClock.Instant) throws -> KiroCLIResult?
    {
        if result != nil || failure != nil {
            guard now <= deadline else { throw KiroStatusProbeError.timeout }
        }
        if let result {
            return result
        }
        if let failure {
            throw failure
        }
        return nil
    }

    private static func ensureBeforeDeadline(
        _ now: ContinuousClock.Instant,
        deadline: ContinuousClock.Instant) throws
    {
        guard now <= deadline else { throw KiroStatusProbeError.timeout }
    }

    private func shouldAcceptPipeResult(_ result: KiroCLIResult, for kind: KiroCommandKind) -> Bool {
        let output = result.output
        if Self.isLoginRequired(output) {
            return true
        }

        switch kind {
        case .whoAmI:
            let account = self.parseWhoAmIOutput(output)
            return account.authMethod != nil || account.email != nil
        case .usage:
            return (try? self.parse(output: output)) != nil
        case .context:
            if self.parseContextUsage(output: output) != nil {
                return true
            }
            return result.terminationStatus == 0
                && !result.stoppedAfterOutput
                && output.isEmpty
        }
    }

    private func shouldReturnPipeResult(
        _ result: KiroCLIResult,
        for kind: KiroCommandKind,
        before deadline: ContinuousClock.Instant,
        now: ContinuousClock.Instant) throws -> Bool
    {
        let accepted = self.shouldAcceptPipeResult(result, for: kind)
        guard accepted else { return false }
        if !Self.isLoginRequired(result.output), now > deadline {
            throw KiroStatusProbeError.timeout
        }
        return true
    }

    private func shouldAcceptPTYResult(_ result: KiroCLIResult, for kind: KiroCommandKind) -> Bool {
        if Self.isLoginRequired(result.output) {
            return true
        }
        return result.terminationStatus == 0 && self.shouldAcceptPipeResult(result, for: kind)
    }

    fileprivate static func timeInterval(from duration: Duration) -> TimeInterval {
        let components = duration.components
        return max(
            0,
            TimeInterval(components.seconds)
                + TimeInterval(components.attoseconds) / 1_000_000_000_000_000_000)
    }
}

extension String {
    fileprivate var nilIfEmpty: String? {
        self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
    }
}
