import Foundation
import NucleicCore

/// A/B renderer selected only for an explicit transcript-performance run.
///
/// Ordinary launches remain on ``production`` and do not collect benchmark-only shape statistics.
/// Set `NUCLEIC_TRANSCRIPT_RENDER_VARIANT`, or pass `--transcript-render-variant`, to opt one app
/// process into a variant. All four consume the same decoded events and projection. Production,
/// skeleton, and generic rendering also retain the same row identities and segmentation; text-only
/// derives its smaller presentation list after that shared projection.
enum TranscriptRenderBenchmarkVariant: String, CaseIterable, Sendable {
    case production
    case cardSkeletons = "skeletons"
    case genericCards = "generic"
    case textOnly = "text-only"
}

struct TranscriptRenderBenchmarkConfiguration: Equatable, Sendable {
    static let environmentKey = "NUCLEIC_TRANSCRIPT_RENDER_VARIANT"
    static let argumentName = "--transcript-render-variant"

    let variant: TranscriptRenderBenchmarkVariant
    /// True only when a launch argument/environment value explicitly selected the benchmark seam.
    /// The default production renderer therefore pays no recursive statistics pass.
    let isEnabled: Bool

    static let current = parse(
        arguments: ProcessInfo.processInfo.arguments,
        environment: ProcessInfo.processInfo.environment)

    static func parse(
        arguments: [String], environment: [String: String]
    ) -> TranscriptRenderBenchmarkConfiguration {
        let equalsPrefix = argumentName + "="
        var requested = arguments.first(where: { $0.hasPrefix(equalsPrefix) })
            .map { String($0.dropFirst(equalsPrefix.count)) }
        if requested == nil, let index = arguments.firstIndex(of: argumentName),
           arguments.indices.contains(index + 1)
        {
            requested = arguments[index + 1]
        }
        if requested == nil { requested = environment[environmentKey] }

        guard let requested else {
            return Self(variant: .production, isEnabled: false)
        }
        return Self(
            variant: TranscriptRenderBenchmarkVariant(rawValue: requested) ?? .production,
            isEnabled: true)
    }
}

/// Payload-shape facts attached to benchmark samples. Measurement is recursive in child count but
/// never materializes, joins, or canonical-encodes a tool input/result, so payload bytes do not
/// distort the renderer A/B it describes.
struct TranscriptRenderBenchmarkStatistics: Equatable, Sendable {
    enum CardKind: String, CaseIterable, Sendable {
        case generic
        case answeredQuestion = "answered-question"
        case plan
        case gitCommit = "git-commit"
        case hostExec = "host-exec"
        case subagent
        case delegatedPlan = "delegated-plan"
        case orchestration
        case toolGroup = "tool-group"
    }

    let topLevelRowCount: Int
    let nestedChildCount: Int
    let maximumNestedDepth: Int
    let toolCallCount: Int
    let maximumGroupSize: Int
    let visibleMarkdownBytes: Int
    let cardKindCounts: [String: Int]

    var cardKindSummary: String {
        cardKindCounts.keys.sorted().map { "\($0):\(cardKindCounts[$0] ?? 0)" }
            .joined(separator: ",")
    }

    static func measure(_ items: [TranscriptItem]) -> Self {
        var nestedChildCount = 0
        var maximumNestedDepth = 0
        var toolCallCount = 0
        var maximumGroupSize = 0
        var visibleMarkdownBytes = 0
        var cardKindCounts: [String: Int] = [:]

        func record(_ kind: CardKind, count: Int = 1) {
            cardKindCounts[kind.rawValue, default: 0] += count
        }

        func cardKind(call: ToolCall, result: ToolResult?) -> CardKind {
            if call.name == AskUserQuestion.toolName,
               result != nil,
               AskUserQuestion.questions(from: call.input) != nil
            {
                return .answeredQuestion
            }
            if call.name == ExitPlanMode.toolName,
               ExitPlanMode.plan(from: call.input) != nil
            {
                return .plan
            }
            if call.name == "Bash",
               let command = call.input["command"]?.stringValue,
               GitCommandSummary.block(for: command)?.isCommit == true
            {
                return .gitCommit
            }
            if call.name == MCPApprovalServer.qualifiedHostExecToolName,
               call.input["command"]?.stringValue?.isEmpty == false
            {
                return .hostExec
            }
            if call.name == MCPApprovalServer.qualifiedOrchestraPlanToolName {
                return .delegatedPlan
            }
            if TranscriptRow.isSubagentCall(call) { return .subagent }
            return .generic
        }

        func alwaysVisibleCardMarkdownBytes(_ call: ToolCall) -> Int {
            if call.name == ExitPlanMode.toolName,
               let plan = ExitPlanMode.plan(from: call.input)
            {
                return plan.markdown.utf8.count
            }
            if call.name == "Bash",
               let command = call.input["command"]?.stringValue,
               let body = GitCommandSummary.block(for: command)?.commit?.body
            {
                return body.utf8.count
            }
            if call.name == MCPApprovalServer.qualifiedOrchestraPlanToolName {
                return call.input["plan"]?.stringValue?.utf8.count ?? 0
            }
            return 0
        }

        func visit(_ items: [TranscriptItem], depth: Int, markdownIsVisible: Bool) {
            maximumNestedDepth = max(maximumNestedDepth, depth)
            for item in items {
                switch item {
                case .assistant(_, let text, _):
                    if markdownIsVisible { visibleMarkdownBytes += text.utf8.count }
                case .tool(_, let call, let result, _, let children):
                    toolCallCount += 1
                    record(cardKind(call: call, result: result))
                    if markdownIsVisible {
                        visibleMarkdownBytes += alwaysVisibleCardMarkdownBytes(call)
                    }
                    if !children.isEmpty {
                        nestedChildCount += children.count
                        visit(children, depth: depth + 1, markdownIsVisible: false)
                    }
                case .toolGroup(_, _, let calls):
                    toolCallCount += calls.count
                    maximumGroupSize = max(maximumGroupSize, calls.count)
                    if calls.allSatisfy({
                        $0.call.name == MCPApprovalServer.qualifiedOrchestraPlanToolName
                    }) {
                        record(.delegatedPlan, count: calls.count)
                    } else if calls.allSatisfy({ TranscriptRow.isSubagentCall($0.call) }) {
                        record(.orchestration)
                    } else {
                        record(.toolGroup)
                    }
                    if markdownIsVisible {
                        visibleMarkdownBytes += calls.reduce(into: 0) { total, entry in
                            total += alwaysVisibleCardMarkdownBytes(entry.call)
                        }
                    }
                    for entry in calls where !entry.children.isEmpty {
                        nestedChildCount += entry.children.count
                        visit(entry.children, depth: depth + 1, markdownIsVisible: false)
                    }
                case .event, .thinking, .thinkingTokens:
                    break
                }
            }
        }

        visit(items, depth: 0, markdownIsVisible: true)
        return Self(
            topLevelRowCount: items.count,
            nestedChildCount: nestedChildCount,
            maximumNestedDepth: maximumNestedDepth,
            toolCallCount: toolCallCount,
            maximumGroupSize: maximumGroupSize,
            visibleMarkdownBytes: visibleMarkdownBytes,
            cardKindCounts: cardKindCounts)
    }
}
