package com.lingniu.ingest.sink.archive;

import com.lingniu.ingest.api.event.RawArchiveKeys;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;

import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class RawArchiveEventSink implements EventSink, AutoCloseable {

    private final Path root;
    private final ExecutorService executor;

    public RawArchiveEventSink(Path root) {
        if (root == null) {
            throw new IllegalArgumentException("root must not be null");
        }
        this.root = root.toAbsolutePath().normalize();
        this.executor = Executors.newVirtualThreadPerTaskExecutor();
    }

    @Override
    public String name() {
        return "raw-archive";
    }

    @Override
    public boolean accepts(VehicleEvent event) {
        return event instanceof VehicleEvent.RawArchive;
    }

    @Override
    public CompletableFuture<Void> publish(VehicleEvent event) {
        if (!(event instanceof VehicleEvent.RawArchive raw)) {
            return CompletableFuture.completedFuture(null);
        }
        return CompletableFuture.runAsync(() -> write(raw), executor);
    }

    @Override
    public void close() {
        executor.shutdown();
    }

    public Path root() {
        return root;
    }

    private void write(VehicleEvent.RawArchive raw) {
        byte[] bytes = raw.rawBytes() == null ? new byte[0] : raw.rawBytes();
        if (bytes.length == 0) {
            return;
        }
        String key = RawArchiveKeys.key(raw);
        Path target = resolveKey(key);
        try {
            createDirectoriesInsideRoot(target.getParent());
            rejectSymlinkPath(target);
            try {
                Files.write(target, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
            } catch (FileAlreadyExistsException duplicate) {
                byte[] existing = Files.readAllBytes(target);
                if (!Arrays.equals(sha256(existing), sha256(bytes))) {
                    throw new IOException("raw archive duplicate key has different content: " + key, duplicate);
                }
            }
        } catch (IOException e) {
            throw new RawArchiveWriteException("failed to write raw archive " + RawArchiveKeys.logicalUri(key), e);
        }
    }

    private Path resolveKey(String key) {
        Path target = root.resolve(key == null ? "" : key).normalize();
        if (!target.startsWith(root)) {
            throw new IllegalArgumentException("raw archive key escapes root: " + key);
        }
        return target;
    }

    private void createDirectoriesInsideRoot(Path directory) throws IOException {
        rejectEscapedPath(directory);
        if (Files.notExists(root, LinkOption.NOFOLLOW_LINKS)) {
            Files.createDirectories(root);
        }
        Path current = root;
        Path relative = root.relativize(directory);
        for (Path segment : relative) {
            current = current.resolve(segment);
            if (Files.isSymbolicLink(current)) {
                throw new IOException("raw archive path traverses symlink: " + current);
            }
            if (Files.notExists(current, LinkOption.NOFOLLOW_LINKS)) {
                Files.createDirectory(current);
            } else if (!Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
                throw new IOException("raw archive path segment is not a directory: " + current);
            }
        }
    }

    private void rejectSymlinkPath(Path target) throws IOException {
        rejectEscapedPath(target);
        Path current = root;
        Path relative = root.relativize(target);
        for (Path segment : relative) {
            current = current.resolve(segment);
            if (Files.isSymbolicLink(current)) {
                throw new IOException("raw archive path traverses symlink: " + current);
            }
            if (Files.notExists(current, LinkOption.NOFOLLOW_LINKS)) {
                return;
            }
        }
    }

    private void rejectEscapedPath(Path path) {
        if (!path.normalize().startsWith(root)) {
            throw new IllegalArgumentException("raw archive path escapes root: " + path);
        }
    }

    private static byte[] sha256(byte[] bytes) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(bytes);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 is unavailable", e);
        }
    }

    static String checksum(byte[] bytes) {
        return "sha256:" + HexFormat.of().formatHex(sha256(bytes == null ? new byte[0] : bytes));
    }

    public static final class RawArchiveWriteException extends RuntimeException {
        public RawArchiveWriteException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}
