//
//  Persistence.swift
//  Shared
//
//  Created by Trey Tartt on 1/5/22.
//

import CoreData
import SwiftUI

class PersistenceController {
    @AppStorage(UserDefaultsStore.Keys.useCloudKit.rawValue, store: GroupUserDefaults.groupDefaults) private var useCloudKit = false
    
    static let shared = PersistenceController.persistenceController
    
    private static var persistenceController: PersistenceController {
        return PersistenceController(inMemory: false)
    }
    
    public var viewContext: NSManagedObjectContext {
        return PersistenceController.shared.container.viewContext
    }
    
    public var switchContainerListeners = [(() -> Void)]()
    
    public func getEntry(byDate date: Date) -> MoodEntry? {
        let predicate = NSPredicate(format: "forDate == %@",
                                    date as NSDate)
        
        let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)]
        let data = try! viewContext.fetch(fetchRequest)
        return data.first
    }
    
    public func add(mood: Mood, forDate date: Date) {
        let newItem = MoodEntry(context: viewContext)
        newItem.timestamp = Date()
        newItem.moodValue = Int16(mood.rawValue)
        newItem.forDate = date
        newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date))
        newItem.canEdit = true
        newItem.canDelete = true
        
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
    
    public var earliestEntry: MoodEntry? {
        let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)]
        let first = try! viewContext.fetch(fetchRequest).first
        return first ?? nil
    }
    
    public var latestEntry: MoodEntry? {
        let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)]
        let last = try! viewContext.fetch(fetchRequest).last
        return last ?? nil
    }
    
    public func getData(startDate: Date, endDate: Date, includedDays: [Int]) -> [MoodEntry] {
        try! viewContext.setQueryGenerationFrom(.current)
        viewContext.refreshAllObjects()
        
        var includedDays16 = [Int16]()
        
        if includedDays.isEmpty {
            includedDays16 = [Int16(1), Int16(2), Int16(3), Int16(4), Int16(5), Int16(6), Int16(7)]
        } else {
            includedDays16 = includedDays.map({
                Int16($0)
            })
        }
        let predicate = NSPredicate(format: "forDate >= %@ && forDate <= %@ && weekDay IN %@",
                                    startDate as NSDate,
                                    endDate as NSDate,
                                    includedDays16)
        
        let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: true)]
        let data = try! viewContext.fetch(fetchRequest)
        return data
    }
    
    func populateTestData() {
        for idx in 1..<120 {
            let newItem = MoodEntry(context: viewContext)
            newItem.timestamp = Date()
            newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
            newItem.canEdit = true
            newItem.canDelete = true
            
            let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
            newItem.forDate = date
            newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date))
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
    
    func populateMemory() {
        for idx in 1..<25 {
            let newItem = MoodEntry(context: viewContext)
            newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
            newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
            newItem.canEdit = true
            newItem.canDelete = true
            
            let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
            newItem.forDate = date
            newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date))
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
    
    func fillInMissingDates() {
        let fetchRequest = NSFetchRequest<MoodEntry>(entityName: "MoodEntry")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "forDate", ascending: false)]
        let entries = try! viewContext.fetch(fetchRequest)
        
        if let earliestDate = entries.last?.forDate,
           let diffInDays = Calendar.current.dateComponents([.day], from: earliestDate, to: Date()).day,
           diffInDays > 1 {
            for idx in 1..<diffInDays {
                if let searchDay = Calendar.current.date(byAdding: .day, value: -idx, to: Date()),
                   entries.filter({ Calendar.current.isDate($0.forDate!, inSameDayAs:searchDay) }).isEmpty {
                    self.add(mood: .missing, forDate: searchDay)
                }
            }
        }
    }
    
    func clearDB() {
        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "MoodEntry")
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        
        do {
            try viewContext.executeAndMergeChanges(using: deleteRequest)
            try viewContext.save()
        } catch let error as NSError {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    }
    
    public func randomEntries(count: Int) -> [MoodEntry] {
        var entries = [MoodEntry]()
        
        for idx in 0..<count {
            let newItem = MoodEntry(context: viewContext)
            newItem.timestamp = Calendar.current.date(byAdding: .day, value: -idx, to: Date())
            newItem.moodValue = Int16(Mood.allValues.randomElement()!.rawValue)
            let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
            newItem.forDate = date
            newItem.weekDay = Int16(Calendar.current.component(.weekday, from: date))
            newItem.canEdit = true
            newItem.canDelete = true
            entries.append(newItem)
        }
        return entries
    }
    
    lazy var container: NSPersistentContainer = {
        setupContainer()
    }()
    
    func switchContainer() {
        try? viewContext.save()
        container = setupContainer()
        for item in switchContainerListeners {
            item()
        }
    }

    private func setupContainer() -> NSPersistentContainer {
        if useCloudKit {
            container = NSPersistentCloudKitContainer(name: "Feels")
        } else {
            container = NSCustomPersistentContainer(name: "Feels")
        }
            
        for description in container.persistentStoreDescriptions {
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            self.container.viewContext.automaticallyMergesChangesFromParent = true

            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        return container
    }
        
    init(inMemory: Bool = false) {
        container = setupContainer()
    }
    
    public func splitIntoYearMonth() ->  [Int: [Int: [MoodEntry]]] {
        let data = PersistenceController.shared.getData(startDate: Date(timeIntervalSince1970: 0),
                                                        endDate: Date(),
                                                        includedDays: [1,2,3,4,5,6,7]).sorted(by: {
            $0.forDate! < $1.forDate!
        })
        var returnData = [Int: [Int: [MoodEntry]]]()
        
        if let earliestEntry = data.first,
           let lastEntry = data.last {
            
            let calendar = Calendar.current
            let components = calendar.dateComponents([.year], from: earliestEntry.forDate!)
            let earliestYear = components.year!
            
            let latestComponents = calendar.dateComponents([.year], from: lastEntry.forDate!)
            let latestYear = latestComponents.year!
            
            for year in earliestYear...latestYear {
                var allMonths = [Int: [MoodEntry]]()
                
                for month in (1...12) {
                    var components = DateComponents()
                    components.month = month
                    components.year = year
                    let startDateOfMonth = Calendar.current.date(from: components)!
                    
                    let items = data.filter({ entry in
                        let components = calendar.dateComponents([.month, .year], from: startDateOfMonth)
                        let entryComponents = calendar.dateComponents([.month, .year], from: entry.forDate!)
                        return (components.month == entryComponents.month && components.year == entryComponents.year)
                    })
                    if !items.isEmpty {
                        allMonths[month] = items
                    }
                }
                returnData[year] = allMonths
            }
        }
        return returnData
    }
}

extension NSManagedObjectContext {
    
    /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
    ///
    /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
    /// - Throws: An error if anything went wrong executing the batch deletion.
    public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
        batchDeleteRequest.resultType = .resultTypeObjectIDs
        let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
        let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
    }
}


class NSCustomPersistentContainer: NSPersistentContainer {
    override open class func defaultDirectoryURL() -> URL {
        var storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.groupShareId)
        storeURL = storeURL?.appendingPathComponent("Feels.sqlite")
#if DEBUG
        storeURL = storeURL?.appendingPathComponent("Feels-Debug.sqlite")
#endif
        return storeURL!
    }
}
