package ru.kirillius.pf.sdn.core;

import lombok.Getter;
import org.w3c.dom.Element;
import ru.kirillius.utils.logging.SystemLogger;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.regex.Pattern;

/**
 * Handles application update discovery by polling a repository and downloading new packages.
 */
public class AppUpdateService extends AppService {
    public final static String EXTENSION = ".pfapp";
    private static final String CTX = AppUpdateService.class.getSimpleName();
    private static final Pattern VERSION_LINK_PATTERN = Pattern.compile("<a\\s+[^>]*href=\"([0-9]+(?:\\.[0-9]+)*\\.pfapp)\"", Pattern.CASE_INSENSITIVE);

    private final String repository;
    private final Path appLibraryPath;
    private final Class<?> anchorClass;
    private final HttpClient httpClient;
    @Getter
    private volatile String latestVersion;
    @Getter
    private volatile String downloadedVersion;


    /**
     * Creates the service bound to the provided context and initialises HTTP access helpers.
     */
    AppUpdateService(Context context) {
        super(context);
        this.repository = context.getLauncherConfig().getRepository();
        this.appLibraryPath = context.getLauncherConfig().getAppLibrary().toPath();
        this.anchorClass = context.getClass();
        this.httpClient = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(10))
                .build();
    }

    private InputStream openPomFile() throws IOException {

        var stream = getClass().getClassLoader().getResourceAsStream(hardcodedPomPath);
        if (stream == null) {
            var pomPath = findPomFile();
            if (pomPath == null) {
                throw new FileNotFoundException("Unable to locate pom.xml to determine application version");
            }
            return Files.newInputStream(pomPath);
        }
        return stream;
    }

    /**
     * Determines the current application version using the nearest {@code pom.xml}.
     *
     * @return semantic version string or {@code "unknown"} when unavailable.
     */
    public String getAppVersion() {

        try (var input = openPomFile()) {
            var factory = DocumentBuilderFactory.newInstance();
            try {
                factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
                factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
                factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
            } catch (Exception ignored) {
            }
            var builder = factory.newDocumentBuilder();
            var document = builder.parse(input);
            document.getDocumentElement().normalize();

            var projectElement = document.getDocumentElement();
            var projectVersion = extractVersion(projectElement);
            if (projectVersion != null) {
                return projectVersion;
            }

            var parentNodes = projectElement.getElementsByTagName("parent");
            if (parentNodes.getLength() > 0) {
                var parentElement = (Element) parentNodes.item(0);
                var parentVersion = extractVersion(parentElement);
                if (parentVersion != null) {
                    return parentVersion;
                }
            }
        } catch (Exception e) {
            SystemLogger.error("Failed to read application version from pom.xml", CTX, e);
        }
        return "unknown";
    }

    /**
     * Fetches the latest available version and caches the result for reuse.
     *
     * @return newest version string or the previously cached value when fetch fails.
     */
    public synchronized String checkVersionForUpdate() {
        SystemLogger.message("Checking application version", CTX);
        var latest = fetchLatestVersion();
        if (latest != null) {
            latestVersion = latest;
            return latest;
        }
        return latestVersion;
    }

    /**
     * Downloads the application package corresponding to the latest known version.
     */
    public void updateApp() {
        var version = latestVersion;
        if (version == null || version.isBlank()) {
            version = checkVersionForUpdate();
        }
        if (version == null || version.isBlank()) {
            SystemLogger.error("No version available for update", CTX);
            return;
        }
        if (repository == null || repository.isBlank()) {
            SystemLogger.error("Repository URL is not configured", CTX);
            return;
        }
        if (appLibraryPath == null) {
            SystemLogger.error("Application library path is not configured", CTX);
            return;
        }

        var fileName = version + EXTENSION;
        var targetDirectory = appLibraryPath;
        var tempFile = targetDirectory.resolve(fileName + ".download");
        var targetFile = targetDirectory.resolve(fileName);

        if (targetFile.toFile().exists()) {
            SystemLogger.error("Latest version is downloaded already", CTX);
            return;
        }

        try {
            Files.createDirectories(targetDirectory);
            Files.deleteIfExists(tempFile);
            var request = HttpRequest.newBuilder()
                    .uri(buildVersionUri(version))
                    .timeout(Duration.ofMinutes(2))
                    .GET()
                    .build();
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(tempFile));
            if (response.statusCode() < 200 || response.statusCode() >= 300) {
                SystemLogger.error("Unexpected response code when downloading update: " + response.statusCode(), CTX);
                return;
            }
            Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
            latestVersion = version;
            downloadedVersion = version;
        } catch (Exception e) {
            SystemLogger.error("Failed to download update", CTX, e);
        } finally {
            try {
                Files.deleteIfExists(tempFile);
            } catch (IOException e) {
                SystemLogger.error("Failed to clean up temporary update file", CTX, e);
            }
        }
    }

    /**
     * Retrieves the newest version identifier from the repository index.
     */
    private String fetchLatestVersion() {
        if (repository == null || repository.isBlank()) {
            return null;
        }
        try {
            var request = HttpRequest.newBuilder()
                    .uri(URI.create(repository))
                    .timeout(Duration.ofSeconds(20))
                    .GET()
                    .build();
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
            if (response.statusCode() < 200 || response.statusCode() >= 300) {
                SystemLogger.error("Unexpected response code when checking updates: " + response.statusCode(), CTX);
                return null;
            }
            return extractLatestVersion(response.body());
        } catch (Exception e) {
            SystemLogger.error("Failed to check version for update", CTX, e);
            return null;
        }
    }

    /**
     * Builds the absolute URI to the package file for the provided version.
     */
    private URI buildVersionUri(String version) {
        var base = repository.endsWith("/") ? repository : repository + "/";
        return URI.create(base + version + EXTENSION);
    }

    /**
     * Locates the closest {@code pom.xml} to infer the running version.
     */
    private Path findPomFile() {
        if (anchorClass != null) {
            try {
                var codeSource = anchorClass.getProtectionDomain().getCodeSource();
                if (codeSource != null) {
                    var location = Paths.get(codeSource.getLocation().toURI());
                    var directory = Files.isDirectory(location) ? location : location.getParent();
                    while (directory != null) {
                        var candidate = directory.resolve("pom.xml");
                        if (Files.exists(candidate)) {
                            return candidate;
                        }
                        directory = directory.getParent();
                    }
                }
            } catch (Exception e) {
                SystemLogger.error("Failed to resolve pom.xml location", CTX, e);
            }
        }

        var workingDirCandidate = Paths.get("pom.xml");
        if (Files.exists(workingDirCandidate)) {
            return workingDirCandidate;
        }

        return null;
    }

    private final static String hardcodedPomPath = "META-INF/maven/ru.kirillius/pf-sdn.app/pom.xml";

    /**
     * Extracts a version value from the given XML element if present.
     */
    private static String extractVersion(Element element) {
        if (element == null) {
            return null;
        }
        var nodes = element.getElementsByTagName("version");
        if (nodes.getLength() == 0) {
            return null;
        }
        var value = nodes.item(0).getTextContent();
        if (value == null) {
            return null;
        }
        var trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    /**
     * Parses the repository index HTML and returns the highest {@code .pfapp} version found.
     */
    private static String extractLatestVersion(String html) {
        if (html == null || html.isBlank()) {
            return null;
        }
        String latest = null;
        var matcher = VERSION_LINK_PATTERN.matcher(html);
        while (matcher.find()) {
            var href = matcher.group(1);
            if (href == null || href.isBlank()) {
                continue;
            }
            var version = href.endsWith(EXTENSION) ? href.substring(0, href.length() - 6) : href;
            if (version.isEmpty()) {
                continue;
            }
            if (latest == null || compareVersions(version, latest) > 0) {
                latest = version;
            }
        }
        return latest;
    }

    /**
     * Compares two semantic version strings using numeric components.
     */
    private static int compareVersions(String first, String second) {
        if (first.equals(second)) {
            return 0;
        }
        var aParts = first.split("\\.");
        var bParts = second.split("\\.");
        var length = Math.max(aParts.length, bParts.length);
        for (int i = 0; i < length; i++) {
            var a = i < aParts.length ? parseVersionPart(aParts[i]) : 0;
            var b = i < bParts.length ? parseVersionPart(bParts[i]) : 0;
            var cmp = Integer.compare(a, b);
            if (cmp != 0) {
                return cmp;
            }
        }
        return Integer.compare(aParts.length, bParts.length);
    }

    /**
     * Converts a version segment into an integer, defaulting to zero when invalid.
     */
    private static int parseVersionPart(String part) {
        try {
            return Integer.parseInt(part);
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    /**
     * Shuts down the underlying HTTP client.
     */
    @Override
    public void close() throws IOException {
        httpClient.close();
    }
}
