package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;

import java.util.*;

/**
 * Expands REST entry points whose {@code @PathVariable} parameters flow into string-keyed
 * switch mappers (e.g. {@code StringCommandMapper.fromString}) into concrete binding variants.
 */
public final class EntryPointBindingExpander {

    private EntryPointBindingExpander() {
    }

    /**
     * Expands an entry point into concrete binding variants from path/query parameters:
     * string switch/if arms and boolean ternary conditions provable in source.
     */
    public static List<Map<String, String>> expandEntryPointBindings(
            EntryPoint entryPoint,
            CodebaseContext context,
            Map<String, List<CallEdge>> callGraph) {
        List<Map<String, String>> pathVariants = expandPathVariableBindings(entryPoint, context, callGraph);
        List<Map<String, String>> booleanVariants = expandBooleanTernaryBindings(entryPoint, context, callGraph);
        if (pathVariants.isEmpty()) {
            return booleanVariants;
        }
        if (booleanVariants.isEmpty()) {
            return pathVariants;
        }
        List<Map<String, String>> combined = new ArrayList<>();
        for (Map<String, String> pathVariant : pathVariants) {
            for (Map<String, String> booleanVariant : booleanVariants) {
                Map<String, String> merged = new LinkedHashMap<>(pathVariant);
                merged.putAll(booleanVariant);
                combined.add(merged);
            }
        }
        return combined;
    }

    public static List<Map<String, String>> expandPathVariableBindings(
            EntryPoint entryPoint,
            CodebaseContext context,
            Map<String, List<CallEdge>> callGraph) {
        if (entryPoint == null || entryPoint.getParameters() == null || entryPoint.getParameters().isEmpty()) {
            return List.of();
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (!isPathOrQueryVariable(parameter)) {
                continue;
            }
            if (isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
                continue;
            }
            List<String> cases = resolveStringCasesForParameter(
                    entryPoint.getClassName() + "." + entryPoint.getMethodName(),
                    parameter.getName(),
                    context,
                    callGraph);
            if (cases.isEmpty()) {
                continue;
            }
            List<Map<String, String>> variants = new ArrayList<>();
            for (String caseValue : cases) {
                variants.add(Map.of(parameter.getName(), caseValue));
            }
            return variants;
        }
        return List.of();
    }

    public static EntryPoint withResolvedPath(EntryPoint entryPoint, Map<String, String> bindings) {
        if (entryPoint == null || bindings == null || bindings.isEmpty()) {
            return entryPoint;
        }
        String name = entryPoint.getName();
        Map<String, String> metadata = entryPoint.getMetadata() == null
                ? new LinkedHashMap<>()
                : new LinkedHashMap<>(entryPoint.getMetadata());
        StringBuilder querySuffix = new StringBuilder();
        for (Map.Entry<String, String> binding : bindings.entrySet()) {
            String placeholder = "{" + binding.getKey() + "}";
            boolean pathPlaceholder = name != null && name.contains(placeholder);
            String path = metadata.get("path");
            if (!pathPlaceholder && path != null) {
                pathPlaceholder = path.contains(placeholder);
            }
            if (pathPlaceholder) {
                if (name != null) {
                    name = name.replace(placeholder, binding.getValue());
                }
                if (path != null) {
                    metadata.put("path", path.replace(placeholder, binding.getValue()));
                }
            } else if (isBooleanBindingValue(binding.getValue())) {
                if (querySuffix.isEmpty()) {
                    querySuffix.append('?');
                } else {
                    querySuffix.append('&');
                }
                querySuffix.append(binding.getKey()).append('=').append(binding.getValue());
            }
        }
        if (!querySuffix.isEmpty()) {
            if (name != null) {
                name = name + querySuffix;
            }
            String path = metadata.get("path");
            if (path != null) {
                metadata.put("path", path + querySuffix);
            }
        }
        return EntryPoint.builder()
                .type(entryPoint.getType())
                .name(name)
                .className(entryPoint.getClassName())
                .methodName(entryPoint.getMethodName())
                .sourceFile(entryPoint.getSourceFile())
                .metadata(metadata)
                .parameters(entryPoint.getParameters())
                .build();
    }

    private static boolean isPlaceholderResolvedInPath(EntryPoint entryPoint, String paramName) {
        String placeholder = "{" + paramName + "}";
        String name = entryPoint.getName();
        if (name != null && !name.contains(placeholder)) {
            return true;
        }
        Map<String, String> metadata = entryPoint.getMetadata();
        if (metadata != null) {
            String path = metadata.get("path");
            if (path != null && !path.contains(placeholder)) {
                return true;
            }
        }
        return false;
    }

    public static List<Map<String, String>> expandBooleanTernaryBindings(
            EntryPoint entryPoint,
            CodebaseContext context,
            Map<String, List<CallEdge>> callGraph) {
        if (entryPoint == null || entryPoint.getParameters() == null || entryPoint.getParameters().isEmpty()) {
            return List.of();
        }
        for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
            if (!isBooleanType(parameter.getType())) {
                continue;
            }
            if (hasBooleanTernaryForParameter(
                    entryPoint.getClassName() + "." + entryPoint.getMethodName(),
                    parameter.getName(),
                    context,
                    callGraph)) {
                return List.of(
                        Map.of(parameter.getName(), "true"),
                        Map.of(parameter.getName(), "false"));
            }
        }
        return List.of();
    }

    private static boolean isBooleanType(String type) {
        return "boolean".equals(type) || "Boolean".equals(type);
    }

    private static boolean isBooleanBindingValue(String value) {
        return "true".equals(value) || "false".equals(value);
    }

    private static boolean hasBooleanTernaryForParameter(
            String startMethod,
            String paramName,
            CodebaseContext context,
            Map<String, List<CallEdge>> callGraph) {
        ArrayDeque<String> queue = new ArrayDeque<>();
        Set<String> visited = new HashSet<>();
        queue.add(startMethod);
        while (!queue.isEmpty()) {
            String methodFqn = queue.poll();
            if (!visited.add(methodFqn)) {
                continue;
            }
            if (visited.size() > 12) {
                break;
            }
            if (containsBooleanTernaryCondition(methodFqn, paramName, context)) {
                return true;
            }
            List<CallEdge> edges = callGraph.get(methodFqn);
            if (edges == null) {
                continue;
            }
            for (CallEdge edge : edges) {
                if (edge.getTargetMethod() != null) {
                    queue.add(edge.getTargetMethod());
                }
            }
        }
        return false;
    }

    static boolean containsBooleanTernaryCondition(String methodFqn, String paramName, CodebaseContext context) {
        MethodDeclaration method = findMethodDeclaration(methodFqn, context);
        if (method == null || method.getBody() == null) {
            return false;
        }
        if (!hasParameter(method, paramName)) {
            return false;
        }
        final boolean[] found = {false};
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ConditionalExpression node) {
                if (isBooleanConditionForParameter(node.getExpression(), paramName)) {
                    found[0] = true;
                    return false;
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(MethodInvocation node) {
                for (Object argObj : node.arguments()) {
                    if (argObj instanceof ConditionalExpression cond
                            && isBooleanConditionForParameter(cond.getExpression(), paramName)) {
                        found[0] = true;
                        return false;
                    }
                }
                return super.visit(node);
            }
        });
        return found[0];
    }

    private static boolean isBooleanConditionForParameter(Expression condition, String paramName) {
        if (condition instanceof SimpleName simpleName) {
            return paramName.equals(simpleName.getIdentifier());
        }
        if (condition instanceof PrefixExpression prefixExpression
                && prefixExpression.getOperator() == PrefixExpression.Operator.NOT
                && prefixExpression.getOperand() instanceof SimpleName simpleName) {
            return paramName.equals(simpleName.getIdentifier());
        }
        return false;
    }

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

    private static List<String> resolveStringCasesForParameter(
            String startMethod,
            String paramName,
            CodebaseContext context,
            Map<String, List<CallEdge>> callGraph) {
        ArrayDeque<String> queue = new ArrayDeque<>();
        Set<String> visited = new HashSet<>();
        queue.add(startMethod);
        while (!queue.isEmpty()) {
            String methodFqn = queue.poll();
            if (!visited.add(methodFqn)) {
                continue;
            }
            if (visited.size() > 12) {
                break;
            }
            List<String> localCases = extractStringSwitchCases(methodFqn, paramName, context);
            if (!localCases.isEmpty()) {
                return localCases;
            }
            List<CallEdge> edges = callGraph.get(methodFqn);
            if (edges == null) {
                continue;
            }
            for (CallEdge edge : edges) {
                if (edge.getTargetMethod() != null) {
                    queue.add(edge.getTargetMethod());
                }
            }
        }
        return List.of();
    }

    static List<String> extractStringSwitchCases(String methodFqn, String paramName, CodebaseContext context) {
        MethodDeclaration method = findMethodDeclaration(methodFqn, context);
        if (method == null || method.getBody() == null) {
            return List.of();
        }
        if (!hasParameter(method, paramName)) {
            return List.of();
        }
        LinkedHashSet<String> cases = new LinkedHashSet<>();
        method.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(SwitchStatement node) {
                collectCasesFromSwitch(node.getExpression(), node.statements(), paramName, cases);
                return super.visit(node);
            }

            @Override
            public boolean visit(SwitchExpression node) {
                collectCasesFromSwitch(node.getExpression(), node.statements(), paramName, cases);
                return super.visit(node);
            }

            @Override
            public boolean visit(IfStatement node) {
                collectCasesFromIfChain(node, paramName, cases);
                return super.visit(node);
            }
        });
        return List.copyOf(cases);
    }

    private static void collectCasesFromSwitch(
            Expression selector,
            List<?> statements,
            String paramName,
            Set<String> cases) {
        if (!(selector instanceof SimpleName simpleName) || !paramName.equals(simpleName.getIdentifier())) {
            return;
        }
        for (Object statementObj : statements) {
            if (statementObj instanceof SwitchCase switchCase && !switchCase.isDefault()) {
                for (Object exprObj : switchCase.expressions()) {
                    if (exprObj instanceof StringLiteral stringLiteral) {
                        cases.add(stringLiteral.getLiteralValue());
                    }
                }
            }
        }
    }

    private static void collectCasesFromIfChain(IfStatement ifStatement, String paramName, Set<String> cases) {
        IfStatement current = ifStatement;
        while (current != null) {
            String literal = extractEqualsLiteralForParameter(current.getExpression(), paramName);
            if (literal != null) {
                cases.add(literal);
            }
            Statement elseStatement = current.getElseStatement();
            if (elseStatement instanceof IfStatement nestedIf) {
                current = nestedIf;
            } else {
                if (elseStatement != null) {
                    String elseLiteral = extractEqualsLiteralForParameterFromStatement(elseStatement, paramName);
                    if (elseLiteral != null) {
                        cases.add(elseLiteral);
                    }
                }
                break;
            }
        }
    }

    private static String extractEqualsLiteralForParameterFromStatement(Statement statement, String paramName) {
        if (statement instanceof IfStatement ifStatement) {
            return extractEqualsLiteralForParameter(ifStatement.getExpression(), paramName);
        }
        return null;
    }

    private static String extractEqualsLiteralForParameter(Expression expression, String paramName) {
        if (!(expression instanceof MethodInvocation invocation)) {
            return null;
        }
        String methodName = invocation.getName().getIdentifier();
        if (!"equals".equals(methodName) && !"equalsIgnoreCase".equals(methodName)) {
            return null;
        }
        if (invocation.arguments().size() != 1) {
            return null;
        }
        Expression argument = (Expression) invocation.arguments().get(0);
        Expression receiver = invocation.getExpression();

        if (receiver instanceof StringLiteral stringLiteral
                && argument instanceof SimpleName simpleName
                && paramName.equals(simpleName.getIdentifier())) {
            return stringLiteral.getLiteralValue();
        }
        if (receiver instanceof SimpleName simpleName
                && paramName.equals(simpleName.getIdentifier())
                && argument instanceof StringLiteral paramLiteral) {
            return paramLiteral.getLiteralValue();
        }
        return null;
    }

    private static boolean hasParameter(MethodDeclaration method, String paramName) {
        for (Object paramObj : method.parameters()) {
            if (paramObj instanceof SingleVariableDeclaration param
                    && paramName.equals(param.getName().getIdentifier())) {
                return true;
            }
        }
        return false;
    }

    private static MethodDeclaration findMethodDeclaration(String methodFqn, CodebaseContext context) {
        if (methodFqn == null || !methodFqn.contains(".")) {
            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;
        }
        return context.findMethodDeclaration(typeDeclaration, methodName, true);
    }
}
