package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.DynamicTriggerExpressions;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
import java.util.HashSet;
import java.util.Set;

/**
 * Single source-derived policy for marking entry-point triggers as external vs internal
 * (REST path/query/body and messaging payload parameters).
 */
public final class ExternalTriggerPolicy {

    private ExternalTriggerPolicy() {
    }

    public static boolean isExternalFromSource(
            EntryPoint entryPoint,
            TriggerPoint trigger,
            String entryMethodFqn,
            String resolvedEventParamName,
            CodebaseContext context) {
        if (trigger == null) {
            return false;
        }
        if (entryPoint != null && entryPointHasUnboundPlaceholders(entryPoint)
                && DynamicTriggerExpressions.isDynamic(trigger.getEvent())) {
            String paramName = resolveRestEventParameterName(entryPoint, resolvedEventParamName);
            if (paramName != null) {
                RestParamBinding binding = findRestParameterBinding(entryPoint, paramName, context);
                if (binding != null && binding.pathVariable() && binding.enumType()) {
                    return false;
                }
            }
            return true;
        }
        if (MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
                == MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM) {
            return false;
        }
        if (trigger.getPolymorphicEvents() != null
                && !trigger.getPolymorphicEvents().isEmpty()
                && MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
            return false;
        }
        if (entryPoint != null && entryPoint.getType() == EntryPoint.Type.REST) {
            String paramName = resolveRestEventParameterName(entryPoint, resolvedEventParamName);
            if (paramName != null) {
                RestParamBinding binding = findRestParameterBinding(entryPoint, paramName, context);
                if (binding != null) {
                    if (binding.pathOrQueryVariable() && !binding.enumType()) {
                        if (binding.pathVariable() && !entryPointHasUnboundPlaceholders(entryPoint)) {
                            return false;
                        }
                        return true;
                    }
                    if (binding.requestBodyEnum()) {
                        return false;
                    }
                }
                if (entryPoint.getClassName() != null && entryPoint.getMethodName() != null) {
                    RestParamBinding requestBodyField = findRequestBodyEnumFieldBinding(
                            entryPoint.getClassName() + "." + entryPoint.getMethodName(), paramName, context);
                    if (requestBodyField != null && requestBodyField.requestBodyEnum()) {
                        return false;
                    }
                }
            }
            if (entryPoint.getName() != null && entryPointHasUnboundPlaceholders(entryPoint)
                    && DynamicTriggerExpressions.isDynamic(trigger.getEvent())) {
                if (paramName != null) {
                    RestParamBinding pathBinding = findRestParameterBinding(entryPoint, paramName, context);
                    if (pathBinding != null && pathBinding.pathVariable() && pathBinding.enumType()) {
                        return false;
                    }
                }
                return true;
            }
        }
        if (entryPoint != null && EventCarrierPolicy.isMessagingEntryPoint(entryPoint)) {
            String paramName = resolveMessagingEventParameterName(entryPoint, resolvedEventParamName);
            if (paramName != null) {
                RestParamBinding binding = findRestParameterBinding(entryPoint, paramName, context);
                if (binding != null && binding.payloadEnum()) {
                    return false;
                }
                RestParamBinding astBinding = entryPoint.getClassName() != null && entryPoint.getMethodName() != null
                        ? findMethodParameterBinding(
                                entryPoint.getClassName() + "." + entryPoint.getMethodName(), paramName, context)
                        : null;
                if (astBinding != null && astBinding.payloadEnum()) {
                    return false;
                }
            }
            if (DynamicTriggerExpressions.isDynamic(trigger.getEvent())
                    && !hasConcreteSinglePoly(trigger)) {
                // Unbound messaging payload feeding valueOf/switch — treat like unbound REST event param.
                return true;
            }
        }
        if (entryMethodFqn != null && resolvedEventParamName != null) {
            RestParamBinding binding = findMethodParameterBinding(entryMethodFqn, resolvedEventParamName, context);
            if (binding != null) {
                if (binding.pathOrQueryVariable() && !binding.enumType()) {
                    return true;
                }
                if (binding.requestBodyEnum() || binding.payloadEnum()) {
                    return false;
                }
            }
            RestParamBinding requestBodyField = findRequestBodyEnumFieldBinding(
                    entryMethodFqn, resolvedEventParamName, context);
            if (requestBodyField != null && requestBodyField.requestBodyEnum()) {
                return false;
            }
        }
        if (entryPoint != null && entryPoint.getType() == EntryPoint.Type.REST
                && !entryPointHasUnboundPlaceholders(entryPoint)
                && DynamicTriggerExpressions.isDynamic(trigger.getEvent())) {
            return false;
        }
        return trigger.isExternal();
    }

    private static boolean hasConcreteSinglePoly(TriggerPoint trigger) {
        if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
            return false;
        }
        return MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())
                && trigger.getPolymorphicEvents().size() == 1;
    }

    private static String resolveMessagingEventParameterName(EntryPoint entryPoint, String resolvedEventParamName) {
        if (resolvedEventParamName != null && !resolvedEventParamName.isBlank()
                && looksLikeParameterName(resolvedEventParamName)) {
            return resolvedEventParamName;
        }
        if (entryPoint.getParameters() == null) {
            return null;
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (parameter.getAnnotations() != null
                    && parameter.getAnnotations().stream().anyMatch(a ->
                            "Payload".equals(a) || (a != null && a.endsWith(".Payload")))) {
                return parameter.getName();
            }
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if ((parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty())
                    && EventCarrierPolicy.isCarrierParamName(parameter.getName())) {
                return parameter.getName();
            }
        }
        return null;
    }

    private static boolean entryPointHasUnboundPlaceholders(EntryPoint entryPoint) {
        if (entryPoint == null) {
            return false;
        }
        if (hasUnboundEventPlaceholder(entryPoint.getName())) {
            return true;
        }
        if (entryPoint.getMetadata() != null) {
            String path = entryPoint.getMetadata().get("path");
            if (hasUnboundEventPlaceholder(path)) {
                return true;
            }
        }
        return false;
    }

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

    private static String placeholderName(String placeholder) {
        if (placeholder == null) {
            return null;
        }
        int colon = placeholder.indexOf(':');
        return colon > 0 ? placeholder.substring(0, colon).trim() : placeholder;
    }

    private static String resolveRestEventParameterName(EntryPoint entryPoint, String resolvedEventParamName) {
        if (resolvedEventParamName != null && !resolvedEventParamName.isBlank()) {
            // Only accept identifiers that look like method parameter names, not event FQNs/expressions.
            if (looksLikeParameterName(resolvedEventParamName)) {
                return resolvedEventParamName;
            }
        }
        if (entryPoint.getParameters() == null) {
            return null;
        }
        // Prefer PathVariable/RequestParam params that look like event carriers.
        // Do NOT fall back to unrelated resource ids ({id}, orderId, userId).
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (parameter.getAnnotations() == null) {
                continue;
            }
            boolean pathVariable = parameter.getAnnotations().stream().anyMatch("PathVariable"::equals);
            boolean requestParam = parameter.getAnnotations().stream().anyMatch("RequestParam"::equals);
            if (!pathVariable && !requestParam) {
                continue;
            }
            if (EventCarrierPolicy.isCarrierParamName(parameter.getName())
                    || EventCarrierPolicy.isMachineDiscriminatorName(parameter.getName())) {
                return parameter.getName();
            }
        }
        return null;
    }

    private static boolean looksLikeParameterName(String value) {
        if (value == null || value.isBlank()) {
            return false;
        }
        // Reject FQNs, method calls, and dotted expressions — those are event values, not param names.
        if (value.contains(".") || value.contains("(") || value.contains(")") || value.contains(" ")) {
            return false;
        }
        return Character.isJavaIdentifierStart(value.charAt(0))
                && value.chars().allMatch(Character::isJavaIdentifierPart);
    }

    private static RestParamBinding findRestParameterBinding(
            EntryPoint entryPoint, String paramName, CodebaseContext context) {
        if (entryPoint.getParameters() == null || paramName == null) {
            return null;
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (!paramName.equals(parameter.getName())) {
                continue;
            }
            return RestParamBinding.fromEntryPointParameter(parameter);
        }
        if (entryPoint.getClassName() != null && entryPoint.getMethodName() != null) {
            return findMethodParameterBinding(
                    entryPoint.getClassName() + "." + entryPoint.getMethodName(), paramName, context);
        }
        return null;
    }

    private static RestParamBinding findMethodParameterBinding(
            String methodFqn, String paramName, CodebaseContext context) {
        if (methodFqn == null || !methodFqn.contains(".") || paramName == null || context == null) {
            return null;
        }
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
        if (typeDeclaration == null) {
            return null;
        }
        MethodDeclaration method = context.findMethodDeclaration(typeDeclaration, methodName, true);
        if (method == null) {
            return null;
        }
        for (Object paramObj : method.parameters()) {
            if (!(paramObj instanceof SingleVariableDeclaration param)) {
                continue;
            }
            if (!paramName.equals(param.getName().getIdentifier())) {
                continue;
            }
            return RestParamBinding.fromAstParameter(param);
        }
        return null;
    }

    private static RestParamBinding findRequestBodyEnumFieldBinding(
            String methodFqn, String fieldName, CodebaseContext context) {
        if (methodFqn == null || !methodFqn.contains(".") || fieldName == null || context == null) {
            return null;
        }
        String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
        String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
        TypeDeclaration typeDeclaration = context.getTypeDeclaration(className);
        if (typeDeclaration == null) {
            return null;
        }
        MethodDeclaration method = context.findMethodDeclaration(typeDeclaration, methodName, true);
        if (method == null) {
            return null;
        }
        for (Object paramObj : method.parameters()) {
            if (!(paramObj instanceof SingleVariableDeclaration param)) {
                continue;
            }
            if (!hasParameterAstAnnotation(param, "RequestBody") || param.getType() == null) {
                continue;
            }
            ITypeBinding bodyBinding = param.getType().resolveBinding();
            if (bodyBinding == null || bodyBinding.isEnum()) {
                continue;
            }
            if (isEnumRecordComponent(bodyBinding, fieldName)) {
                return new RestParamBinding(false, false, true, false, true);
            }
            if (isEnumDtoField(bodyBinding, fieldName, context)) {
                return new RestParamBinding(false, false, true, false, true);
            }
            if (containsNestedEnumField(bodyBinding, fieldName, context)) {
                return new RestParamBinding(false, false, true, false, true);
            }
        }
        return null;
    }

    private static boolean isEnumRecordComponent(ITypeBinding bodyBinding, String fieldName) {
        if (bodyBinding == null || fieldName == null) {
            return false;
        }
        for (IVariableBinding component : bodyBinding.getDeclaredFields()) {
            if (!fieldName.equals(component.getName())) {
                continue;
            }
            ITypeBinding fieldType = component.getType();
            return fieldType != null && fieldType.isEnum();
        }
        return false;
    }

    private static boolean isEnumDtoField(ITypeBinding bodyBinding, String fieldName, CodebaseContext context) {
        if (bodyBinding == null || fieldName == null || context == null) {
            return false;
        }
        String bodyFqn = bodyBinding.getQualifiedName();
        if (bodyFqn == null) {
            return false;
        }
        AbstractTypeDeclaration bodyType = context.getAbstractTypeDeclaration(bodyFqn);
        if (!(bodyType instanceof TypeDeclaration td)) {
            return false;
        }
        for (FieldDeclaration field : td.getFields()) {
            for (Object fragObj : field.fragments()) {
                if (!(fragObj instanceof VariableDeclarationFragment fragment)) {
                    continue;
                }
                if (!fieldName.equals(fragment.getName().getIdentifier())) {
                    continue;
                }
                ITypeBinding fieldBinding = field.getType().resolveBinding();
                return fieldBinding != null && fieldBinding.isEnum();
            }
        }
        return false;
    }

    private static boolean containsNestedEnumField(
            ITypeBinding bodyBinding, String fieldName, CodebaseContext context) {
        return containsNestedEnumField(bodyBinding, fieldName, context, new HashSet<>());
    }

    private static boolean containsNestedEnumField(
            ITypeBinding bodyBinding,
            String fieldName,
            CodebaseContext context,
            Set<String> visited) {
        if (bodyBinding == null || fieldName == null || context == null) {
            return false;
        }
        String bodyFqn = bodyBinding.getQualifiedName();
        if (bodyFqn == null || !visited.add(bodyFqn)) {
            return false;
        }
        for (IVariableBinding component : bodyBinding.getDeclaredFields()) {
            ITypeBinding nestedType = component.getType();
            if (nestedType == null || nestedType.isPrimitive() || nestedType.isEnum()) {
                continue;
            }
            if (isEnumRecordComponent(nestedType, fieldName) || isEnumDtoField(nestedType, fieldName, context)) {
                return true;
            }
            if (containsNestedEnumField(nestedType, fieldName, context, visited)) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasParameterAstAnnotation(SingleVariableDeclaration param, String simpleName) {
        for (Object modifier : param.modifiers()) {
            if (modifier instanceof Annotation annotation
                    && annotation.getTypeName().getFullyQualifiedName().endsWith(simpleName)) {
                return true;
            }
        }
        return false;
    }

    private record RestParamBinding(
            boolean pathVariable,
            boolean requestParam,
            boolean requestBody,
            boolean payload,
            boolean enumType) {
        boolean pathOrQueryVariable() {
            return pathVariable || requestParam;
        }

        boolean requestBodyEnum() {
            return requestBody && enumType;
        }

        boolean payloadEnum() {
            return payload && enumType;
        }

        static RestParamBinding fromEntryPointParameter(EntryPoint.Parameter parameter) {
            boolean pathVariable = hasAnnotation(parameter.getAnnotations(), "PathVariable");
            boolean requestParam = hasAnnotation(parameter.getAnnotations(), "RequestParam");
            boolean requestBody = hasAnnotation(parameter.getAnnotations(), "RequestBody");
            boolean payload = hasAnnotation(parameter.getAnnotations(), "Payload");
            boolean enumType = isEnumTypeName(parameter.getType());
            return new RestParamBinding(pathVariable, requestParam, requestBody, payload, enumType);
        }

        static RestParamBinding fromAstParameter(SingleVariableDeclaration param) {
            boolean pathVariable = hasParameterAstAnnotation(param, "PathVariable");
            boolean requestParam = hasParameterAstAnnotation(param, "RequestParam");
            boolean requestBody = hasParameterAstAnnotation(param, "RequestBody");
            boolean payload = hasParameterAstAnnotation(param, "Payload");
            boolean enumType = param.getType() != null && param.getType().resolveBinding() != null
                    && param.getType().resolveBinding().isEnum();
            return new RestParamBinding(pathVariable, requestParam, requestBody, payload, enumType);
        }

        private static boolean hasAnnotation(java.util.List<String> annotations, String name) {
            return annotations != null && annotations.stream().anyMatch(a ->
                    name.equals(a) || (a != null && a.endsWith("." + name)));
        }

        private static boolean isEnumTypeName(String typeName) {
            if (typeName == null || typeName.isBlank()) {
                return false;
            }
            int lastDot = typeName.lastIndexOf('.');
            String simple = lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName;
            return Character.isUpperCase(simple.charAt(0)) && !simple.endsWith("String");
        }
    }
}
