package com.r35157.nenjim.service.journal.impl.ref;

import com.r35157.nenjim.service.journal.JournalService;
import com.r35157.nenjim.service.journal.JournalServiceManager;
import com.r35157.nenjim.service.journal.model.Journal;
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

/** Filesystem-backed reference lifecycle manager for a read-only Journal service. */
public final class JournalServiceManagerImpl implements JournalServiceManager {
    private static final String JOURNAL_SUFFIX = ".journal";
    private static final String EXAMPLE_SUFFIX = ".journal.example";

    private final Path journalRoot;
    private JournalServiceImpl activeService;

    public JournalServiceManagerImpl(Path journalRoot) {
        this.journalRoot = Objects.requireNonNull(journalRoot, "journalRoot").toAbsolutePath().normalize();
    }

    @Override
    public synchronized void start() throws IOException {
        if (activeService != null) {
            return;
        }

        JournalServiceImpl candidate = new JournalServiceImpl();
        loadAll(candidate);
        activeService = candidate;
    }

    @Override
    public synchronized void stop() {
        activeService = null;
    }

    @Override
    public Path journalRoot() {
        return journalRoot;
    }

    @Override
    public synchronized JournalService journalService() {
        return requireActiveService();
    }

    @Override
    public synchronized void refresh() throws IOException {
        loadAll(requireActiveService());
    }

    private JournalServiceImpl requireActiveService() {
        if (activeService == null) {
            throw new IllegalStateException("Journal service manager has not been started");
        }
        return activeService;
    }

    private void loadAll(JournalServiceImpl service) throws IOException {
        validateRoot();
        for (Path artifactDirectory : listEntries(journalRoot)) {
            if (!Files.isDirectory(artifactDirectory)) {
                throw new IOException("Unexpected entry in Journal root; expected an artifact directory: "
                        + artifactDirectory);
            }
            loadArtifactDirectory(service, artifactDirectory);
        }
    }

    private void validateRoot() throws IOException {
        if (!Files.exists(journalRoot)) {
            throw new IOException("Configured Journal root does not exist: " + journalRoot);
        }
        if (!Files.isDirectory(journalRoot)) {
            throw new IOException("Configured Journal root is not a directory: " + journalRoot);
        }
        if (!Files.isReadable(journalRoot)) {
            throw new IOException("Configured Journal root is not readable: " + journalRoot);
        }
    }

    private static void loadArtifactDirectory(
            JournalServiceImpl service,
            Path directory) throws IOException {
        String directoryName = directory.getFileName().toString();
        ArtifactCoordinate expectedCoordinate;
        try {
            expectedCoordinate = ArtifactCoordinate.parse(directoryName);
        } catch (IllegalArgumentException exception) {
            throw new IOException("Invalid artifact-coordinate directory name: " + directory, exception);
        }
        if (!Files.isReadable(directory)) {
            throw new IOException("Artifact Journal directory is not readable: " + directory);
        }

        ArrayList<JournalFile> runtimeFiles = new ArrayList<>();
        for (Path entry : listEntries(directory)) {
            if (!Files.isRegularFile(entry)) {
                throw new IOException("Unexpected entry in artifact Journal directory: " + entry);
            }
            String filename = entry.getFileName().toString();
            if (filename.endsWith(EXAMPLE_SUFFIX)) {
                continue;
            }
            if (!filename.endsWith(JOURNAL_SUFFIX)) {
                throw new IOException("Unexpected file in artifact Journal directory: " + entry);
            }
            if (!Files.isReadable(entry)) {
                throw new IOException("Journal file is not readable: " + entry);
            }

            String versionText = filename.substring(0, filename.length() - JOURNAL_SUFFIX.length());
            JournalVersion expectedVersion;
            try {
                expectedVersion = JournalVersion.parse(versionText);
            } catch (IllegalArgumentException exception) {
                throw new IOException("Invalid Journal filename; expected <journal-version>.journal: " + entry,
                        exception);
            }
            runtimeFiles.add(new JournalFile(entry, expectedVersion));
        }

        runtimeFiles.sort(Comparator.comparing(JournalFile::journalVersion));
        for (JournalFile runtimeFile : runtimeFiles) {
            String text = Files.readString(runtimeFile.path, StandardCharsets.UTF_8);
            String source = runtimeFile.path.toString();
            Journal parsed = service.parse(text, source);
            if (!parsed.metadata().coordinate().equals(expectedCoordinate)) {
                throw new IOException("Journal metadata coordinate " + parsed.metadata().coordinate()
                        + " does not match directory " + expectedCoordinate + ": " + runtimeFile.path);
            }
            if (!parsed.journalVersion().equals(runtimeFile.journalVersion)) {
                throw new IOException("JOURNAL_VERSION " + parsed.journalVersion()
                        + " does not match filename version " + runtimeFile.journalVersion + ": " + runtimeFile.path);
            }
            service.ingest(text, source);
        }
    }

    private static List<Path> listEntries(Path directory) throws IOException {
        try (Stream<Path> entries = Files.list(directory)) {
            return entries.sorted(Comparator.comparing(path -> path.getFileName().toString())).toList();
        } catch (SecurityException exception) {
            throw new IOException("Cannot read Journal directory: " + directory, exception);
        }
    }

    private record JournalFile(Path path, JournalVersion journalVersion) {
        private JournalFile {
            Objects.requireNonNull(path, "path");
            Objects.requireNonNull(journalVersion, "journalVersion");
        }
    }
}
