package p.packer.services;

import p.packer.messages.PackerProjectContext;

import java.io.Closeable;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public final class PackerProjectWriteCoordinator implements Closeable {
    private final ConcurrentMap<ProjectKey, ExecutorService> executors = new ConcurrentHashMap<>();
    private final AtomicInteger nextThreadId = new AtomicInteger(1);

    public <T> Future<T> submit(PackerProjectContext project, Callable<T> task) {
        final PackerProjectContext safeProject = Objects.requireNonNull(project, "project");
        final Callable<T> safeTask = Objects.requireNonNull(task, "task");
        return executorFor(safeProject).submit(safeTask);
    }

    public <T> T execute(PackerProjectContext project, Callable<T> task) {
        try {
            return submit(project, task).get();
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Write execution was interrupted for project: " + project.projectId(), exception);
        } catch (ExecutionException exception) {
            final Throwable cause = exception.getCause();
            if (cause instanceof RuntimeException runtimeException) {
                throw runtimeException;
            }
            throw new IllegalStateException("Write execution failed for project: " + project.projectId(), cause);
        }
    }

    @Override
    public void close() {
        executors.values().forEach(executor -> {
            executor.shutdownNow();
            try {
                executor.awaitTermination(5, TimeUnit.SECONDS);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
            }
        });
        executors.clear();
    }

    private ExecutorService executorFor(PackerProjectContext project) {
        final ProjectKey key = ProjectKey.from(project);
        return executors.computeIfAbsent(key, ignored -> new ThreadPoolExecutor(
                1,
                1,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(),
                runnable -> {
                    final Thread thread = new Thread(
                            runnable,
                            "packer-write-" + project.projectId() + "-" + nextThreadId.getAndIncrement());
                    thread.setDaemon(true);
                    return thread;
                }));
    }

    private record ProjectKey(
            String projectId,
            Path rootPath) {

        private static ProjectKey from(PackerProjectContext project) {
            return new ProjectKey(
                    Objects.requireNonNull(project.projectId(), "projectId"),
                    Objects.requireNonNull(project.rootPath(), "rootPath").toAbsolutePath().normalize());
        }
    }
}
