//
//  TranscriptView.swift
//  kordophone2
//
//  Created by James Magahern on 8/24/25.
//

import SwiftUI

struct TranscriptView: View
{
    @Binding var model: ViewModel
    
    @Environment(\.xpcClient) private var xpcClient

    init(model: Binding<ViewModel>) {
        self._model = model
    }

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                // For resetting scroll position to the "bottom"
                EmptyView()
                    .id(ViewID.bottomAnchor)

                LazyVStack(spacing: 6.0) {
                    ForEach($model.displayItems.reversed()) { item in
                        displayItemView(item.wrappedValue)
                            .id(item.id)
                            .scaleEffect(CGSize(width: 1.0, height: -1.0))
                            .transition(
                                .push(from: .top)
                                .combined(with: .opacity)
                            )
                    }
                }
                .padding()
            }

            // Flip vertically so newest messages are at the bottom.
            .scaleEffect(CGSize(width: 1.0, height: -1.0))

            // Watch for xpc events
            .task { await watchForMessageListChanges() }

            // On conversation change, reload displayed messages and mark as read.
            .onChange(of: model.displayedConversation) { oldValue, newValue in
                Task {
                    guard oldValue != newValue else { return }

                    // Reload NOW
                    await model.reloadMessages(animated: false) {
                        // Once that's done, scroll to the "bottom" (actually top)
                        proxy.scrollTo(ViewID.bottomAnchor, anchor: .top)
                    }
                }

                Task.detached {
                    // Mark as read on server, and trigger a sync.
                    await model.markAsRead()
                    await model.triggerSync()
                }
            }
        }
    }
    
    private func watchForMessageListChanges() async {
        for await event in xpcClient.eventStream() {
            switch event {
                case .attachmentDownloaded(let attachmentId):
                    model.attachmentDownloaded(id: attachmentId)
                case .messagesUpdated(let conversationId):
                    if let displayedConversation = model.displayedConversation,
                        conversationId == displayedConversation.id
                    {
                        model.setNeedsReload(animated: true)
                    }
                case .updateStreamReconnected:
                    await model.triggerSync()
                default:
                    break
            }
        }
    }
    
    @ViewBuilder
    private func displayItemView(_ item: DisplayItem) -> some View {
        switch item {
        case .message(let message):
            TextBubbleItemView(text: message.text, sender: message.sender, date: message.date)
        case .date(let date):
            DateItemView(date: date)
        case .senderAttribition(let message):
            SenderAttributionView(sender: message.sender)
        case .spacer(let length, _):
            Spacer(minLength: length)
        case .attachment(let attachment):
            if attachment.isPreviewDownloaded {
                ImageItemView(
                    sender: attachment.sender,
                    date: attachment.dateSent,
                    attachment: attachment
                )
            } else {
                PlaceholderImageItemView(
                    sender: attachment.sender,
                    date: attachment.dateSent,
                    size: attachment.size
                )
            }
        }
    }

    // MARK: - Types

    enum ViewID: String
    {
        case bottomAnchor
    }

    // MARK: - View Model

    @Observable
    class ViewModel
    {
        var displayItems: [DisplayItem] = []
        var displayedConversation: Display.Conversation? = nil

        internal var needsReload: NeedsReload = .no
        internal var messages: [Display.Message]
        internal let client = XPCClient()

        private var needsMarkAsRead: Bool = false
        private var lastMarkAsRead: Date = .now

        init(messages: [Display.Message] = []) {
            self.messages = messages
            rebuildDisplayItems()
        }
        
        func setNeedsReload(animated: Bool) {
            guard case .no = needsReload else {
                return
            }

            needsReload = .yes(animated)
            Task { @MainActor [weak self] in
                guard let self else { return }
                await reloadIfNeeded()
            }
        }

        func setNeedsMarkAsRead() {
            guard needsMarkAsRead == false else { return }
            guard Date.now.timeIntervalSince(lastMarkAsRead) > 5.0 else { return }

            needsMarkAsRead = true
            Task { @MainActor [weak self] in
                guard let self else { return }
                await markAsRead()
                needsMarkAsRead = false
                lastMarkAsRead = .now
            }
        }

        func attachmentDownloaded(id: String) {
            // TODO: should be smarter here
            setNeedsReload(animated: false)
        }
        
        func markAsRead() async {
            guard let displayedConversation else { return }
            
            do {
                try await client.markConversationAsRead(conversationId: displayedConversation.id)
            } catch {
                print("Error triggering sync: \(error)")
            }
        }
        
        func triggerSync() async {
            guard let displayedConversation else { return }
            
            do {
                try await client.syncConversation(conversationId: displayedConversation.id)
            } catch {
                print("Error triggering sync: \(error)")
            }
        }

        func reloadIfNeeded(completion: () -> Void = {}) async {
            guard case .yes(let animated) = needsReload else { return }
            needsReload = .no

            await reloadMessages(animated: animated, completion: completion)
        }

        func reloadMessages(animated: Bool, completion: () -> Void) async {
            guard let displayedConversation else { return }
            
            do {
                let clientMessages = try await client.getMessages(conversationId: displayedConversation.id)
                    .map { Display.Message(from: $0) }

                let newIds = Set(clientMessages.map(\.id))
                    .subtracting(self.messages.map(\.id))

                // Only animate for incoming messages.
                let shouldAnimate = (newIds.count == 1)

                await MainActor.run {
                    self.messages = clientMessages
                    self.rebuildDisplayItems(animated: animated && shouldAnimate, completion: completion)
                }
            } catch {
                print("Message fetch error: \(error)")
            }
        }
        
        // MARK: - Types
        
        enum NeedsReload
        {
            case no
            case yes(Bool) // animated
        }
    }
}

#Preview {
    @Previewable @State var model = TranscriptView.ViewModel(messages: [
        .init(sender: .me,                 text: "Hello, how are you?"),
        .init(sender: .counterpart("Bob"), text: "I am doing fine!")
    ])
    
    TranscriptView(model: $model)
}
