import APIClient
import ComposableArchitecture
import Foundation
import Get
import GenAPI

/// Protocol for Orpheus API service
public protocol OrpheusServiceProtocol {
    func streamChat(_ request: OrpheusChatSpec) async throws -> GenAPI.StreamingResponse
    func getAblyAuthToken() async throws -> AblyTokenResponse
    func registerContextState(_ request: ContextStateRequest) async throws
    func registerSession(_ request: RegisterSessionRequest) async throws
}

/// Protocol for fetching clips by ID
public protocol ClipsRepositoryProtocol {
    func getClipById(_ clipId: String) async throws -> Clip // Returns full Clip model
}

/// Repository for managing Orpheus chat messages and events.
///
/// This repository manages the message flow, processes real-time events through handlers,
/// and sends messages to the Orpheus backend. It uses the new architecture with:
/// - OrpheusSessionStore: Manages session state
/// - OrpheusConnectionManager: Manages real-time connections
/// - OrpheusRealtimeEventProcessor: Processes events into session events
public actor OrpheusChatRepository {
    @Dependency(\.date) private var date
    private let orpheusService: OrpheusServiceProtocol
    private let clipsRepository: ClipsRepositoryProtocol?
    private let sessionStore: OrpheusSessionStore
    private let connectionManager: OrpheusConnectionManager
    private let realtimeEventProcessor: OrpheusRealtimeEventProcessor

    private var realtimeEventsTask: Task<Void, Never>?

    /// Messages flow. Emits the current list of messages from the session.
    public var messages: AsyncStream<IdentifiedArrayOf<OrpheusMessage>> {
        AsyncStream { continuation in
            let task = Task { [weak self] in
                guard let self = self else { return }

                let sessionStream = await sessionStore.sessionFlow.stream()
                for await session in sessionStream {
                    continuation.yield(session?.messages ?? [])
                }
            }

            continuation.onTermination = { _ in
                task.cancel()
            }
        }
    }

    public init(
        orpheusService: OrpheusServiceProtocol,
        clipsRepository: ClipsRepositoryProtocol? = nil,
        sessionStore: OrpheusSessionStore,
        connectionManager: OrpheusConnectionManager,
        realtimeEventProcessor: OrpheusRealtimeEventProcessor
    ) {
        self.orpheusService = orpheusService
        self.clipsRepository = clipsRepository
        self.sessionStore = sessionStore
        self.connectionManager = connectionManager
        self.realtimeEventProcessor = realtimeEventProcessor

        // Observe real-time events and process them
        Task { [weak self] in
            guard let self else { return }
            await self.observeRealtimeEvents()
        }
    }

    private func observeRealtimeEvents() {
        realtimeEventsTask = Task { [weak self] in
            guard let self = self else { return }

            let eventStream = await connectionManager.realtimeEvents()
            for await realtimeEvent in eventStream {
                await self.processRealtimeEvent(realtimeEvent)
            }
        }
    }

    /// Processes a real-time event synchronously and updates the session store.
    /// This matches Android's approach where events are processed synchronously to avoid race conditions.
    private func processRealtimeEvent(_ event: OrpheusRealtimeEvent) async {
        let session = await sessionStore.sessionFlow.value
        guard let session = session else {
            return
        }

        // Process event synchronously (handlers are now non-async)
        let sessionEvent = realtimeEventProcessor.processEvent(
            event: event,
            session: session
        )

        // Handle the session event based on its type
        switch sessionEvent {
        case .messageAdded(let message):
            await sessionStore.addMessage(message)

        case .messageUpdated(let messageId, let updatedMessage, let clipIdsToFetch):
            // First update the message synchronously
            await sessionStore.updateMessage(messageId: messageId) { _ in updatedMessage }

            // Then fetch clips asynchronously if needed
            if let clipIds = clipIdsToFetch, !clipIds.isEmpty {
                Task { [weak self] in
                    guard let self = self else { return }
                    await self.fetchAndUpdateMessageClips(
                        messageId: messageId,
                        clipIds: Set(clipIds),
                        replaceClips: true
                    )
                }
            }

        case .messagesReplaced(let newMessages):
            // For messagesReplaced, we need to update all messages
            // This is a legacy case, but we'll handle it
            await sessionStore.reduce(sessionEvent)

        case .workspaceUpdated(let workspaceId):
            await sessionStore.updateWorkspace(workspaceId)

        case .noOp:
            // No operation needed - event handled but no state change
            break
        }
    }

    /// Starts a new session, clearing existing messages and registering the session.
    public func startNewSession() async {
        let session = await sessionStore.startNewSession()
        await connectionManager.connect(sessionId: session.id)

        // Register session in background
        Task { [weak self] in
            guard let self = self else { return }
            await self.registerSession(sessionId: session.id)
        }
    }

    /// Fetches clips by IDs and updates a message with the fetched clips.
    /// - Parameters:
    ///   - messageId: The ID of the message to update
    ///   - clipIds: Set of clip IDs to fetch
    ///   - replaceClips: If true, replaces existing clips; if false, appends to existing clips
    public func fetchAndUpdateMessageClips(
        messageId: String,
        clipIds: Set<String>,
        replaceClips: Bool
    ) async {
        if clipIds.isEmpty {
            return
        }
        // Fetch clips in parallel
        print("📡 fetchAndUpdateMessageClips: messageId=\(messageId), clipIds=\(clipIds), replaceClips=\(replaceClips)")
        let fetchedClips = await withTaskGroup(of: String?.self) { group in
            var successfulClipIds: [String] = []

            for clipId in clipIds {
                group.addTask {
                    do {
                        @Dependency(\.apiClientV2) var apiClient
                        let clip = try await apiClient.getClip(clipId)
                        print("✅ Fetched clip: \(clipId) - \(clip.title)")
                        return clipId
                    } catch {
                        print("⚠️ Failed to fetch clip: \(clipId) - \(error.localizedDescription)")
                        return nil
                    }
                }
            }

            for await clipId in group {
                if let clipId = clipId {
                    successfulClipIds.append(clipId)
                }
            }

            return successfulClipIds
        }

        if fetchedClips.isEmpty {
            print("⚠️ No clips fetched for message: \(messageId)")
            return
        }

        // Update the message with fetched clips
        await sessionStore.updateMessage(messageId: messageId) { message in
            let updatedClips = if replaceClips {
                fetchedClips
            } else {
                message.generatedClips + fetchedClips
            }
            return message.copy(generatedClips: updatedClips)
        }

        print("📡 Fetched \(fetchedClips.count) clips for message \(messageId), replace clips: \(replaceClips)")
    }

    /// Sends a message to the Orpheus backend.
    public func sendMessage(content: String) async throws {
        let session = await sessionStore.sessionFlow.value
        guard let session = session else {
            throw NSError(domain: "OrpheusChatRepository", code: -1, userInfo: [NSLocalizedDescriptionKey: "No active session"])
        }

        let userMessageId = UUID().uuidString
        let userMessage = OrpheusMessage(
            id: userMessageId,
            role: .user,
            content: content,
            timestamp: date.now.timeIntervalSince1970,
            contentType: .chat,
            accumulatingToolCall: nil,
            generatedClips: [],
            isStreaming: false
        )

        await sessionStore.addMessage(userMessage)

        // Send HTTP request to trigger backend processing
        // Note: OrpheusService.streamChat() will handle user authentication via ClerkClient
        // and throw an error if the user is not available, so we don't need to check here
        let request = OrpheusChatSpec(
            message: content,
            messageId: userMessageId,
            sessionId: session.id,
            toolCallId: nil
        )

        print("📡 Sending Orpheus chat request: \(request)")

        do {
            _ = try await orpheusService.streamChat(request)
            print("📡 Chat request sent, waiting for realtime messages...")
        } catch {
            print("❌ Error triggering chat request: \(error)")
            throw error
        }
    }

    /// Registers a model change with the backend.
    public func registerModelChange(modelName: String) async {
        let session = await sessionStore.sessionFlow.value
        guard let session = session else {
            print("⚠️ No session available for model registration")
            return
        }

        let request = ContextStateRequest(
            sessionId: session.id,
            type: .modelStatus,
            metadata: .modelStatus(.init(modelName: modelName))
        )

        do {
            try await orpheusService.registerContextState(request)
            print("📡 Registered model change: \(modelName)")
        } catch {
            print("⚠️ Failed to register model change: \(error)")
        }
    }

    /// Registers a session with the backend workspace.
    private func registerSession(sessionId: String) async {
        // TODO: (JY) pick workspace based off of last used
        // Get default workspace
        let workspaceId = await {
            @Dependency(\.apiClientV2) var apiClient
            // Use the underlying Get.APIClient to call the project endpoint (backend API term)
            // This matches Android's approach: projectsRepo.getMyProjects(page = 0)?.projects?.firstOrNull()
            do {
                let api = APIClientV2.underlying
                let response = try await api.send(
                    Paths.project.me.get(parameters: .init(page: 0))
                )
                // Return the first workspace's ID as the default workspace ID
                // ProjectMetadataSchema.id is a nested struct with uuid/string, extract string value
                return response.value.projects.first?.id.string ?? response.value.projects.first?.id.uuid?.uuidString
            } catch {
                print("⚠️ Failed to get default workspace: \(error)")
                return nil
            }
        }()

        guard let workspaceId else {
            print("⚠️ No default workspace found, skipping session registration")
            return
        }

        let request = RegisterSessionRequest(
            sessionId: sessionId,
            workspaceId: workspaceId,
            creationSource: "orpheus"
        )

        do {
            try await orpheusService.registerSession(request)
            await sessionStore.updateWorkspace(workspaceId)
            print("📡 Registered session: \(sessionId) with workspace: \(workspaceId)")
        } catch {
            print("⚠️ Failed to register session: \(error)")
        }
    }

    /// Registers a clip status change with the backend.
    private func registerClipStatusChange(clipId: String, status: String, duration: Double) async {
        let session = await sessionStore.sessionFlow.value
        guard let session = session else {
            return
        }

        let request = ContextStateRequest(
            sessionId: session.id,
            type: .clipStatus,
            metadata: .clipStatus(.init(
                clipId: clipId,
                status: status,
                duration: duration
            ))
        )

        do {
            try await orpheusService.registerContextState(request)
            print("📡 Registered clip status: clipId=\(clipId), status=\(status), duration=\(duration)")
        } catch {
            print("⚠️ Failed to register clip status: \(error)")
        }
    }

    /// Updates clip reaction in a message.
    public func updateClipReactionInMessage(messageId: String, clipId: String, newReaction: String?) {
        Task { [weak self] in
            guard let self = self else { return }

            let session = await sessionStore.sessionFlow.value
            guard let session = session else { return }

            guard let messageIndex = session.messages.firstIndex(where: { $0.id == messageId }) else {
                return
            }

            let message = session.messages[messageIndex]
            // In a full implementation, we'd update the clip's reaction field
            // For now, we just dispatch an update event
            await sessionStore.reduce(.messageUpdated(messageId: messageId, message: message))
        }
    }

    /// Updates clip status in messages when clips become ready.
    ///
    /// This is called when clips transition from streaming/generating to ready status.
    /// It fetches the latest clip data, updates the corresponding messages, and registers status changes.
    public func updateClipsStatusInMessages(readyClipIds: Set<String>) async {
        guard !readyClipIds.isEmpty, let clipsRepository = clipsRepository else {
            return
        }

        // Fetch clips in parallel
        let readyClips = await withTaskGroup(of: (String, Clip?).self) { group in
            var results: [String: Clip] = [:]

            for clipId in readyClipIds {
                group.addTask {
                    do {
                        let clip = try await clipsRepository.getClipById(clipId)
                        return (clipId, clip)
                    } catch {
                        print("⚠️ Error fetching updated clip: \(clipId) - \(error)")
                        return (clipId, nil)
                    }
                }
            }

            for await (clipId, clip) in group {
                if let clip = clip {
                    results[clipId] = clip
                }
            }

            return results
        }

        if readyClips.isEmpty {
            print("📡 No clip updates available")
            return
        }

        // Register clip status changes
        for (clipId, clip) in readyClips {
            await registerClipStatusChange(
                clipId: clipId,
                status: clip.status.rawValue,
                duration: clip.duration
            )
        }

        // Update messages with ready clips
        let session = await sessionStore.sessionFlow.value
        guard let session = session else { return }

        let updatedMessages = session.messages
        var updated = false

        for i in updatedMessages.indices {
            let message = updatedMessages[i]
            if !message.generatedClips.isEmpty {
                // Check if any of the message's clips are in the ready set
                let hasReadyClips = message.generatedClips.contains { readyClipIds.contains($0) }
                if hasReadyClips {
                    // In a full implementation, we'd update the clip data structure here
                    // For now, we just mark that updates occurred
                    updated = true
                }
            }
        }

        if updated {
            await sessionStore.reduce(.messagesReplaced(updatedMessages))
            print("📡 Updated \(readyClips.count) ready clips in messages")
        }
    }

    /// Closes the repository and cleans up resources.
    public func close() {
        realtimeEventsTask?.cancel()
        realtimeEventsTask = nil

        Task {
            await connectionManager.disconnect()
        }
    }
}
