PreferredUrlResolver.java

package com.cloudforge.core.local;

import java.net.URI;
import java.util.List;
import java.util.Map;

/**
 * Resolves the best "open this application" URL from a deployed stack's CFN outputs.
 *
 * <p>Shared by {@code cloudforge-manager}'s inventory (live AWS/LocalStack/MiniStack stacks)
 * and {@code cfc-testing}'s post-deploy hint (freshly deployed stack's own output map) so both
 * apply the exact same precedence — edge hostnames for local emulators first, then AWS-style
 * {@code ApplicationUrl}/{@code LoadBalancerDNS}/{@code Url} outputs — instead of two
 * implementations drifting apart.</p>
 *
 * @since 3.2.0
 */
public final class PreferredUrlResolver {

    /** CFN output key CloudForge stacks tag with the deploying application's id. */
    public static final String OUTPUT_APPLICATION_ID = "CloudForgeApplicationId";

    private PreferredUrlResolver() {
    }

    /**
     * @param outputs CFN stack outputs (key → value), or {@code null}/empty
     * @return the best URL to open for this deployment, or {@code null} if none of the known
     *         output keys are present
     */
    public static String preferredUrl(Map<String, String> outputs) {
        if (outputs == null || outputs.isEmpty()) {
            return null;
        }
        String applicationId = outputs.get(OUTPUT_APPLICATION_ID);
        // Local emulator Open links: prefer edge hostnames over ELB / localhost:port.
        if (hasLocalEmulatorBrowserUrl(outputs)) {
            String edge = LocalEmulatorDefaults.edgeApplicationUrl(applicationId);
            if (edge != null) {
                return edge;
            }
        }
        String localApp = outputs.get("LocalStackApplicationUrl");
        if (localApp != null && !localApp.isBlank()) {
            if (requiresLocalStackPathPrefix(applicationId)) {
                return mergeLocalStackHostUrl(localApp, outputs.get("LocalStackLocalUrl"));
            }
            return localApp.trim();
        }
        for (String key : List.of(
            "LocalStackAuthenticatedUrl",
            "MiniStackApplicationUrl",
            "MiniStackAuthenticatedUrl",
            "MiniStackLocalUrl",
            "ApplicationUrl",
            "LoadBalancerDNS",
            "Url")) {
            String value = outputs.get(key);
            if (value != null && !value.isBlank()) {
                return value.trim();
            }
        }
        return null;
    }

    public static boolean hasLocalEmulatorBrowserUrl(Map<String, String> outputs) {
        if (outputs == null) {
            return false;
        }
        for (String key : List.of(
            "LocalStackApplicationUrl",
            "LocalStackLocalUrl",
            "LocalStackElbHostnameUrl",
            "LocalStackAuthenticatedUrl",
            "MiniStackApplicationUrl",
            "MiniStackLocalUrl",
            "MiniStackAuthenticatedUrl")) {
            String value = outputs.get(key);
            if (value != null && !value.isBlank()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Jenkins (and Manager) on LocalStack use {@code --prefix=/_aws/elb/{name}}; the
     * host-mapped port alone returns Jetty 404 unless the prefix is included.
     */
    public static boolean requiresLocalStackPathPrefix(String applicationId) {
        if (applicationId == null || applicationId.isBlank()) {
            return false;
        }
        return switch (applicationId.trim().toLowerCase()) {
            case "jenkins", "cloudforge-manager" -> true;
            default -> false;
        };
    }

    public static String mergeLocalStackHostUrl(String appUrl, String localUrl) {
        if (appUrl == null || appUrl.isBlank()) {
            return null;
        }
        if (localUrl == null || localUrl.isBlank()) {
            return appUrl.trim();
        }
        try {
            URI app = URI.create(appUrl.trim());
            URI local = URI.create(localUrl.trim());
            String path = local.getPath();
            if (path == null || path.isBlank() || "/".equals(path)) {
                return app.toString();
            }
            if (!path.endsWith("/")) {
                path = path + "/";
            }
            return new URI(app.getScheme(), null, app.getHost(), app.getPort(), path, null, null).toString();
        } catch (Exception ignored) {
            return appUrl.trim();
        }
    }
}