import APIClient
import ComponentLibrary
import ComposableArchitecture
import SunoModelClient
import SwiftUI
import Utilities
import Errors
import Localization

// MARK: - Chat Reducer

@Reducer
public struct Chat {
    @ObservableState
    public struct State: Equatable {
        public var messages: IdentifiedArrayOf<ChatMessage> = []
        public var currentMessage: String = ""
        public var isLoading: Bool = false
        public var isWaitingForResponse: Bool = false
        public var streamingResponse: String = ""
        public var suggestions: [String] = []
        public var showSuggestions: Bool = false
        public var latestSongResponse: SongResponse?
        public var currentLyrics: String?
        public var chatTitle: String?
        public var currentlyPlayingSongId: String?
        public var currentlySelectedSongId: String?
        public var isCreatingMore: Bool = false
        public var showGenreSuggestions: Bool = false
        public var selectedGenres: [String] = []
        public var creativeGenreSuggestions: [String] = []
        public var showOnlySongs: Bool = false
        public var showExtendSheet: Bool = false
        public var showReplaceSheet: Bool = false
        public var extensionTime: String = "0:30s"
        @ObservationStateIgnored @ObservedBox public var chatAudio: ChatAudio.State = ChatAudio.State()
        public var pendingAudioClipId: String? // clipId from completed upload
        public var showChatSheet: Bool = true
        public var currentDetent: PresentationDetent = .height(120)
        public var customSheetDetent: CustomSheetDetent = .flex
        public var chatSheetHeight: CGFloat = 0
        public var isChatBarFocused: Bool = false
        public var isEditingLyrics: Bool = false
        public var isInLyricsFocusMode: Bool = false
        public var isInExtendMode: Bool = false
        public var isWorkspaceMenuPresented: Bool = false
        public var wasKeyboardVisible: Bool = false
        public var workspaces: OrpheusWorkspacesReducer.State = .init()
        @ObservationStateIgnored @ObservedBox public var orpheusCustomCreate: OrpheusCustomCreate.State

        public var detentHeight: CGFloat = 0

        // Create form state variables (matching swift-vibes)
        public var lyricsDescription: String = ""
        public var styleDescription: String = ""
        public var selectedModel: String = "v4.5"

        // Advanced options state
        public var weirdnessValue: Double = 0.5
        public var styleInfluenceValue: Double = 0.5
        public var audioInfluenceValue: Double = 0.5
        public var vocalGender: String = "Female"

        /// The currently selected model's external key (e.g., "chirp-v3-5")
        /// This is tracked so we can use it when creating more clips or extending songs
        public var selectedModelExternalKey: String = "chirp-v3-5"

        /// Whether the model has been registered with Orpheus backend
        /// This prevents race conditions where messages are sent before model registration completes
        public var isModelRegistered: Bool = false

        @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
        @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault
        
        @Shared public var me: Me

        public init(me: Shared<Me>) {
            self._me = me
            self.orpheusCustomCreate = OrpheusCustomCreate.State(
                mode: .default,
                me: me,
                prompt: nil
            )
        }
    }

    public enum Action: BindableAction {
        case task

        case sendMessage(String)
        case messageReceived(String)
        case orpheusMessagesUpdated(IdentifiedArrayOf<OrpheusMessage>)
        case newChat
        case suggestionTapped(String)
        case songPlayPause(String)
        case songSelected(String)
        case createMore
        case lyricsExpanded
        case genreSuggestionTapped(String)
        case showGenreSuggestionsToggled(Bool)
        case uploadAudio
        case recordAudio
        case extendSong(String, String)
        case replaceSong(String, String, String)
        case toggleSongsOnly
        case clearChat
        case chatSheetToggled(Bool)
        case detentChanged(PresentationDetent)
        case chatBarFocusChanged(Bool)
        case closeAudioFile
        case chatAudio(ChatAudio.Action)
        case editingLyricsToggled(Bool)
        case extendModeToggled(Bool)
        case customModeActivated
        case workspacesMenuToggled(Bool)
        case workspaces(OrpheusWorkspacesReducer.Action)
        case keyboardDismissedByTap
        case streamingError(String)
        case updateMessageWithSongData(messageId: String, songResponse: SongResponse)
        case selectedModelChanged(String) // externalKey
        case modelChanged // Triggered when selectedSunoModel Shared state changes
        case sendCreateMessage // Send message from CreateView form
        case orpheusCustomCreate(OrpheusCustomCreate.Action)
//        case isNavigatingToWorkspaceChanged(Bool)
        case binding(BindingAction<State>)
    }

    @Dependency(\.orpheusClient) var orpheusClient
    @Dependency(\.apiClientV2) var apiClientV2

    public init() {}

    public var body: some ReducerOf<Self> {
        BindingReducer()

        Scope(state: \.workspaces, action: \.workspaces) {
            OrpheusWorkspacesReducer()
        }

        Scope(state: \.chatAudio, action: \.chatAudio) {
            ChatAudio()
        }
        
        Scope(state: \.orpheusCustomCreate, action: \.orpheusCustomCreate) {
            OrpheusCustomCreate()
        }
        
        Reduce { state, action in
            struct OrpheusMessageStreamCancellable: Hashable {}
            switch action {
            case .task:
                return .merge(
                    // Start observing messages from repository
                    .run { send in
                        // Start new session first (needed for model registration)
                        await orpheusClient.startNewSession()

                        // Register initial model (matches Android's observeModelSelectionUpdates)
                        // This must complete before messages can be sent
                        @Dependency(SunoModelClient.self) var sunoModelClient
                        let modelKey = sunoModelClient.getCurrentModelExternalKey(isAudioUpload: false)
                        await orpheusClient.registerModelChange(modelKey)

                        // Update state with the selected model key and mark as registered
                        await send(.selectedModelChanged(modelKey))
                    },
                    /// We can't use `.stream` here because `orpheusClient.messages()` needs to be accessed via `await`
                    .run { send in
                        let messagesStream = await orpheusClient.messages()
                        for await messages in messagesStream {
                            await send(.orpheusMessagesUpdated(messages))
                        }
                    }
                    .cancellable(id: OrpheusMessageStreamCancellable(), cancelInFlight: true)
                )

            case let .sendMessage(message):
                guard !message.isEmpty && !state.isLoading else { return .none }

                // Ensure model is registered before sending message (prevents race condition)
                // If model isn't registered yet, register it first, then send the message
                if !state.isModelRegistered {
                    return .run { [orpheusClient] send in
                        @Dependency(SunoModelClient.self) var sunoModelClient
                        let modelKey = sunoModelClient.getCurrentModelExternalKey(isAudioUpload: false)
                        await orpheusClient.registerModelChange(modelKey)
                        await send(.selectedModelChanged(modelKey))
                        // Now send the message after model is registered
                        await send(.sendMessage(message))
                    }
                }

                state.currentMessage = ""
                state.isLoading = true
                state.isWaitingForResponse = true
                state.streamingResponse = ""

                // Note: User message will be added by orpheusClient when sending
                // We don't add it here to avoid duplicates

                // Generate chat title if first message
                if state.chatTitle == nil {
                    state.chatTitle = generateFallbackTitle(from: message)
                }

                // Send message to OrpheusChatRepository
                // TODO: Include clipIds when sending message (pendingAudioClipId)
                state.pendingAudioClipId = nil

                return .run { send in
                    do {
                        try await orpheusClient.sendMessage(message)
                        // Messages will be updated via orpheusMessagesUpdated action
                    } catch {
                        print("❌ Failed to send message: \(error)")
                        await send(.streamingError(error.localizedDescription))
                    }
                }

            case let .messageReceived(response):
                state.isLoading = false
                state.streamingResponse = ""

                // Check if we received a new assistant response
                // For song responses or non-empty text responses, clear waiting flag
                let receivedNewResponse = state.isWaitingForResponse &&
                    (response.contains("Generated songs!") || response.contains("song") || !response.isEmpty)
                if receivedNewResponse {
                    state.isWaitingForResponse = false
                }

                // Parse for song response
                if let songResponse = parseSongResponse(from: response) {
                    state.latestSongResponse = songResponse
                    state.suggestions = songResponse.suggestions

                    if let lyrics = songResponse.lyrics {
                        state.currentLyrics = lyrics
                    }

                    // Auto-select first song
                    if let firstSong = songResponse.songs.first {
                        state.currentlySelectedSongId = firstSong.id
                    }

                    // For "create more" responses, don't show the reply text - only songs
                    let isCreateMoreResponse = state.isCreatingMore
                    let messageContent = isCreateMoreResponse ? "" : (songResponse.reply.isEmpty ? "Generated songs!" : songResponse.reply)
                    let message = ChatMessage(content: messageContent, isOutgoing: false, songData: songResponse)
                    state.messages.append(message)

                    // Reset creating more flag
                    state.isCreatingMore = false
                } else {
                    // For non-song responses, extract text if it's JSON, otherwise use raw response
                    // Skip if this was a "create more" request that failed
                    if !state.isCreatingMore {
                        let displayText = extractTextFromJSON(response) ?? response
                        if !displayText.isEmpty {
                            let message = ChatMessage(content: displayText, isOutgoing: false)
                            state.messages.append(message)
                        }
                    }
                    // Reset creating more flag
                    state.isCreatingMore = false
                }

                return .none

            case let .suggestionTapped(suggestion):
                let lowercased = suggestion.lowercased()

                if lowercased.contains("edit lyrics") || lowercased.contains("lyrics editor") {
                    state.isEditingLyrics = true
                    state.isInLyricsFocusMode = true
                } else if lowercased.contains("extend") {
                    state.isInExtendMode = true
                    state.isEditingLyrics = false
                    state.isInLyricsFocusMode = false
                } else if lowercased.contains("replace") {
                    state.showReplaceSheet = true
                } else if lowercased.contains("change genre") {
                    state.showGenreSuggestions = true
                    state.selectedGenres = []
                } else {
                    state.suggestions = []
                    return Effect.send(.sendMessage(suggestion))
                }

                return .none

            case let .songPlayPause(songId):
                // Find the song to get its audioURL
                var audioURL: String? = nil
                for message in state.messages {
                    if let songData = message.songData {
                        for song in songData.songs {
                            if song.id == songId {
                                audioURL = song.audioURL
                                break
                            }
                        }
                    }
                }

                // Use AudioManager to handle playback
                // This is a synchronous side-effect; use fireAndForget to avoid ambiguity with async .run
                return .run { [audioURL] _ in
                    AudioManager.shared.togglePlayback(songId: songId, audioURL: audioURL)
                }

            case let .songSelected(songId):
                if state.currentlySelectedSongId == songId {
                    state.currentlySelectedSongId = nil
                    if state.currentlyPlayingSongId == songId {
                        state.currentlyPlayingSongId = nil
                    }
                } else {
                    state.currentlySelectedSongId = songId
                }
                return .none

            case .createMore:
                guard !state.isLoading else { return .none }
                // Set flag first, then send message (sendMessage will set isLoading)
                state.isCreatingMore = true
                // Send actual message to generate more songs (like reference)
                return Effect.send(.sendMessage("create another version"))

            case .lyricsExpanded:
                // Mark that we're editing lyrics to change header title
                state.isEditingLyrics = true
                state.isInLyricsFocusMode = true
                // Expand chat sheet to large detent to show CreateView
                state.currentDetent = .large
                return .none

            case let .genreSuggestionTapped(genre):
                let cleanGenre = genre.hasPrefix("+") ? String(genre.dropFirst()) : genre
                state.selectedGenres.append(cleanGenre)
                return .none

            case let .showGenreSuggestionsToggled(show):
                state.showGenreSuggestions = show
                return .none

            case .uploadAudio:
                return .send(.chatAudio(.uploadTapped))

            case .recordAudio:
                return .send(.chatAudio(.recordTapped))

            case let .extendSong(songTitle, extensionTime):
                let message = "Extend \(songTitle) after \(extensionTime)"
                return Effect.send(.sendMessage(message))

            case let .replaceSong(songTitle, startTime, endTime):
                let message = "Replace \(songTitle) from \(startTime) - \(endTime)"
                return Effect.send(.sendMessage(message))

            case .toggleSongsOnly:
                state.showOnlySongs.toggle()
                return .none

            case let .chatSheetToggled(isShowing):
                state.showChatSheet = isShowing
                // Reset keyboard visibility flag when sheet is manually toggled
                if isShowing {
                    state.wasKeyboardVisible = false
                }
                return .none

            case let .detentChanged(detent):
                state.currentDetent = detent
                return .none

            case let .chatBarFocusChanged(isFocused):
                state.isChatBarFocused = isFocused
                return .none

            case .closeAudioFile:
                state.pendingAudioClipId = nil
                return .send(.chatAudio(.closeAudioTapped))

            // MARK: - ChatAudio Integration
            case .chatAudio(.delegate(.uploadCompleted(let clipId))):
                state.pendingAudioClipId = clipId
                return .none

            case .chatAudio(.delegate(.clipSelectedFromLibrary(let snippet))):
                // TODO: handle
                return .none

            case .chatAudio:
                return .none

            case let .editingLyricsToggled(isEditing):
                state.isEditingLyrics = isEditing
                if isEditing {
                    state.isInExtendMode = false
                    state.currentDetent = .large
                }
                return .none

            case let .extendModeToggled(isExtending):
                state.isInExtendMode = isExtending
                if isExtending {
                    state.isEditingLyrics = false
                    state.currentDetent = .large
                }
                return .none

            case .customModeActivated:
                state.isEditingLyrics = false
                state.isInExtendMode = false
                state.currentDetent = .large
                return .none

            case let .workspacesMenuToggled(isShowing):
                state.isWorkspaceMenuPresented = isShowing
                // Hide chat sheet when showing workspaces menu
                if isShowing {
                    state.showChatSheet = false
                } else {
                    state.showChatSheet = true
                }
                return .none

            case .keyboardDismissedByTap:
                if state.isChatBarFocused {
                    state.wasKeyboardVisible = true
                    state.isChatBarFocused = false
                }
                return .none

            case let .streamingError(error):
                state.isWaitingForResponse = false
                // Show error to user (you may want to add an error message to the chat)
                print("❌ Streaming error: \(error)")
                return .none

            // FIXME: Jimmy - This should not happen in the reducer. The reducer should only get a model change
            case let .orpheusMessagesUpdated(orpheusMessages):
                // Optimize: Only convert and update messages that have changed
                // This prevents unnecessary re-renders of unchanged messages
                print("📨 Received \(orpheusMessages.count) Orpheus messages")

                // Convert OrpheusMessage to ChatMessage efficiently
                // Use incremental updates: only update messages that changed
                var updatedMessages = state.messages

                // Track which message IDs we've seen
                var seenMessageIds = Set<String>()

                for orpheusMessage in orpheusMessages {
                    seenMessageIds.insert(orpheusMessage.id)

                    // Check if this message already exists and if it changed
                    if let existingIndex = updatedMessages.index(id: orpheusMessage.id) {
                        let existingMessage = updatedMessages[existingIndex]
                        let newChatMessage = convertOrpheusMessageToChatMessage(orpheusMessage)

                        // Always update the existing message to preserve songData if it exists
                        // This ensures we don't lose songData when content updates during streaming
                        // Match swift-vibes: one message with both content (reply) and songData
                        if existingMessage.songData != nil {
                            // Preserve existing songData - only update content
                            updatedMessages[existingIndex] = ChatMessage(
                                id: existingMessage.id,
                                content: newChatMessage.content, // Update content (reply text)
                                isOutgoing: existingMessage.isOutgoing,
                                songData: existingMessage.songData, // Keep existing songData
                                audioFileName: existingMessage.audioFileName
                            )
                        } else {
                            // No songData yet - update normally
                            if existingMessage.content != newChatMessage.content ||
                               existingMessage.songData != newChatMessage.songData {
                                updatedMessages[existingIndex] = newChatMessage
                            }
                        }
                    } else {
                        // New message - add it
                        // For assistant messages with generatedClips, this will be updated with songData later
                        updatedMessages.append(convertOrpheusMessageToChatMessage(orpheusMessage))
                    }
                }

                // Remove messages that are no longer in the Orpheus list (shouldn't happen, but safety check)
                // Also deduplicate: if we have multiple OrpheusMessages that should be merged (e.g.,
                // one with generatedClips and one with simple_message content), we should only show one
                // The MessageToolCallHandler should have merged them, but we add this as a safety check
                updatedMessages.removeAll { !seenMessageIds.contains($0.id) }

                // Deduplicate by ID (shouldn't be necessary if handlers work correctly, but safety check)
                var uniqueMessages: IdentifiedArrayOf<ChatMessage> = []
                var seenIds = Set<String>()
                for message in updatedMessages {
                    if !seenIds.contains(message.id) {
                        uniqueMessages.append(message)
                        seenIds.insert(message.id)
                    } else {
                        // If we see a duplicate ID, merge the content and songData
                        if let existingIndex = uniqueMessages.index(id: message.id) {
                            let existing = uniqueMessages[existingIndex]
                            uniqueMessages[existingIndex] = ChatMessage(
                                id: existing.id,
                                content: message.content.isEmpty ? existing.content : message.content,
                                isOutgoing: existing.isOutgoing,
                                songData: message.songData ?? existing.songData,
                                audioFileName: existing.audioFileName
                            )
                        }
                    }
                }
                updatedMessages = uniqueMessages

                state.messages = updatedMessages
                print("💬 Updated to \(updatedMessages.count) ChatMessages")

                // Check if we need to fetch clips for any messages
                // Only fetch when streaming is complete (isStreaming == false) to avoid showing songs before reply finishes
                // This ensures the reply text from simple_message is fully streamed before showing lyrics/songs
                let messagesNeedingClips = orpheusMessages.filter { message in
                    !message.generatedClips.isEmpty &&
                    message.role == .assistant &&
                    !message.isStreaming && // Only fetch when streaming is complete
                    updatedMessages.first(where: { message.id == $0.id })?.songData == nil
                }

                // Check if we received an assistant response
                // MessageFinished event sets isStreaming to false, indicating the message is complete
                let lastMessage = orpheusMessages.last
                let hasAssistantMessage = lastMessage?.role == .assistant
                let isStreaming = lastMessage?.isStreaming == true
                let hasAnyStreamingMessage = orpheusMessages.contains { $0.isStreaming }

                // Clear waiting flag if:
                // 1. We have an assistant message that's finished (not streaming), OR
                // 2. We have an assistant message with content or clips (even if still streaming)
                if state.isWaitingForResponse && hasAssistantMessage {
                    if !isStreaming {
                        // Message is finished (MessageFinished event was received)
                        state.isWaitingForResponse = false
                    } else if !(lastMessage?.content.isEmpty ?? true) || !(lastMessage?.generatedClips.isEmpty ?? true) {
                        // Message has content or clips (even if still streaming)
                        state.isWaitingForResponse = false
                    }
                }

                // Update loading state: keep loading if waiting for response OR if any message is streaming
                // This ensures "Crafting..." shows while waiting for initial response or while streaming
                if state.isWaitingForResponse || hasAnyStreamingMessage {
                    // Keep loading state while waiting or streaming
                    state.isLoading = true
                } else {
                    // Only clear loading state when no messages are streaming and we're not waiting
                    state.isLoading = false
                    state.streamingResponse = ""
                }

                // Fetch clips asynchronously if needed (don't return early - let loading state logic run)
                if !messagesNeedingClips.isEmpty {
                    return .run { send in
                        for message in messagesNeedingClips {
                            do {
                                let clips = try await fetchClipsForMessage(message)
                                if !clips.isEmpty {
                                    // Use the message's content directly - MessageToolCallHandler should have already
                                    // extracted content from simple_message tool calls and updated message.content
                                    // No need to look for "subsequent messages" - everything is in the same message
                                    let songResponse = convertClipsToSongResponse(clips: clips, messageContent: message.content)
                                    // Send an action to update the message with song data
                                    await send(.updateMessageWithSongData(messageId: message.id, songResponse: songResponse))
                                }
                            } catch {
                                print("⚠️ Failed to fetch clips for message \(message.id): \(error)")
                            }
                        }
                    }
                }

                return .none

            case let .updateMessageWithSongData(messageId, songResponse):
                guard let messageIndex = state.messages.index(id: messageId) else {
                    assertionFailure("Message missing from messages list") // Prob shouldn't be an assertionFail
                    return .none
                }
                let oldMessage = state.messages[messageIndex]
                // The message.content should already contain the reply text from simple_message tool calls
                // (extracted by MessageToolCallHandler). Use it directly, or fall back to songResponse.reply if empty.
                // This ensures the reply text is merged into the same message as the song generation.
                let messageContent = oldMessage.content.isEmpty ? songResponse.reply : oldMessage.content
                state.messages.update(
                    ChatMessage(
                        id: oldMessage.id,
                        content: messageContent,
                        isOutgoing: oldMessage.isOutgoing,
                        songData: songResponse,
                        audioFileName: oldMessage.audioFileName
                    ),
                    at: messageIndex
                )

                // Update suggestions and lyrics if needed
                state.suggestions = songResponse.suggestions
                if let lyrics = songResponse.lyrics {
                    state.currentLyrics = lyrics
                }

                // Auto-select first song if none selected
                if state.currentlySelectedSongId == nil,
 let firstSong = songResponse.songs.first {
                    state.currentlySelectedSongId = firstSong.id
                }
                return .none

            case .newChat:
                return .run { send in
                    await orpheusClient.startNewSession()
                    await send(.orpheusMessagesUpdated([]))
                }

            case let .selectedModelChanged(externalKey):
                state.selectedModelExternalKey = externalKey
                state.isModelRegistered = true
                return .none

            case .modelChanged:
                // Handle model change from Shared state observation
                // Get the current model key and register it
                return .run { send in
                    @Dependency(SunoModelClient.self) var sunoModelClient
                    @Dependency(\.orpheusClient) var orpheusClient

                    let modelKey = sunoModelClient.getCurrentModelExternalKey(isAudioUpload: false)
                    await orpheusClient.registerModelChange(modelKey)
                    await send(.selectedModelChanged(modelKey))
                }

            case .clearChat:
                let me = state.$me
                state = State(me: me)
                return .run { send in
                    await orpheusClient.startNewSession()
                    await send(.orpheusMessagesUpdated([]))
                }

            case .sendCreateMessage:
                // Build formatted message from create form (matches swift-vibes)
                var messageParts: [String] = []

                messageParts.append("Create a song with:")

                // Lyrics
                if !state.lyricsDescription.isEmpty {
                    messageParts.append("Lyrics: \(state.lyricsDescription)")
                }

                // Style
                if !state.styleDescription.isEmpty {
                    messageParts.append("Style: \(state.styleDescription)")
                }

                // Advanced Options (only include if not default values)
                var advancedOptionsParts: [String] = []

                if state.weirdnessValue != 0.5 {
                    advancedOptionsParts.append("Weirdness: \(Int(state.weirdnessValue * 100))%")
                }

                if state.styleInfluenceValue != 0.5 {
                    advancedOptionsParts.append("Style Influence: \(Int(state.styleInfluenceValue * 100))%")
                }

                if state.audioInfluenceValue != 0.5 {
                    advancedOptionsParts.append("Audio Influence: \(Int(state.audioInfluenceValue * 100))%")
                }

                if state.vocalGender != "Female" {
                    advancedOptionsParts.append("Vocal Gender: \(state.vocalGender)")
                }

                if !advancedOptionsParts.isEmpty {
                    messageParts.append("Advanced Options: \(advancedOptionsParts.joined(separator: ", "))")
                }

                // Model
                messageParts.append("Model: Suno \(state.selectedModel)")

                // Join all parts with newlines
                let formattedMessage = messageParts.joined(separator: "\n")

                // Reset form fields after sending (matches swift-vibes)
                state.lyricsDescription = ""
                state.styleDescription = ""
                state.weirdnessValue = 0.5
                state.styleInfluenceValue = 0.5
                state.audioInfluenceValue = 0.5
                state.vocalGender = "Female"

                // Change sheet detent back to .flex
                state.customSheetDetent = .flex

                // Send the message using existing sendMessage logic
                return Effect.send(.sendMessage(formattedMessage))
            
            case .orpheusCustomCreate(.delegate(.createTapped(let mode, let sourceClip, let prompt))):
                var messageParts: [String] = []
                messageParts.append("Create a song with:")
                
                if !prompt.lyrics.isEmpty {
                    messageParts.append("Lyrics: \(prompt.lyrics)")
                }
                
                if !prompt.styles.isEmpty {
                    messageParts.append("Style: \(prompt.styles)")
                }
                
                var advancedOptionsParts: [String] = []
                if let weirdness = prompt.weirdnessConstraint, weirdness != 0.5 {
                    advancedOptionsParts.append("Weirdness: \(Int(weirdness * 100))%")
                }
                if let styleInfluence = prompt.styleWeight, styleInfluence != 0.5 {
                    advancedOptionsParts.append("Style Influence: \(Int(styleInfluence * 100))%")
                }
                if let audioInfluence = prompt.audioWeight, audioInfluence != 0.5 {
                    advancedOptionsParts.append("Audio Influence: \(Int(audioInfluence * 100))%")
                }
                
                if !advancedOptionsParts.isEmpty {
                    messageParts.append("Advanced Options: \(advancedOptionsParts.joined(separator: ", "))")
                }
                
                let formattedMessage = messageParts.joined(separator: "\n")
                
                // Sync state back to Chat.State before dismissing
                state.lyricsDescription = state.orpheusCustomCreate.lyrics
                state.styleDescription = state.orpheusCustomCreate.styles
                state.weirdnessValue = state.orpheusCustomCreate.weirdnessConstraint
                state.styleInfluenceValue = state.orpheusCustomCreate.styleWeight
                state.audioInfluenceValue = state.orpheusCustomCreate.audioWeight
                
                // Reset detent
                state.customSheetDetent = .flex

                return .send(.sendMessage(formattedMessage))
            
            case .orpheusCustomCreate:
                // Handled by Scope
                return .none
                
            case .workspaces:
                // Handled by Scope
                return .none

            case .binding:
                return .none
            }
        }
        ._printChanges()
    }
}

// MARK: - Chat Models

public struct ChatMessage: Equatable, Identifiable {
    public let id: String
    public let content: String
    public let isOutgoing: Bool
    public let songData: SongResponse?
    public let audioFileName: String?

    public init(
        id: String = UUID().uuidString,
        content: String,
        isOutgoing: Bool,
        songData: SongResponse? = nil,
        audioFileName: String? = nil
    ) {
        self.id = id
        self.content = content
        self.isOutgoing = isOutgoing
        self.songData = songData
        self.audioFileName = audioFileName
    }
}

public struct SongResponse: Equatable, Codable {
    public let reply: String
    public let songs: [Song]
    public let suggestions: [String]
    public let lyrics: String?

    public init(reply: String, songs: [Song], suggestions: [String], lyrics: String? = nil) {
        self.reply = reply
        self.songs = songs
        self.suggestions = suggestions
        self.lyrics = lyrics
    }
}

public struct Song: Equatable, Identifiable, Codable {
    public let id: String
    public let title: String
    public let genres: [String]
    public let artwork: String?
    public let audioURL: String?

    public init(id: String, title: String, genres: [String], artwork: String? = nil, audioURL: String? = nil) {
        self.id = id
        self.title = title
        self.genres = genres
        self.artwork = artwork
        self.audioURL = audioURL
    }

    // Custom decoder to generate ID when missing from JSON
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // Generate ID if not present in JSON
        self.id = (try? container.decode(String.self, forKey: .id)) ?? UUID().uuidString
        self.title = try container.decode(String.self, forKey: .title)
        self.genres = try container.decode([String].self, forKey: .genres)
        self.artwork = try container.decodeIfPresent(String.self, forKey: .artwork)
        self.audioURL = try container.decodeIfPresent(String.self, forKey: .audioURL)
    }

    // Custom encoder to exclude id from JSON if needed (or include it)
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // Optionally encode id - remove this line if id should never be in JSON
        try container.encode(id, forKey: .id)
        try container.encode(title, forKey: .title)
        try container.encode(genres, forKey: .genres)
        try container.encodeIfPresent(artwork, forKey: .artwork)
        try container.encodeIfPresent(audioURL, forKey: .audioURL)
    }

    private enum CodingKeys: String, CodingKey {
        case id, title, genres, artwork, audioURL
    }
}

//public struct LyricsStructure: Equatable, Codable {
//    public let sections: [LyricsSection]
//
//    public init(sections: [LyricsSection]) {
//        self.sections = sections
//    }
//}
//
//public struct LyricsSection: Equatable, Codable {
//    public let type: String
//    public let content: String
//
//    public init(type: String, content: String) {
//        self.type = type
//        self.content = content
//    }
//}

// MARK: - Chat View

public struct ChatView: View {
    @Bindable var store: StoreOf<Chat>
    @ObservedObject private var audioManager = AudioManager.shared
    @Environment(\.dismiss) private var dismiss

    // Credits animation state
    @State private var displayCreditsCount: Int = 0
    @State private var isCreditsAnimating: Bool = false
    @State private var creditsAnimationOffset: CGFloat = 0

    public init(store: StoreOf<Chat>) {
        self.store = store
    }

    public var body: some View {
        NavigationStack {
            ZStack {
                // Chat content container
                ZStack {
                    if store.messages.isEmpty {
                        EmptyChat(isAuraVisible: store.messages.isEmpty && !store.isLoading)
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                    } else {
                        ChatsView(
                            messages: store.messages,
                            currentlyPlayingSongId: audioManager.currentlyPlayingSongId,
                            currentlySelectedSongId: store.currentlySelectedSongId,
                            isLoading: store.isLoading,
                            onSongPlayPause: { store.send(.songPlayPause($0)) },
                            onSongSelect: { store.send(.songSelected($0)) },
                            onCreateMore: { store.send(.createMore) },
                            onLyricsExpand: { store.send(.lyricsExpanded) },
                            onLinkTap: { url in
                                // Handle link taps
                                print("Link tapped: \(url)")
                            },
                            shouldAutoScroll: false
                        )
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .opacity(store.showOnlySongs ? 0 : 1)

                        // Song grid view when playlist filter is active
                        if store.showOnlySongs {
                            SongGridView(
                                messages: store.messages,
                                currentlyPlayingSongId: audioManager.currentlyPlayingSongId,
                                onSongPlayPause: { store.send(.songPlayPause($0)) }
                            )
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                        }
                    }
                }
                .toolbar {
                    if !store.isWorkspaceMenuPresented {
                        ToolbarItem(placement: .navigationBarLeading) {
                            Button {
                                store.send(.workspacesMenuToggled(true))
                            } label: {
                                Image.FigmaMCP.workspace
                                    .figmaMCPIconStyle(
                                        size: Image.FigmaMCPSize.medium,
                                        semanticColor: ChatConstants.Colors.Foreground.secondary
                                    )
                            }
                        }

                        ToolbarItem(placement: .title) {
                            VStack(spacing: 2) {
                                if !store.messages.isEmpty {
                                    Menu {
                                        // Filter section
                                        Section(header: Text("FILTER")) {
                                            Button(action: {
                                                if store.showOnlySongs {
                                                    store.send(.toggleSongsOnly)
                                                }
                                            }) {
                                                Label("All", systemImage: !store.showOnlySongs ? "checkmark" : "")
                                            }

                                            Button(action: {
                                                if !store.showOnlySongs {
                                                    store.send(.toggleSongsOnly)
                                                }
                                            }) {
                                                Label("Songs only", systemImage: store.showOnlySongs ? "checkmark" : "")
                                            }
                                        }

                                        Divider()

                                        Button("Clear chat") {
                                            store.send(.clearChat)
                                        }
                                    } label: {
                                        HStack(spacing: 4) {
                                            Text(store.chatTitle ?? "Chat")
                                                .figmaMCPTypography(TypographyV1.FigmaMCP.mediumTitle)
                                                .foregroundColor(ChatConstants.Colors.Foreground.primary)

                                            Image.FigmaMCP.triangleDown
                                                .figmaMCPIconStyle(
                                                    size: Image.FigmaMCPSize.small,
                                                    semanticColor: ChatConstants.Colors.Foreground.primary
                                                )
                                        }
                                    }
                                } else {
                                    Text(store.chatTitle ?? "Chat")
                                        .figmaMCPTypography(TypographyV1.FigmaMCP.mediumTitle)
                                        .foregroundColor(ChatConstants.Colors.Foreground.primary)
                                }

                                // Credits display below title with animation
                                if let billingInfo = store.billingInfo {
                                    Text("\(displayCreditsCount) credits")
                                        .figmaMCPTypography(TypographyV1.FigmaMCP.xSmallTitle)
                                        .foregroundColor(creditsTextColor(count: displayCreditsCount))
                                        .tracking(0.2)
                                        .offset(y: creditsAnimationOffset)
                                        .animation(.easeInOut(duration: 0.3), value: isCreditsAnimating)
                                        .animation(.easeOut(duration: 0.6), value: creditsAnimationOffset)
                                        .onChange(of: billingInfo.totalCreditsLeft) { oldValue, newValue in
                                            animateCreditsChange(to: newValue)
                                        }
                                        .onAppear {
                                            displayCreditsCount = billingInfo.totalCreditsLeft
                                        }
                                }
                            }
                        }

                        ToolbarItem(placement: .navigationBarTrailing) {
                            Button {
                                dismiss()
                            } label: {
                                Image.FigmaMCP.chevronDown
                                    .figmaMCPIconStyle(
                                        size: Image.FigmaMCPSize.medium,
                                        semanticColor: ChatConstants.Colors.Foreground.primary
                                    )
                            }
                        }
                    }
                }

                // Slide-out workspaces menu overlay
                WorkspacesOverlay(
                    store: store.scope(state: \.workspaces, action: \.workspaces),
                    isPresented: Binding(
                        get: { store.isWorkspaceMenuPresented },
                        set: { newValue in
                            store.send(.workspacesMenuToggled(newValue))
                        }
                    )
                )
            }
        }
        .background {
            ChatConstants.Colors.Background.primary
                .ignoresSafeArea()
                .preferredColorScheme(.dark)
        }
        .task { store.send(.task) }
        .onChange(of: store.selectedSunoModel) { oldValue, newValue in
            // When model changes, register it with Orpheus (matches Android's observeModelSelectionUpdates)
            // This ensures the backend always knows the current model
            // Send an action to handle this in the reducer where we have access to dependencies
            store.send(.modelChanged)
        }
        .onTapGesture {
            // Dismiss keyboard when tapping on chat content (matches ChatView.swift)
            // Placed on NavigationStack level to work with presentationBackgroundInteraction
            if store.isChatBarFocused {
                store.send(.keyboardDismissedByTap)
            }
            dismissKeyboard()
        }
        .overlay {
            // CustomSheet overlays the entire view
            CustomSheet(
                isPresented: $store.showChatSheet.sending(\.chatSheetToggled),
                currentDetent: $store.customSheetDetent,
                detents: [.flex, .full],
                style: .floating,
                background: .glass,
                sheetHeight: $store.chatSheetHeight
            ) {
                // Conditionally show OrpheusCustomCreateSheetView in full mode, ChatBar in flex mode
                // TODO: (Asad) - deviation from swift-vibes; revisit later
                if store.customSheetDetent == .full {
                    OrpheusCustomCreateSheetView(store: store.scope(state: \.orpheusCustomCreate, action: \.orpheusCustomCreate))
                } else {
                    VStack(spacing: 0) {
                        chatBarContent
                    }
                }
            } aboveSheetContent: {
                // Presets scroller - appears above sheet in .flex mode only when empty
                if store.messages.isEmpty {
                    PresetsScroller(
                        onPresetTap: { preset in
                            store.currentMessage = preset
                            store.send(.sendMessage(preset))
                        },
                        bottomPadding: 0
                    )
                }

                // Suggestions - appears above sheet when there are suggestions
                if !store.messages.isEmpty && shouldShowSuggestions {
                    suggestionsView
                }
            }
            .onChange(of: store.customSheetDetent) { oldDetent, newDetent in
                // Populate form when swiping up to full mode with a selected song (matches swift-vibes)
                if oldDetent == .flex && newDetent == .full {
                    populateFormFromSelectedSong(store: store)
                    
                    // Update OrpheusCustomCreate state with current form values
                    let prompt = Prompt(
                        title: "",
                        lyrics: store.lyricsDescription,
                        instrumental: false,
                        styles: store.styleDescription,
                        excludeStyles: "",
                        text: "",
                        generationType: .text,
                        audioWeight: store.audioInfluenceValue != 0.5 ? store.audioInfluenceValue : nil,
                        styleWeight: store.styleInfluenceValue != 0.5 ? store.styleInfluenceValue : nil,
                        weirdnessConstraint: store.weirdnessValue != 0.5 ? store.weirdnessValue : nil
                    )
                    
                    // Update child state with new prompt
                    store.orpheusCustomCreate = OrpheusCustomCreate.State(
                        mode: .default,
                        me: store.$me,
                        prompt: prompt
                    )
                }

                // Handle dismissal when sheet is swiped down to flex size (clean TCA pattern)
                if oldDetent == .full && newDetent == .flex {
                    // Sync OrpheusCustomCreate state back to Chat.State properties
                    store.lyricsDescription = store.orpheusCustomCreate.lyrics
                    store.styleDescription = store.orpheusCustomCreate.styles
                    store.weirdnessValue = store.orpheusCustomCreate.weirdnessConstraint
                    store.styleInfluenceValue = store.orpheusCustomCreate.styleWeight
                    store.audioInfluenceValue = store.orpheusCustomCreate.audioWeight
                    
                    // Reset modes when sheet is swiped down to flex size (matches swift-vibes)
                    store.isInLyricsFocusMode = false
                    store.isEditingLyrics = false
                    resetExtendMode(store: store)
                }
            }
        }
        .sheet(item: $store.scope(state: \.chatAudio.destination?.termsOfService, action: \.chatAudio.destination.termsOfService)) { _ in
            ChatAudioTermsOfServiceView(
                onAccept: {
                    store.send(.chatAudio(.termsOfServiceAccepted))
                },
                onDismiss: {
                    store.send(.chatAudio(.termsOfServiceDismissed))
                }
            )
        }
        .sheet(item: $store.scope(state: \.chatAudio.destination?.recording, action: \.chatAudio.destination.recording)) { store in
            ChatAudioRecorderView(store: store)
        }
        .sheet(item: $store.scope(
            state: \.chatAudio.destination?.libraryClipPicker,
            action: \.chatAudio.destination.libraryClipPicker
        )) { store in
            LibraryClipPickerSheet(store: store)
        }
        .alert(
            L10n.FeatureCreateClip.chatMicrophonePermissionTitle,
            isPresented: Binding(
                get: { store.chatAudio.showMicrophonePermissionAlert },
                set: { _ in store.send(.chatAudio(.microphonePermissionAlertDismissed)) }
            )
        ) {
            Button(L10n.FeatureCreateClip.notNow, role: .cancel) {
                store.send(.chatAudio(.microphonePermissionAlertDismissed))
            }
            Button(L10n.FeatureCreateClip.settings) {
                if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
                    UIApplication.shared.open(settingsURL)
                }
                store.send(.chatAudio(.microphonePermissionAlertDismissed))
            }
        } message: {
            Text(L10n.FeatureCreateClip.chatMicrophonePermissionMessage)
        }
        .fullScreenCover(item: $store.scope(state: \.chatAudio.destination?.documentPicker, action: \.chatAudio.destination.documentPicker)) { _ in
            DocumentPickerView { result in
                switch result {
                case .success(let url):
                    store.send(.chatAudio(.processAudioFile(.success(url))))
                case .failure(let error):
                    store.send(.chatAudio(.processAudioFile(.failure(AnyError(error)))))
                }
            }
        }
    }

    // MARK: - ChatBar Content (matches swift-vibes pattern)

    @ViewBuilder
    private var chatBarContent: some View {
        let isFullMode = store.customSheetDetent == .full

        ChatBar(
            text: $store.currentMessage,
            suggestions: [],
            onPlusTap: {
                // Handle plus tap
            },
            onUploadTap: { store.send(.uploadAudio) },
            onRecordTap: { store.send(.recordAudio) },
            onLibraryTap: { store.send(.chatAudio(.libraryTapped)) },
            onMicrophoneTap: { store.send(.recordAudio) },
            onSendTap: {
                if store.customSheetDetent == .full {
                    store.send(.sendCreateMessage)
                } else {
                    store.send(.sendMessage(store.currentMessage))
                }
            },
            onSuggestionTap: { suggestion in
                // Handle special suggestions that trigger modes
                let lowercased = suggestion.lowercased()
                if lowercased.contains("edit lyrics") {
                    store.send(.editingLyricsToggled(true))
                } else if lowercased.contains("extend") {
                    store.send(.extendModeToggled(true))
                } else {
                    store.send(.suggestionTapped(suggestion))
                }
            },
            placeholder: "Chat to make music",
            scrollToBeginning: false,
            isChatBarFocused: $store.isChatBarFocused.sending(\.chatBarFocusChanged),
            audioFileName: store.chatAudio.audioFileName,
            uploadProgress: store.chatAudio.uploadProgress,
            isUploading: store.chatAudio.isUploading,
            uploadCompleted: store.chatAudio.uploadCompleted,
            onCloseAudio: { store.send(.closeAudioFile) },
            errorState: store.chatAudio.errorState,
            onRetryUpload: { store.send(.chatAudio(.retryUpload)) },
            onPlayPauseSelected: {
                // Handle selected song play/pause
                if let selectedId = store.currentlySelectedSongId {
                    store.send(.songPlayPause(selectedId))
                }
            },
            currentlyPlayingSongTitle: getCurrentlySelectedSongTitle(store: store),
            currentArtworkGradient: getCurrentlySelectedArtworkGradient(store: store),
            isSelectedSongPlaying: isSelectedSongPlaying(store: store, audioManager: audioManager),
            onDeselectSong: {
                store.send(.songSelected(store.currentlySelectedSongId ?? ""))
            },
            hideTextInput: isFullMode,
            showCreateView: isFullMode,
            showSendButton: isFullMode,
            lyricsDescription: $store.lyricsDescription,
            styleDescription: $store.styleDescription,
            selectedModel: store.selectedModel,
            currentlyPlayingSong: getCurrentlySelectedSong(store: store),
            currentlyPlayingSongId: store.currentlyPlayingSongId,
            onSongPlayPause: { songId in
                store.send(.songPlayPause(songId))
            },
            lyricsFocusMode: store.isInLyricsFocusMode,
            audioManager: audioManager,
            onModelVersionChange: { newModel in
                store.selectedModel = newModel
            },
            weirdnessValue: $store.weirdnessValue,
            styleInfluenceValue: $store.styleInfluenceValue,
            audioInfluenceValue: $store.audioInfluenceValue,
            vocalGender: $store.vocalGender
        )
        .padding(.horizontal, 16)
        .padding(.vertical, 16)
    }

    // MARK: - Suggestions View

    private var shouldShowSuggestions: Bool {
        let displaySuggestions = store.showGenreSuggestions ? store.creativeGenreSuggestions : (store.currentlySelectedSongId != nil ? store.suggestions : [])
        return !displaySuggestions.isEmpty
    }

    private var displaySuggestions: [String] {
        store.showGenreSuggestions ? store.creativeGenreSuggestions : (store.currentlySelectedSongId != nil ? store.suggestions : [])
    }

    private var suggestionsView: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                // Suggestion pills
                ForEach(Array(displaySuggestions.prefix(5).enumerated()), id: \.offset) { index, suggestion in
                    Button(action: {
                        // Handle special suggestions that trigger modes
                        let lowercased = suggestion.lowercased()
                        if lowercased.contains("edit lyrics") {
                            store.send(.editingLyricsToggled(true))
                        } else if lowercased.contains("extend") {
                            store.send(.extendModeToggled(true))
                        } else {
                            store.send(.suggestionTapped(suggestion))
                        }
                    }) {
                        Text(suggestion.uppercased())
                            .figmaMCPTypography(TypographyV1.FigmaMCP.timecode)
                            .tracking(0.2)
                            .foregroundColor(ChatConstants.Colors.Foreground.primary)
                            .lineLimit(1)
                            .padding(.horizontal, 12)
                            .padding(.vertical, 8)
                            .glassBackground(shape: .capsule, type: nil, fallbackStyle: .ultraThinMaterial)
                    }
                    .buttonStyle(PlainButtonStyle())
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 12)
        }
    }

    // MARK: - Credits Animation Helpers

    private func creditsTextColor(count: Int) -> Color {
        if isCreditsAnimating {
            return Color.FigmaMCP.Semantic.accentBrand
        } else if count <= 0 {
            return Color.FigmaMCP.Semantic.accentError
        } else {
            return Color.FigmaMCP.Semantic.foregroundTertiary
        }
    }

    private func animateCreditsChange(to newValue: Int) {
        guard newValue != displayCreditsCount else { return }

        // Start animation
        isCreditsAnimating = true

        // Create dropping animation
        withAnimation(.easeOut(duration: 0.4)) {
            creditsAnimationOffset = 10 // Drop down effect
        }

        // Animate the number counting down/up
        let startValue = displayCreditsCount
        let difference = newValue - startValue
        let steps = 20 // Number of animation steps
        let stepValue = Double(difference) / Double(steps)

        for i in 1...steps {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.02) {
                displayCreditsCount = startValue + Int(stepValue * Double(i))
            }
        }

        // Reset animation state after completion
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
            withAnimation(.easeInOut(duration: 0.2)) {
                isCreditsAnimating = false
                creditsAnimationOffset = 0
            }
        }
    }
}

// MARK: - Supporting Views

struct SongCardView: View {
    let song: Song
    let isPlaying: Bool
    let isSelected: Bool
    let onPlayPause: () -> Void
    let onSelect: () -> Void
    let onExpand: (() -> Void)? // Optional expand callback
    let onThumbsUp: (() -> Void)? // Optional thumbs up callback
    let onThumbsDown: (() -> Void)? // Optional thumbs down callback
    let onShare: (() -> Void)? // Optional share callback
    let onMore: (() -> Void)? // Optional more callback

    init(
        song: Song,
        isPlaying: Bool,
        isSelected: Bool,
        onPlayPause: @escaping () -> Void,
        onSelect: @escaping () -> Void,
        onExpand: (() -> Void)? = nil,
        onThumbsUp: (() -> Void)? = nil,
        onThumbsDown: (() -> Void)? = nil,
        onShare: (() -> Void)? = nil,
        onMore: (() -> Void)? = nil
    ) {
        self.song = song
        self.isPlaying = isPlaying
        self.isSelected = isSelected
        self.onPlayPause = onPlayPause
        self.onSelect = onSelect
        self.onExpand = onExpand
        self.onThumbsUp = onThumbsUp
        self.onThumbsDown = onThumbsDown
        self.onShare = onShare
        self.onMore = onMore
    }

    var body: some View {
        VStack(spacing: 16) {
            // Album Artwork Section (Play/Pause tap area)
            Button(action: {
                // If song is not currently playing AND not selected, select it when starting playback
                if !isPlaying && !isSelected {
                    onSelect()
                }
                onPlayPause()
            }) {
                ZStack {
                    // Artwork with gradient (using gradient instead of ArtworkShader for now)
                    RoundedRectangle(cornerRadius: 24)
                        .fill(createGradientForSong(song))
                        .aspectRatio(1, contentMode: .fit)
                        .frame(maxWidth: 343)

                    // Expand Button (Top Right Corner)
                    if let onExpand = onExpand {
                        VStack {
                            HStack {
                                Spacer()
                                Button(action: onExpand) {
                                    Image.FigmaMCP.expandContent
                                        .resizable()
                                        .renderingMode(.template)
                                        .frame(width: 16, height: 16)
                                        .foregroundColor(.white)
                                }
                                .padding(.trailing, 12)
                                .padding(.top, 12)
                            }
                            Spacer()
                        }
                    }

                    // Play/Pause Button Overlay
                    if isPlaying {
                        Image.FigmaMCP.pause
                            .figmaMCPIconStyle(
                                size: Image.FigmaMCPSize.large,
                                semanticColor: .white
                            )
                    } else {
                        Image.FigmaMCP.play
                            .figmaMCPIconStyle(
                                size: Image.FigmaMCPSize.large,
                                semanticColor: .white
                            )
                    }
                }
            }
            .buttonStyle(PlainButtonStyle())

            // Song Info and Actions Section (Selection tap area)
            Button(action: onSelect) {
                VStack(spacing: 8) {
                    // Song Info Section
                    VStack(spacing: 0) {
                        HStack {
                            Text(song.title)
                                .figmaMCPTypography(TypographyV1.FigmaMCP.smallTitle)
                                .kerning(0.24)
                                .foregroundColor(isSelected ? ChatConstants.Colors.Accent.brand : ChatConstants.Colors.Foreground.primary)
                            Spacer()
                        }

                        HStack {
                            Text(song.genres.joined(separator: ", "))
                                .figmaMCPTypography(TypographyV1.FigmaMCP.small)
                                .kerning(0.24)
                                .foregroundColor(ChatConstants.Colors.Foreground.secondary.opacity(0.7))
                                .lineLimit(2)
                                .multilineTextAlignment(.leading)
                            Spacer()
                        }
                    }

                    // Action Buttons (if callbacks provided)
                    if onThumbsUp != nil || onThumbsDown != nil || onShare != nil || onMore != nil {
                        HStack(spacing: 16) {
                            // Thumbs Up
                            if let onThumbsUp = onThumbsUp {
                                Button(action: onThumbsUp) {
                                    Image.FigmaMCP.thumbsUp
                                        .resizable()
                                        .renderingMode(.template)
                                        .frame(width: 16, height: 16)
                                        .foregroundColor(ChatConstants.Colors.Background.Fog.dense)
                                }
                            }

                            // Thumbs Down
                            if let onThumbsDown = onThumbsDown {
                                Button(action: onThumbsDown) {
                                    Image.FigmaMCP.thumbsDown
                                        .resizable()
                                        .renderingMode(.template)
                                        .frame(width: 16, height: 16)
                                        .foregroundColor(ChatConstants.Colors.Background.Fog.dense)
                                }
                            }

                            // Share
                            if let onShare = onShare {
                                Button(action: onShare) {
                                    Image.FigmaMCP.shareArrow
                                        .resizable()
                                        .renderingMode(.template)
                                        .frame(width: 16, height: 16)
                                        .foregroundColor(ChatConstants.Colors.Background.Fog.dense)
                                }
                            }

                            // More
                            if let onMore = onMore {
                                Button(action: onMore) {
                                    Image.FigmaMCP.moreHorizontal
                                        .resizable()
                                        .renderingMode(.template)
                                        .frame(width: 16, height: 16)
                                        .foregroundColor(ChatConstants.Colors.Background.Fog.dense)
                                }
                            }

                            Spacer()
                        }
                    }
                }
                .padding(.horizontal, 8)
            }
            .buttonStyle(PlainButtonStyle())
        }
        .padding(.horizontal, 8)
        .padding(.top, 8)
        .padding(.bottom, 16)
        .frame(minWidth: 175)
        .fixedSize(horizontal: false, vertical: true)
        .background(
            RoundedRectangle(cornerRadius: 24)
                .fill(isSelected ? ChatConstants.Colors.Background.Fog.thin : Color.clear)
        )
    }
}

struct SongGridView: View {
    let messages: IdentifiedArrayOf<ChatMessage>

    let currentlyPlayingSongId: String?
    let onSongPlayPause: (String) -> Void

    @ObservedObject private var audioManager = AudioManager.shared

    private var allSongs: [Song] {
        var songs: [Song] = []
        for message in messages {
            if let songData = message.songData {
                songs.append(contentsOf: songData.songs)
            }
        }
        return songs
    }

    var body: some View {
        ScrollView {
            LazyVGrid(columns: [
                GridItem(.flexible(), spacing: 16),
                GridItem(.flexible(), spacing: 16)
            ], spacing: 16) {
                ForEach(allSongs) { song in
                    SimpleSongCard(
                        song: song,
                        isPlaying: currentlyPlayingSongId == song.id && audioManager.isCurrentlyPlaying,
                        isSelected: false,
                        onPlayPause: { onSongPlayPause(song.id) },
                        onSelect: { }
                    )
                }
            }
            .padding(.vertical, 12)
            .padding(.horizontal, 16)
        }
    }
}


// MARK: - Helper Functions

private func getCurrentlySelectedSongTitle(store: StoreOf<Chat>) -> String? { // SLOW WTF
    guard let songId = store.currentlySelectedSongId else { return nil }

    // Search through all messages for the song with the matching ID
    for message in store.messages.reversed() {
        if let songData = message.songData {
            for song in songData.songs {
                if song.id == songId {
                    return song.title
                }
            }
        }
    }

    return nil
}

private func getCurrentlySelectedArtworkGradient(store: StoreOf<Chat>) -> LinearGradient? { // SLOW
    guard let songId = store.currentlySelectedSongId else { return nil }

    // Search through all messages for the song with the matching ID
    for message in store.messages {
        if let songData = message.songData {
            for song in songData.songs {
                if song.id == songId {
                    return createGradientForSong(song)
                }
            }
        }
    }

    return nil
}

private func isSelectedSongPlaying(store: StoreOf<Chat>, audioManager: AudioManager) -> Bool {
    // Check if the currently selected song is the one that's playing
    guard let selectedSongId = store.currentlySelectedSongId,
          let playingSongId = audioManager.currentlyPlayingSongId else {
        return false
    }

    return selectedSongId == playingSongId
}

private func getCurrentlySelectedSong(store: StoreOf<Chat>) -> Song? {
    guard let songId = store.currentlySelectedSongId else { return nil }

    // Search through all messages for the song with the matching ID
    for message in store.messages {
        if let songData = message.songData {
            for song in songData.songs {
                if song.id == songId {
                    return song
                }
            }
        }
    }

    return nil
}

private func customDetentHeight(store: StoreOf<Chat>) -> PresentationDetent {
    return .height(store.detentHeight)
}

private func parseSongResponse(from response: String) -> SongResponse? {
    // Try to parse JSON from the response
    print("🔍 Parsing response: \(response.prefix(200))...")

    guard let data = response.data(using: .utf8) else {
        print("❌ Failed to convert response to data")
        return nil
    }

    do {
        let songResponse = try JSONDecoder().decode(SongResponse.self, from: data)
        print("✅ Successfully parsed song response with \(songResponse.songs.count) songs")
        return songResponse
    } catch {
        print("❌ JSON parsing error: \(error)")
        return nil
    }
}

private func extractTextFromJSON(_ jsonString: String) -> String? {
    // Try to extract a text/reply/message field from JSON if it exists
    guard let data = jsonString.data(using: .utf8),
          let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        return nil
    }

    // Try common field names for text content
    if let text = json["text"] as? String {
        return text
    } else if let reply = json["reply"] as? String {
        return reply
    } else if let message = json["message"] as? String {
        return message
    } else if let content = json["content"] as? String {
        return content
    }

    return nil
}

private func generateFallbackTitle(from message: String) -> String {
    let lowercased = message.lowercased()

    if lowercased.contains("love") {
        return "Love song"
    } else if lowercased.contains("sad") {
        return "Sad song"
    } else if lowercased.contains("happy") {
        return "Happy song"
    } else {
        let words = message.components(separatedBy: .whitespacesAndNewlines).prefix(3)
        return "Song about " + words.joined(separator: " ").lowercased()
    }
}

// MARK: - Orpheus Message Conversion

/// Converts a single OrpheusMessage to ChatMessage for display
private func convertOrpheusMessageToChatMessage(_ orpheusMessage: OrpheusMessage) -> ChatMessage {
    let isOutgoing = orpheusMessage.role == .user

    // For assistant messages with generated clips:
    // - Show the message with content (reply text) while streaming
    // - songData will be added later when clips are fetched (only when isStreaming == false)
    // - Match swift-vibes: one message with both content (reply) and songData
    // - The same message will be updated with songData, not a separate message created
    let songData: SongResponse? = nil // Will be populated asynchronously when clips are fetched (only when isStreaming == false)

    return ChatMessage(
        id: orpheusMessage.id,
        content: orpheusMessage.content, // This should already contain reply text from simple_message tool calls
        isOutgoing: isOutgoing,
        songData: songData,
        audioFileName: nil
    )
}

/// Converts OrpheusMessage array to ChatMessage array (legacy - use incremental updates instead)
private func convertOrpheusMessagesToChatMessages(_ orpheusMessages: [OrpheusMessage]) -> IdentifiedArrayOf<ChatMessage> {
    var chatMessages: IdentifiedArrayOf<ChatMessage> = []

    for orpheusMessage in orpheusMessages {
        chatMessages.append(convertOrpheusMessageToChatMessage(orpheusMessage))
    }

    return chatMessages
}

/// Fetches clips for a message
private func fetchClipsForMessage(_ message: OrpheusMessage) async throws -> [Clip] {
    var clips: [Clip] = []

    @Dependency(\.apiClientV2.getClip) var getClip
    for clipId in message.generatedClips {
        do {
            let clipID = ClipID(remoteId: clipId)
            let clip = try await getClip(clipID.remoteId)
            clips.append(clip)
        } catch {
            print("⚠️ Failed to fetch clip \(clipId): \(error)")
            // Continue with other clips even if one fails
        }
    }

    return clips
}

/// Converts Clip objects to SongResponse format
private func convertClipsToSongResponse(clips: [Clip], messageContent: String) -> SongResponse {
    let songs = clips.map { clip in
        Song(
            id: clip.id.remoteId,
            title: clip.title,
            genres: clip.tags.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) },
            artwork: clip.largeImageUrl.isEmpty ? nil : clip.largeImageUrl,
            audioURL: clip.audioUrl.isEmpty ? nil : clip.audioUrl
        )
    }

    // Extract suggestions from message content if available
    // For now, return empty suggestions - these might come from the message content parsing
    let suggestions: [String] = []

    // Get lyrics from song fetch if possible
    // Might not be available right off the bat
    let lyrics = clips.first?.prompt

    return SongResponse(
        reply: messageContent,
        songs: songs,
        suggestions: suggestions,
        lyrics: lyrics
    )
}

private func populateFormFromSelectedSong(store: StoreOf<Chat>) {
    guard let selectedSong = getCurrentlySelectedSong(store: store) else {
        // No song selected, clear the form
        store.lyricsDescription = ""
        store.styleDescription = ""
        return
    }

    // Populate lyrics from currentLyrics if available
    if let lyrics = store.currentLyrics {
//        let lyricsText = lyrics.sections.map { section in
//            "[\(section.type)]\n\(section.content)"
//        }.joined(separator: "\n\n")
        store.lyricsDescription = lyrics
    } else {
        store.lyricsDescription = ""
    }

    // Populate style from song genres
    if !selectedSong.genres.isEmpty {
        store.styleDescription = selectedSong.genres.joined(separator: ", ")
    } else {
        store.styleDescription = ""
    }
}

private func resetExtendMode(store: StoreOf<Chat>) {
    store.isInExtendMode = false
    store.extensionTime = "0:30s"
}

private func createGradientForSong(_ song: Song) -> LinearGradient {
    guard let artwork = song.artwork else {
        return LinearGradient(
            colors: [
                ChatConstants.Colors.Accent.purple,
                ChatConstants.Colors.Accent.pink
            ],
            startPoint: .topLeading,
            endPoint: .bottomTrailing
        )
    }

    let hash = artwork.hashValue
    let seed1 = abs(hash % 1000)
    let seed2 = abs((hash / 1000) % 1000)

    return LinearGradient(
        colors: [
            Color(
                red: 0.3 + (Double(seed1 % 600) / 1000.0),
                green: 0.3 + (Double((seed1 + 200) % 600) / 1000.0),
                blue: 0.3 + (Double((seed1 + 400) % 600) / 1000.0)
            ),
            Color(
                red: 0.3 + (Double(seed2 % 600) / 1000.0),
                green: 0.3 + (Double((seed2 + 200) % 600) / 1000.0),
                blue: 0.3 + (Double((seed2 + 400) % 600) / 1000.0)
            )
        ],
        startPoint: .topLeading,
        endPoint: .bottomTrailing
    )
}

// MARK: - Helper Modifiers

private struct OffsetModifier: ViewModifier {
    let offset: CGSize

    func body(content: Content) -> some View {
        content.offset(offset)
    }
}

// MARK: - Preview

#Preview {
    ChatView(store: Store(initialState: Chat.State(me: Shared(value: Me(models: [], roles: [:], flags: [:], user: User.mock())))) {
        Chat()
    })
}
