package com.xfestudio.xfeservermanager.core.policy;

import com.xfestudio.xfeservermanager.api.command.InvocationAst;
import com.xfestudio.xfeservermanager.api.policy.ActorContext;
import com.xfestudio.xfeservermanager.api.policy.DecisionEffect;
import com.xfestudio.xfeservermanager.api.policy.PolicyDecision;
import com.xfestudio.xfeservermanager.api.policy.PolicyEvaluationContext;
import com.xfestudio.xfeservermanager.api.policy.PolicyRule;
import com.xfestudio.xfeservermanager.api.policy.PolicySnapshot;
import com.xfestudio.xfeservermanager.api.policy.PolicyViolation;
import com.xfestudio.xfeservermanager.api.policy.RuleTier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/** Deterministic, immutable-snapshot command policy evaluator. */
public final class CommandPolicyEngine {
    private static final Comparator<MatchedRule> PRECEDENCE = Comparator
            .comparingInt((MatchedRule match) -> match.rule().tier().priority())
            .thenComparingInt(MatchedRule::roleWeight)
            .thenComparingInt(match -> match.rule().specificity());

    private final PolicySnapshotProvider snapshots;
    private final ConstraintEvaluator constraints;

    public CommandPolicyEngine(PolicySnapshotProvider snapshots) {
        this(snapshots, new ConstraintEvaluator());
    }

    public CommandPolicyEngine(PolicySnapshotProvider snapshots, ConstraintEvaluator constraints) {
        this.snapshots = Objects.requireNonNull(snapshots, "snapshots");
        this.constraints = Objects.requireNonNull(constraints, "constraints");
    }

    public PolicyDecision evaluate(PolicyEvaluationContext context) {
        Objects.requireNonNull(context, "context");
        return evaluate(snapshots.activeSnapshot(), context.actor(), context.invocation(), 0);
    }

    private PolicyDecision evaluate(
            PolicySnapshot snapshot,
            ActorContext actor,
            InvocationAst invocation,
            int depth
    ) {
        if (depth > 32) {
            return denied(snapshot.version(), List.of(),
                    new PolicyViolation("nested_command_depth", "", "Nested command depth exceeds 32"),
                    "Nested command analysis limit exceeded");
        }

        List<MatchedRule> matches = snapshot.rules().stream()
                .filter(PolicyRule::effectiveEnabled)
                .filter(rule -> rule.subject().matches(actor))
                .filter(rule -> rule.command().matches(invocation))
                .map(rule -> new MatchedRule(rule,
                        rule.tier() == RuleTier.ROLE ? rule.subject().matchingRoleWeight(actor) : 0))
                .toList();

        List<MatchedRule> hardRules = matches.stream()
                .filter(match -> match.rule().tier() == RuleTier.HARD_SAFETY)
                .toList();
        List<String> matchedIds = new ArrayList<>();
        hardRules.forEach(match -> matchedIds.add(match.rule().id()));

        PolicyDecision hardFailure = evaluateLayer(snapshot.version(), hardRules, invocation, matchedIds, true);
        if (hardFailure != null) {
            return hardFailure;
        }

        List<MatchedRule> subjectMatches = matches.stream()
                .filter(match -> match.rule().tier() != RuleTier.HARD_SAFETY)
                .toList();
        List<MatchedRule> selected = mostSpecific(subjectMatches);
        selected.forEach(match -> matchedIds.add(match.rule().id()));
        PolicyDecision layerFailure = evaluateLayer(snapshot.version(), selected, invocation, matchedIds, false);
        if (layerFailure != null) {
            return layerFailure;
        }

        DecisionEffect effect = selectEffect(selected, hardRules);
        if (effect == DecisionEffect.GRANT && !invocation.semanticsKnown()) {
            return denied(snapshot.version(), matchedIds,
                    new PolicyViolation("unknown_command_elevation", "",
                            "Commands without known argument semantics cannot receive elevated permission"),
                    "Unsafe permission elevation was blocked");
        }

        for (InvocationAst nested : invocation.nestedInvocations()) {
            PolicyDecision nestedDecision = evaluate(snapshot, actor, nested, depth + 1);
            if (nestedDecision.isDenied()) {
                List<PolicyViolation> nestedViolations = new ArrayList<>(nestedDecision.violations());
                nestedViolations.add(new PolicyViolation(
                        "nested_command_denied", "", "Nested command " + nested.commandId() + " was denied"));
                List<String> combinedIds = combineIds(matchedIds, nestedDecision.matchedRuleIds());
                return new PolicyDecision(DecisionEffect.DENY, snapshot.version(), combinedIds,
                        nestedViolations, "Nested command policy denied execution");
            }
            if (effect == DecisionEffect.GRANT && !nestedDecision.grantsPermission()) {
                return denied(snapshot.version(), combineIds(matchedIds, nestedDecision.matchedRuleIds()),
                        new PolicyViolation("nested_command_not_granted", "",
                                "Every nested command requires an explicit grant when its parent is elevated"),
                        "Nested command did not receive explicit permission");
            }
        }

        String explanation = switch (effect) {
            case PASS_THROUGH -> "No authorizing policy matched; use the platform permission result";
            case GRANT -> "An explicit policy grant matched";
            case CONSTRAIN -> "Policy constraints passed; use the platform permission result";
            case DENY -> throw new IllegalStateException("Denied effects return before this point");
        };
        return new PolicyDecision(effect, snapshot.version(), matchedIds, List.of(), explanation);
    }

    /** Returns a denial or null when a layer permits evaluation to continue. */
    private PolicyDecision evaluateLayer(
            long version,
            List<MatchedRule> layer,
            InvocationAst invocation,
            List<String> allMatchedIds,
            boolean hardLayer
    ) {
        if (layer.stream().anyMatch(match -> match.rule().effect() == DecisionEffect.DENY)) {
            String explanation = hardLayer ? "A hard safety rule denied the command" : "A policy rule denied the command";
            return denied(version, allMatchedIds,
                    new PolicyViolation("rule_denied", "", explanation), explanation);
        }
        List<PolicyViolation> violations = layer.stream()
                .filter(match -> match.rule().effect() == DecisionEffect.CONSTRAIN
                        || match.rule().effect() == DecisionEffect.GRANT)
                .flatMap(match -> constraints.evaluate(match.rule().constraints(), invocation).stream())
                .distinct()
                .toList();
        if (!violations.isEmpty()) {
            return new PolicyDecision(DecisionEffect.DENY, version, allMatchedIds, violations,
                    hardLayer ? "A hard safety constraint was violated" : "A command constraint was violated");
        }
        return null;
    }

    private static List<MatchedRule> mostSpecific(List<MatchedRule> matches) {
        MatchedRule best = matches.stream().max(PRECEDENCE).orElse(null);
        if (best == null) {
            return List.of();
        }
        return matches.stream().filter(candidate -> PRECEDENCE.compare(candidate, best) == 0).toList();
    }

    private static DecisionEffect selectEffect(List<MatchedRule> selected, List<MatchedRule> hardRules) {
        if (selected.stream().anyMatch(match -> match.rule().effect() == DecisionEffect.GRANT)) {
            return DecisionEffect.GRANT;
        }
        if (selected.stream().anyMatch(match -> match.rule().effect() == DecisionEffect.CONSTRAIN)
                || hardRules.stream().anyMatch(match -> match.rule().effect() == DecisionEffect.CONSTRAIN)) {
            return DecisionEffect.CONSTRAIN;
        }
        return DecisionEffect.PASS_THROUGH;
    }

    private static PolicyDecision denied(
            long version, List<String> ruleIds, PolicyViolation violation, String explanation) {
        return new PolicyDecision(DecisionEffect.DENY, version, ruleIds, List.of(violation), explanation);
    }

    private static List<String> combineIds(List<String> first, List<String> second) {
        List<String> result = new ArrayList<>(first);
        second.stream().filter(id -> !result.contains(id)).forEach(result::add);
        return result;
    }

    private record MatchedRule(PolicyRule rule, int roleWeight) {
    }
}
