//
//  MainViewModel.swift
//  Nextcloud Cookbook iOS Client
//
//  Created by Vincent Meilinger on 06.09.23.
//

import Foundation
import OSLog
import SwiftUI
import UIKit


@MainActor class AppState: ObservableObject {
    @Published var categories: [Category] = []
    @Published var recipes: [String: [Recipe]] = [:]
    @Published var recipeDetails: [Int: RecipeDetail] = [:]
    @Published var timers: [String: RecipeTimer] = [:]
    @Published var categoryImages: [String: UIImage] = [:]
    @Published var recentRecipes: [Recipe] = []
    var recipeImages: [Int: [String: UIImage]] = [:]
    var imagesNeedUpdate: [Int: [String: Bool]] = [:]
    var lastUpdates: [String: Date] = [:]
    var allKeywords: [RecipeKeyword] = []

    private let dataStore: DataStore
    private let api: CookbookApiProtocol

    init(api: CookbookApiProtocol? = nil) {
        Logger.network.debug("Created AppState")
        self.dataStore = DataStore()
        self.api = api ?? CookbookApiFactory.makeClient()

        if UserSettings.shared.authString == "" {
            let loginString = "\(UserSettings.shared.username):\(UserSettings.shared.token)"
            let loginData = loginString.data(using: String.Encoding.utf8)!
            UserSettings.shared.authString = loginData.base64EncodedString()
        }
    }

    enum FetchMode {
        case preferLocal, preferServer, onlyLocal, onlyServer
    }


    // MARK: - Categories

    func getCategories() async {
        do {
            let categories = try await api.getCategories()
            Logger.data.debug("Successfully loaded categories")
            self.categories = categories
            await saveLocal(self.categories, path: "categories.data")
        } catch {
            Logger.data.debug("Loading categories from store ...")
            if let categories: [Category] = await loadLocal(path: "categories.data") {
                self.categories = categories
                Logger.data.debug("Loaded categories from local store")
            } else {
                Logger.data.error("Failed to load categories from local store")
            }
        }

        for category in self.categories {
            lastUpdates[category.name] = Date.distantPast
        }
    }

    func getCategory(named name: String, fetchMode: FetchMode) async {
        Logger.data.debug("getCategory(\(name), fetchMode: \(String(describing: fetchMode)))")
        func getLocal() async -> Bool {
            let categoryString = name == "*" ? "_" : name
            if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") {
                self.recipes[name] = recipes
                return true
            }
            return false
        }

        func getServer(store: Bool = false) async -> Bool {
            let categoryString = name == "*" ? "_" : name
            do {
                let recipes = try await api.getCategory(named: categoryString)
                self.recipes[name] = recipes
                if store {
                    await saveLocal(recipes, path: "category_\(categoryString).data")
                }
                return true
            } catch {
                return false
            }
        }

        switch fetchMode {
        case .preferLocal:
            if await getLocal() { return }
            if await getServer(store: true) { return }
        case .preferServer:
            if await getServer(store: true) { return }
            if await getLocal() { return }
        case .onlyLocal:
            if await getLocal() { return }
        case .onlyServer:
            if await getServer() { return }
        }
    }

    // MARK: - Recipe details

    func updateAllRecipeDetails() async {
        for category in self.categories {
            await updateRecipeDetails(in: category.name)
        }
        UserSettings.shared.lastUpdate = Date()
    }

    func updateRecipeDetails(in category: String) async {
        guard UserSettings.shared.storeRecipes else { return }
        guard let recipes = self.recipes[category] else { return }
        for recipe in recipes {
            if let dateModified = recipe.dateModified {
                if needsUpdate(category: category, lastModified: dateModified) {
                    Logger.data.debug("\(recipe.name) needs an update. (last modified: \(recipe.dateModified ?? "unknown"))")
                    await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
                } else {
                    Logger.data.debug("\(recipe.name) is up to date.")
                }
            } else {
                await updateRecipeDetail(id: recipe.recipe_id, withThumb: UserSettings.shared.storeThumb, withImage: UserSettings.shared.storeImages)
            }
        }
    }

    func getRecipes() async -> [Recipe] {
        do {
            return try await api.getRecipes()
        } catch {
            Logger.network.error("Failed to fetch recipes: \(error.localizedDescription)")
        }
        var allRecipes: [Recipe] = []
        for category in categories {
            if let recipeArray = self.recipes[category.name] {
                allRecipes.append(contentsOf: recipeArray)
            }
        }
        return allRecipes.sorted(by: { $0.name < $1.name })
    }

    func getRecipe(id: Int, fetchMode: FetchMode, save: Bool = false) async -> RecipeDetail? {
        func getLocal() async -> RecipeDetail? {
            if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") { return recipe }
            return nil
        }

        func getServer() async -> RecipeDetail? {
            do {
                let recipe = try await api.getRecipe(id: id)
                if save {
                    self.recipeDetails[id] = recipe
                    await self.saveLocal(recipe, path: "recipe\(id).data")
                }
                return recipe
            } catch {
                Logger.network.error("Failed to fetch recipe \(id): \(error.localizedDescription)")
                return nil
            }
        }

        switch fetchMode {
        case .preferLocal:
            if let recipe = await getLocal() { return recipe }
            if let recipe = await getServer() { return recipe }
        case .preferServer:
            if let recipe = await getServer() { return recipe }
            if let recipe = await getLocal() { return recipe }
        case .onlyLocal:
            if let recipe = await getLocal() { return recipe }
        case .onlyServer:
            if let recipe = await getServer() { return recipe }
        }
        return nil
    }

    func updateRecipeDetail(id: Int, withThumb: Bool, withImage: Bool) async {
        if let recipeDetail = await getRecipe(id: id, fetchMode: .onlyServer) {
            await saveLocal(recipeDetail, path: "recipe\(id).data")
        }

        if withThumb {
            let thumbnail = await getImage(id: id, size: .THUMB, fetchMode: .onlyServer)
            guard let thumbnail = thumbnail else { return }
            guard let thumbnailData = thumbnail.pngData() else { return }
            await saveLocal(thumbnailData.base64EncodedString(), path: "image\(id)_thumb")
        }

        if withImage {
            let image = await getImage(id: id, size: .FULL, fetchMode: .onlyServer)
            guard let image = image else { return }
            guard let imageData = image.pngData() else { return }
            await saveLocal(imageData.base64EncodedString(), path: "image\(id)_full")
        }
    }

    func recipeDetailExists(recipeId: Int) -> Bool {
        return dataStore.recipeDetailExists(recipeId: recipeId)
    }

    // MARK: - Images

    func getImage(id: Int, size: RecipeImage.RecipeImageSize, fetchMode: FetchMode) async -> UIImage? {
        func getLocal() async -> UIImage? {
            return await imageFromStore(id: id, size: size)
        }

        func getServer() async -> UIImage? {
            do {
                return try await api.getImage(id: id, size: size)
            } catch {
                return nil
            }
        }

        switch fetchMode {
        case .preferLocal:
            if let image = imageFromCache(id: id, size: size) {
                return image
            }
            if !imageUpdateNeeded(id: id, size: size) { return nil }
            if let image = await getLocal() {
                imageToCache(id: id, size: size, image: image)
                return image
            }
            if let image = await getServer() {
                await imageToStore(id: id, size: size, image: image)
                imageToCache(id: id, size: size, image: image)
                return image
            }
        case .preferServer:
            if let image = await getServer() {
                await imageToStore(id: id, size: size, image: image)
                imageToCache(id: id, size: size, image: image)
                return image
            }
            if let image = imageFromCache(id: id, size: size) {
                return image
            }
            if let image = await getLocal() {
                imageToCache(id: id, size: size, image: image)
                return image
            }
        case .onlyLocal:
            if let image = imageFromCache(id: id, size: size) {
                return image
            }
            if !imageUpdateNeeded(id: id, size: size) { return nil }
            if let image = await getLocal() {
                imageToCache(id: id, size: size, image: image)
                return image
            }
        case .onlyServer:
            if let image = imageFromCache(id: id, size: size) {
                return image
            }
            if let image = await getServer() {
                imageToCache(id: id, size: size, image: image)
                return image
            }
        }
        imagesNeedUpdate[id] = [size.rawValue: false]
        return nil
    }

    // MARK: - Keywords

    func getKeywords(fetchMode: FetchMode) async -> [RecipeKeyword] {
        func getLocal() async -> [RecipeKeyword]? {
            return await loadLocal(path: "keywords.data")
        }

        func getServer() async -> [RecipeKeyword]? {
            do {
                return try await api.getTags()
            } catch {
                return nil
            }
        }

        switch fetchMode {
        case .preferLocal:
            if let keywords = await getLocal() { return keywords }
            if let keywords = await getServer() {
                await saveLocal(keywords, path: "keywords.data")
                return keywords
            }
        case .preferServer:
            if let keywords = await getServer() {
                await saveLocal(keywords, path: "keywords.data")
                return keywords
            }
            if let keywords = await getLocal() { return keywords }
        case .onlyLocal:
            if let keywords = await getLocal() { return keywords }
        case .onlyServer:
            if let keywords = await getServer() { return keywords }
        }
        return []
    }

    // MARK: - Category images

    func getCategoryImage(for categoryName: String) async {
        guard categoryImages[categoryName] == nil else { return }
        // Ensure recipes for this category are loaded
        if self.recipes[categoryName] == nil || self.recipes[categoryName]!.isEmpty {
            await getCategory(named: categoryName, fetchMode: .preferLocal)
        }
        guard let recipes = self.recipes[categoryName], !recipes.isEmpty else { return }
        for recipe in recipes {
            if let image = await getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .preferLocal) {
                self.categoryImages[categoryName] = image
                return
            }
        }
    }

    // MARK: - Recent recipes

    func addToRecentRecipes(_ recipe: Recipe) {
        recentRecipes.removeAll { $0.recipe_id == recipe.recipe_id }
        recentRecipes.insert(recipe, at: 0)
        if recentRecipes.count > 10 {
            recentRecipes = Array(recentRecipes.prefix(10))
        }
        Task {
            await saveLocal(recentRecipes, path: "recent_recipes.data")
        }
    }

    func loadRecentRecipes() async {
        if let loaded: [Recipe] = await loadLocal(path: "recent_recipes.data") {
            self.recentRecipes = loaded
        }
    }

    func clearRecentRecipes() {
        recentRecipes = []
        dataStore.delete(path: "recent_recipes.data")
    }

    // MARK: - Data management

    func deleteAllData() {
        if dataStore.clearAll() {
            self.categories = []
            self.recipes = [:]
            self.recipeDetails = [:]
            self.recipeImages = [:]
            self.imagesNeedUpdate = [:]
        }
    }

    func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert? {
        do {
            try await api.deleteRecipe(id: id)
        } catch {
            return .REQUEST_DROPPED
        }

        let path = "recipe\(id).data"
        dataStore.delete(path: path)
        if recipes[categoryName] != nil {
            recipes[categoryName]!.removeAll(where: { recipe in
                recipe.recipe_id == id
            })
            recipeDetails.removeValue(forKey: id)
        }
        return nil
    }

    func checkServerConnection() async -> Bool {
        do {
            let categories = try await api.getCategories()
            self.categories = categories
            await saveLocal(categories, path: "categories.data")
            return true
        } catch {
            return false
        }
    }

    func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> (Int?, RequestAlert?) {
        do {
            if createNew {
                let id = try await api.createRecipe(recipeDetail)
                return (id, nil)
            } else {
                let id = try await api.updateRecipe(recipeDetail)
                return (id, nil)
            }
        } catch {
            return (nil, .REQUEST_DROPPED)
        }
    }

    func importRecipe(url: String) async -> (RecipeDetail?, RequestAlert?) {
        do {
            let recipeDetail = try await api.importRecipe(url: url)
            return (recipeDetail, nil)
        } catch {
            return (nil, .REQUEST_DROPPED)
        }
    }
}


// MARK: - Local storage helpers

extension AppState {
    func loadLocal<T: Codable>(path: String) async -> T? {
        do {
            return try await dataStore.load(fromPath: path)
        } catch {
            Logger.data.debug("Failed to load local data: \(error.localizedDescription)")
            return nil
        }
    }

    func saveLocal<T: Codable>(_ object: T, path: String) async {
        await dataStore.save(data: object, toPath: path)
    }

    private func imageFromStore(id: Int, size: RecipeImage.RecipeImageSize) async -> UIImage? {
        do {
            let localPath = "image\(id)_\(size == .FULL ? "full" : "thumb")"
            if let data: String = try await dataStore.load(fromPath: localPath) {
                guard let dataDecoded = Data(base64Encoded: data) else { return nil }
                let image = UIImage(data: dataDecoded)
                return image
            }
        } catch {
            Logger.data.debug("Could not find image in local storage.")
            return nil
        }
        return nil
    }

    private func imageToStore(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) async {
        if let data = image.pngData() {
            await saveLocal(data.base64EncodedString(), path: "image\(id)_\(size.rawValue)")
        }
    }

    private func imageToCache(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) {
        if recipeImages[id] != nil {
            recipeImages[id]![size.rawValue] = image
        } else {
            recipeImages[id] = [size.rawValue : image]
        }
        if imagesNeedUpdate[id] != nil {
            imagesNeedUpdate[id]![size.rawValue] = false
        } else {
            imagesNeedUpdate[id] = [size.rawValue: false]
        }
    }

    private func imageFromCache(id: Int, size: RecipeImage.RecipeImageSize) -> UIImage? {
        if recipeImages[id] != nil {
            return recipeImages[id]![size.rawValue]
        }
        return nil
    }

    private func imageUpdateNeeded(id: Int, size: RecipeImage.RecipeImageSize) -> Bool {
        if imagesNeedUpdate[id] != nil {
            if imagesNeedUpdate[id]![size.rawValue] != nil {
                return imagesNeedUpdate[id]![size.rawValue]!
            }
        }
        return true
    }

    private func needsUpdate(category: String, lastModified: String) -> Bool {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)

        if let date = dateFormatter.date(from: lastModified), let lastUpdate = lastUpdates[category] {
            if date < lastUpdate {
                Logger.data.debug("No update needed for \(category)")
                return false
            } else {
                Logger.data.debug("Update needed for \(category)")
                return true
            }
        }
        Logger.data.debug("Date parse failed, update needed for \(category)")
        return true
    }
}


extension DateFormatter {
    static func utcToString(date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return dateFormatter.string(from: date)
    }
}


// Timer logic
extension AppState {
    func createTimer(forRecipe recipeId: String, duration: DurationComponents) -> RecipeTimer {
        let timer = RecipeTimer(duration: duration)
        timers[recipeId] = timer
        return timer
    }

    func getTimer(forRecipe recipeId: String, duration: DurationComponents) -> RecipeTimer {
        return timers[recipeId] ?? createTimer(forRecipe: recipeId, duration: duration)
    }

    func deleteTimer(forRecipe recipeId: String) {
        timers.removeValue(forKey: recipeId)
    }
}
