//
//  DraftsMigrator.swift
//  ComposeUI
//
//  Created by Shadowfacts on 4/22/23.
//

import Foundation
import OSLog
import UniformTypeIdentifiers
import Pachyderm
import PencilKit
import CoreData

struct DraftsMigrator {
    private init() {}
    
    private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsMigrator")
    private static let decoder = PropertyListDecoder()
    
    static func migrate(from url: URL, to context: NSManagedObjectContext) -> Result<(), any Error> {
        do {
            let data = try Data(contentsOf: url)
            let container = try decoder.decode(DraftsContainer.self, from: data)
            for old in container.drafts.values {
                let new = Draft(context: context)
                new.id = old.id
                new.lastModified = old.lastModified
                new.accountID = old.accountID
                new.text = old.text
                new.contentWarningEnabled = old.contentWarningEnabled
                new.contentWarning = old.contentWarning
                new.inReplyToID = old.inReplyToID
                new.visibility = old.visibility
                new.localOnly = old.localOnly
                new.initialText = old.initialText
                
                if let oldPoll = old.poll {
                    let newPoll = Poll(context: context)
                    newPoll.draft = new
                    new.poll = newPoll
                    newPoll.multiple = oldPoll.multiple
                    newPoll.duration = oldPoll.duration
                    for oldOption in oldPoll.options {
                        let newOption = PollOption(context: context)
                        newOption.text = oldOption.text
                        newOption.poll = newPoll
                        newPoll.options.add(newOption)
                    }
                }
                
                for oldAttachment in old.attachments {
                    let newAttachment = DraftAttachment(context: context)
                    newAttachment.draft = new
                    new.attachments.add(newAttachment)
                    newAttachment.id = oldAttachment.id
                    newAttachment.attachmentDescription = oldAttachment.attachmentDescription
                    switch oldAttachment.data {
                    case .asset(let assetID):
                        newAttachment.assetID = assetID
                    case .image(let data, originalType: let type):
                        newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: type)
                        newAttachment.fileType = type.identifier
                    case .video(_):
                        fatalError("unreachable, video attachments weren't encodable")
                    case .drawing(let drawing):
                        newAttachment.drawing = drawing
                    case .gif(let data):
                        newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: .gif)
                        newAttachment.fileType = UTType.gif.identifier
                    }
                }
            }
            
            try FileManager.default.removeItem(at: url)
        } catch {
            logger.error("Error migrating: \(String(describing: error))")
            return .failure(error)
        }
        return .success(())
    }
    
    // MARK: Supporting Types
    
    struct DraftsContainer: Decodable {
        let drafts: [UUID: OldDraft]
        
        init(drafts: [UUID: OldDraft]) {
            self.drafts = drafts
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.drafts = try container.decode([UUID: SafeDraft].self, forKey: .drafts).compactMapValues(\.draft)
        }
        
        enum CodingKeys: CodingKey {
            case drafts
        }
    }
    
    // a container that always succeeds at decoding
    // so if a single draft can't be decoded, we don't lose all drafts
    struct SafeDraft: Decodable {
        let draft: OldDraft?

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            self.draft = try? container.decode(OldDraft.self)
        }
    }
    
    struct OldDraft: Decodable {
        let id: UUID
        let lastModified: Date
        let accountID: String
        let text: String
        let contentWarningEnabled: Bool
        let contentWarning: String
        let attachments: [OldDraftAttachment]
        let inReplyToID: String?
        let visibility: Visibility
        let poll: OldPoll?
        let localOnly: Bool
        let initialText: String
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            
            self.id = try container.decode(UUID.self, forKey: .id)
            self.lastModified = try container.decode(Date.self, forKey: .lastModified)
            
            self.accountID = try container.decode(String.self, forKey: .accountID)
            self.text = try container.decode(String.self, forKey: .text)
            self.contentWarningEnabled = try container.decode(Bool.self, forKey: .contentWarningEnabled)
            self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
            self.attachments = try container.decode([OldDraftAttachment].self, forKey: .attachments)
            self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
            self.visibility = try container.decode(Visibility.self, forKey: .visibility)
            self.poll = try container.decode(OldPoll?.self, forKey: .poll)
            self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
            
            self.initialText = try container.decode(String.self, forKey: .initialText)
        }
        
        enum CodingKeys: String, CodingKey {
            case id
            case lastModified
            
            case accountID
            case text
            case contentWarningEnabled
            case contentWarning
            case attachments
            case inReplyToID
            case visibility
            case poll
            case localOnly
            
            case initialText
        }
    }
    
    struct OldDraftAttachment: Decodable {
        let id: UUID
        let data: OldDraftAttachmentData
        let attachmentDescription: String
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            
            self.id = try container.decode(UUID.self, forKey: .id)
            self.data = try container.decode(OldDraftAttachmentData.self, forKey: .data)
            self.attachmentDescription = try container.decode(String.self, forKey: .attachmentDescription)
        }
    
        enum CodingKeys: String, CodingKey {
            case id
            case data
            case attachmentDescription
        }
    }
    
    enum OldDraftAttachmentData: Decodable {
        case asset(String)
        case image(Data, originalType: UTType)
        case video(URL)
        case drawing(PKDrawing)
        case gif(Data)
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            
            switch try container.decode(String.self, forKey: .type) {
            case "asset":
                let identifier = try container.decode(String.self, forKey: .assetIdentifier)
                self = .asset(identifier)
            case "image":
                let data = try container.decode(Data.self, forKey: .imageData)
                if let type = try container.decodeIfPresent(UTType.self, forKey: .imageType) {
                    self = .image(data, originalType: type)
                } else {
                    guard let image = UIImage(data: data) else {
                        throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "CompositionAttachment data could not be decoded into UIImage")
                    }
                    let jpegData = image.jpegData(compressionQuality: 1)!
                    self = .image(jpegData, originalType: .jpeg)
                }
            case "drawing":
                let drawingData = try container.decode(Data.self, forKey: .drawing)
                let drawing = try PKDrawing(data: drawingData)
                self = .drawing(drawing)
            default:
                throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing")
            }
        }
        
        enum CodingKeys: CodingKey {
            case type
            case imageData
            case imageType
            /// The local identifier of the PHAsset for this attachment
            case assetIdentifier
            /// The PKDrawing object for this attachment.
            case drawing
        }
    }
    
    struct OldPoll: Decodable {
        let options: [OldPollOption]
        let multiple: Bool
        let duration: TimeInterval
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.options = try container.decode([OldPollOption].self, forKey: .options)
            self.multiple = try container.decode(Bool.self, forKey: .multiple)
            self.duration = try container.decode(TimeInterval.self, forKey: .duration)
        }
        
        enum CodingKeys: String, CodingKey {
            case options
            case multiple
            case duration
        }
    }
    
    struct OldPollOption: Decodable {
        let text: String
        
        init(from decoder: Decoder) throws {
            self.text = try decoder.singleValueContainer().decode(String.self)
        }
    }
}
