import Combine
import Foundation

enum AppState: Equatable {
    case unpaired
    case connecting
    case connected(peerID: String, peerName: String)
    case error(String)
}

@MainActor
final class AppSession: ObservableObject {
    static let shared = AppSession()

    @Published var state: AppState = .unpaired
    @Published var messages: [ChatMessage] = []

    private let relayAPI = RelayAPI.shared
    private let webSocket = WSClient(url: URL(string: "wss://fchaty.diyaa.de/ws")!)
    private let messageStore = MessageStore.shared
    private var incomingMessagesTask: Task<Void, Never>?

    private init() {
        Task { [weak self] in
            guard let self else {
                return
            }

            await NotificationManager.shared.requestPermission()
            await loadStoredMessages()
            await restoreSession()
        }
    }

    deinit {
        incomingMessagesTask?.cancel()
    }

    func createPairingCode(displayName: String) async throws -> String {
        state = .connecting

        do {
            let response = try await relayAPI.createPairing(displayName: displayName)
            KeychainStore.authToken = response.token
            KeychainStore.peerID = nil
            KeychainStore.peerName = nil

            try await webSocket.connect(token: response.token)
            startIncomingMessages()
            return response.code
        } catch {
            state = .error(message(for: error))
            throw error
        }
    }

    func joinWithCode(_ code: String, displayName: String) async throws {
        state = .connecting

        do {
            let response = try await relayAPI.joinPairing(code: code, displayName: displayName)
            KeychainStore.authToken = response.token
            KeychainStore.peerID = response.peer.id
            KeychainStore.peerName = response.peer.displayName

            try await webSocket.connect(token: response.token)
            startIncomingMessages()
            state = .connected(peerID: response.peer.id, peerName: response.peer.displayName)
        } catch {
            state = .error(message(for: error))
            throw error
        }
    }

    func sendText(_ body: String) async throws {
        let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedBody.isEmpty else {
            throw AppSessionError.emptyMessage
        }

        let message = outgoingMessage(body: trimmedBody, attachment: nil)
        try await sendAndStore(message)
    }

    func sendFile(url: URL) async throws {
        guard let token = KeychainStore.authToken else {
            throw AppSessionError.notPaired
        }

        let uploadedFile = try await relayAPI.uploadFile(url: url, token: token)
        let attachment = AttachmentInfo(
            fileID: uploadedFile.id,
            name: uploadedFile.name,
            size: uploadedFile.size
        )
        let message = outgoingMessage(
            body: "Shared a file: \(uploadedFile.name)",
            attachment: attachment
        )

        try await sendAndStore(message)
    }

    func markVisibleMessagesRead() async {
        let unread = messages.filter { !$0.isRead && $0.from != KeychainStore.installationID }
        guard !unread.isEmpty else { return }

        let ids = unread.map { $0.id }
        ids.forEach { markMessageAsRead(id: $0) }

        let receipt = WSMessage(type: "read", messageIDs: ids)
        try? await webSocket.send(receipt)
    }

    func unpair() {
        incomingMessagesTask?.cancel()
        incomingMessagesTask = nil
        KeychainStore.clearAll()
        let webSocket = webSocket
        Task {
            await webSocket.disconnect()
        }
        state = .unpaired
    }

    private func restoreSession() async {
        guard
            let token = KeychainStore.authToken,
            let peerID = KeychainStore.peerID,
            let peerName = KeychainStore.peerName
        else {
            return
        }

        state = .connecting

        do {
            try await webSocket.connect(token: token)
            startIncomingMessages()
            state = .connected(peerID: peerID, peerName: peerName)
        } catch {
            state = .error(message(for: error))
        }
    }

    private func loadStoredMessages() async {
        do {
            messages = try await messageStore.loadAll()
        } catch {
            state = .error("Unable to load saved messages.")
        }
    }

    private func startIncomingMessages() {
        guard incomingMessagesTask == nil else {
            return
        }

        let webSocket = webSocket
        incomingMessagesTask = Task { [weak self] in
            let stream = await webSocket.incoming

            for await message in stream {
                guard !Task.isCancelled else {
                    return
                }

                await self?.handleIncomingMessage(message)
            }
        }
    }

    private static let iso8601: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return f
    }()

    private func handleIncomingMessage(_ message: WSMessage) async {
        switch message.type {
        case "chat.message":
            let attachment = message.attachment.map {
                AttachmentInfo(fileID: $0.fileID, name: $0.name, size: $0.size)
            }
            // Use the timestamp the sender embedded; fall back to now only if missing.
            let sentAt: Date
            if let raw = message.sentAt,
               let parsed = AppSession.iso8601.date(from: raw) {
                sentAt = parsed
            } else {
                sentAt = Date()
            }
            let chatMessage = ChatMessage(
                id: message.id ?? UUID().uuidString,
                from: message.from ?? "unknown",
                fromName: message.fromName ?? "Unknown",
                body: message.body ?? "",
                sentAt: sentAt,
                attachment: attachment,
                isRead: false
            )

            await storeIncomingMessage(chatMessage)
            NotificationManager.shared.notify(from: chatMessage.fromName, body: chatMessage.body)

        case "peer.joined":
            // The partner just came online — save their identity and transition to connected.
            if let peerID = message.peerID, let peerName = message.fromName {
                KeychainStore.peerID = peerID
                KeychainStore.peerName = peerName
                state = .connected(peerID: peerID, peerName: peerName)
                NotificationManager.shared.notify(from: "Fchati", body: "\(peerName) connected")
            }

        case "read":
            for id in message.messageIDs ?? [] {
                markMessageAsRead(id: id)
            }

        default:
            break
        }
    }

    private func sendAndStore(_ message: ChatMessage) async throws {
        let attachment = message.attachment.map {
            WSAttachment(fileID: $0.fileID, name: $0.name, size: $0.size)
        }
        let webSocketMessage = WSMessage(
            type: "chat.message",
            id: message.id,
            body: message.body,
            sentAt: AppSession.iso8601.string(from: message.sentAt),
            attachment: attachment
        )

        try await webSocket.send(webSocketMessage)
        await storeOutgoingMessage(message)
    }

    private func outgoingMessage(body: String, attachment: AttachmentInfo?) -> ChatMessage {
        ChatMessage(
            id: UUID().uuidString,
            from: KeychainStore.installationID,
            fromName: "You",
            body: body,
            sentAt: Date(),
            attachment: attachment,
            isRead: true
        )
    }

    private func storeIncomingMessage(_ message: ChatMessage) async {
        upsert(message)

        do {
            try await messageStore.save(message)
        } catch {
            state = .error("Unable to save an incoming message.")
        }
    }

    private func storeOutgoingMessage(_ message: ChatMessage) async {
        upsert(message)

        do {
            try await messageStore.save(message)
        } catch {
            state = .error("Unable to save an outgoing message.")
        }
    }

    private func markMessageAsRead(id: String) {
        guard let index = messages.firstIndex(where: { $0.id == id }) else {
            return
        }

        messages[index].isRead = true

        Task {
            do {
                try await messageStore.markRead(id: id)
            } catch {
                state = .error("Unable to update a message receipt.")
            }
        }
    }

    private func upsert(_ message: ChatMessage) {
        if let index = messages.firstIndex(where: { $0.id == message.id }) {
            messages[index] = message
        } else {
            messages.append(message)
            messages.sort { $0.sentAt < $1.sentAt }
        }
    }

    private func message(for error: Error) -> String {
        if let error = error as? LocalizedError, let description = error.errorDescription {
            return description
        }

        return String(describing: error)
    }
}

enum AppSessionError: LocalizedError {
    case emptyMessage
    case notPaired

    var errorDescription: String? {
        switch self {
        case .emptyMessage:
            "A message cannot be empty."
        case .notPaired:
            "Pair with someone before sending a file."
        }
    }
}
