//
//  EmojiHoarder.swift
//  StickerSlack
//
//  Created by neon443 on 15/10/2025.
//

import Foundation
import SwiftUI
import Combine
import UniformTypeIdentifiers
import Haptics

class EmojiHoarder: ObservableObject {
	static let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.neon443.StickerSlack")!.appendingPathComponent("Library", conformingTo: .directory)
	nonisolated static let localEmojiDB: URL = EmojiHoarder.container.appendingPathComponent("_localEmojiDB.json", conformingTo: .fileURL)
	nonisolated static let localTrie: URL = EmojiHoarder.container.appendingPathComponent("_localTrie.json", conformingTo: .fileURL)
	nonisolated static let localTrieDict: URL = EmojiHoarder.container.appendingPathComponent("_localTrieDict.json", conformingTo: .fileURL)
	private let endpoint: URL = URL(string: "https://cachet.dunkirk.sh/emojis")!
	private let encoder = JSONEncoder()
	private let decoder = JSONDecoder()
	
	@Published var emojis: [Emoji] = []
	@Published var emojiPacks: [EmojiPack] = []
	
	@Published var trie: Trie = Trie()
	@Published var downloadedEmojis: Set<String> = []
	@Published var downloadedEmojisArr: [String] = []
	@Published var searchTerm: String = ""
	
	@Published var letterStats: [EmojiHoarder.LetterStat] = []
	@Published var letterStatsSorting: EmojiHoarder.LetterStatSorting = .init(by: .letter, ascending: true)
	
	@Published var showWelcome: Bool = false
	
	init(localOnly: Bool = false, skipIndex: Bool = false) {
		self.showWelcome = !UserDefaults.standard.bool(forKey: "showWelcome")
		
		let localDB = loadLocalDB()
		withAnimation { self.emojis = localDB }
		loadTrie()
		if !skipIndex { buildTrie() }
		
		guard !localOnly else { return }
		Task {
			print("start loading remote db")
			await self.loadRemoteDB()
			print("end")
			if !skipIndex {
				await self.buildTrie()
			}
		}
	}
	
	@MainActor
	func downloadAllStickers() async {
		let start: Date = .now
		
		downloadedEmojisArr = []
		
		let cores = ProcessInfo.processInfo.processorCount-1
		var ranges: [Range<Int>] = []
		for i in 0..<cores {
			let onething = emojis.count/cores
			ranges.append(onething*i..<onething + (onething*i))
			if i == (0..<cores).last {
				let last = ranges.last!
				ranges.removeLast()
				ranges.append(onething*i..<(onething + (onething*i)+emojis.count-last.upperBound))
			}
		}
		
		await withTaskGroup { group in
			for range in ranges {
				group.addTask {
					for i in range {
						await MainActor.run { self.downloadedEmojisArr.append(self.emojis[i].name) }
						guard !self.downloadedEmojis.contains(await self.emojis[i].name) else { continue }
						Task { await self.download(emoji: self.emojis[i], skipStoreIndex: true) }
						await MainActor.run { self.downloadedEmojis.insert(self.emojis[i].name) }
						if i == self.emojis.count {
							Task { @MainActor in
								self.storeDownloadedIndexes()
							}
						}
					}
				}
			}
		}
		
		print(Date.now.timeIntervalSince(start))
	}
	
	@MainActor
	func deleteAllStickers() {
		for i in emojis.indices {
			guard downloadedEmojis.contains(emojis[i].name) else { continue }
			delete(emoji: emojis[i], skipStoreIndex: true)
		}
		downloadedEmojis = []
		downloadedEmojisArr = []
		storeDownloadedIndexes()
	}
	
	private func storeDB() {
		try! encoder.encode(emojis).write(to: EmojiHoarder.localEmojiDB)
	}
	
	private func storeDB(data: Data) {
		try! data.write(to: EmojiHoarder.localEmojiDB)
	}
	
	func resetAllIndexes() {
		trie.root = TrieNode()
		trie.dict = [:]
		try? FileManager.default.removeItem(at: EmojiHoarder.localTrieDict)
		
		downloadedEmojis = []
		downloadedEmojisArr = []
		UserDefaults.standard.removeObject(forKey: "downloadedEmojis")
		UserDefaults.standard.removeObject(forKey: "downloadedEmojisArr")
		searchTerm = ""
		letterStats = []
	}
	
	//cl i disabled ts cos its quicker to rebuild it then to load ts
	private func saveTrie() {
//		return
//		guard let data = try? encoder.encode(trie.root) else {
//			fatalError("failed to encode trie")
//		}
//		try! data.write(to: EmojiHoarder.localTrie)
		
		guard let dataDict = try? encoder.encode(trie.dict) else {
			fatalError("failed to encode trie dict")
		}
		try! dataDict.write(to: EmojiHoarder.localTrieDict)
	}
	
	private func loadTrie() {
//		return
//		guard FileManager.default.fileExists(atPath: EmojiHoarder.localTrie.path) else { return }
//		guard let data = try? Data(contentsOf: EmojiHoarder.localTrie) else { return }
//		guard let decoded = try? decoder.decode(TrieNode.self, from: data) else {
//			fatalError("failed to decode trie")
//		}
//		self.trie.root = decoded
		
		guard FileManager.default.fileExists(atPath: EmojiHoarder.localTrieDict.path) else { return }
		guard let dataDict = try? Data(contentsOf: EmojiHoarder.localTrieDict) else { return }
		guard let decodedDict = try? decoder.decode([String:Emoji].self, from: dataDict) else {
			fatalError("failed to decode dict")
		}
		self.trie.dict = decodedDict
	}
	
	func buildTrie() {
		let start = Date().timeIntervalSince1970
		for emoji in emojis {
			trie.insert(word: emoji.name)
		}
		buildTrieDict()
		saveTrie()
		generateLetterStats()
		print("done building trie in", Date().timeIntervalSince1970-start)
	}
	
	private func buildTrieDict() {
		for emoji in emojis {
			trie.dict[emoji.name] = emoji
		}
		buildDownloadedEmojis()
	}
	
	private func buildDownloadedEmojis() {
		downloadedEmojis = []
		downloadedEmojisArr = []
		downloadedEmojisArr = (try? decoder.decode([String].self, from: UserDefaults.standard.data(forKey: "downloadedEmojisArr") ?? Data())) ?? []
		downloadedEmojis = (try? decoder.decode(Set<String>.self, from: UserDefaults.standard.data(forKey: "downloadedEmojis") ?? Data())) ?? []
		
		if downloadedEmojis.isEmpty || downloadedEmojisArr.isEmpty {
			for emoji in emojis {
				guard emoji.isLocal else { continue }
				downloadedEmojis.insert(emoji.name)
				downloadedEmojisArr.append(emoji.name)
			}
		}
	}
	
	func storeDownloadedIndexes() {
		UserDefaults.standard.set(try! encoder.encode(downloadedEmojis), forKey: "downloadedEmojis")
		UserDefaults.standard.set(try! encoder.encode(downloadedEmojisArr), forKey: "downloadedEmojisArr")
	}
	
	nonisolated
	private func loadLocalDB() -> [Emoji] {
		if let localEmojiDB = try? Data(contentsOf: EmojiHoarder.localEmojiDB) {
			let decoded = try! decoder.decode([Emoji].self, from: localEmojiDB)
			return decoded
		}
		return []
	}
	
	private func loadRemoteDB() async {
		async let fetched = self.fetchRemoteDB()
		if let fetched = await fetched {
			withAnimation { self.emojis = fetched }
		}
	}
	
	nonisolated
	private func fetchRemoteDB() async -> [Emoji]? {
		do {
			async let (data, _) = try URLSession.shared.data(from: endpoint)
			decoder.dateDecodingStrategy = .iso8601
			let decoded: [SlackResponse] = try decoder.decode([SlackResponse].self, from: await data)
			try await storeDB(data: await data)
			return await SlackResponse.toEmojis(from: decoded)
		} catch {
			print(error.localizedDescription)
			return nil
		}
	}
	
	private func refreshDB() async {
		guard let fetched = await self.fetchRemoteDB() else {
			let local = loadLocalDB()
			await MainActor.run {
				emojis = local
				buildTrie()
			}
			return
		}
		await MainActor.run {
			withAnimation { self.emojis = fetched }
			buildTrie()
		}
	}
	
	nonisolated func download(emoji: Emoji, skipStoreIndex: Bool = false) async {
		try? await emoji.downloadImage()
		await MainActor.run {
			if !skipStoreIndex {
				self.downloadedEmojis.insert(emoji.name)
				self.downloadedEmojisArr.append(emoji.name)
				self.storeDownloadedIndexes()
			}
			self.trie.dict[emoji.name]?.refresh()
			if !skipStoreIndex { Haptic.success.trigger() }
		}
	}
	
	@MainActor
	func delete(emoji: Emoji, skipStoreIndex: Bool = false) {
		emoji.deleteImage()
		if !skipStoreIndex {
			downloadedEmojis.remove(emoji.name)
			downloadedEmojisArr.removeAll(where: { $0 == emoji.name })
			storeDownloadedIndexes()
		}
		self.trie.dict[emoji.name]?.refresh()
		if !skipStoreIndex { Haptic.heavy.trigger() }
	}
	
	func setShowWelcome(to newValue: Bool) {
		UserDefaults.standard.set(!newValue, forKey: "showWelcome")
		self.showWelcome = newValue
	}
	
	func generateLetterStats() {
		var result: [EmojiHoarder.LetterStat] = []
		for child in trie.root.children {
			let count = trie.collectWords(startingWith: child.key, from: child.value).count
			let stat = LetterStat(char: child.key, count: count)
			result.append(stat)
		}
		self.letterStats = result
		sortLetterStats(by: self.letterStatsSorting)
	}
	
	func sortLetterStats(by: EmojiHoarder.LetterStatSorting) {
		self.letterStatsSorting = by
		let sortByLetter = letterStatsSorting.by == .letter
		switch by.ascending {
		case true:
			letterStats.sort {
				if sortByLetter {
					$0.char > $1.char
				} else {
					$0.count > $1.count
				}
			}
		case false:
			letterStats.sort {
				if sortByLetter {
					$0.char > $1.char
				} else {
					$0.count < $1.count
				}
			}
		}
	}
	
	struct LetterStat: Hashable {
		var char: String
		var count: Int
	}
	enum SortLetterStatsBy: String, CaseIterable {
		case letter = "Letter"
		case count = "Count"
	}
	struct LetterStatSorting: Hashable, Equatable {
		var by: EmojiHoarder.SortLetterStatsBy = .count
		var ascending: Bool = true
	}
}
