package me.proxylink.common;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

public record AgentConfig(
        Role role,
        String nodeName,
        TunnelConfig tunnel,
        TlsConfig tls,
        List<FrontendEndpointConfig> frontends,
        List<RouteConfig> routes
) {
    private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+");

    public AgentConfig {
        Objects.requireNonNull(role, "role");
        nodeName = requireNonBlank(nodeName, "nodeName");
        Objects.requireNonNull(tunnel, "tunnel");
        Objects.requireNonNull(tls, "tls");
        frontends = List.copyOf(Objects.requireNonNull(frontends, "frontends"));
        routes = List.copyOf(Objects.requireNonNull(routes, "routes"));
    }

    public void validateOrThrow() {
        List<String> errors = new ArrayList<>();

        if (routes.isEmpty()) {
            errors.add("At least one route is required");
        }

        if (tunnel.authToken().length() < 32) {
            errors.add("tunnel.authToken must be at least 32 characters");
        }
        if (isPlaceholderSecret(tunnel.authToken())) {
            errors.add("tunnel.authToken is still the default placeholder and must be changed");
        }

        if (!tls.enabled() && !tunnel.allowInsecure()) {
            errors.add("TLS is disabled, so tunnel.tls.allowInsecure must be true for explicit local testing");
        }

        if (role == Role.FRONTEND) {
            validateHostPort(errors, "tunnel.listen", tunnel.listenHost(), tunnel.listenPort());
            if (tls.enabled() && !tls.autoGenerate()) {
                requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
                requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
            }
        }

        if (role == Role.BACKEND) {
            if (frontends.isEmpty()) {
                errors.add("At least one frontend endpoint is required for backend mode");
            }
            Set<String> frontendNames = new HashSet<>();
            for (FrontendEndpointConfig frontend : frontends) {
                if (!ROUTE_NAME.matcher(frontend.name()).matches()) {
                    errors.add("Frontend endpoint name contains invalid characters: " + frontend.name());
                }
                if (!frontendNames.add(frontend.name())) {
                    errors.add("Duplicate frontend endpoint name: " + frontend.name());
                }
                validateHostPort(errors, "frontend." + frontend.name() + ".connect", frontend.connectHost(), frontend.connectPort());
            }
            if (tls.enabled()) {
                if (tls.trustStorePath() == null && !tls.trustOnFirstUse()) {
                    for (FrontendEndpointConfig frontend : frontends) {
                        if (frontend.pinnedCertificateSha256().isBlank() && tls.pinnedCertificateSha256().isBlank()) {
                            errors.add("TLS is enabled on backend, so set frontend." + frontend.name()
                                    + ".tls.pinnedCertificateSha256, set tunnel.tls.trustOnFirstUse=true, or configure a truststore");
                        }
                    }
                }
                if (tls.trustStorePath() != null) {
                    requireSecret(errors, "tunnel.tls.trustStorePassword", tls.trustStorePassword());
                }
                if (tls.requireClientAuth()) {
                    requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
                    requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
                }
            }
        }

        if (tunnel.connectTimeoutMillis() < 1000) {
            errors.add("tunnel.connectTimeoutMillis must be at least 1000");
        }
        if (tunnel.heartbeatIntervalMillis() < 1000) {
            errors.add("tunnel.heartbeatIntervalMillis must be at least 1000");
        }
        if (tunnel.heartbeatTimeoutMillis() <= tunnel.heartbeatIntervalMillis()) {
            errors.add("tunnel.heartbeatTimeoutMillis must be greater than tunnel.heartbeatIntervalMillis");
        }
        if (tunnel.heartbeatMissesBeforeDisconnect() < 2) {
            errors.add("tunnel.heartbeatMissesBeforeDisconnect must be at least 2");
        }
        if (tunnel.reconnectInitialMillis() < 250) {
            errors.add("tunnel.reconnectInitialMillis must be at least 250");
        }
        if (tunnel.reconnectMaxMillis() < tunnel.reconnectInitialMillis()) {
            errors.add("tunnel.reconnectMaxMillis must be greater than or equal to tunnel.reconnectInitialMillis");
        }

        Set<String> routeNames = new HashSet<>();
        Set<String> frontendBinds = new HashSet<>();
        for (RouteConfig route : routes) {
            if (!ROUTE_NAME.matcher(route.name()).matches()) {
                errors.add("Route name contains invalid characters: " + route.name());
            }
            if (!routeNames.add(route.name())) {
                errors.add("Duplicate route name: " + route.name());
            }
            if (role == Role.FRONTEND) {
                validateHostPort(errors, "route." + route.name() + ".frontendBind", route.frontendBindHost(), route.frontendBindPort());
                String bindKey = route.frontendBindHost() + ":" + route.frontendBindPort();
                if (!frontendBinds.add(bindKey)) {
                    errors.add("Duplicate frontend bind address: " + bindKey);
                }
                if (!route.backendNode().isBlank() && !ROUTE_NAME.matcher(route.backendNode()).matches()) {
                    errors.add("Route " + route.name() + " has an invalid backendNode: " + route.backendNode());
                }
            }
            if (role == Role.BACKEND) {
                validateHostPort(errors, "route." + route.name() + ".backendTarget", route.backendTargetHost(), route.backendTargetPort());
            }
        }

        if (!errors.isEmpty()) {
            throw new IllegalArgumentException("Invalid configuration:\n - " + String.join("\n - ", errors));
        }
    }

    public RouteConfig requireRoute(String routeName) throws ProtocolException {
        for (RouteConfig route : routes) {
            if (route.name().equals(routeName)) {
                return route;
            }
        }
        throw new ProtocolException("Unknown route: " + routeName);
    }

    private static void validateHostPort(List<String> errors, String keyPrefix, String host, int port) {
        if (host == null || host.isBlank()) {
            errors.add(keyPrefix + "Host must not be blank");
        }
        if (port < 1 || port > 65535) {
            errors.add(keyPrefix + "Port must be between 1 and 65535");
        }
    }

    private static void requirePath(List<String> errors, String key, Path path) {
        if (path == null) {
            errors.add(key + " is required when TLS is enabled");
        }
    }

    private static void requireSecret(List<String> errors, String key, String value) {
        if (value == null || value.isBlank()) {
            errors.add(key + " is required when TLS is enabled");
        }
    }

    private static String requireNonBlank(String value, String name) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(name + " must not be blank");
        }
        return value;
    }

    private static boolean isPlaceholderSecret(String value) {
        String normalized = value.toLowerCase(Locale.ROOT);
        return normalized.contains("change_me")
                || normalized.contains("replace-with")
                || normalized.contains("placeholder");
    }

    public enum Role {
        FRONTEND,
        BACKEND;

        public static Role parse(String value) {
            return Role.valueOf(value.trim().toUpperCase(Locale.ROOT));
        }
    }

    public record TunnelConfig(
            String listenHost,
            int listenPort,
            String connectHost,
            int connectPort,
            String authToken,
            boolean allowInsecure,
            int connectTimeoutMillis,
            long heartbeatIntervalMillis,
            long heartbeatTimeoutMillis,
            int heartbeatMissesBeforeDisconnect,
            long reconnectInitialMillis,
            long reconnectMaxMillis
    ) {
        public TunnelConfig {
            authToken = requireNonBlank(authToken, "authToken");
        }
    }

    public record TlsConfig(
            boolean enabled,
            Path keyStorePath,
            String keyStorePassword,
            Path trustStorePath,
            String trustStorePassword,
            boolean requireClientAuth,
            boolean autoGenerate,
            boolean trustOnFirstUse,
            String pinnedCertificateSha256
    ) {
        public TlsConfig {
            keyStorePassword = keyStorePassword == null ? "" : keyStorePassword;
            trustStorePassword = trustStorePassword == null ? "" : trustStorePassword;
            pinnedCertificateSha256 = pinnedCertificateSha256 == null ? "" : pinnedCertificateSha256.trim();
        }
    }

    public record FrontendEndpointConfig(
            String name,
            String connectHost,
            int connectPort,
            String pinnedCertificateSha256
    ) {
        public FrontendEndpointConfig {
            name = requireNonBlank(name, "name");
            connectHost = requireNonBlank(connectHost, "connectHost");
            pinnedCertificateSha256 = pinnedCertificateSha256 == null ? "" : pinnedCertificateSha256.trim();
        }
    }

    public record RouteConfig(
            String name,
            String frontendBindHost,
            int frontendBindPort,
            String backendNode,
            String backendTargetHost,
            int backendTargetPort
    ) {
        public RouteConfig {
            name = requireNonBlank(name, "name");
            backendNode = backendNode == null ? "" : backendNode.trim();
        }
    }
}
