/**
 **
 ** McpFsAccess.java - MCP handler for file-system read/write access
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  McpFsAccess.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.mcp;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import com.elphel.imagej.cameras.EyesisCorrectionParameters;
import com.elphel.imagej.cameras.EyesisCorrectionParameters.CorrectionParameters;

public class McpFsAccess {
	private static final String PROP_ALLOWED_CONFIG = "elphel.mcp.allowed.config";
	private static final String PROP_ALLOWED_CONFIG_DIR = "elphel.mcp.allowed.configdir";
	private static final String PROP_ALLOWED_CONFIG_FILE = "elphel.mcp.allowed.configfile";
	private static final String PROP_ALLOWED_EXTRA = "elphel.mcp.allowed.extra";
	private static final String CONFIG_LIST_KEY = "CORRECTION_PARAMETERS.sourceSequencesList";

	private static List<Path> cachedRoots = null;
	private static String cachedConfigKey = null;
	private static long cachedConfigMtime = -1L;

	private McpFsAccess() {
	}

	public static synchronized List<Path> getAllowedRoots() {
		String configPath = System.getProperty(PROP_ALLOWED_CONFIG);
		String configDir = System.getProperty(PROP_ALLOWED_CONFIG_DIR);
		String configFile = System.getProperty(PROP_ALLOWED_CONFIG_FILE);
		String[] resolved = resolveConfigFromFile(configPath, configDir, configFile);
		configPath = resolved[0];
		configDir = resolved[1];
		String cacheKey = buildCacheKey(configPath, configDir, configFile);
		long mtime = getMtime(configPath, configDir, configFile);
		if (cachedRoots != null && configEquals(cacheKey, cachedConfigKey) && mtime == cachedConfigMtime) {
			return cachedRoots;
		}
		cachedRoots = buildAllowedRoots(configPath, configDir);
		cachedConfigKey = cacheKey;
		cachedConfigMtime = mtime;
		return cachedRoots;
	}

	public static boolean hasConfig() {
		String configPath = System.getProperty(PROP_ALLOWED_CONFIG);
		String configDir = System.getProperty(PROP_ALLOWED_CONFIG_DIR);
		String configFile = System.getProperty(PROP_ALLOWED_CONFIG_FILE);
		String[] resolved = resolveConfigFromFile(configPath, configDir, configFile);
		configPath = resolved[0];
		configDir = resolved[1];
		return (configPath != null && !configPath.trim().isEmpty())
				|| (configDir != null && !configDir.trim().isEmpty());
	}

	public static void ensureConfigured() {
		if (!hasConfig()) {
			throw new IllegalStateException(
					"MCP enabled but no allowed config specified. Set -Delphel.mcp.allowed.configdir "
							+ "or -Delphel.mcp.allowed.config, or provide -Delphel.mcp.allowed.configfile "
							+ "pointing to a file with allowed.configdir.");
		}
	}

	public static boolean isAllowed(Path path) {
		if (path == null) {
			return false;
		}
		List<Path> roots = getAllowedRoots();
		if (roots == null || roots.isEmpty()) {
			return false;
		}
		for (Path root : roots) {
			if (path.startsWith(root)) {
				return true;
			}
		}
		return false;
	}

	public static Path normalizePath(String pathStr) throws IOException {
		if (pathStr == null || pathStr.trim().isEmpty()) {
			return null;
		}
		Path path = Paths.get(pathStr).toAbsolutePath().normalize();
		if (Files.exists(path)) {
			return path.toRealPath();
		}
		return path;
	}

	private static List<Path> buildAllowedRoots(String configPath, String configDir) {
		Set<Path> roots = new HashSet<Path>();
		if (configPath != null && !configPath.trim().isEmpty()) {
			Path configFile = Paths.get(configPath).toAbsolutePath().normalize();
			roots.add(configFile);
			Path configDirFileParent = configFile.getParent();
			if (configDirFileParent != null) {
				roots.add(configDirFileParent);
			}
			addRootsFromConfig(configFile, roots);
		}
		if (configDir != null && !configDir.trim().isEmpty()) {
			Path dirPath = Paths.get(configDir).toAbsolutePath().normalize();
			roots.add(dirPath);
			addRootsFromConfigDir(dirPath, roots);
		}
		addExtraRoots(roots);
		List<Path> result = new ArrayList<Path>(roots.size());
		for (Path root : roots) {
			try {
				Path normalized = root.toRealPath();
				result.add(normalized);
			} catch (IOException e) {
				result.add(root.toAbsolutePath().normalize());
			}
		}
		return result;
	}

	private static void addRootsFromConfig(Path configFile, Set<Path> roots) {
		if (configFile == null) {
			return;
		}
		Properties props = new Properties();
		try {
			props.loadFromXML(Files.newInputStream(configFile));
		} catch (Exception e) {
			return;
		}
		String listPath = props.getProperty(CONFIG_LIST_KEY);
		if (listPath == null || listPath.trim().isEmpty()) {
			return;
		}
		Path listFile = Paths.get(listPath).toAbsolutePath().normalize();
		roots.add(listFile);
		Path listDir = listFile.getParent();
		if (listDir != null) {
			roots.add(listDir);
		}
		CorrectionParameters cp = new EyesisCorrectionParameters.CorrectionParameters();
		EyesisCorrectionParameters.CorrectionParameters.PathFirstLast[] sets = cp.getSourceSets(listFile.toString());
		addIfPresent(roots, cp.sourceDirectory);
		addIfPresent(roots, cp.linkedModels);
		addIfPresent(roots, cp.linkedCenters);
		addIfPresent(roots, cp.videoDirectory);
		addIfPresent(roots, cp.x3dDirectory);
		addIfPresent(roots, cp.resultsDirectory);
		addIfPresent(roots, cp.cuasSeedDir);
		addIfPresent(roots, cp.cuasUasLogs);
		addIfPresent(roots, cp.cuasSkyMask);
		if (sets != null) {
			for (EyesisCorrectionParameters.CorrectionParameters.PathFirstLast set : sets) {
				if (set != null && set.path != null) {
					File parent = new File(set.path).getParentFile();
					if (parent != null) {
						roots.add(parent.toPath());
					}
				}
			}
		}
	}

	private static void addRootsFromConfigDir(Path configDir, Set<Path> roots) {
		if (configDir == null || !Files.isDirectory(configDir)) {
			return;
		}
		try (java.util.stream.Stream<Path> stream = Files.list(configDir)) {
			java.util.Iterator<Path> it = stream.iterator();
			while (it.hasNext()) {
				Path entry = it.next();
				if (Files.isRegularFile(entry) && entry.getFileName().toString().endsWith(".corr-xml")) {
					roots.add(entry);
					addRootsFromConfig(entry, roots);
				}
			}
		} catch (IOException e) {
			return;
		}
	}

	private static void addIfPresent(Set<Path> roots, String value) {
		if (value == null || value.trim().isEmpty()) {
			return;
		}
		roots.add(Paths.get(value).toAbsolutePath().normalize());
	}

	private static void addExtraRoots(Set<Path> roots) {
		String extra = System.getProperty(PROP_ALLOWED_EXTRA);
		if (extra == null || extra.trim().isEmpty()) {
			return;
		}
		String[] parts = extra.split("[,;]");
		for (String part : parts) {
			String trimmed = part.trim();
			if (!trimmed.isEmpty()) {
				roots.add(Paths.get(trimmed).toAbsolutePath().normalize());
			}
		}
	}

	private static long getMtime(String configPath, String configDir) {
		long max = -1L;
		if (configPath != null && !configPath.trim().isEmpty()) {
			try {
				max = Math.max(max, Files.getLastModifiedTime(Paths.get(configPath)).toMillis());
			} catch (IOException e) {
				// ignore
			}
		}
		if (configDir != null && !configDir.trim().isEmpty()) {
			try {
				max = Math.max(max, Files.getLastModifiedTime(Paths.get(configDir)).toMillis());
			} catch (IOException e) {
				// ignore
			}
		}
		if (max >= 0) {
			return max;
		}
		return -1L;
	}

	private static long getMtime(String configPath, String configDir, String configFile) {
		long max = getMtime(configPath, configDir);
		if (configFile != null && !configFile.trim().isEmpty()) {
			try {
				max = Math.max(max, Files.getLastModifiedTime(Paths.get(configFile)).toMillis());
			} catch (IOException e) {
				// ignore
			}
		}
		return max;
	}

	private static boolean configEquals(String a, String b) {
		if (a == null && b == null) {
			return true;
		}
		if (a == null || b == null) {
			return false;
		}
		return a.equals(b);
	}

	private static String buildCacheKey(String configPath, String configDir, String configFile) {
		StringBuilder sb = new StringBuilder();
		if (configPath != null) {
			sb.append(configPath);
		}
		sb.append("|");
		if (configDir != null) {
			sb.append(configDir);
		}
		sb.append("|");
		if (configFile != null) {
			sb.append(configFile);
		}
		return sb.toString();
	}

	private static String[] resolveConfigFromFile(String configPath, String configDir, String configFile) {
		if ((configPath != null && !configPath.trim().isEmpty())
				|| (configDir != null && !configDir.trim().isEmpty())) {
			return new String[] { configPath, configDir };
		}
		if (configFile == null || configFile.trim().isEmpty()) {
			return new String[] { configPath, configDir };
		}
		Path filePath = Paths.get(configFile).toAbsolutePath().normalize();
		if (!Files.exists(filePath)) {
			return new String[] { configPath, configDir };
		}
		try {
			List<String> lines = Files.readAllLines(filePath);
			for (String line : lines) {
				String trimmed = line.trim();
				if (trimmed.isEmpty() || trimmed.startsWith("#")) {
					continue;
				}
				int eq = trimmed.indexOf('=');
				if (eq > 0) {
					String key = trimmed.substring(0, eq).trim();
					String value = trimmed.substring(eq + 1).trim();
					if (key.equals(PROP_ALLOWED_CONFIG)) {
						configPath = value;
					} else if (key.equals(PROP_ALLOWED_CONFIG_DIR) || key.equals("allowed.configdir")) {
						configDir = value;
					}
				} else if (configDir == null || configDir.trim().isEmpty()) {
					configDir = trimmed;
				}
			}
		} catch (IOException e) {
			return new String[] { configPath, configDir };
		}
		return new String[] { configPath, configDir };
	}
}
