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

import com.r35157.libs.valuetypes.basic.SemanticVersion;
import com.r35157.nenjim.service.journal.JournalService;
import com.r35157.nenjim.service.journal.exception.InvalidJournalException;
import com.r35157.nenjim.service.journal.model.ArtifactRelease;
import com.r35157.nenjim.service.journal.model.Journal;
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
import com.r35157.nenjim.service.journal.valuetypes.ContentDigest;
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * Filesystem-independent, thread-safe reference service for immutable per-artifact Journal chains.
 */
public final class JournalServiceImpl implements JournalService {
    private static final String DEFAULT_SOURCE = "<journal>";

    private final Object stateLock = new Object();
    private final JournalTextParser parser;
    private final Map<ArtifactCoordinate, NavigableMap<JournalVersion, StoredRevision>> chains = new HashMap<>();
    private final Map<String, ArtifactCoordinate> digestOwners = new HashMap<>();

    JournalServiceImpl() {
        this(new JournalTextParser());
    }

    JournalServiceImpl(JournalTextParser parser) {
        this.parser = Objects.requireNonNull(parser, "parser");
    }

    /** Parses and validates a complete snapshot without changing service state. */
    Journal parse(String completeJournalText) {
        return parse(completeJournalText, DEFAULT_SOURCE);
    }

    /** Parses and validates a complete snapshot without changing service state. */
    Journal parse(String completeJournalText, String logicalSourceName) {
        return parser.parse(completeJournalText, sourceName(logicalSourceName));
    }

    /** Atomically accepts one complete textual Journal snapshot. */
    Journal ingest(String completeJournalText) {
        return ingest(completeJournalText, DEFAULT_SOURCE);
    }

    /** Atomically accepts one complete textual Journal snapshot with a diagnostic source name. */
    Journal ingest(String completeJournalText, String logicalSourceName) {
        String source = sourceName(logicalSourceName);
        Journal candidate = parser.parse(completeJournalText, source);
        ContentDigest digest = exactTextDigest(completeJournalText);
        ArtifactCoordinate coordinate = candidate.metadata().coordinate();
        JournalVersion version = candidate.journalVersion();

        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> existingChain = chains.get(coordinate);
            StoredRevision existingRevision = existingChain == null ? null : existingChain.get(version);
            if (existingRevision != null) {
                if (existingRevision.completeText.equals(completeJournalText)) {
                    return existingRevision.journal;
                }
                throw chainError(source,
                        "Conflicting text already exists for " + coordinate + " at Journal version " + version);
            }

            if (existingChain == null) {
                if (candidate.previousJournalDigest().isPresent()) {
                    throw invalidPredecessor(source, coordinate, candidate.previousJournalDigest().orElseThrow());
                }
            } else {
                if (candidate.previousJournalDigest().isEmpty()) {
                    throw chainError(source,
                            "Artifact " + coordinate + " already has a genesis revision; a successor must name its predecessor");
                }

                StoredRevision head = existingChain.lastEntry().getValue();
                if (version.compareTo(head.journal.journalVersion()) <= 0) {
                    throw chainError(source,
                            "Journal version " + version + " must be strictly greater than current head "
                                    + head.journal.journalVersion() + " for " + coordinate);
                }

                ContentDigest predecessor = candidate.previousJournalDigest().orElseThrow();
                if (!predecessor.equals(head.digest)) {
                    throw invalidPredecessor(source, coordinate, predecessor);
                }
            }

            TreeMap<JournalVersion, StoredRevision> updated = existingChain == null
                    ? new TreeMap<>() : new TreeMap<>(existingChain);
            updated.put(version, new StoredRevision(candidate, completeJournalText, digest));
            chains.put(coordinate, Collections.unmodifiableNavigableMap(updated));
            digestOwners.put(digest.value(), coordinate);
            return candidate;
        }
    }

    /** Lists known artifact coordinates in stable lexical order. */
    @Override
    public Set<ArtifactCoordinate> artifactCoordinates() {
        synchronized (stateLock) {
            TreeSet<ArtifactCoordinate> coordinates = new TreeSet<>(Comparator.comparing(ArtifactCoordinate::toString));
            coordinates.addAll(chains.keySet());
            return Collections.unmodifiableSet(coordinates);
        }
    }

    /** Lists complete revisions in ascending Journal-version order. */
    @Override
    public List<Journal> revisions(ArtifactCoordinate coordinate) {
        Objects.requireNonNull(coordinate, "coordinate");
        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
            if (chain == null) return List.of();
            ArrayList<Journal> result = new ArrayList<>(chain.size());
            chain.values().forEach(stored -> result.add(stored.journal));
            return List.copyOf(result);
        }
    }

    /** Returns the current complete worldview for an artifact. */
    @Override
    public Optional<Journal> latest(ArtifactCoordinate coordinate) {
        Objects.requireNonNull(coordinate, "coordinate");
        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
            return chain == null ? Optional.empty() : Optional.of(chain.lastEntry().getValue().journal);
        }
    }

    /** Returns one exact historical complete worldview. */
    @Override
    public Optional<Journal> revision(ArtifactCoordinate coordinate, JournalVersion journalVersion) {
        Objects.requireNonNull(coordinate, "coordinate");
        Objects.requireNonNull(journalVersion, "journalVersion");
        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
            if (chain == null) return Optional.empty();
            StoredRevision stored = chain.get(journalVersion);
            return stored == null ? Optional.empty() : Optional.of(stored.journal);
        }
    }

    /** Returns the newest complete worldview published no later than the requested Journal version. */
    @Override
    public Optional<Journal> revisionAtOrBefore(
            ArtifactCoordinate coordinate,
            JournalVersion journalVersion) {
        Objects.requireNonNull(coordinate, "coordinate");
        Objects.requireNonNull(journalVersion, "journalVersion");
        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
            if (chain == null) return Optional.empty();
            Map.Entry<JournalVersion, StoredRevision> entry = chain.floorEntry(journalVersion);
            return entry == null ? Optional.empty() : Optional.of(entry.getValue().journal);
        }
    }

    /** Returns an exact release from one selected Journal worldview. */
    @Override
    public Optional<ArtifactRelease> release(
            ArtifactCoordinate coordinate,
            JournalVersion journalVersion,
            SemanticVersion releaseVersion) {
        Objects.requireNonNull(releaseVersion, "releaseVersion");
        return revision(coordinate, journalVersion).flatMap(journal -> journal.release(releaseVersion));
    }

    /** Returns the exact UTF-8 text digest stored for a selected revision. */
    Optional<ContentDigest> revisionDigest(
            ArtifactCoordinate coordinate,
            JournalVersion journalVersion) {
        Objects.requireNonNull(coordinate, "coordinate");
        Objects.requireNonNull(journalVersion, "journalVersion");
        synchronized (stateLock) {
            NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
            if (chain == null) return Optional.empty();
            StoredRevision stored = chain.get(journalVersion);
            return stored == null ? Optional.empty() : Optional.of(stored.digest);
        }
    }

    private InvalidJournalException invalidPredecessor(
            String source,
            ArtifactCoordinate candidateCoordinate,
            ContentDigest predecessor) {
        ArtifactCoordinate owner = digestOwners.get(predecessor.value());
        if (owner == null) {
            return chainError(source,
                    "PREVIOUS_JOURNAL_DIGEST names a missing predecessor: " + predecessor);
        }
        if (!owner.equals(candidateCoordinate)) {
            return chainError(source,
                    "PREVIOUS_JOURNAL_DIGEST belongs to artifact " + owner
                            + ", not " + candidateCoordinate);
        }
        return chainError(source,
                "PREVIOUS_JOURNAL_DIGEST names a non-head predecessor and would fork the chain for "
                        + candidateCoordinate);
    }

    private static ContentDigest exactTextDigest(String completeJournalText) {
        Objects.requireNonNull(completeJournalText, "completeJournalText");
        try {
            MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
            byte[] digest = sha256.digest(completeJournalText.getBytes(StandardCharsets.UTF_8));
            return ContentDigest.sha256(HexFormat.of().formatHex(digest));
        } catch (NoSuchAlgorithmException impossible) {
            throw new IllegalStateException("Java runtime does not provide SHA-256", impossible);
        }
    }

    private static String sourceName(String source) {
        return source == null || source.isBlank() ? DEFAULT_SOURCE : source;
    }

    private static InvalidJournalException chainError(String source, String reason) {
        return new InvalidJournalException(source, 0, reason);
    }

    private record StoredRevision(Journal journal, String completeText, ContentDigest digest) {
        private StoredRevision {
            Objects.requireNonNull(journal, "journal");
            Objects.requireNonNull(completeText, "completeText");
            Objects.requireNonNull(digest, "digest");
        }
    }
}
