//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Combine
import LanguageModelChatUI
import SwiftUI

/// Primary landing surface. Voice is the only interaction here; chat is one tap
/// away in the top bar. The orb is the single hero — it is both the visual
/// identity and the action button. Everything else is supporting context:
/// concise top-bar provider chip, live transcript bubbles when there's a turn
/// in flight, and a quiet rotating tip when there isn't.
struct OpenRockyVoiceHomeView: View {
    @ObservedObject var sessionRuntime: OpenRockySessionRuntime
    @ObservedObject var chatProviderStore: OpenRockyProviderStore
    @ObservedObject var realtimeProviderStore: OpenRockyRealtimeProviderStore
    let onOpenSettings: () -> Void
    let onOpenChat: () -> Void

    @ObservedObject private var mediaService = OpenRockyMediaPlayerService.shared
    @StateObject private var recentHistory = RecentHistoryObserver()
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass

    @State private var showsVoiceNotConfiguredAlert = false
    @State private var tipIndex = 0
    @State private var tipTask: Task<Void, Never>?

    /// Quiet rotating examples shown only when idle and there's no transcript.
    /// Each prompt is chosen to advertise a distinct Rocky capability — native
    /// iOS bridges (Health, Reminders, Calendar, Location), contextual
    /// multi-tool reasoning, and `delegate-task` for multi-step research —
    /// rather than reading like a generic AI-app suggestion.
    private let tips: [LocalizedStringKey] = [
        "Try “Set a 25-minute focus timer”",
        "Try “How did I sleep last night?”",
        "Try “Will it rain when I head home?”",
        "Try “Add bread and milk to my reminders”",
        "Try “What's on my calendar tomorrow?”",
        "Try “Plan a weekend trip to Hangzhou”"
    ]

    var body: some View {
        ZStack {
            ambientBackground

            VStack(spacing: 0) {
                topBar
                    .padding(.horizontal, 16)
                    // Offset when media bar is visible so the two don't collide
                    .padding(.top, mediaService.currentIndex != nil ? 56 : 6)
                    .padding(.bottom, 6)

                if isWideLayout {
                    wideContent
                } else {
                    compactContent
                }
            }
        }
        .alert("Voice Not Configured", isPresented: $showsVoiceNotConfiguredAlert) {
            Button("Open Settings", action: onOpenSettings)
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("Please set up a voice provider in Settings before starting a voice session.")
        }
        .onAppear {
            recentHistory.bind(to: sessionRuntime)
            startTipRotation()
        }
        .onDisappear {
            tipTask?.cancel()
            tipTask = nil
        }
    }

    private var isWideLayout: Bool {
        horizontalSizeClass == .regular
    }

    // MARK: - Ambient Background

    /// Soft radial halo at the top, tinted by current mode. More saturated when
    /// the mic is live so the whole canvas pulses subtly with the conversation.
    private var ambientBackground: some View {
        ZStack {
            OpenRockyPalette.background.ignoresSafeArea()

            RadialGradient(
                colors: [modeTint.opacity(isMicActive ? 0.30 : 0.16), .clear],
                center: .init(x: 0.5, y: 0.0),
                startRadius: 30,
                endRadius: 480
            )
            .ignoresSafeArea()
            .animation(.easeInOut(duration: 0.9), value: isMicActive)
            .animation(.easeInOut(duration: 0.6), value: sessionRuntime.session.mode)
        }
    }

    // MARK: - Top Bar

    private var topBar: some View {
        HStack(spacing: 10) {
            iconButton(symbol: "gearshape.fill", action: onOpenSettings)
            Spacer(minLength: 8)
            providerChip
            Spacer(minLength: 8)
            iconButton(symbol: "bubble.left.and.bubble.right.fill", action: onOpenChat)
        }
    }

    private func iconButton(symbol: String, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            Image(systemName: symbol)
                .font(.system(size: 15, weight: .semibold))
                .foregroundStyle(OpenRockyPalette.text.opacity(0.78))
                .frame(width: 38, height: 38)
                .background(.ultraThinMaterial, in: Circle())
                .overlay(Circle().stroke(OpenRockyPalette.stroke, lineWidth: 0.5))
        }
        .buttonStyle(.plain)
    }

    private var providerChip: some View {
        Button(action: onOpenSettings) {
            HStack(spacing: 7) {
                Circle()
                    .fill(isVoiceReady ? OpenRockyPalette.success : OpenRockyPalette.warning)
                    .frame(width: 6, height: 6)
                Text(providerLabel)
                    .font(.system(size: 12, weight: .semibold, design: .rounded))
                    .foregroundStyle(OpenRockyPalette.text.opacity(0.82))
                    .lineLimit(1)
                    .truncationMode(.middle)
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 8)
            .background(.ultraThinMaterial, in: Capsule())
            .overlay(Capsule().stroke(OpenRockyPalette.stroke, lineWidth: 0.5))
        }
        .buttonStyle(.plain)
    }

    private var providerLabel: String {
        let cfg = realtimeProviderStore.configuration
        if cfg.isConfigured {
            return "\(cfg.provider.displayName) · \(cfg.modelID)"
        }
        return String(localized: "Voice not configured")
    }

    // MARK: - Layouts

    /// Orb is always anchored at the bottom. The space above is the info surface:
    /// transcript when there's a turn in flight or recent history, otherwise a
    /// quiet greeting + rotating tip. Anchoring the orb avoids the vertical jump
    /// users used to see on the first tap.
    private var compactContent: some View {
        VStack(spacing: 0) {
            ZStack {
                if hasAnyTranscript {
                    transcriptFeed
                        .padding(.horizontal, 16)
                        .transition(.opacity)
                } else {
                    idleHeader
                        .transition(.opacity)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)

            voiceCanvas
                .padding(.horizontal, 20)
                .padding(.bottom, 28)
        }
        .animation(.easeInOut(duration: 0.28), value: hasAnyTranscript)
    }

    private var wideContent: some View {
        HStack(alignment: .center, spacing: 24) {
            voiceCanvas
                .frame(maxWidth: .infinity)
                .padding(.leading, 24)

            ZStack {
                if hasAnyTranscript {
                    transcriptFeed
                        .padding(.trailing, 24)
                        .transition(.opacity)
                } else {
                    idleHeader
                        .padding(.trailing, 24)
                        .transition(.opacity)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .animation(.easeInOut(duration: 0.28), value: hasAnyTranscript)
    }

    // MARK: - Voice Canvas (orb + status + waveform/tip)

    private var voiceCanvas: some View {
        VStack(spacing: 18) {
            ZStack {
                pulseRings
                    .frame(width: 240, height: 240)

                Circle()
                    .fill(modeTint.opacity(0.18))
                    .frame(width: 188, height: 188)
                    .blur(radius: 14)

                Button(action: triggerVoice) {
                    ZStack {
                        Circle()
                            .fill(orbGradient)
                            .frame(width: 156, height: 156)
                            .shadow(color: modeTint.opacity(0.45), radius: 36, y: 10)
                        Circle()
                            .stroke(.white.opacity(0.14), lineWidth: 1)
                            .frame(width: 156, height: 156)
                        Image(systemName: orbIcon)
                            .font(.system(size: 46, weight: .black))
                            .foregroundStyle(.white)
                            .symbolRenderingMode(.hierarchical)
                            .contentTransition(.symbolEffect(.replace))
                    }
                }
                .buttonStyle(.plain)
                .sensoryFeedback(.impact(weight: .heavy), trigger: sessionRuntime.isMicrophoneActive)
                .accessibilityLabel(stateLabel)
                .accessibilityHint(Text("Double tap to start or stop voice"))
            }
            .frame(height: 240)

            statusPill

            // Auxiliary: live waveform when the mic is hot. Constant height keeps
            // the canvas itself stable; the idle tip lives in `idleHeader` now.
            ZStack {
                if isMicActive && sessionRuntime.session.mode == .listening {
                    liveWaveform
                        .transition(.opacity.combined(with: .scale(scale: 0.92)))
                }
            }
            .frame(height: 30)
            .animation(.easeInOut(duration: 0.25), value: isMicActive)
            .animation(.easeInOut(duration: 0.25), value: sessionRuntime.session.mode)
        }
    }

    /// Three concentric rings expanding outward at staggered phases. Driven by
    /// `TimelineView` so the redraw is smooth without storing animation state.
    private var pulseRings: some View {
        TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
            let t = context.date.timeIntervalSinceReferenceDate
            let speed = isMicActive ? 0.55 : 0.32
            let amp = isMicActive ? 0.50 : 0.26
            let opacityAmp = isMicActive ? 0.55 : 0.30

            ZStack {
                ForEach(0..<3, id: \.self) { i in
                    let phase = (t * speed + Double(i) * 0.33).truncatingRemainder(dividingBy: 1.0)
                    Circle()
                        .stroke(modeTint, lineWidth: isMicActive ? 1.6 : 1.1)
                        .scaleEffect(1.0 + CGFloat(phase) * CGFloat(amp))
                        .opacity((1.0 - phase) * opacityAmp)
                }
            }
            .frame(width: 178, height: 178)
        }
    }

    private var orbGradient: RadialGradient {
        RadialGradient(
            colors: [
                modeTint.opacity(0.97),
                modeTint.opacity(0.42),
                OpenRockyPalette.card
            ],
            center: .center,
            startRadius: 18,
            endRadius: 78
        )
    }

    private var statusPill: some View {
        HStack(spacing: 8) {
            Circle()
                .fill(stateDotColor)
                .frame(width: 7, height: 7)
                .opacity(isMicActive ? 1 : 0.6)
            Text(stateLabel)
                .font(.system(size: 13, weight: .bold, design: .rounded))
                .foregroundStyle(OpenRockyPalette.text)
                .contentTransition(.opacity)
        }
        .padding(.horizontal, 14)
        .padding(.vertical, 7)
        .background(.ultraThinMaterial, in: Capsule())
        .overlay(Capsule().stroke(OpenRockyPalette.stroke, lineWidth: 0.5))
        .animation(.easeInOut(duration: 0.2), value: stateLabel)
    }

    /// Sine-driven bars; not coupled to actual audio level (the realtime bridge
    /// doesn't expose one), but conveys "I'm listening" with motion alone.
    private var liveWaveform: some View {
        TimelineView(.animation(minimumInterval: 1.0 / 24.0)) { context in
            let t = context.date.timeIntervalSinceReferenceDate
            HStack(spacing: 5) {
                ForEach(0..<7, id: \.self) { i in
                    let phase = t * 6.0 + Double(i) * 0.85
                    let normalized = (sin(phase) * 0.5 + 0.5)
                    let height: CGFloat = 6 + CGFloat(normalized) * 22
                    Capsule()
                        .fill(modeTint.opacity(0.9))
                        .frame(width: 4, height: height)
                }
            }
            .frame(height: 30)
        }
    }

    // MARK: - Idle Header (shown above the orb when there's no transcript)

    /// Sits in the otherwise-empty space above the anchored orb. Calm, glanceable,
    /// and replaced by the transcript feed the moment a turn starts so the orb
    /// itself never moves.
    private var idleHeader: some View {
        VStack(spacing: 14) {
            Spacer(minLength: 0)
            VStack(spacing: 6) {
                Text(greeting)
                    .font(.system(size: 26, weight: .bold, design: .rounded))
                    .foregroundStyle(OpenRockyPalette.text.opacity(0.92))
                Text("How can I help today?")
                    .font(.system(size: 14, weight: .medium, design: .rounded))
                    .foregroundStyle(OpenRockyPalette.muted)
            }
            Spacer(minLength: 8)
            rotatingTipCard
            Spacer(minLength: 4)
        }
        .padding(.horizontal, 24)
        .padding(.bottom, 4)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private var rotatingTipCard: some View {
        HStack(spacing: 8) {
            Image(systemName: "sparkles")
                .font(.system(size: 12, weight: .semibold))
                .foregroundStyle(modeTint.opacity(0.9))
            Text(tips[tipIndex % tips.count])
                .font(.system(size: 13, weight: .medium, design: .rounded))
                .foregroundStyle(OpenRockyPalette.text.opacity(0.82))
                .lineLimit(2)
                .multilineTextAlignment(.center)
                .id(tipIndex)
                .transition(.opacity)
        }
        .padding(.horizontal, 14)
        .padding(.vertical, 10)
        .background(.ultraThinMaterial, in: Capsule())
        .overlay(Capsule().stroke(OpenRockyPalette.stroke, lineWidth: 0.5))
    }

    private var greeting: String {
        let hour = Calendar.current.component(.hour, from: Date())
        switch hour {
        case 5..<12: return String(localized: "Good morning")
        case 12..<18: return String(localized: "Good afternoon")
        default: return String(localized: "Good evening")
        }
    }

    // MARK: - Transcript Feed (chat-bubble style)

    private var transcriptFeed: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack(spacing: 8) {
                    let total = max(1, recentHistory.recentPairs.count)
                    ForEach(Array(recentHistory.recentPairs.enumerated()), id: \.element.id) { idx, pair in
                        // Older pairs are dimmer. Newest history pair sits at ~0.85.
                        let freshness = 0.55 + (Double(idx) / Double(total)) * 0.30
                        bubblesForPair(pair, freshness: freshness)
                    }
                    if let userText = liveUserText {
                        userBubble(userText, freshness: 1.0)
                            .id("liveUser")
                    }
                    if let assistantText = liveAssistantText {
                        assistantBubble(assistantText, freshness: 1.0)
                            .id("liveAssistant")
                    }
                }
                .padding(.vertical, 12)
            }
            .scrollIndicators(.hidden)
            .onChange(of: scrollSignature) { _, _ in
                withAnimation(.easeOut(duration: 0.25)) {
                    if liveAssistantText != nil {
                        proxy.scrollTo("liveAssistant", anchor: .bottom)
                    } else if liveUserText != nil {
                        proxy.scrollTo("liveUser", anchor: .bottom)
                    }
                }
            }
        }
    }

    @ViewBuilder
    private func bubblesForPair(_ pair: TranscriptPair, freshness: Double) -> some View {
        if let userText = pair.userText, !userText.isEmpty {
            userBubble(userText, freshness: freshness)
        }
        if let assistantText = pair.assistantText, !assistantText.isEmpty {
            assistantBubble(assistantText, freshness: freshness)
        }
    }

    private func userBubble(_ text: String, freshness: Double) -> some View {
        HStack(alignment: .bottom) {
            Spacer(minLength: 48)
            Text(text)
                .font(.system(size: 14, weight: .semibold, design: .rounded))
                .foregroundStyle(.white.opacity(min(1.0, freshness + 0.05)))
                .padding(.horizontal, 14)
                .padding(.vertical, 10)
                .background(
                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                        .fill(modeTint.opacity(0.55 * freshness + 0.18))
                )
                .lineLimit(freshness >= 1 ? nil : 3)
        }
    }

    private func assistantBubble(_ text: String, freshness: Double) -> some View {
        HStack(alignment: .bottom) {
            Text(text)
                .font(.system(size: 14, weight: .medium, design: .rounded))
                .foregroundStyle(OpenRockyPalette.text.opacity(freshness))
                .padding(.horizontal, 14)
                .padding(.vertical, 10)
                .background(
                    RoundedRectangle(cornerRadius: 18, style: .continuous)
                        .fill(OpenRockyPalette.cardElevated.opacity(0.55 * freshness + 0.20))
                )
                .lineLimit(freshness >= 1 ? nil : 3)
            Spacer(minLength: 48)
        }
    }

    // MARK: - State Helpers

    /// Filters out the `liveSeed`/idle placeholders so the first-launch UI is clean.
    /// Without this we'd render "OpenRocky is idle. Start voice or send text…" as a real bubble.
    private static let placeholderUserTexts: Set<String> = [
        "Speak or type a request to start a real session."
    ]
    private static let placeholderAssistantTexts: Set<String> = [
        "Rocky is waiting for a model reply.",
        "Rocky is idle. Start voice or send text to attach a live runtime."
    ]

    private var liveUserText: String? {
        let text = sessionRuntime.session.liveTranscript
        guard !text.isEmpty, !Self.placeholderUserTexts.contains(text) else { return nil }
        return text
    }

    private var liveAssistantText: String? {
        let text = sessionRuntime.session.assistantReply
        guard !text.isEmpty, !Self.placeholderAssistantTexts.contains(text) else { return nil }
        return text
    }

    private var hasAnyTranscript: Bool {
        liveUserText != nil || liveAssistantText != nil || !recentHistory.recentPairs.isEmpty
    }

    /// Combined hash to drive auto-scroll without observing every prop separately.
    private var scrollSignature: String {
        "\(liveUserText ?? "")|\(liveAssistantText ?? "")|\(recentHistory.recentPairs.count)"
    }

    private var modeTint: Color {
        switch sessionRuntime.session.mode {
        case .listening, .ready:
            return OpenRockyPalette.voicePrimary
        case .planning, .executing:
            return OpenRockyPalette.voiceDeep
        }
    }

    private var orbIcon: String {
        if isMicActive {
            switch sessionRuntime.session.mode {
            case .listening: return "waveform"
            case .planning: return "ellipsis"
            case .executing: return "bolt.fill"
            case .ready: return "waveform"
            }
        }
        return "mic.fill"
    }

    private var stateLabel: String {
        if isMicActive {
            switch sessionRuntime.session.mode {
            case .listening: return String(localized: "Listening")
            case .planning: return String(localized: "Thinking")
            case .executing: return String(localized: "Responding")
            case .ready: return String(localized: "Ready")
            }
        }
        // Idle path: distinguish "connecting" (start in flight) from "idle".
        if sessionRuntime.session.mode == .planning {
            return String(localized: "Connecting…")
        }
        return String(localized: "Tap to talk")
    }

    private var stateDotColor: Color {
        if !isMicActive {
            if sessionRuntime.session.mode == .planning { return OpenRockyPalette.warning }
            return OpenRockyPalette.muted
        }
        switch sessionRuntime.session.mode {
        case .listening: return OpenRockyPalette.success
        case .planning: return OpenRockyPalette.warning
        case .executing: return modeTint
        case .ready: return OpenRockyPalette.muted
        }
    }

    private var isMicActive: Bool { sessionRuntime.isMicrophoneActive }
    private var isVoiceReady: Bool { realtimeProviderStore.configuration.isConfigured }

    // MARK: - Actions

    private func triggerVoice() {
        if !isMicActive && !isVoiceReady {
            showsVoiceNotConfiguredAlert = true
            return
        }
        sessionRuntime.toggleVoiceSession(voiceConfiguration: realtimeProviderStore.configuration)
    }

    private func startTipRotation() {
        tipTask?.cancel()
        tipTask = Task { @MainActor [tips] in
            while !Task.isCancelled {
                try? await Task.sleep(for: .seconds(4.5))
                guard !Task.isCancelled else { break }
                withAnimation(.easeInOut(duration: 0.4)) {
                    tipIndex = (tipIndex + 1) % tips.count
                }
            }
        }
    }
}

// MARK: - Supporting Types

fileprivate struct TranscriptPair: Identifiable {
    let id: String
    let userText: String?
    let assistantText: String?
}

/// Pulls the last few user/assistant pairs from persistent storage for the current
/// conversation and republishes as `recentPairs`. Observes storage changes so the
/// voice home strip stays live as new turns land.
@MainActor
fileprivate final class RecentHistoryObserver: ObservableObject {
    @Published var recentPairs: [TranscriptPair] = []
    private let storage = OpenRockyPersistentStorageProvider.shared
    private var boundRuntime: OpenRockySessionRuntime?
    private var refreshTask: Task<Void, Never>?

    /// Number of completed (user,assistant) pairs to show. Live turn is rendered separately.
    private let maxPairs = 3

    func bind(to runtime: OpenRockySessionRuntime) {
        boundRuntime = runtime
        refresh()
        // Poll on assistantReply changes — the cheapest way to know a turn completed.
        refreshTask?.cancel()
        refreshTask = Task { @MainActor [weak self] in
            while !Task.isCancelled {
                try? await Task.sleep(for: .seconds(1))
                self?.refresh()
            }
        }
    }

    private func refresh() {
        guard let cid = boundRuntime?.conversationID, !cid.isEmpty else {
            if !recentPairs.isEmpty { recentPairs = [] }
            return
        }
        let messages = storage.messages(in: cid)
        // Group into pairs (user, assistant), keep only completed pairs, and drop the
        // very last pair because that's the "live turn" rendered separately.
        var pairs: [TranscriptPair] = []
        var i = 0
        while i < messages.count {
            let msg = messages[i]
            if msg.role == .user {
                let userText = msg.textContent
                var assistantText: String?
                if i + 1 < messages.count, messages[i + 1].role == .assistant {
                    assistantText = messages[i + 1].textContent
                    i += 2
                } else {
                    i += 1
                }
                pairs.append(TranscriptPair(id: msg.id, userText: userText, assistantText: assistantText))
            } else {
                i += 1
            }
        }
        // Drop the live turn (it's rendered from sessionRuntime.session.liveTranscript / assistantReply)
        let trimmed = pairs.dropLast().suffix(maxPairs)
        let newPairs = Array(trimmed)
        let newIDs = newPairs.map(\.id)
        let oldIDs = recentPairs.map(\.id)
        let newAssistantTexts = newPairs.map { $0.assistantText ?? "" }
        let oldAssistantTexts = recentPairs.map { $0.assistantText ?? "" }
        if newIDs != oldIDs || newAssistantTexts != oldAssistantTexts {
            recentPairs = newPairs
        }
    }

    deinit {
        refreshTask?.cancel()
    }
}
