//
//  PostService.swift
//  ComposeUI
//
//  Created by Shadowfacts on 4/27/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import Foundation
import Pachyderm
import UniformTypeIdentifiers

@MainActor
class PostService: ObservableObject {
    private let mastodonController: ComposeMastodonContext
    private let config: ComposeUIConfig
    private let draft: Draft
    
    @Published var currentStep = 1
    @Published private(set) var totalSteps = 2
    
    init(mastodonController: ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) {
        self.mastodonController = mastodonController
        self.config = config
        self.draft = draft
    }
    
    func post() async throws {
        guard draft.hasContent || draft.editedStatusID != nil else {
            return
        }
        
        // save before posting, so if a crash occurs during network request, the status won't be lost
        DraftsPersistentContainer.shared.save()
        
        let uploadedAttachments = try await uploadAttachments()
        
        let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : ""
        let sensitive = !contentWarning.isEmpty
        
        let request: Request<Status>
        
        if let editedStatusID = draft.editedStatusID {
            if mastodonController.instanceFeatures.needsEditAttachmentsInSeparateRequest {
                await updateEditedAttachments()
            }
            
            request = Client.editStatus(
                id: editedStatusID,
                text: textForPosting(),
                contentType: config.contentType,
                spoilerText: contentWarning,
                sensitive: sensitive,
                language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
                mediaIDs: uploadedAttachments,
                mediaAttributes: draft.draftAttachments.compactMap {
                    if let id = $0.editedAttachmentID {
                        return EditStatusMediaAttributes(id: id, description: $0.attachmentDescription, focus: nil)
                    } else {
                        return nil
                    }
                },
                poll: draft.poll.map {
                    EditPollParameters(options: $0.pollOptions.map(\.text), expiresIn: Int($0.duration), multiple: $0.multiple)
                }
            )
        } else {
            request = Client.createStatus(
                text: textForPosting(),
                contentType: config.contentType,
                inReplyTo: draft.inReplyToID,
                mediaIDs: uploadedAttachments,
                sensitive: sensitive,
                spoilerText: contentWarning,
                visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
                language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
                pollOptions: draft.poll?.pollOptions.map(\.text),
                pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
                pollMultiple: draft.poll?.multiple,
                localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
                idempotencyKey: draft.id.uuidString
            )
        }
        
        do {
            let (status, _) = try await mastodonController.run(request)
            currentStep += 1
            mastodonController.storeCreatedStatus(status)
        } catch let error as Client.Error {
            throw Error.posting(error)
        }
    }
    
    private func uploadAttachments() async throws -> [String] {
        // 2 steps (request data, then upload) for each attachment
        self.totalSteps += 2 * draft.attachments.count
        
        var attachments: [String] = []
        attachments.reserveCapacity(draft.attachments.count)
        for (index, attachment) in draft.draftAttachments.enumerated() {
            // if this attachment already exists and is being edited, we don't do anything
            // edits to the description are handled as part of the edit status request
            if let editedAttachmentID = attachment.editedAttachmentID {
                attachments.append(editedAttachmentID)
                currentStep += 2
                continue
            }
            
            let data: Data
            let utType: UTType
            do {
                (data, utType) = try await getData(for: attachment)
                currentStep += 1
            } catch let error as DraftAttachment.ExportError {
                throw Error.attachmentData(index: index, cause: error)
            }
            let uploaded = try await uploadAttachment(index: index, data: data, utType: utType, description: attachment.attachmentDescription)
            attachments.append(uploaded.id)
            currentStep += 1
        }
        return attachments
    }
    
    private func getData(for attachment: DraftAttachment) async throws -> (Data, UTType) {
        return try await withCheckedThrowingContinuation { continuation in
            attachment.getData(features: mastodonController.instanceFeatures) { result in
                switch result {
                case let .success(res):
                    continuation.resume(returning: res)
                case let .failure(error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    private func uploadAttachment(index: Int, data: Data, utType: UTType, description: String?) async throws -> Attachment {
        guard let mimeType = utType.preferredMIMEType else {
            throw Error.attachmentMissingMimeType(index: index, type: utType)
        }
        var filename = "file"
        if let ext = utType.preferredFilenameExtension {
            filename.append(".\(ext)")
        }
        let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: filename)
        let req = Client.upload(attachment: formAttachment, description: description)
        do {
            return try await mastodonController.run(req).0
        } catch let error as Client.Error {
            throw Error.attachmentUpload(index: index, cause: error)
        }
    }
    
    private func textForPosting() -> String {
        var text = draft.text
        // when using dictation, iOS sometimes leaves a U+FFFC OBJECT REPLACEMENT CHARACTER behind in the text,
        // which we want to strip out before actually posting the status
        text = text.replacingOccurrences(of: "\u{fffc}", with: "")
        
        if draft.localOnly && mastodonController.instanceFeatures.needsLocalOnlyEmojiHack {
            text += " 👁"
        }
        
        return text
    }
    
    // only needed for akkoma, not used on regular mastodon
    private func updateEditedAttachments() async {
        for attachment in draft.draftAttachments {
            guard let id = attachment.editedAttachmentID else {
                continue
            }
            let req = Client.updateAttachment(id: id, description: attachment.attachmentDescription, focus: nil)
            _ = try? await mastodonController.run(req)
        }
    }
    
    enum Error: Swift.Error, LocalizedError {
        case attachmentData(index: Int, cause: DraftAttachment.ExportError)
        case attachmentMissingMimeType(index: Int, type: UTType)
        case attachmentUpload(index: Int, cause: Client.Error)
        case posting(Client.Error)
        
        var localizedDescription: String {
            switch self {
            case let .attachmentData(index: index, cause: cause):
                return "Attachment \(index + 1): \(cause.localizedDescription)"
            case let .attachmentMissingMimeType(index: index, type: type):
                return "Attachment \(index + 1): unknown MIME type for \(type.identifier)"
            case let .attachmentUpload(index: index, cause: cause):
                return "Attachment \(index + 1): \(cause.localizedDescription)"
            case let .posting(error):
                return error.localizedDescription
            }
        }
    }
}
