package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstraintClauses;
import click.kamil.springstatemachineexporter.analysis.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.service.EntryPointBindingExpander;
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * When an expandable event-carrier entry (REST {@code {event}} / PathVariable carrier / messaging
 * payload) still has an unbound {@code Enum.valueOf} multi-constant widen, synthesize one call
 * chain per concrete polymorphic event so linking can proceed without softening fail-closed for
 * non-expandable shared dispatchers.
 */
public final class ExpandableEntryPolySplitter {

    private ExpandableEntryPolySplitter() {
    }

    /**
     * @return the original chain alone, or N split chains (never empty)
     */
    public static List<CallChain> maybeExpand(
            CallChain chain,
            TriggerPoint preparedTrigger,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (chain == null || preparedTrigger == null) {
            return chain == null ? List.of() : List.of(chain);
        }
        CallChain withPrepared = chain.toBuilder().triggerPoint(preparedTrigger).build();
        if (!shouldSplit(withPrepared, preparedTrigger, machineEventTypeFqn, context)) {
            return List.of(withPrepared);
        }
        List<String> concrete = CallChainLinkPolicy.concretePolymorphicCandidates(
                preparedTrigger.getPolymorphicEvents(),
                eventTypeFqn(preparedTrigger, machineEventTypeFqn),
                context);
        if (concrete.size() <= 1) {
            return List.of(withPrepared);
        }
        String carrierParam = resolveCarrierParamName(chain.getEntryPoint());
        List<CallChain> split = new ArrayList<>(concrete.size());
        for (String constantFqn : concrete) {
            String literal = constantLiteral(constantFqn);
            Map<String, String> bindings = new LinkedHashMap<>();
            if (chain.getPathBindings() != null) {
                bindings.putAll(chain.getPathBindings());
            }
            if (carrierParam != null && literal != null) {
                bindings.put(carrierParam, literal);
            }
            EntryPoint entryPoint = chain.getEntryPoint();
            if (entryPoint != null && !bindings.isEmpty()) {
                entryPoint = EntryPointBindingExpander.withResolvedPath(entryPoint, bindings);
            }
            TriggerPoint single = preparedTrigger.toBuilder()
                    .polymorphicEvents(List.of(constantFqn))
                    .ambiguous(false)
                    .external(false)
                    .build();
            split.add(chain.toBuilder()
                    .entryPoint(entryPoint)
                    .triggerPoint(single)
                    .pathBindings(bindings.isEmpty() ? null : Map.copyOf(bindings))
                    .build());
        }
        return split;
    }

    static boolean shouldSplit(
            CallChain chain,
            TriggerPoint trigger,
            String machineEventTypeFqn,
            CodebaseContext context) {
        if (chain == null || trigger == null) {
            return false;
        }
        if (!isExpandableEventCarrierEntry(chain.getEntryPoint())) {
            return false;
        }
        if (hasUnboundMachineDiscriminator(chain.getEntryPoint(), chain.getPathBindings())) {
            // Dual templates like /{machineType}/…/{event} stay fail-closed until the machine
            // route is bound; event-only fan-out would over-link across domains.
            return false;
        }
        if (!ConstraintClauses.carrierLiteralsFromPathBindings(chain.getPathBindings()).isEmpty()) {
            // Already narrowed by expander / dataflow — do not re-split.
            return false;
        }
        if (!ConstraintClauses.boundCarrierLiterals(trigger.getConstraint(), chain.getPathBindings()).isEmpty()
                && ConstraintClauses.boundCarrierLiterals(trigger.getConstraint(), chain.getPathBindings()).size()
                == 1) {
            return false;
        }
        String event = trigger.getEvent();
        if (event == null || !DynamicTriggerExpressions.isDynamic(event) || !event.contains(".valueOf(")) {
            return false;
        }
        if (!CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context)) {
            return false;
        }
        List<String> concrete = CallChainLinkPolicy.concretePolymorphicCandidates(
                trigger.getPolymorphicEvents(),
                eventTypeFqn(trigger, machineEventTypeFqn),
                context);
        return concrete.size() > 1;
    }

    static boolean isExpandableEventCarrierEntry(EntryPoint entryPoint) {
        if (entryPoint == null) {
            return false;
        }
        if (hasEventLikePlaceholder(entryPoint.getName())) {
            return true;
        }
        if (entryPoint.getMetadata() != null) {
            Object path = entryPoint.getMetadata().get("path");
            if (path instanceof String pathValue && hasEventLikePlaceholder(pathValue)) {
                return true;
            }
        }
        if (entryPoint.getParameters() == null) {
            return false;
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (EventCarrierPolicy.isCarrierParameter(entryPoint, parameter)) {
                return true;
            }
            if (isPathOrQueryVariable(parameter) && EventCarrierPolicy.isCarrierParamName(parameter.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * True when the entry advertises a machine-route discriminant that is still unbound.
     */
    static boolean hasUnboundMachineDiscriminator(EntryPoint entryPoint, Map<String, String> pathBindings) {
        if (entryPoint == null) {
            return false;
        }
        for (String name : machineDiscriminatorNames(entryPoint)) {
            if (!isBound(pathBindings, name)) {
                return true;
            }
        }
        return false;
    }

    private static List<String> machineDiscriminatorNames(EntryPoint entryPoint) {
        List<String> names = new ArrayList<>();
        collectMachineDiscriminatorPlaceholders(entryPoint.getName(), names);
        if (entryPoint.getMetadata() != null
                && entryPoint.getMetadata().get("path") instanceof String path) {
            collectMachineDiscriminatorPlaceholders(path, names);
        }
        if (entryPoint.getParameters() != null) {
            for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
                if (parameter == null || parameter.getName() == null) {
                    continue;
                }
                if (EventCarrierPolicy.isMachineDiscriminatorName(parameter.getName())
                        && (isPathOrQueryVariable(parameter)
                        || entryPoint.getType() == EntryPoint.Type.REST)) {
                    if (!names.contains(parameter.getName())) {
                        names.add(parameter.getName());
                    }
                }
            }
        }
        return names;
    }

    private static void collectMachineDiscriminatorPlaceholders(String pathOrName, List<String> out) {
        if (pathOrName == null || pathOrName.isBlank() || !pathOrName.contains("{")) {
            return;
        }
        int start = 0;
        while (true) {
            int open = pathOrName.indexOf('{', start);
            if (open < 0) {
                return;
            }
            int close = pathOrName.indexOf('}', open + 1);
            if (close < 0) {
                return;
            }
            String placeholder = pathOrName.substring(open + 1, close).trim();
            int colon = placeholder.indexOf(':');
            if (colon > 0) {
                placeholder = placeholder.substring(0, colon);
            }
            if (EventCarrierPolicy.isMachineDiscriminatorName(placeholder) && !out.contains(placeholder)) {
                out.add(placeholder);
            }
            start = close + 1;
        }
    }

    private static boolean isBound(Map<String, String> pathBindings, String name) {
        if (pathBindings == null || name == null || name.isBlank()) {
            return false;
        }
        String value = pathBindings.get(name);
        if (value != null && !value.isBlank()) {
            return true;
        }
        // Case-insensitive key fallback (expander sometimes normalizes).
        for (Map.Entry<String, String> entry : pathBindings.entrySet()) {
            if (entry.getKey() != null
                    && entry.getKey().equalsIgnoreCase(name)
                    && entry.getValue() != null
                    && !entry.getValue().isBlank()) {
                return true;
            }
        }
        return false;
    }

    private static String resolveCarrierParamName(EntryPoint entryPoint) {
        if (entryPoint == null) {
            return null;
        }
        if (entryPoint.getParameters() != null) {
            for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
                if (EventCarrierPolicy.isCarrierParameter(entryPoint, parameter)
                        || (isPathOrQueryVariable(parameter)
                        && EventCarrierPolicy.isCarrierParamName(parameter.getName()))) {
                    return parameter.getName();
                }
            }
        }
        String fromName = firstEventLikePlaceholder(entryPoint.getName());
        if (fromName != null) {
            return fromName;
        }
        if (entryPoint.getMetadata() != null
                && entryPoint.getMetadata().get("path") instanceof String path) {
            return firstEventLikePlaceholder(path);
        }
        return null;
    }

    private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
        if (parameter == null || parameter.getAnnotations() == null) {
            return false;
        }
        return parameter.getAnnotations().stream().anyMatch(a ->
                a != null && (a.equals("PathVariable")
                        || a.endsWith(".PathVariable")
                        || a.equals("RequestParam")
                        || a.endsWith(".RequestParam")));
    }

    private static boolean hasEventLikePlaceholder(String pathOrName) {
        return firstEventLikePlaceholder(pathOrName) != null;
    }

    private static String firstEventLikePlaceholder(String pathOrName) {
        if (pathOrName == null || pathOrName.isBlank() || !pathOrName.contains("{")) {
            return null;
        }
        int start = 0;
        while (true) {
            int open = pathOrName.indexOf('{', start);
            if (open < 0) {
                return null;
            }
            int close = pathOrName.indexOf('}', open + 1);
            if (close < 0) {
                return null;
            }
            String placeholder = pathOrName.substring(open + 1, close).trim();
            int colon = placeholder.indexOf(':');
            if (colon > 0) {
                placeholder = placeholder.substring(0, colon);
            }
            if (EventCarrierPolicy.isCarrierParamName(placeholder)) {
                return placeholder;
            }
            start = close + 1;
        }
    }

    private static String eventTypeFqn(TriggerPoint trigger, String machineEventTypeFqn) {
        if (trigger.getEventTypeFqn() != null && !trigger.getEventTypeFqn().isBlank()) {
            return trigger.getEventTypeFqn();
        }
        return machineEventTypeFqn;
    }

    private static String constantLiteral(String constantFqn) {
        if (constantFqn == null || constantFqn.isBlank()) {
            return null;
        }
        int lastDot = constantFqn.lastIndexOf('.');
        return lastDot >= 0 ? constantFqn.substring(lastDot + 1) : constantFqn;
    }
}
