package com.xfestudio.xfeservermanager.core.policy;

import com.xfestudio.xfeservermanager.api.policy.PolicyRule;
import com.xfestudio.xfeservermanager.api.policy.PolicySnapshot;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;
import java.util.List;
import java.util.Base64;

public final class PolicySnapshotFactory {
    private PolicySnapshotFactory() {
    }

    public static PolicySnapshot create(
            long version, Instant createdAt, String createdBy, String description, List<PolicyRule> rules) {
        return new PolicySnapshot(version, createdAt, createdBy, description, rules,
                checksum(version, createdAt, createdBy, description, rules, true));
    }

    public static boolean hasValidChecksum(PolicySnapshot snapshot) {
        String expected = create(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
                snapshot.description(), snapshot.rules()).checksum();
        if (equalChecksum(expected, snapshot.checksum())) return true;
        // Existing v1 snapshots did not serialize policy-group metadata. Accept their checksum
        // only when every rule resolves to the exact backward-compatible default group.
        if (snapshot.rules().stream().anyMatch(rule ->
                !PolicyRule.DEFAULT_GROUP_ID.equals(rule.groupId())
                        || !PolicyRule.DEFAULT_GROUP_NAME.equals(rule.groupName())
                        || !rule.groupEnabled())) {
            return false;
        }
        return equalChecksum(checksum(snapshot.version(), snapshot.createdAt(), snapshot.createdBy(),
                snapshot.description(), snapshot.rules(), false), snapshot.checksum());
    }

    static String checksum(long version, Instant createdAt, String createdBy, String description,
                           List<PolicyRule> rules, boolean includeGroups) {
        String canonical = field(version) + '|' + field(createdAt) + '|' + field(createdBy) + '|'
                + field(description) + '\n' + canonicalize(rules, includeGroups);
        return sha256(canonical);
    }

    private static String canonicalize(List<PolicyRule> rules, boolean includeGroups) {
        StringBuilder result = new StringBuilder();
        for (PolicyRule rule : rules.stream().sorted(java.util.Comparator.comparing(PolicyRule::id)).toList()) {
            result.append(field(rule.id())).append('|')
                    .append(field(rule.enabled())).append('|');
            if (includeGroups) {
                result.append(field(rule.groupId())).append('|')
                        .append(field(rule.groupName())).append('|')
                        .append(field(rule.groupEnabled())).append('|');
            }
            result.append(field(rule.tier())).append('|')
                    .append(sorted(rule.subject().actorIds())).append('|')
                    .append(sorted(rule.subject().roles())).append('|')
                    .append(field(rule.subject().minimumOpLevel())).append('|')
                    .append(field(rule.subject().maximumOpLevel())).append('|')
                    .append(sorted(rule.subject().sources())).append('|')
                    .append(field(rule.subject().onlineMode())).append('|')
                    .append(sorted(rule.command().commandIds())).append('|')
                    .append(sequence(rule.command().pathPrefix())).append('|')
                    .append(field(rule.effect())).append('|')
                    .append(sorted(rule.constraints().selector().deniedSelectorTypes())).append('|')
                    .append(sorted(rule.constraints().selector().deniedSorts())).append('|')
                    .append(field(rule.constraints().selector().allowBroadSelectorWithExplicitType())).append('|')
                    .append(field(rule.constraints().selector().maximumTargets())).append('|')
                    .append(field(rule.constraints().selector().maximumDistance())).append('|')
                    .append(sorted(rule.constraints().selector().allowedEntityTypes())).append('|')
                    .append(sorted(rule.constraints().selector().deniedEntityTypes())).append('|')
                    .append(field(rule.constraints().selector().allowTagFilters())).append('|')
                    .append(field(rule.constraints().selector().allowNbtFilters())).append('|')
                    .append(field(rule.constraints().selector().allowPredicateFilters())).append('|')
                    .append(sorted(rule.constraints().item().allowedItemIds())).append('|')
                    .append(sorted(rule.constraints().item().deniedItemIds())).append('|')
                    .append(sorted(rule.constraints().item().allowedItemTags())).append('|')
                    .append(sorted(rule.constraints().item().deniedItemTags())).append('|')
                    .append(field(rule.constraints().item().maximumCount())).append('|')
                    .append(sorted(rule.constraints().item().allowedDataKeys())).append('|')
                    .append(sorted(rule.constraints().item().deniedDataKeys())).append('|')
                    .append(sorted(rule.constraints().allowedFunctionIds())).append('|')
                    .append(field(rule.constraints().denyUnlistedFunctions())).append('|')
                    .append(field(rule.reason())).append('\n');
        }
        return result.toString();
    }

    private static boolean equalChecksum(String first, String second) {
        return MessageDigest.isEqual(
                first.getBytes(StandardCharsets.US_ASCII),
                second.getBytes(StandardCharsets.US_ASCII));
    }

    private static String sorted(java.util.Collection<?> values) {
        return values.stream().map(String::valueOf).sorted().map(PolicySnapshotFactory::field)
                .collect(java.util.stream.Collectors.joining(",", "[", "]"));
    }

    private static String sequence(java.util.Collection<?> values) {
        return values.stream().map(String::valueOf).map(PolicySnapshotFactory::field)
                .collect(java.util.stream.Collectors.joining(",", "[", "]"));
    }

    private static String field(Object value) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(
                String.valueOf(value).getBytes(StandardCharsets.UTF_8));
    }

    private static String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException exception) {
            throw new IllegalStateException("SHA-256 is required by the Java runtime", exception);
        }
    }
}
