package com.dynamicwallpaper.core;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;

/**
 * Manages the bundled Swift command-line helper ("heic-builder") that does
 * the actual HEIC assembly and XMP metadata embedding via Apple's ImageIO /
 * CoreGraphics frameworks - APIs that are only reachable from Objective-C or
 * Swift, not from pure Java.
 *
 * <p>The helper's source is shipped as a resource inside this application's
 * jar. On first run (or whenever that source changes) it is extracted to
 * {@code ~/Library/Application Support/DynamicWallpaperCreator/} and
 * compiled there with {@code swiftc}, which ships with Xcode Command Line
 * Tools. The compiled binary is then reused on subsequent runs.
 */
public final class NativeHelper {

    private static final String RESOURCE_PATH = "/native/HeicBuilder.swift";

    private final Path appSupportDir;
    private final Path sourceFile;
    private final Path hashFile;
    private final Path binaryFile;

    public NativeHelper() {
        String home = System.getProperty("user.home");
        this.appSupportDir = Path.of(home, "Library", "Application Support", "DynamicWallpaperCreator");
        this.sourceFile = appSupportDir.resolve("HeicBuilder.swift");
        this.hashFile = appSupportDir.resolve(".source.sha256");
        this.binaryFile = appSupportDir.resolve("heic-builder");
    }

    /** Returns true if {@code swiftc} is available on PATH (via xcrun). */
    public boolean isSwiftToolchainAvailable() {
        try {
            Process process = new ProcessBuilder("xcrun", "--find", "swiftc")
                    .redirectErrorStream(true)
                    .start();
            boolean finished = process.waitFor(10, TimeUnit.SECONDS);
            return finished && process.exitValue() == 0;
        } catch (IOException | InterruptedException e) {
            return false;
        }
    }

    /**
     * Ensures the helper binary exists and is up to date, compiling it if
     * necessary. Returns the path to the runnable binary.
     */
    public Path ensureBuilt() throws NativeHelperException {
        try {
            Files.createDirectories(appSupportDir);
            String bundledSource = readBundledSource();
            String bundledHash = sha256(bundledSource);

            boolean needsCompile = !Files.exists(binaryFile)
                    || !Files.exists(hashFile)
                    || !bundledHash.equals(readExistingHash());

            if (needsCompile) {
                Files.writeString(sourceFile, bundledSource, StandardCharsets.UTF_8);
                compile();
                Files.writeString(hashFile, bundledHash, StandardCharsets.UTF_8);
            }
            return binaryFile;
        } catch (IOException e) {
            throw new NativeHelperException("Could not prepare native helper: " + e.getMessage(), e);
        }
    }

    private String readExistingHash() throws IOException {
        if (!Files.exists(hashFile)) {
            return "";
        }
        return Files.readString(hashFile, StandardCharsets.UTF_8).trim();
    }

    private void compile() throws NativeHelperException {
        if (!isSwiftToolchainAvailable()) {
            throw new NativeHelperException(
                    "The Swift compiler (swiftc) was not found. Install the Xcode Command Line Tools with "
                            + "'xcode-select --install' and try again.");
        }
        try {
            ProcessBuilder pb = new ProcessBuilder(
                    "xcrun", "swiftc", "-O", sourceFile.toString(), "-o", binaryFile.toString());
            pb.redirectErrorStream(true);
            Process process = pb.start();
            String output;
            try (InputStream in = process.getInputStream()) {
                output = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            }
            boolean finished = process.waitFor(180, TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                throw new NativeHelperException("Compiling the native helper timed out.");
            }
            if (process.exitValue() != 0) {
                throw new NativeHelperException("Failed to compile native helper:\n" + output);
            }
        } catch (IOException | InterruptedException e) {
            throw new NativeHelperException("Failed to compile native helper: " + e.getMessage(), e);
        }
    }

    private String readBundledSource() throws IOException {
        try (InputStream in = NativeHelper.class.getResourceAsStream(RESOURCE_PATH)) {
            if (in == null) {
                throw new IOException("Missing bundled resource " + RESOURCE_PATH);
            }
            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
        }
    }

    private static String sha256(String text) throws IOException {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new IOException(e);
        }
    }

    /** Thrown when the native helper cannot be prepared or fails to run. */
    public static final class NativeHelperException extends Exception {
        public NativeHelperException(String message) {
            super(message);
        }

        public NativeHelperException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}
