//
//  BrowserHistory.swift
//  App
//
//  Created by James Magahern on 8/14/20.
//

import Foundation
import CoreData
import Combine

class BrowserHistory
{
    class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
        @Published var searchQuery: String = ""
        @Published private(set) var historyItems: [HistoryItem] = []
        
        fileprivate let historyController: BrowserHistory
        fileprivate var searchQueryObserver: AnyCancellable? = nil
        fileprivate var fetchedResultsController: NSFetchedResultsController<HistoryItemEntity> {
            didSet { fetchedResultsController.delegate = self; performInitialFetch() }
        }
        
        fileprivate init(fetchedResultsController: NSFetchedResultsController<HistoryItemEntity>, historyController: BrowserHistory) {
            self.fetchedResultsController = fetchedResultsController
            self.historyController = historyController
            super.init()
            
            searchQueryObserver = $searchQuery.sink { [unowned self] newValue in
                self.fetchedResultsController = historyController.fetchRequestController(forQuery: newValue)
            }
        }
        
        public func item(forIdentifier identifier: HistoryItem.ID) -> HistoryItem? {
            if let object = fetchedResultsController.managedObjectContext.object(with: identifier) as? HistoryItemEntity {
                return HistoryItem(entity: object)
            }
            
            return nil
        }
        
        public func deleteItems(_ items: Set<HistoryItem.ID>) {
            items.forEach { identifier in
                historyController.deleteItem(withIdentifier: identifier)
            }
        }
        
        func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
            if let objects = controller.fetchedObjects as? [HistoryItemEntity] {
                self.historyItems = objects.map { HistoryItem(entity: $0) }
            }
        }
        
        fileprivate func performInitialFetch() {
            try? fetchedResultsController.performFetch()
            if let objects = fetchedResultsController.fetchedObjects {
                self.historyItems = objects.map { HistoryItem(entity: $0) }
            }
        }
    }
    
    static public let shared = BrowserHistory()
    
    lazy fileprivate var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "History")
        container.loadPersistentStores { description, error in
            assert(error == nil)
        }
        
        return container
    }()
    
    public func didNavigate(toURL url: URL, title: String) {
        let dataContext = persistentContainer.viewContext
        
        let entity = HistoryItemEntity(context: dataContext)
        entity.url = url
        entity.lastVisited = Date()
        entity.title = title
        entity.host = url.host
        
        do { try dataContext.save() }
        catch {
            let nserror = error as NSError
            fatalError("Failed saving persistent entity to store: \(nserror), \(nserror.userInfo)")
        }
        
        UIMenuSystem.main.setNeedsRebuild()
    }
    
    public func viewModel(forFilterString filterString: String? = nil, limit: Int = 500) -> ViewModel {
        let resultsController = fetchRequestController(forQuery: filterString, limit: limit)
        return ViewModel(fetchedResultsController: resultsController, historyController: self)
    }
    
    public func historyItem(forIdentifier: HistoryItem.ID) -> HistoryItem? {
        if let entity = try? persistentContainer.viewContext.existingObject(with: forIdentifier) as? HistoryItemEntity {
            return HistoryItem(entity: entity)
        }
        
        return nil
    }
    
    public func deleteItem(withIdentifier identifier: HistoryItem.ID) {
        let dataContext = persistentContainer.viewContext
        if let object = try? dataContext.existingObject(with: identifier) {
            dataContext.delete(object)
            try? dataContext.save()
        }
    }
    
    public func clearAllHistory() {
        let dataContext = persistentContainer.viewContext
        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = HistoryItemEntity.fetchRequest()
        let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        
        do    { try dataContext.execute(request) }
        catch { print("Error clearing history: \(error.localizedDescription)") }
    }
    
    public func allHistory(filteredBy filterString: String? = nil, limit: Int = 500) -> [HistoryItem] {
        let dataContext = persistentContainer.viewContext
        let fetchRequest = fetchRequest(forStringContaining: filterString, limit: limit)
        let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? []
        
        return entities.map { (entity) -> HistoryItem in
            HistoryItem(entity: entity)
        }
    }
    
    public func visitedToplevelHistoryItems(matching: String) -> [HistoryItem] {
        let dataContext = persistentContainer.viewContext
        
        let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: """
                                             host CONTAINS[cd] %@
                                             OR title CONTAINS[cd] %@
                                             OR url ENDSWITH[cd] %@
                                             """, matching, matching, matching)
        fetchRequest.fetchLimit = 200
        fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "visitCount", ascending: false) ]
        
        let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? []
        let allItems: [HistoryItem] = entities.map { HistoryItem(entity: $0) }
        
        var topLevelItems: [URL: (HistoryItem, Int)] = [:]
        for item in allItems {
            if item.url.pathComponents.count <= 2 {
                var score = 1
                let topLevelURL = item.url.topLevelURL()
                var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0)
                topLevelItem.0.url = topLevelURL
                if item.url.path == "/" || item.url.path == "" {
                    score += 10
                    topLevelItem.0.title = item.title
                }
                topLevelItem.1 += score
                topLevelItems[topLevelURL] = topLevelItem
                
                if topLevelItems.count == 20 {
                    break
                }
            }
        }
        
        return topLevelItems.values.map { return $0.0 }.sorted { (item1, item2) -> Bool in
            return topLevelItems[item1.url]!.1 > topLevelItems[item2.url]!.1
        }
    }
}

extension BrowserHistory
{
    fileprivate func fetchRequestController(forQuery query: String? = nil, limit: Int = 500) -> NSFetchedResultsController<HistoryItemEntity> {
        let fetchRequest = fetchRequest(forStringContaining: query, limit: limit)
        return NSFetchedResultsController(fetchRequest: fetchRequest,
                                          managedObjectContext: persistentContainer.viewContext,
                                          sectionNameKeyPath: nil, cacheName: nil)
    }
    
    fileprivate func fetchRequest(forStringContaining filterString: String? = nil, limit: Int = 500) -> NSFetchRequest<HistoryItemEntity> {
        let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
        fetchRequest.fetchLimit = limit
        fetchRequest.sortDescriptors = [
            // Sort by date
            NSSortDescriptor(keyPath: \HistoryItemEntity.lastVisited, ascending: false)
        ]
        
        if let filterString, filterString.count > 0 {
            fetchRequest.predicate = NSPredicate(format: """
                                                 host CONTAINS[cd] %@
                                                 OR title CONTAINS[cd] %@
                                                 OR url CONTAINS[cd] %@
                                                 """, filterString, filterString, filterString)
        }

        return fetchRequest
    }
}

extension URL
{
    public func topLevelURL() -> URL {
        if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
            components.query = nil
            components.queryItems = nil
            components.fragment = nil
            if let url = components.url {
                return url
            }
        }
        
        return self
    }
}
