package click.kamil.springstatemachineexporter.analysis.pipeline;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeMethodReference;

import java.util.Set;

/**
 * Shared string-transform policy for EventIdentity peels and ACE enum-expansion hints.
 * <p>
 * Two roles (Locale arity intentionally split):
 * <ul>
 *   <li><b>Peel</b> — identity unwrap for unique proofs: 0-arg {@code trim}/{@code strip}/
 *       {@code toUpperCase}/{@code toLowerCase}. Locale arity stays unpeeled (fail-closed).</li>
 *   <li><b>Expand hint</b> — license to widen unbound {@code <SYMBOLIC: Enum.*>}: 0-arg
 *       {@code name()}, plus {@code toUpperCase}/{@code toLowerCase} with any arity,
 *       including method references ({@code String::toUpperCase}, {@code Enum::name}).
 *       {@code trim}/{@code strip} alone do not expand.</li>
 * </ul>
 */
public final class IdentityStringTransformSupport {

    private static final Set<String> PEEL_NAMES = Set.of(
            "trim", "strip", "toUpperCase", "toLowerCase");

    private static final Set<String> EXPAND_HINT_NAMES = Set.of(
            "name", "toUpperCase", "toLowerCase");

    private IdentityStringTransformSupport() {
    }

    /** Identity-preserving String instance method with 0 args. */
    public static boolean isPeelInvocation(MethodInvocation invocation) {
        if (invocation == null || invocation.getName() == null) {
            return false;
        }
        if (!PEEL_NAMES.contains(invocation.getName().getIdentifier())) {
            return false;
        }
        return invocation.arguments().isEmpty();
    }

    /**
     * License to widen SYMBOLIC poly: 0-arg {@code name()}, or case transforms any arity.
     * Does not match {@code getName()} or {@code trim}/{@code strip} alone.
     */
    public static boolean isExpandHintInvocation(MethodInvocation invocation) {
        if (invocation == null || invocation.getName() == null) {
            return false;
        }
        String name = invocation.getName().getIdentifier();
        if ("name".equals(name)) {
            return invocation.arguments().isEmpty();
        }
        return "toUpperCase".equals(name) || "toLowerCase".equals(name);
    }

    /**
     * Method references that carry the same expand license as invocations
     * ({@code String::toUpperCase}, {@code OrderEvent::name}). Not peels.
     */
    public static boolean isExpandHintMethodReference(Expression expression) {
        String name = methodReferenceName(expression);
        if (name == null) {
            return false;
        }
        return EXPAND_HINT_NAMES.contains(name);
    }

    /**
     * Repeatedly unwraps 0-arg peel invocations (and parentheses). Does not peel casts,
     * Optional/container wrappers, or method references — callers own those.
     */
    public static Expression peelIdentityStringOps(Expression expression) {
        Expression current = expression;
        boolean progressed;
        do {
            progressed = false;
            while (current instanceof ParenthesizedExpression pe) {
                current = pe.getExpression();
                progressed = true;
            }
            if (current instanceof MethodInvocation mi
                    && mi.getExpression() != null
                    && isPeelInvocation(mi)) {
                current = mi.getExpression();
                progressed = true;
            }
        } while (progressed);
        return current;
    }

    public static boolean expressionHasExpandHint(Expression expr) {
        return nodeHasExpandHint(expr);
    }

    /**
     * Whole-body scan (includes logging / dead code). Prefer
     * {@link #returnExpressionsHaveExpandHint(MethodDeclaration)} for ACE helper follow-up.
     */
    public static boolean methodBodyHasExpandHint(MethodDeclaration methodDeclaration) {
        if (methodDeclaration == null || methodDeclaration.getBody() == null) {
            return false;
        }
        return nodeHasExpandHint(methodDeclaration.getBody());
    }

    /**
     * True when any {@code return} expression carries an expand hint.
     * Side-effect invocations elsewhere in the body do not count.
     */
    public static boolean returnExpressionsHaveExpandHint(MethodDeclaration methodDeclaration) {
        if (methodDeclaration == null || methodDeclaration.getBody() == null) {
            return false;
        }
        final boolean[] hint = {false};
        methodDeclaration.getBody().accept(new ASTVisitor() {
            @Override
            public boolean visit(ReturnStatement node) {
                if (expressionHasExpandHint(node.getExpression())) {
                    hint[0] = true;
                    return false;
                }
                return super.visit(node);
            }
        });
        return hint[0];
    }

    private static boolean nodeHasExpandHint(ASTNode node) {
        if (node == null) {
            return false;
        }
        final boolean[] hint = {false};
        node.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation invocation) {
                if (isExpandHintInvocation(invocation)) {
                    hint[0] = true;
                    return false;
                }
                return super.visit(invocation);
            }

            @Override
            public boolean visit(ExpressionMethodReference node) {
                if (isExpandHintMethodReference(node)) {
                    hint[0] = true;
                    return false;
                }
                return super.visit(node);
            }

            @Override
            public boolean visit(TypeMethodReference node) {
                if (isExpandHintMethodReference(node)) {
                    hint[0] = true;
                    return false;
                }
                return super.visit(node);
            }
        });
        return hint[0];
    }

    private static String methodReferenceName(Expression expression) {
        if (expression instanceof ExpressionMethodReference emr && emr.getName() != null) {
            return emr.getName().getIdentifier();
        }
        if (expression instanceof TypeMethodReference tmr && tmr.getName() != null) {
            return tmr.getName().getIdentifier();
        }
        if (expression instanceof SimpleName) {
            return null;
        }
        return null;
    }
}
