import Foundation
import WidgetKit
import CocoaLumberjackSwift

@objc(WMFWidgetController)
public final class WidgetController: NSObject {

    // MARK: Nested Types

    public enum SupportedWidget: String {
        case featuredArticle = "org.wikimedia.wikipedia.widgets.featuredArticle"
        case onThisDay = "org.wikimedia.wikipedia.widgets.onThisDay"
        case pictureOfTheDay = "org.wikimedia.wikipedia.widgets.potd"
        case topRead = "org.wikimedia.wikipedia.widgets.topRead"

        public var identifier: String {
            return self.rawValue
        }
    }

    // MARK: Properties

	@objc public static let shared = WidgetController()
    private let sharedCache = SharedContainerCache<WidgetCache>(fileName: SharedContainerCacheCommonNames.widgetCache, defaultCache: { WidgetCache(settings: .default, featuredContent: nil) })

    // MARK: Public

	@objc public func reloadAllWidgetsIfNecessary() {
        guard !Bundle.main.isAppExtension else {
            return
        }

        let dataStore = MWKDataStore.shared()
        let appLanguage = dataStore.languageLinkController.appLanguage
        if let siteURL = appLanguage?.siteURL, let languageCode = appLanguage?.languageCode {
            let updatedWidgetSettings = WidgetSettings(siteURL: siteURL, languageCode: languageCode, languageVariantCode: appLanguage?.languageVariantCode)
            updateCacheWith(settings: updatedWidgetSettings)
        }

        WidgetCenter.shared.reloadAllTimelines()
	}
    
    public func reloadFeaturedArticleWidgetIfNecessary() {
        guard !Bundle.main.isAppExtension else {
            return
        }

        let dataStore = MWKDataStore.shared()
        let appLanguage = dataStore.languageLinkController.appLanguage
        if let siteURL = appLanguage?.siteURL, let languageCode = appLanguage?.languageCode {
            let updatedWidgetSettings = WidgetSettings(siteURL: siteURL, languageCode: languageCode, languageVariantCode: appLanguage?.languageVariantCode)
            updateCacheWith(settings: updatedWidgetSettings)
        }
        
        WidgetCenter.shared.reloadTimelines(ofKind: SupportedWidget.featuredArticle.rawValue)
    }
    
    /// For requesting background time from widgets
    /// - Parameter userCompletion: the completion block to call with the result
    /// - Parameter task: block that takes the `MWKDataStore` to use for updates and the completion block to call when done as parameters
    public func startWidgetUpdateTask<T>(_ userCompletion: @escaping (T) -> Void, _ task: @escaping (MWKDataStore, @escaping (T) -> Void) -> Void) {
        getRetainedSharedDataStore { dataStore in
            task(dataStore, { result in
                DispatchQueue.main.async {
                    self.releaseSharedDataStore {
                        userCompletion(result)
                    }
                }
            })
        }
    }
    
    public func fetchNewestWidgetContentGroup(with kind: WMFContentGroupKind, in dataStore: MWKDataStore, isNetworkFetchAllowed: Bool, isAnyLanguageAllowed: Bool = false, completion:  @escaping (WMFContentGroup?) -> Void) {
        fetchCachedWidgetContentGroup(with: kind, isAnyLanguageAllowed: isAnyLanguageAllowed, in: dataStore) { (contentGroup) in
            guard let todaysContentGroup = contentGroup, todaysContentGroup.isForToday else {
                guard isNetworkFetchAllowed else {
                    completion(contentGroup)
                    return
                }
                self.updateFeedContent(in: dataStore) {
                    // if there's no cached content group after update, return nil
                    self.fetchCachedWidgetContentGroup(with: kind, isAnyLanguageAllowed: isAnyLanguageAllowed, in: dataStore, completion: completion)
                }
                return
            }
            completion(todaysContentGroup)
        }
    }
    
    private func fetchCachedWidgetContentGroup(with kind: WMFContentGroupKind, isAnyLanguageAllowed: Bool, in dataStore: MWKDataStore, completion:  @escaping (WMFContentGroup?) -> Void) {
        assert(Thread.isMainThread, "Cached widget content group must be fetched from the main queue")
        let moc = dataStore.viewContext
        let siteURL = isAnyLanguageAllowed ? dataStore.primarySiteURL : nil
        completion(moc.newestGroup(of: kind, forSiteURL: siteURL))
    }
    
    public func updateFeedContent(in dataStore: MWKDataStore, completion: @escaping () -> Void) {
        dataStore.feedContentController.performDeduplicatedFetch(completion)
    }
    
    private var dataStoreRetainCount: Int = 0
    private var _dataStore: MWKDataStore?
    private var completions: [(MWKDataStore) -> Void] = []
    private var isCreatingDataStore: Bool = false
    private var backgroundActivity: NSObjectProtocol?
    
    /// Returns a `MWKDataStore`for use with widget updates.
    /// Manages a shared instance and a reference count for use by multiple widgets.
    /// Call `releaseSharedDataStore()` when finished with the data store.
    private func getRetainedSharedDataStore(completion: @escaping (MWKDataStore) -> Void) {
        assert(Thread.isMainThread, "Data store must be obtained from the main queue")
        dataStoreRetainCount += 1
        if let dataStore = _dataStore {
            completion(dataStore)
            return
        }
        completions.append(completion)
        guard !isCreatingDataStore else {
            return
        }
        isCreatingDataStore = true
        backgroundActivity = ProcessInfo.processInfo.beginActivity(options: [.background, .suddenTerminationDisabled, .automaticTerminationDisabled], reason: "Wikipedia Extension - " + UUID().uuidString)
        let dataStore = MWKDataStore()
        dataStore.performLibraryUpdates {
            DispatchQueue.main.async {
                self._dataStore = dataStore
                self.isCreatingDataStore = false
                self.completions.forEach { $0(dataStore) }
                self.completions.removeAll()
            }
        } needsMigrateBlock: {
            DDLogDebug("Needed a migration from the widgets")
        }
    }
    
    /// Releases the shared `MWKDataStore` returned by `getRetainedSharedDataStore()`.
    private func releaseSharedDataStore(completion: @escaping () -> Void) {
        assert(Thread.isMainThread, "Data store must be released from the main queue")
        dataStoreRetainCount -= 1
        guard dataStoreRetainCount <= 0 else {
            completion()
            return
        }
        dataStoreRetainCount = 0
        let asyncBackgroundActivity = backgroundActivity
        defer {
            backgroundActivity = nil
        }
        let finishBackgroundActivity = {
            if let asyncBackgroundActivity = asyncBackgroundActivity {
                ProcessInfo.processInfo.endActivity(asyncBackgroundActivity)
            }
        }
        guard let dataStoreToTeardown = _dataStore else {
            completion()
            finishBackgroundActivity()
            return
        }
        _dataStore = nil
        dataStoreToTeardown.teardown {
            completion()
            finishBackgroundActivity()
            #if DEBUG
            guard !self.isCreatingDataStore, self._dataStore == nil else { // Don't check open files if another MWKDataStore was created after this one was destroyed
                return
            }
            let openFiles = self.openFilePaths()
            let openSqliteFile = openFiles.first(where: { $0.hasSuffix(".sqlite") })
            assert(openSqliteFile == nil, "There should be no open sqlite files (which in our case are Core Data persistent stores) in the shared app container after the data store is released. The widget still has a lock on these files: \(openFiles)")
            #endif
        }
    }
    
    #if DEBUG
    /// From https://developer.apple.com/forums/thread/655225?page=2
    func openFilePaths() -> [String] {
        (0..<getdtablesize()).compactMap { fd in
            // Return nil for invalid file descriptors.
            var flags: CInt = 0
            guard fcntl(fd, F_GETFL, &flags) >= 0 else {
                return nil
            }
            // Return "?" for file descriptors not associated with a path, for
            // example, a socket.
            var path = [CChar](repeating: 0, count: Int(MAXPATHLEN))
            guard fcntl(fd, F_GETPATH, &path) >= 0 else {
                return "?"
            }
            return String(cString: path)
        }
    }
    #endif
    
}

/// When the old widget data loading model is removed, this should be moved out of this extension into the class itself and refactored (e.g. the properties here don't need to be computed).
public extension WidgetController {

    /// This is currently unused. It will be useful when we update the main app to also update the widget's cache when it performs any updates to the featured content in the explore feed.
    func updateCacheWith(featuredContent: WidgetFeaturedContent) {
        var updatedCache = sharedCache.loadCache()
        updatedCache.featuredContent = featuredContent
        sharedCache.saveCache(updatedCache)
    }

    func updateCacheWith(settings: WidgetSettings) {
        var updatedCache = sharedCache.loadCache()
        updatedCache.settings = settings
        sharedCache.saveCache(updatedCache)
    }

    // MARK: - Featured Article Widget

    func fetchFeaturedContent(isSnapshot: Bool = false, completion: @escaping (WidgetContentFetcher.FeaturedContentResult) -> Void) {
        func performCompletion(result: WidgetContentFetcher.FeaturedContentResult) {
            DispatchQueue.main.async {
                completion(result)
            }
        }

        let fetcher = WidgetContentFetcher.shared
        var widgetCache = sharedCache.loadCache()

        guard !isSnapshot else {
            let previewSnapshot = widgetCache.featuredContent ?? WidgetFeaturedContent.previewContent() ?? WidgetFeaturedContent(featuredArticle: nil)
            performCompletion(result: .success(previewSnapshot))
            return
        }
        
        // If cached data is still relevant, use it
        if let cachedContent = widgetCache.featuredContent, let fetchDate = cachedContent.fetchDate, Calendar.current.isDateInToday(fetchDate), let cachedLanguageCode = cachedContent.featuredArticle?.languageCode, cachedLanguageCode == widgetCache.settings.languageCode, widgetCache.settings.languageVariantCode == cachedContent.fetchedLanguageVariantCode {
            performCompletion(result: .success(cachedContent))
            return
        }

        // Fetch fresh feed content from network
        fetcher.fetchFeaturedContent(forDate: Date(), siteURL: widgetCache.settings.siteURL, languageCode: widgetCache.settings.languageCode, languageVariantCode: widgetCache.settings.languageVariantCode) { result in
            switch result {
            case .success(var featuredContent):
                if let featuredArticleThumbnailImageSource = featuredContent.featuredArticle?.thumbnailImageSource {
                    fetcher.fetchImageDataFrom(imageSource: featuredArticleThumbnailImageSource) { imageResult in
                        featuredContent.featuredArticle?.thumbnailImageSource?.data = try? imageResult.get()
                        widgetCache.featuredContent = featuredContent
                        self.sharedCache.saveCache(widgetCache)
                        performCompletion(result: .success(featuredContent))
                    }
                } else {
                    widgetCache.featuredContent = featuredContent
                    self.sharedCache.saveCache(widgetCache)
                    performCompletion(result: .success(featuredContent))
                }
            case .failure(let error):
                self.sharedCache.saveCache(widgetCache)
                performCompletion(result: .failure(error))
            }
        }
    }

}
