package jadx.plugins.tools;

import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import jadx.core.utils.files.FileUtils;
import jadx.plugins.tools.data.JadxPluginListCache;
import jadx.plugins.tools.data.JadxPluginListEntry;
import jadx.plugins.tools.resolvers.github.GithubTools;
import jadx.plugins.tools.resolvers.github.LocationInfo;
import jadx.plugins.tools.resolvers.github.data.Asset;
import jadx.plugins.tools.resolvers.github.data.Release;
import jadx.plugins.tools.utils.PluginUtils;
import jadx.zip.ZipReader;

import static jadx.core.utils.GsonUtils.buildGson;
import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE;

public class JadxPluginsList {
	private static final Logger LOG = LoggerFactory.getLogger(JadxPluginsList.class);
	private static final JadxPluginsList INSTANCE = new JadxPluginsList();

	private static final Type LIST_TYPE = new TypeToken<List<JadxPluginListEntry>>() {
	}.getType();

	private static final Type CACHE_TYPE = new TypeToken<JadxPluginListCache>() {
	}.getType();

	public static JadxPluginsList getInstance() {
		return INSTANCE;
	}

	private @Nullable JadxPluginListCache loadedList;

	private JadxPluginsList() {
	}

	/**
	 * List provider with update callback.
	 * Can be called one or two times:
	 * <br>
	 * - Apply cached data first
	 * <br>
	 * - If update is available, apply data after fetch
	 * <br>
	 * Method call is blocking.
	 */
	public synchronized void get(Consumer<List<JadxPluginListEntry>> consumer) {
		if (loadedList != null) {
			consumer.accept(loadedList.getList());
			return;
		}
		JadxPluginListCache listCache = loadCache();
		if (listCache != null) {
			consumer.accept(listCache.getList());
			loadedList = listCache;
		}
		Release release = fetchLatestRelease();
		if (listCache == null || !listCache.getVersion().equals(release.getName())) {
			JadxPluginListCache updatedList = fetchBundle(release);
			saveCache(updatedList);
			consumer.accept(updatedList.getList());
			loadedList = updatedList;
		}
	}

	public List<JadxPluginListEntry> get() {
		AtomicReference<List<JadxPluginListEntry>> holder = new AtomicReference<>();
		get(holder::set);
		return holder.get();
	}

	private @Nullable JadxPluginListCache loadCache() {
		if (!Files.isRegularFile(PLUGINS_LIST_CACHE)) {
			return null;
		}
		try {
			String jsonStr = FileUtils.readFile(PLUGINS_LIST_CACHE);
			return buildGson().fromJson(jsonStr, CACHE_TYPE);
		} catch (Exception e) {
			return null;
		}
	}

	private void saveCache(JadxPluginListCache listCache) {
		try {
			String jsonStr = buildGson().toJson(listCache, CACHE_TYPE);
			FileUtils.writeFile(PLUGINS_LIST_CACHE, jsonStr);
		} catch (Exception e) {
			throw new RuntimeException("Error saving file: " + PLUGINS_LIST_CACHE, e);
		}
	}

	private Release fetchLatestRelease() {
		LOG.debug("Fetching latest plugins-list release info");
		LocationInfo pluginsList = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list");
		Release release = GithubTools.fetchRelease(pluginsList);
		List<Asset> assets = release.getAssets();
		if (assets.isEmpty()) {
			throw new RuntimeException("Release don't have assets");
		}
		return release;
	}

	private JadxPluginListCache fetchBundle(Release release) {
		LOG.debug("Fetching plugins-list bundle: {}", release.getName());
		try {
			Asset listAsset = release.getAssets().get(0);
			Path tmpListFile = Files.createTempFile("plugins-list", ".zip");
			try {
				PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile);
				JadxPluginListCache listCache = new JadxPluginListCache();
				listCache.setVersion(release.getName());
				listCache.setList(loadListBundle(tmpListFile));
				return listCache;
			} finally {
				Files.deleteIfExists(tmpListFile);
			}
		} catch (Exception e) {
			throw new RuntimeException("Failed to load plugin-list bundle for release:" + release.getName(), e);
		}
	}

	private static List<JadxPluginListEntry> loadListBundle(Path tmpListFile) {
		Gson gson = buildGson();
		List<JadxPluginListEntry> entries = new ArrayList<>();
		new ZipReader().visitEntries(tmpListFile.toFile(), entry -> {
			if (entry.getName().endsWith(".json")) {
				try (Reader reader = new InputStreamReader(entry.getInputStream())) {
					entries.addAll(gson.fromJson(reader, LIST_TYPE));
				} catch (Exception e) {
					throw new RuntimeException("Failed to read plugins list entry: " + entry.getName());
				}
			}
			return null;
		});
		return entries;
	}
}
