//
//  EventModalView.swift
//  lichunWebsocket
//
//  Modal view for displaying game events/questions with answer options
//  Completely redesigned with cozy iOS game design principles
//

import SwiftUI
import SDWebImageSwiftUI

struct EventModalView: View {
    @EnvironmentObject var webSocketService: WebSocketService

    var questionID: String
    var question: String
    var answers: [AnswerOption]
    var title: String?
    var image: String?
    var characters: [SimplePerson]?
    var dismiss: () -> Void

    // Grid layout with 2 columns
    var columns: [GridItem] = [
        GridItem(.flexible(), spacing: AppSpacing.md),
        GridItem(.flexible(), spacing: AppSpacing.md)
    ]

    /// Drives the modal's entrance: the card pops in (scale + fade) and the
    /// answer cards stagger up. Toggled true once in `onAppear` so the implicit
    /// `.animation(_, value: appeared)` modifiers play the arrival.
    @State private var appeared = false

    var body: some View {
        ZStack {
            // Soft blurred background overlay with warm tint
            AppColors.modalOverlay
                .edgesIgnoringSafeArea(.all)
                .background(
                    LinearGradient(
                        colors: [
                            AppColors.primary.opacity(0.1),
                            AppColors.secondary.opacity(0.1)
                        ],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    )
                )

            VStack(spacing: 0) {
                // CRITICAL: Stats at the top
                cozyStatsHeader()

                // Main content card
                ScrollView {
                    VStack(spacing: AppSpacing.lg) {
                        // Decorative top element
                        decorativeTopBorder()

                        // Character avatars (if present)
                        if let characters = characters, !characters.isEmpty {
                            HStack(spacing: 8) {
                                ForEach(characters.prefix(4)) { character in
                                    CharacterAvatar(
                                        person: personFrom(character),
                                        size: 48,
                                        showBorder: true,
                                        borderGradient: [.white, .white.opacity(0.8)],
                                        showGlow: false
                                    )
                                }
                            }
                            .padding(.top, AppSpacing.sm)
                        }

                        // Optional title with decorative elements
                        if let title = title {
                            titleSection(title)
                        }

                        // Question text with decorative frame
                        questionSection()

                        // Decision result detail: narrative outcome + concrete
                        // stat deltas + per-character affinity changes. Only
                        // present when this modal is showing a resolved decision
                        // (populated from the server's event_resolved payload).
                        if let result = resultDetail {
                            resultDetailSection(result)
                        }

                        // Answer options with cozy design, or a dismiss button when
                        // this modal is showing a result/notification (no answers).
                        if !answers.isEmpty {
                            answerOptionsSection()
                        } else {
                            cozyOkayButton()
                        }

                        // Bottom spacing
                        Spacer(minLength: AppSpacing.md)
                    }
                    .padding(.horizontal, AppSpacing.lg)
                    .padding(.bottom, AppSpacing.xl)
                }
                .background(backgroundContent)
                .cornerRadius(AppSpacing.largeCornerRadius)
                .shadow(
                    color: Color.black.opacity(0.3),
                    radius: AppSpacing.Shadow.radiusLarge,
                    x: 0,
                    y: AppSpacing.Shadow.offsetYLarge
                )
            }
            .padding(AppSpacing.md)
            // Subtle pop-in: the card scales up from 96% and fades in so the
            // modal arrives with a soft, playful presence instead of snapping in.
            .scaleEffect(appeared ? 1 : 0.96)
            .opacity(appeared ? 1 : 0)
            .animation(AppAnimations.gentle, value: appeared)
        }
        .onAppear { appeared = true }
    }

    // MARK: - Cozy Stats Header (CRITICAL - Keep at top)
    @ViewBuilder
    private func cozyStatsHeader() -> some View {
        HStack(spacing: AppSpacing.sm) {
            // Energy pill
            CozyResourcePill(
                icon: "⚡️",
                value: webSocketService.person.calcEnergy,
                color: AppColors.energy
            )

            // Diamonds pill
            CozyResourcePill(
                icon: "💎",
                value: webSocketService.person.diamonds,
                color: AppColors.diamond
            )

            // Money pill
            CozyResourcePill(
                icon: "💰",
                value: webSocketService.person.money,
                color: AppColors.money
            )
        }
        .frame(maxWidth: .infinity)
        .padding(.horizontal, AppSpacing.md)
        .padding(.vertical, AppSpacing.sm)
        .background(
            LinearGradient(
                colors: [AppColors.primary.opacity(0.2), AppColors.secondary.opacity(0.2)],
                startPoint: .leading,
                endPoint: .trailing
            )
        )
        .cornerRadius(AppSpacing.largeCornerRadius, corners: [.topLeft, .topRight])
    }

    // MARK: - Decorative Top Border
    @ViewBuilder
    private func decorativeTopBorder() -> some View {
        HStack(spacing: AppSpacing.xs) {
            ForEach(0..<5) { _ in
                Circle()
                    .fill(AppColors.primary.opacity(0.3))
                    .frame(width: 6, height: 6)
            }
        }
        .padding(.top, AppSpacing.md)
    }

    // MARK: - Title Section
    @ViewBuilder
    private func titleSection(_ title: String) -> some View {
        VStack(spacing: AppSpacing.sm) {
            // Decorative line
            Rectangle()
                .fill(
                    LinearGradient(
                        colors: [AppColors.primary.opacity(0), AppColors.primary, AppColors.primary.opacity(0)],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
                .frame(height: 2)
                .frame(maxWidth: 100)

            Text(title)
                .font(.appTitle)
                .foregroundColor(AppColors.primaryText)
                .multilineTextAlignment(.center)
                .padding(.horizontal, AppSpacing.md)

            // Decorative line
            Rectangle()
                .fill(
                    LinearGradient(
                        colors: [AppColors.primary.opacity(0), AppColors.primary, AppColors.primary.opacity(0)],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
                .frame(height: 2)
                .frame(maxWidth: 100)
        }
    }

    // MARK: - Cozy Image View
    @ViewBuilder
    private func cozyImageView(url: URL) -> some View {
        WebImage(url: url)
            .onSuccess { image, data, cacheType in
                print("📍 EventModalImage: ✅ Loaded main event image - size: \(image.size), cache: \(cacheType)")
            }
            .onFailure { error in
                print("📍 EventModalImage: ❌ Failed main event image - URL: \(url.absoluteString) - Error: \(error.localizedDescription)")
            }
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(maxWidth: .infinity, maxHeight: 220)
            .cornerRadius(AppSpacing.cornerRadius)
            .overlay(
                RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                    .stroke(AppColors.primary.opacity(0.3), lineWidth: 3)
            )
            .shadow(
                color: AppColors.primary.opacity(0.2),
                radius: AppSpacing.Shadow.radiusMedium,
                x: 0,
                y: AppSpacing.Shadow.offsetY
            )
            .padding(.vertical, AppSpacing.sm)
            .onAppear {
                print("📍 EventModalImage: 🔄 Loading main event image from: \(url.absoluteString)")
            }
    }

    // MARK: - Question Section
    @ViewBuilder
    private func questionSection() -> some View {
        VStack(spacing: AppSpacing.sm) {
            Text(question)
                .font(.appTitle2)
                .foregroundColor(AppColors.primaryText)
                .multilineTextAlignment(.center)
                .lineSpacing(4)
                .fixedSize(horizontal: false, vertical: true)
                .padding(AppSpacing.md)
                .background(
                    RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                        .fill(AppColors.surfaceSubtle)
                        .shadow(
                            color: AppColors.primary.opacity(0.1),
                            radius: AppSpacing.Shadow.radiusSoft,
                            x: 0,
                            y: 2
                        )
                )
        }
    }

    // MARK: - Result Detail (decision confirmation)

    /// The structured result for the decision currently shown in this modal, if
    /// any. Sourced from the active `currentMessageEvent` so the server's
    /// resolution (outcome narrative + stat deltas + per-character affinity)
    /// can be displayed on the same confirmation card the player is looking at.
    private var resultDetail: ResultDetail? {
        guard answers.isEmpty,
              let event = webSocketService.currentMessageEvent,
              event.id == questionID else {
            return nil
        }

        // Only the message text differs from `question`: don't repeat it.
        let outcome = (event.outcomeText?.isEmpty == false && event.outcomeText != question)
            ? event.outcomeText
            : nil
        let stats = event.statDeltas ?? [:]
        let affinity = event.affinityChanges ?? []

        if outcome == nil && stats.isEmpty && affinity.isEmpty {
            return nil
        }
        return ResultDetail(outcomeText: outcome, statDeltas: stats, affinityChanges: affinity)
    }

    @ViewBuilder
    private func resultDetailSection(_ detail: ResultDetail) -> some View {
        VStack(spacing: AppSpacing.sm) {
            // Narrative outcome (what goes to the life timeline)
            if let outcome = detail.outcomeText {
                Text(outcome)
                    .font(.appBody)
                    .foregroundColor(AppColors.primaryText)
                    .multilineTextAlignment(.center)
                    .fixedSize(horizontal: false, vertical: true)
                    .padding(.horizontal, AppSpacing.md)
            }

            // Concrete changes: stat deltas + per-character affinity
            if !detail.statDeltas.isEmpty || !detail.affinityChanges.isEmpty {
                VStack(spacing: AppSpacing.xs) {
                    Text("What changed")
                        .font(.appCaption)
                        .foregroundColor(AppColors.secondaryText)

                    ForEach(detail.sortedStatDeltas, id: \.key) { entry in
                        deltaRow(
                            label: EventModalView.statDisplayName(entry.key),
                            delta: entry.value
                        )
                    }

                    ForEach(detail.affinityChanges) { change in
                        deltaRow(
                            label: "\(change.name) affinity",
                            delta: change.delta
                        )
                    }
                }
                .padding(AppSpacing.md)
                .frame(maxWidth: .infinity)
                .background(
                    RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                        .fill(AppColors.surfaceSubtle)
                )
            }
        }
    }

    /// A single "Stat +N" / "Mom -3 affinity" row with sign-aware coloring.
    @ViewBuilder
    private func deltaRow(label: String, delta: Int) -> some View {
        let positive = delta >= 0
        HStack(spacing: AppSpacing.sm) {
            Text(label)
                .font(.appBody)
                .foregroundColor(AppColors.primaryText)
            Spacer(minLength: AppSpacing.sm)
            Text("\(positive ? "+" : "")\(delta)")
                .font(.appBodyBold)
                .foregroundColor(positive ? AppColors.success : AppColors.error)
        }
    }

    // MARK: - Cozy Okay Button
    @ViewBuilder
    private func cozyOkayButton() -> some View {
        Button(action: {
            dismiss()
        }) {
            HStack(spacing: AppSpacing.sm) {
                Text("✓")
                    .font(.system(size: 20, weight: .bold))
                Text("Okay")
                    .font(.appBodyBold)
            }
            .foregroundColor(.white)
            .frame(maxWidth: .infinity)
            .padding(.vertical, AppSpacing.md)
            .background(
                LinearGradient(
                    colors: [AppColors.primary, AppColors.primaryDark],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
            )
            .cornerRadius(AppSpacing.pillCornerRadius)
            .shadow(
                color: AppColors.primary.opacity(0.4),
                radius: AppSpacing.Shadow.radiusMedium,
                x: 0,
                y: AppSpacing.Shadow.offsetY
            )
        }
        .buttonStyle(SquishButtonStyle(pressHaptic: .soft))
        .padding(.vertical, AppSpacing.sm)
    }

    // MARK: - Answer Options Section
    @ViewBuilder
    private func answerOptionsSection() -> some View {
        VStack(spacing: AppSpacing.md) {
            // Section divider
            HStack(spacing: AppSpacing.sm) {
                Rectangle()
                    .fill(AppColors.secondaryText.opacity(0.3))
                    .frame(height: 1)

                Text("Choose Your Path")
                    .font(.appCaption)
                    .foregroundColor(AppColors.primaryText)

                Rectangle()
                    .fill(AppColors.secondaryText.opacity(0.3))
                    .frame(height: 1)
            }
            .padding(.top, AppSpacing.sm)

            // Answer buttons grid. Affordable/actionable choices are ordered first
            // (see orderedAnswers) so when some choices are disabled (unaffordable
            // money costs), the player always sees a pickable option without having
            // to scroll past dimmed ones — avoiding a "looks stuck" dead-end on
            // dilemmas whose choices overflow the screen.
            LazyVGrid(columns: columns, alignment: .center, spacing: AppSpacing.md) {
                ForEach(Array(orderedAnswers.enumerated()), id: \.element.id) { index, answer in
                    cozyAnswerButton(answer)
                        // Gentle staggered arrival: each card floats up + fades
                        // in just after the previous one, so the choices feel
                        // like they're being dealt out. Kept fast (≈0.04s steps)
                        // and they remain tappable throughout.
                        .opacity(appeared ? 1 : 0)
                        .scaleEffect(appeared ? 1 : 0.9)
                        .animation(
                            AppAnimations.standard.delay(Double(index) * 0.04),
                            value: appeared
                        )
                }
            }
        }
    }

    // MARK: - Affordability

    /// Answers ordered so affordable/actionable choices appear first, with
    /// unaffordable (dimmed/disabled) money choices moved to the end. Stable
    /// within each group via the original index, so relative order is preserved
    /// and this is a no-op when every choice is affordable (or every choice is
    /// unaffordable). Prevents the "looks stuck" case where disabled choices sit
    /// above the affordable ones on a dilemma whose options overflow the screen.
    private var orderedAnswers: [AnswerOption] {
        answers.enumerated()
            .sorted { lhs, rhs in
                let lAfford = canAffordMoney(lhs.element)
                let rAfford = canAffordMoney(rhs.element)
                if lAfford != rAfford { return lAfford }
                return lhs.offset < rhs.offset
            }
            .map { $0.element }
    }

    /// Money-only affordability check, mirroring the server's INSUFFICIENT_FUNDS
    /// gate in EventResponder. A choice with a positive moneyCost the player
    /// can't cover is not affordable; everything else (no cost, energy/diamond
    /// costs, money rewards) is treated as affordable here. Energy and diamond
    /// gating are intentionally untouched.
    private func canAffordMoney(_ answer: AnswerOption) -> Bool {
        guard let moneyCost = answer.moneyCost, moneyCost > 0 else { return true }
        return webSocketService.person.money >= moneyCost
    }

    /// Whether at least one answer in this event is affordable. Used to avoid a
    /// soft-lock: if EVERY choice is an unaffordable money spend, none should be
    /// disabled so the player can always proceed (going into debt — the
    /// sanctioned fallback). When an affordable alternative exists, the gate
    /// still nudges toward it by disabling the unaffordable ones.
    private func anyChoiceAffordable() -> Bool {
        answers.contains { canAffordMoney($0) }
    }

    // MARK: - Cozy Answer Button
    @ViewBuilder
    private func cozyAnswerButton(_ answer: AnswerOption) -> some View {
        // Only block an unaffordable money choice when an affordable alternative
        // exists. If all choices are unaffordable spends, leave them enabled so
        // the player can proceed into debt rather than soft-locking.
        let blocked = !canAffordMoney(answer) && anyChoiceAffordable()
        Button(action: {
            handleAnswerSelection(answer)
        }) {
            VStack(spacing: AppSpacing.sm) {
                // Answer text in cozy card
                ZStack {
                    // Background with gradient
                    RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                        .fill(
                            LinearGradient(
                                colors: [AppColors.surfaceElevated, Color.white],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing
                            )
                        )

                    // Border
                    RoundedRectangle(cornerRadius: AppSpacing.cornerRadius)
                        .stroke(AppColors.primary.opacity(0.3), lineWidth: 2)

                    // Text content
                    Text(answer.option)
                        .font(.appBody)
                        .foregroundColor(AppColors.primaryText)
                        .multilineTextAlignment(.center)
                        .lineLimit(5)
                        .padding(AppSpacing.md)
                }
                .frame(height: 140)

                // Cost indicator pill
                cozyCostIndicator(for: answer)
            }
        }
        .buttonStyle(CozyAnswerButtonStyle())
        // Money-only gate: an unaffordable money choice can't be tapped and is
        // dimmed, matching the server's INSUFFICIENT_FUNDS rejection — but only
        // when an affordable alternative exists. Affordable and non-money
        // choices are unchanged.
        .disabled(blocked)
        .opacity(blocked ? 0.5 : 1.0)
    }

    // MARK: - Cozy Cost Indicator
    @ViewBuilder
    private func cozyCostIndicator(for answer: AnswerOption) -> some View {
        Group {
            if let energyCost = answer.energyCost, energyCost > 0 {
                CostPill(icon: "⚡️", value: energyCost, color: AppColors.energy)
            } else if let moneyCost = answer.moneyCost, moneyCost > 0 {
                CostPill(icon: "💰", value: moneyCost, color: AppColors.money)
            } else if let diamondCost = answer.diamondCost, diamondCost > 0 {
                CostPill(icon: "💎", value: diamondCost, color: AppColors.diamond)
            } else {
                // Invisible placeholder for consistent spacing
                CostPill(icon: "", value: 0, color: .clear)
                    .opacity(0)
            }
        }
    }

    // MARK: - Answer Selection Handler
    private func handleAnswerSelection(_ answer: AnswerOption) {
        guard answer.choiceId != nil else {
            hapticNotification(type: .error)
            webSocketService.currentError = .invalidOperation(message: "Invalid choice. Please try again.")
            return
        }

        // Money-only affordability guard (defense in depth behind the disabled
        // button state). No send, no optimistic deduct if the player can't cover
        // the money cost — mirrors the server's INSUFFICIENT_FUNDS rejection.
        // Soft-lock guard: allow the tap when nothing is affordable (every choice
        // is an unaffordable money spend) so the player can proceed into debt.
        guard canAffordMoney(answer) || !anyChoiceAffordable() else {
            hapticNotification(type: .warning)
            webSocketService.currentError = .invalidOperation(message: "Not enough money for this choice.")
            return
        }

        // Crisp "decision locked in" tap, distinct from the soft press-down, so
        // committing to a choice feels deliberate and rewarding.
        hapticFeedback(style: .rigid)

        // Send answer to server. The backend resolves the choice and replies with
        // an `event_resolved` envelope (resolutionText + effects). That async reply
        // lands in `lifeEvents`, but the decision screen would otherwise vanish with
        // no feedback. Show an immediate, persistent result so the player sees the
        // outcome of their choice on the decision screen itself.
        webSocketService.sendAnswer(answer: answer, questionID: questionID)

        // Deduct costs (optimistic, mirrors server effect application)
        if let energyCost = answer.energyCost, energyCost > 0 {
            webSocketService.person.calcEnergy -= energyCost
        }
        if let moneyCost = answer.moneyCost, moneyCost > 0 {
            webSocketService.person.money -= moneyCost
        }
        if let diamondCost = answer.diamondCost, diamondCost > 0 {
            webSocketService.person.diamonds -= diamondCost
        }

        // Present the outcome via the dedicated message-event modal, which is gated
        // by `currentMessageEvent` (independent of `currentQuestion`) and has its own
        // "Okay" dismiss button. This survives the question being cleared from the
        // queue when the server's resolution arrives.
        let resultEvent = EventModalView.makeResultEvent(
            for: answer,
            questionID: questionID,
            date: webSocketService.player.date,
            hour: webSocketService.player.hourOfDay
        )
        webSocketService.removeQuestion(eventId: questionID)
        webSocketService.currentMessageEvent = resultEvent
    }

    /// Builds the result/outcome MessageEvent shown after the player answers a
    /// decision. Pure + static so the outcome copy can be unit tested without a
    /// running view. Summarizes the chosen option and any immediate resource cost.
    ///
    /// This is the OPTIMISTIC card shown instantly on selection. When the
    /// server's `event_resolved` arrives, `EventService` enriches this same
    /// card (matched by id `result_<questionID>`) with the narrative outcome,
    /// stat deltas, and per-character affinity changes.
    static func makeResultEvent(
        for answer: AnswerOption,
        questionID: String,
        date: String?,
        hour: Int
    ) -> MessageEvent {
        var lines: [String] = ["You chose: \(answer.option)"]

        var effectLines: [String] = []
        if let energyCost = answer.energyCost, energyCost > 0 {
            effectLines.append("⚡️ -\(energyCost) energy")
        }
        if let moneyCost = answer.moneyCost, moneyCost > 0 {
            effectLines.append("💰 -\(moneyCost) money")
        }
        if let diamondCost = answer.diamondCost, diamondCost > 0 {
            effectLines.append("💎 -\(diamondCost) diamonds")
        }
        if !effectLines.isEmpty {
            lines.append("")
            lines.append(effectLines.joined(separator: "   "))
        }

        return MessageEvent(
            id: "result_\(questionID)",
            message: lines.joined(separator: "\n"),
            type: "event_result",
            date: date,
            hour: String(hour),
            energyCost: nil,
            diamondCost: nil,
            moneyCost: nil,
            affinityChange: nil,
            title: "Your Decision",
            image: nil,
            characters: nil,
            claimed: true,
            claimedAt: Date(),
            category: .neutral
        )
    }

    /// Human-readable label for a backend stat key (e.g. "happiness" -> "Happiness",
    /// "physicalHealth" -> "Physical Health").
    static func statDisplayName(_ key: String) -> String {
        let overrides: [String: String] = [
            "social": "Social",
            "happiness": "Happiness",
            "mood": "Mood",
            "health": "Health",
            "intelligence": "Intelligence",
            "prestige": "Prestige",
            "energy": "Energy",
            "money": "Money",
            "diamonds": "Diamonds"
        ]
        if let mapped = overrides[key] {
            return mapped
        }
        // Fallback: split camelCase and capitalize each word.
        var result = ""
        for character in key {
            if character.isUppercase, !result.isEmpty {
                result.append(" ")
            }
            result.append(character)
        }
        return result.prefix(1).uppercased() + result.dropFirst()
    }

    // MARK: - Background Content

    /// Background with optional location image overlay
    @ViewBuilder
    private var backgroundContent: some View {
        ZStack {
            // Base gradient background
            LinearGradient(
                colors: [
                    AppColors.surfaceElevated,
                    AppColors.background
                ],
                startPoint: .top,
                endPoint: .bottom
            )

            // Optional location image overlay
            if let imageURL = image, let url = URL(string: imageURL) {
                AsyncImage(url: url) { phase in
                    switch phase {
                    case .success(let image):
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .opacity(0.1)
                            .blur(radius: 3)
                            .onAppear {
                                print("📍 EventModalBG: ✅ Loaded background image from: \(imageURL)")
                            }
                    case .failure(let error):
                        EmptyView()
                            .onAppear {
                                print("📍 EventModalBG: ❌ Failed background image - URL: \(imageURL) - Error: \(error.localizedDescription)")
                            }
                    case .empty:
                        EmptyView()
                            .onAppear {
                                print("📍 EventModalBG: 🔄 Loading background image from: \(imageURL)")
                            }
                    @unknown default:
                        EmptyView()
                    }
                }
            }
        }
    }

    // MARK: - Helper Functions

    /// Converts SimplePerson to Person for AvatarView
    private func personFrom(_ simple: SimplePerson) -> Person {
        let person = Person()
        person.id = simple.id
        person.firstname = simple.firstname
        person.lastname = simple.lastname
        person.image = simple.image
        return person
    }
}

// MARK: - Supporting Views

/// Cozy resource pill for stats display
private struct CozyResourcePill: View {
    let icon: String
    let value: Int
    let color: Color

    var body: some View {
        HStack(spacing: 4) {
            Text(icon)
                .font(.system(size: 14))
            Text("\(value)")
                .font(.appCaptionBold)
                .foregroundColor(AppColors.primaryText)
        }
        .padding(.horizontal, AppSpacing.sm)
        .padding(.vertical, 6)
        .background(
            Capsule()
                .fill(color.opacity(0.3))
                .overlay(
                    Capsule()
                        .stroke(color.opacity(0.5), lineWidth: 1.5)
                )
        )
        .shadow(
            color: color.opacity(0.3),
            radius: 4,
            x: 0,
            y: 2
        )
    }
}

/// Cost pill for answer buttons
private struct CostPill: View {
    let icon: String
    let value: Int
    let color: Color

    var body: some View {
        HStack(spacing: 4) {
            Text(icon)
                .font(.system(size: 12))
            Text("\(value)")
                .font(.appCaption)
                .foregroundColor(AppColors.primaryText)
        }
        .padding(.horizontal, AppSpacing.sm)
        .padding(.vertical, 4)
        .background(
            Capsule()
                .fill(color.opacity(0.4))
                .overlay(
                    Capsule()
                        .stroke(color.opacity(0.6), lineWidth: 1)
                )
        )
    }
}

/// Cozy button style for answer buttons. The press is intentionally subtle and
/// fast — a small squish with a quick, low-overshoot spring — paired with a
/// gentle `.soft` haptic the instant the finger lands, plus the shadow pulling
/// in so the card feels physically pressed. This is the "satisfying tap" at the
/// heart of the choice modal.
private struct CozyAnswerButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.96 : 1.0)
            .shadow(
                color: AppColors.primary.opacity(configuration.isPressed ? 0.2 : 0.3),
                radius: configuration.isPressed ? 4 : 8,
                x: 0,
                y: configuration.isPressed ? 2 : 4
            )
            .animation(AppAnimations.quick, value: configuration.isPressed)
            .onChange(of: configuration.isPressed) { isPressed in
                if isPressed {
                    hapticFeedback(style: .soft)
                }
            }
    }
}

/// Structured outcome shown on the decision-confirmation screen: the narrative
/// result that goes to the life timeline plus the concrete stat/affinity changes.
struct ResultDetail: Equatable {
    var outcomeText: String?
    var statDeltas: [String: Int]
    var affinityChanges: [AffinityChange]

    /// Stat deltas sorted by descending magnitude (largest impact first) with a
    /// stable key tiebreak, so the list ordering is deterministic.
    var sortedStatDeltas: [(key: String, value: Int)] {
        statDeltas
            .map { (key: $0.key, value: $0.value) }
            .sorted { lhs, rhs in
                if abs(lhs.value) != abs(rhs.value) {
                    return abs(lhs.value) > abs(rhs.value)
                }
                return lhs.key < rhs.key
            }
    }
}

// MARK: - Preview
// Preview removed - requires proper model initialization
