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

import com.r35157.libs.valuetypes.basic.SemanticVersion;
import com.r35157.nenjim.service.journal.exception.InvalidJournalException;
import com.r35157.nenjim.service.journal.model.ArtifactDependency;
import com.r35157.nenjim.service.journal.model.ArtifactRelease;
import com.r35157.nenjim.service.journal.model.Journal;
import com.r35157.nenjim.service.journal.model.JournalMetadata;
import com.r35157.nenjim.service.journal.model.JournalPolicy;
import com.r35157.nenjim.service.journal.model.PolicyRule;
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
import com.r35157.nenjim.service.journal.valuetypes.ContentDigest;
import com.r35157.nenjim.service.journal.valuetypes.DependencyTarget;
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;
import com.r35157.nenjim.service.journal.valuetypes.VersionExpression;
import com.r35157.nenjim.service.journal.valuetypes.VersionExpressionSet;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Strict line-oriented parser for Journal format version 1. */
final class JournalTextParser {
    private static final Pattern FIELD_NAME = Pattern.compile("[A-Z][A-Z0-9_]*");
    private static final Pattern RELEASE_SECTION = Pattern.compile("\\[RELEASE ([^]]+)]");
    private static final String VERSION_NUMBER = "(?:0|[1-9]\\d*)";
    private static final Pattern VERSION_ENDPOINT = Pattern.compile(
            "(" + VERSION_NUMBER + ")\\.(" + VERSION_NUMBER + ")(?:\\.(" + VERSION_NUMBER + "))?");
    private static final Pattern EXACT_VERSION = Pattern.compile(
            VERSION_NUMBER + "\\." + VERSION_NUMBER + "\\." + VERSION_NUMBER);

    Journal parse(String completeJournalText) {
        return parse(completeJournalText, "<journal>");
    }

    Journal parse(String completeJournalText, String logicalSourceName) {
        if (completeJournalText == null) {
            throw new InvalidJournalException(logicalSourceName, 0, "Journal text cannot be null");
        }
        if (completeJournalText.isBlank()) {
            throw new InvalidJournalException(logicalSourceName, 0, "Journal text cannot be empty or blank");
        }
        String source = logicalSourceName == null || logicalSourceName.isBlank()
                ? "<journal>" : logicalSourceName;
        Cursor cursor = new Cursor(tokenize(completeJournalText), source);
        if (!cursor.hasNext()) {
            throw new InvalidJournalException(source, 0, "Journal contains no configuration entries");
        }

        Line formatLine = cursor.next();
        Field format = parseField(formatLine, source);
        if (!"FORMAT_VERSION".equals(format.name)) {
            throw invalid(source, formatLine,
                    "FORMAT_VERSION=1 must be the first actual configuration entry");
        }
        if (!"1".equals(format.value)) {
            throw invalid(source, formatLine,
                    "Unsupported FORMAT_VERSION '" + format.value + "'; only version 1 is supported");
        }

        Line versionLine = cursor.requireNext("Missing required JOURNAL_VERSION");
        Field versionField = parseField(versionLine, source);
        if (!"JOURNAL_VERSION".equals(versionField.name)) {
            throw invalid(source, versionLine, "Expected JOURNAL_VERSION after FORMAT_VERSION");
        }
        JournalVersion journalVersion = parseJournalVersion(versionField.value, versionLine, source);

        Optional<ContentDigest> previousDigest = Optional.empty();
        if (cursor.hasNext() && cursor.peek().content.startsWith("PREVIOUS_JOURNAL_DIGEST")) {
            Line previousLine = cursor.next();
            Field previousField = parseField(previousLine, source);
            if (!"PREVIOUS_JOURNAL_DIGEST".equals(previousField.name)) {
                throw invalid(source, previousLine, "Malformed PREVIOUS_JOURNAL_DIGEST entry");
            }
            ContentDigest digest = parseContentDigest(previousField.value, previousLine, source);
            if (!ContentDigest.SHA256.equals(digest.scheme())) {
                throw invalid(source, previousLine, "PREVIOUS_JOURNAL_DIGEST must use sha256");
            }
            previousDigest = Optional.of(digest);
        }

        Line metadataHeader = cursor.requireNext("Missing required [METADATA] section");
        if (!"[METADATA]".equals(metadataHeader.content)) {
            throw invalid(source, metadataHeader, "Expected [METADATA] section");
        }
        JournalMetadata metadata = parseMetadata(cursor, metadataHeader, source);

        ParsedPolicy parsedPolicy = new ParsedPolicy(JournalPolicy.empty(), List.of());
        if (cursor.hasNext() && "[POLICY]".equals(cursor.peek().content)) {
            Line policyHeader = cursor.next();
            parsedPolicy = parsePolicy(cursor, policyHeader, source);
        }

        if (!cursor.hasNext()) {
            throw invalid(source, metadataHeader, "A Journal requires at least one [RELEASE <version>] section");
        }

        LinkedHashMap<SemanticVersion, ArtifactRelease> releases = new LinkedHashMap<>();
        while (cursor.hasNext()) {
            Line releaseHeader = cursor.next();
            Matcher sectionMatcher = RELEASE_SECTION.matcher(releaseHeader.content);
            if (!sectionMatcher.matches()) {
                throw invalid(source, releaseHeader,
                        "Expected [RELEASE <major.minor.patch>] section, found '" + releaseHeader.content + "'");
            }
            SemanticVersion releaseVersion = parseExactVersion(sectionMatcher.group(1), releaseHeader, source);
            if (releases.containsKey(releaseVersion)) {
                throw invalid(source, releaseHeader, "Duplicate release section for " + releaseVersion);
            }
            ArtifactRelease release = parseRelease(cursor, releaseHeader, releaseVersion, source);
            releases.put(releaseVersion, release);
        }

        validateRecommendations(parsedPolicy.recommendedRules, releases, source);
        try {
            return new Journal(1, journalVersion, previousDigest, metadata, parsedPolicy.policy, releases);
        } catch (IllegalArgumentException exception) {
            throw invalid(source, metadataHeader, exception.getMessage(), exception);
        }
    }

    private static JournalMetadata parseMetadata(Cursor cursor, Line header, String source) {
        Map<String, FieldAtLine> values = new LinkedHashMap<>();
        Set<String> allowed = Set.of("GROUP", "MODULE", "ARTIFACT", "DESCRIPTION", "INFORMATION_URI");
        while (cursor.hasNext() && !isSection(cursor.peek())) {
            Line line = cursor.next();
            Field field = parseField(line, source);
            if (!allowed.contains(field.name)) {
                throw invalid(source, line, "Unknown or misplaced [METADATA] field '" + field.name + "'");
            }
            if (values.putIfAbsent(field.name, new FieldAtLine(field.value, line)) != null) {
                throw invalid(source, line, "Duplicate [METADATA] field '" + field.name + "'");
            }
        }

        FieldAtLine group = requireField(values, "GROUP", header, source);
        FieldAtLine module = requireField(values, "MODULE", header, source);
        FieldAtLine artifact = requireField(values, "ARTIFACT", header, source);
        FieldAtLine description = requireField(values, "DESCRIPTION", header, source);

        requirePlainValue(group, "GROUP", source);
        requirePlainValue(module, "MODULE", source);
        requirePlainValue(artifact, "ARTIFACT", source);
        String decodedDescription = parseQuoted(description.value, "DESCRIPTION", description.line, source);

        ArtifactCoordinate coordinate;
        try {
            coordinate = new ArtifactCoordinate(group.value, module.value, artifact.value);
        } catch (IllegalArgumentException exception) {
            throw invalid(source, group.line, exception.getMessage(), exception);
        }

        Optional<URI> informationUri = Optional.empty();
        FieldAtLine information = values.get("INFORMATION_URI");
        if (information != null) {
            String decoded = parseQuoted(information.value, "INFORMATION_URI", information.line, source);
            try {
                URI uri = new URI(decoded);
                if (!uri.isAbsolute()) {
                    throw invalid(source, information.line,
                            "INFORMATION_URI must be an absolute URI: '" + decoded + "'");
                }
                informationUri = Optional.of(uri);
            } catch (URISyntaxException exception) {
                throw invalid(source, information.line,
                        "INFORMATION_URI is not a syntactically valid URI: '" + decoded + "'", exception);
            }
        }
        return new JournalMetadata(coordinate, decodedDescription, informationUri);
    }

    private static ParsedPolicy parsePolicy(Cursor cursor, Line header, String source) {
        List<PolicyRule> recommended = new ArrayList<>();
        List<RuleAtLine> recommendedAtLines = new ArrayList<>();
        List<PolicyRule> discouraged = new ArrayList<>();
        List<PolicyRule> blacklist = new ArrayList<>();

        while (cursor.hasNext() && !isSection(cursor.peek())) {
            Line line = cursor.next();
            Field field = parseField(line, source);
            switch (field.name) {
                case "RECOMMENDED" -> {
                    PolicyRule rule = parseRule(field.value, line, source, true);
                    recommended.add(rule);
                    recommendedAtLines.add(new RuleAtLine(rule, line));
                }
                case "DISCOURAGED" -> discouraged.add(parseRule(field.value, line, source, false));
                case "BLACKLIST" -> blacklist.add(parseRule(field.value, line, source, false));
                default -> throw invalid(source, line,
                        "Unknown or misplaced [POLICY] field '" + field.name + "'");
            }
        }
        try {
            return new ParsedPolicy(
                    new JournalPolicy(recommended, discouraged, blacklist),
                    List.copyOf(recommendedAtLines));
        } catch (IllegalArgumentException exception) {
            throw invalid(source, header, exception.getMessage(), exception);
        }
    }

    private static ArtifactRelease parseRelease(
            Cursor cursor,
            Line header,
            SemanticVersion version,
            String source) {
        FieldAtLine license = null;
        FieldAtLine publishedAt = null;
        FieldAtLine contentSize = null;
        LinkedHashMap<String, ContentDigest> digests = new LinkedHashMap<>();
        LinkedHashMap<DependencyTarget, DependencyBuilder> dependencyBuilders = new LinkedHashMap<>();

        while (cursor.hasNext() && !isSection(cursor.peek())) {
            Line line = cursor.next();
            Field field = parseField(line, source);
            switch (field.name) {
                case "LICENSE" -> {
                    if (license != null) throw invalid(source, line, "Duplicate LICENSE field");
                    license = new FieldAtLine(field.value, line);
                }
                case "PUBLISHED_AT" -> {
                    if (publishedAt != null) throw invalid(source, line, "Duplicate PUBLISHED_AT field");
                    publishedAt = new FieldAtLine(field.value, line);
                }
                case "CONTENT_DIGEST" -> {
                    ContentDigest digest = parseContentDigest(field.value, line, source);
                    if (digests.putIfAbsent(digest.scheme(), digest) != null) {
                        throw invalid(source, line,
                                "Duplicate CONTENT_DIGEST scheme '" + digest.scheme() + "'");
                    }
                }
                case "CONTENT_SIZE" -> {
                    if (contentSize != null) throw invalid(source, line, "Duplicate CONTENT_SIZE field");
                    contentSize = new FieldAtLine(field.value, line);
                }
                case "DEPENDS_ON", "EXCLUDES", "PREFERRED" -> parseDependencyRule(
                        field, line, source, dependencyBuilders);
                default -> throw invalid(source, line,
                        "Unknown or misplaced release field '" + field.name + "'");
            }
        }

        if (license == null) throw invalid(source, header, "Release " + version + " is missing LICENSE");
        if (publishedAt == null) throw invalid(source, header, "Release " + version + " is missing PUBLISHED_AT");
        if (digests.isEmpty()) throw invalid(source, header, "Release " + version + " requires CONTENT_DIGEST");
        if (contentSize == null) throw invalid(source, header, "Release " + version + " is missing CONTENT_SIZE");

        String decodedLicense = parseQuoted(license.value, "LICENSE", license.line, source);
        Instant publicationTime = parseJournalVersion(publishedAt.value, publishedAt.line, source).instant();
        long size;
        try {
            size = Long.parseLong(contentSize.value);
            if (size < 0) throw new NumberFormatException("negative");
        } catch (NumberFormatException exception) {
            throw invalid(source, contentSize.line,
                    "CONTENT_SIZE must be a non-negative long: '" + contentSize.value + "'", exception);
        }

        List<ArtifactDependency> dependencies = new ArrayList<>();
        for (DependencyBuilder builder : dependencyBuilders.values()) {
            if (builder.dependsOn == null) {
                throw invalid(source, builder.firstLine,
                        builder.target + " has EXCLUDES or PREFERRED without DEPENDS_ON");
            }
            try {
                dependencies.add(new ArtifactDependency(
                        builder.target, builder.dependsOn, builder.exclusions, builder.preferences));
            } catch (IllegalArgumentException exception) {
                throw invalid(source, builder.firstLine, exception.getMessage(), exception);
            }
        }

        try {
            return new ArtifactRelease(
                    version,
                    decodedLicense,
                    publicationTime,
                    List.copyOf(digests.values()),
                    size,
                    dependencies);
        } catch (IllegalArgumentException exception) {
            throw invalid(source, header, exception.getMessage(), exception);
        }
    }

    private static void parseDependencyRule(
            Field field,
            Line line,
            String source,
            Map<DependencyTarget, DependencyBuilder> builders) {
        int assignment = field.value.indexOf('=');
        if (assignment <= 0 || assignment == field.value.length() - 1) {
            throw invalid(source, line,
                    field.name + " must have the form <scheme>:<coordinate>=<version-expression-list>");
        }
        String targetText = field.value.substring(0, assignment).trim();
        String ruleText = field.value.substring(assignment + 1).trim();
        DependencyTarget target = parseDependencyTarget(targetText, line, source);
        DependencyBuilder builder = builders.computeIfAbsent(
                target, ignored -> new DependencyBuilder(target, line));

        if ("DEPENDS_ON".equals(field.name)) {
            if (builder.dependsOn != null) {
                throw invalid(source, line, "Duplicate DEPENDS_ON for " + target);
            }
            builder.dependsOn = parseVersionExpressionSet(ruleText, line, source);
            return;
        }

        PolicyRule rule = parseRule(ruleText, line, source, false);
        if ("EXCLUDES".equals(field.name)) builder.exclusions.add(rule);
        else builder.preferences.add(rule);
    }

    private static DependencyTarget parseDependencyTarget(String text, Line line, String source) {
        int separator = text.indexOf(':');
        if (separator <= 0 || separator == text.length() - 1) {
            throw invalid(source, line,
                    "Dependency target must have the form <scheme>:<scheme-specific-coordinate>");
        }
        String scheme = text.substring(0, separator);
        if (!DependencyTarget.NENJIM_SCHEME.equals(scheme)) {
            throw invalid(source, line,
                    "Unsupported dependency scheme '" + scheme + "'; format version 1 supports only nenjim");
        }
        try {
            return DependencyTarget.nenjim(ArtifactCoordinate.parse(text.substring(separator + 1)));
        } catch (IllegalArgumentException exception) {
            throw invalid(source, line, exception.getMessage(), exception);
        }
    }

    private static PolicyRule parseRule(String text, Line line, String source, boolean exactOnly) {
        DescribedValue described = splitDescription(text, line, source);
        List<String> expressionTexts = splitExpressionList(described.value, line, source);
        if (exactOnly && expressionTexts.size() != 1) {
            throw invalid(source, line, "RECOMMENDED must identify exactly one exact release per line");
        }
        if (exactOnly && expressionTexts.getFirst().contains("-->")) {
            throw invalid(source, line, "RECOMMENDED does not accept range syntax");
        }
        List<VersionExpression> expressions = new ArrayList<>();
        for (String expressionText : expressionTexts) {
            expressions.add(parseVersionExpression(expressionText, line, source));
        }
        VersionExpressionSet set = new VersionExpressionSet(expressions);
        if (exactOnly && (set.expressions().size() != 1 || !set.expressions().getFirst().isExact())) {
            throw invalid(source, line, "RECOMMENDED must identify one exact major.minor.patch release");
        }
        return new PolicyRule(set, described.description);
    }

    private static VersionExpressionSet parseVersionExpressionSet(String text, Line line, String source) {
        List<VersionExpression> expressions = new ArrayList<>();
        for (String expressionText : splitExpressionList(text, line, source)) {
            expressions.add(parseVersionExpression(expressionText, line, source));
        }
        return new VersionExpressionSet(expressions);
    }

    private static List<String> splitExpressionList(String text, Line line, String source) {
        if (text.isBlank()) throw invalid(source, line, "Version expression list cannot be empty");
        String[] elements = text.split(",", -1);
        List<String> result = new ArrayList<>(elements.length);
        for (String element : elements) {
            String trimmed = element.trim();
            if (trimmed.isEmpty()) throw invalid(source, line, "Version expression list contains an empty element");
            result.add(trimmed);
        }
        return result;
    }

    private static VersionExpression parseVersionExpression(String text, Line line, String source) {
        if (EXACT_VERSION.matcher(text).matches()) {
            return VersionExpression.exact(parseExactVersion(text, line, source));
        }

        if (text.startsWith("[") && text.endsWith("]") && !text.contains("-->")) {
            Endpoint endpoint = parseEndpoint(text.substring(1, text.length() - 1), line, source);
            if (endpoint.hasPatch()) {
                return VersionExpression.exact(endpoint.toSemanticVersion());
            }
            return VersionExpression.minorSeries(endpoint.major, endpoint.minor);
        }

        if ((text.startsWith("[") || text.startsWith("(")) && text.contains("-->")) {
            char lowerDelimiter = text.charAt(0);
            int arrow = text.indexOf("-->");
            if (text.indexOf("-->", arrow + 3) >= 0) {
                throw invalid(source, line, "Version range contains more than one '-->' separator");
            }
            Endpoint lowerEndpoint = parseEndpoint(text.substring(1, arrow).trim(), line, source);
            VersionExpression.Boundary lower = lowerEndpoint.boundary();
            if (lowerDelimiter == '(') {
                lower = lowerEndpoint.hasPatch() ? lower.nextPatch() : lower.nextMinor();
            }

            String upperText = text.substring(arrow + 3).trim();
            VersionExpression.Boundary upper;
            if (upperText.isEmpty()) {
                upper = lowerEndpoint.boundary().nextMajor();
            } else {
                char upperDelimiter = upperText.charAt(upperText.length() - 1);
                if (upperDelimiter != ']' && upperDelimiter != ')') {
                    throw invalid(source, line, "A bounded version range must end with ']' or ')'");
                }
                Endpoint upperEndpoint = parseEndpoint(
                        upperText.substring(0, upperText.length() - 1).trim(), line, source);
                upper = upperEndpoint.boundary();
                if (upperDelimiter == ']') {
                    upper = upperEndpoint.hasPatch() ? upper.nextPatch() : upper.nextMinor();
                }
            }
            try {
                return new VersionExpression(lower, upper);
            } catch (IllegalArgumentException exception) {
                throw invalid(source, line, exception.getMessage(), exception);
            }
        }

        throw invalid(source, line,
                "Invalid version expression '" + text
                        + "'; use an exact major.minor.patch value or a Nenjim bracket range");
    }

    private static Endpoint parseEndpoint(String text, Line line, String source) {
        Matcher matcher = VERSION_ENDPOINT.matcher(text);
        if (!matcher.matches()) {
            throw invalid(source, line,
                    "Version-expression endpoints require major.minor with no internal whitespace: '" + text + "'");
        }
        try {
            int major = Integer.parseInt(matcher.group(1));
            int minor = Integer.parseInt(matcher.group(2));
            Integer patch = matcher.group(3) == null ? null : Integer.parseInt(matcher.group(3));
            return new Endpoint(major, minor, patch);
        } catch (NumberFormatException exception) {
            throw invalid(source, line, "Version element is outside the supported integer range: '" + text + "'", exception);
        }
    }

    private static SemanticVersion parseExactVersion(String text, Line line, String source) {
        if (!EXACT_VERSION.matcher(text).matches()) {
            throw invalid(source, line,
                    "Artifact release version must contain exact major.minor.patch without prerelease or build metadata: '"
                            + text + "'");
        }
        Endpoint endpoint = parseEndpoint(text, line, source);
        return endpoint.toSemanticVersion();
    }

    private static DescribedValue splitDescription(String text, Line line, String source) {
        int separator = text.indexOf(':');
        if (separator < 0) return new DescribedValue(text.trim(), Optional.empty());

        String value = text.substring(0, separator).trim();
        if (value.isEmpty()) throw invalid(source, line, "Rule value cannot be empty");
        String descriptionText = text.substring(separator + 1).trim();
        String description = parseQuoted(descriptionText, "rule description", line, source);
        return new DescribedValue(value, Optional.of(description));
    }

    private static String parseQuoted(String text, String fieldName, Line line, String source) {
        String value = text.trim();
        if (value.length() < 2 || value.charAt(0) != '\'' || value.charAt(value.length() - 1) != '\'') {
            throw invalid(source, line, fieldName + " must be enclosed in single quotes");
        }
        StringBuilder decoded = new StringBuilder();
        for (int index = 1; index < value.length() - 1; index++) {
            char current = value.charAt(index);
            if (current == '\\') {
                if (index + 1 >= value.length() - 1) {
                    throw invalid(source, line, fieldName + " ends with an incomplete escape");
                }
                char escaped = value.charAt(++index);
                if (escaped != '\'' && escaped != '\\') {
                    throw invalid(source, line,
                            fieldName + " supports only \\' and \\\\ escapes");
                }
                decoded.append(escaped);
            } else if (current == '\'') {
                throw invalid(source, line, fieldName + " contains an unescaped single quote");
            } else {
                decoded.append(current);
            }
        }
        if (decoded.toString().isBlank()) {
            throw invalid(source, line, fieldName + " cannot be empty or blank");
        }
        return decoded.toString();
    }

    private static ContentDigest parseContentDigest(String text, Line line, String source) {
        try {
            return ContentDigest.parse(text);
        } catch (IllegalArgumentException exception) {
            throw invalid(source, line, exception.getMessage(), exception);
        }
    }

    private static JournalVersion parseJournalVersion(String text, Line line, String source) {
        try {
            return JournalVersion.parse(text);
        } catch (IllegalArgumentException exception) {
            throw invalid(source, line, exception.getMessage(), exception);
        }
    }

    private static void validateRecommendations(
            List<RuleAtLine> recommendedRules,
            Map<SemanticVersion, ArtifactRelease> releases,
            String source) {
        for (RuleAtLine ruleAtLine : recommendedRules) {
            SemanticVersion version = ruleAtLine.rule.versions()
                    .expressions().getFirst().exactVersion().orElseThrow();
            if (!releases.containsKey(version)) {
                throw invalid(source, ruleAtLine.line,
                        "RECOMMENDED release " + version + " does not exist in the same Journal snapshot");
            }
        }
    }

    private static Field parseField(Line line, String source) {
        int separator = line.content.indexOf('=');
        if (separator <= 0) {
            throw invalid(source, line, "Expected KEY=value configuration entry");
        }
        String name = line.content.substring(0, separator).trim();
        String value = line.content.substring(separator + 1).trim();
        if (!FIELD_NAME.matcher(name).matches()) {
            throw invalid(source, line, "Malformed field name '" + name + "'");
        }
        if (value.isEmpty()) {
            throw invalid(source, line, "Field '" + name + "' cannot have an empty value");
        }
        return new Field(name, value);
    }

    private static void requirePlainValue(FieldAtLine field, String name, String source) {
        if (field.value.isBlank()) throw invalid(source, field.line, name + " cannot be blank");
        if (field.value.startsWith("'") || field.value.endsWith("'")) {
            throw invalid(source, field.line, name + " must be an unquoted coordinate component");
        }
    }

    private static FieldAtLine requireField(
            Map<String, FieldAtLine> values,
            String name,
            Line header,
            String source) {
        FieldAtLine value = values.get(name);
        if (value == null) throw invalid(source, header, "[METADATA] is missing required field '" + name + "'");
        return value;
    }

    private static boolean isSection(Line line) {
        return line.content.startsWith("[");
    }

    private static List<Line> tokenize(String text) {
        String[] physicalLines = text.split("\\R", -1);
        ArrayList<Line> lines = new ArrayList<>();
        for (int index = 0; index < physicalLines.length; index++) {
            String content = stripComment(physicalLines[index]).trim();
            if (!content.isEmpty()) lines.add(new Line(index + 1, content));
        }
        return List.copyOf(lines);
    }

    private static String stripComment(String line) {
        StringBuilder content = new StringBuilder();
        boolean inQuote = false;
        boolean escaped = false;
        for (int index = 0; index < line.length(); index++) {
            char current = line.charAt(index);
            if (!inQuote && current == '#') break;
            content.append(current);
            if (inQuote && escaped) {
                escaped = false;
            } else if (inQuote && current == '\\') {
                escaped = true;
            } else if (current == '\'') {
                inQuote = !inQuote;
            }
        }
        return content.toString();
    }

    private static InvalidJournalException invalid(String source, Line line, String reason) {
        return new InvalidJournalException(source, line.number, reason);
    }

    private static InvalidJournalException invalid(
            String source,
            Line line,
            String reason,
            Throwable cause) {
        return new InvalidJournalException(source, line.number, reason, cause);
    }

    private record Line(int number, String content) {
    }

    private record Field(String name, String value) {
    }

    private record FieldAtLine(String value, Line line) {
    }

    private record DescribedValue(String value, Optional<String> description) {
    }

    private record RuleAtLine(PolicyRule rule, Line line) {
    }

    private record ParsedPolicy(JournalPolicy policy, List<RuleAtLine> recommendedRules) {
    }

    private record Endpoint(int major, int minor, Integer patch) {
        boolean hasPatch() {
            return patch != null;
        }

        VersionExpression.Boundary boundary() {
            return new VersionExpression.Boundary(major, minor, patch == null ? 0 : patch);
        }

        SemanticVersion toSemanticVersion() {
            if (patch == null) throw new IllegalStateException("Endpoint is not exact");
            return new SemanticVersion(major, minor, patch);
        }
    }

    private static final class DependencyBuilder {
        private final DependencyTarget target;
        private final Line firstLine;
        private VersionExpressionSet dependsOn;
        private final List<PolicyRule> exclusions = new ArrayList<>();
        private final List<PolicyRule> preferences = new ArrayList<>();

        private DependencyBuilder(DependencyTarget target, Line firstLine) {
            this.target = Objects.requireNonNull(target, "target");
            this.firstLine = Objects.requireNonNull(firstLine, "firstLine");
        }
    }

    private static final class Cursor {
        private final List<Line> lines;
        private final String source;
        private int index;

        private Cursor(List<Line> lines, String source) {
            this.lines = lines;
            this.source = source;
        }

        boolean hasNext() {
            return index < lines.size();
        }

        Line peek() {
            return lines.get(index);
        }

        Line next() {
            return lines.get(index++);
        }

        Line requireNext(String reason) {
            if (!hasNext()) throw new InvalidJournalException(source, 0, reason);
            return next();
        }
    }
}
