package chat.vojo.app;

import android.content.Context;
import android.util.Log;

import java.io.IOException;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.URL;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Routes the five native HttpURLConnection paths (VojoPollWorker, AvatarLoader,
 * CallDeclineReceiver, ReplyReceiver, MarkAsReadReceiver) through the local
 * relay when a proxy is enabled, closing the §15 #3 native-path leak. These run
 * outside the WebView so ProxyController never touches them.
 *
 * When the proxy is OFF (the default for ~all users) this is byte-for-byte the
 * old behaviour — {@code url.openConnection()} — so the common path carries
 * zero risk from the proxy feature.
 *
 * When the proxy is ON but the relay can't be started (e.g. AndroidKeyStore not
 * yet usable before the first unlock after a reboot), the request is REFUSED
 * (fail-closed), NOT sent direct: a direct send would leak the user's real IP +
 * the homeserver SNI to the censor on the exact network the proxy exists to
 * evade. The callers already degrade gracefully — the poll worker skips the
 * cycle, ReplyReceiver re-posts a send-error, the avatar falls back to initials.
 *
 * Auth to the relay: a process-wide {@link Authenticator} that returns the
 * relay's loopback token ONLY for a PROXY request to 127.0.0.1:&lt;relayPort&gt;,
 * and null for anything else. NOTE: this is a deliberate, scoped use of
 * Authenticator.setDefault — proxy_android_impl.md §3 flags the un-scoped
 * pattern as unsafe because it can leak the *upstream* creds across unrelated
 * connections. Here the secret handed out is the loopback relay token (not the
 * upstream credential — those live only inside the relay), it is scoped to the
 * exact loopback host+port+PROXY requestor, and Vojo has no other native
 * HttpURLConnection users. HttpURLConnection.setAuthenticator (the per-call
 * setter) does not exist on Android, hence the default-Authenticator route.
 *
 * Validated on-device: NO (no device available this session) — see the summary.
 */
final class ProxyRouting {

    private static final String TAG = "ProxyRouting";
    private static final AtomicBoolean AUTH_INSTALLED = new AtomicBoolean(false);

    private static volatile int relayPort = -1;
    private static volatile String relayUser = "vojo";
    private static volatile String relayToken;

    private ProxyRouting() {}

    /**
     * Open a connection for `url`, routed through the relay when the proxy is
     * enabled. If the proxy is enabled but the relay can't be started, throws
     * {@link RelayUnavailableException} (fail-closed) rather than leaking the
     * request direct. When the proxy is off, a plain direct connection.
     */
    static HttpURLConnection openConnection(Context ctx, URL url) throws IOException {
        ProxyManager pm = ProxyManager.get(ctx);
        if (!pm.isProxyActiveForNative()) {
            return (HttpURLConnection) url.openConnection();
        }
        int port = pm.ensureRelayForNative();
        String token = pm.getRelayToken();
        if (port < 0 || token == null) {
            // FAIL-CLOSED: refuse the send instead of going direct (would leak
            // IP/SNI on the censored network). Record it so the UI can warn.
            pm.onNativeBlocked();
            Log.w(TAG, "relay unavailable; native request blocked (fail-closed)");
            throw new RelayUnavailableException();
        }
        relayPort = port;
        relayToken = token;
        relayUser = LocalProxyRelay.getAuthUser();
        installAuthenticator();
        // Relay is up and serving — clear any stale "background blocked" flag.
        pm.onNativeServed();
        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", port));
        return (HttpURLConnection) url.openConnection(proxy);
    }

    /** Forget the relay endpoint + token when the relay is torn down, so the
     *  scoped Authenticator stops handing anything out (defense-in-depth). */
    static void clearRelay() {
        relayPort = -1;
        relayToken = null;
    }

    /** Thrown when the proxy is enabled but the loopback relay can't be started,
     *  so a native request is refused rather than sent in the clear (F17). */
    static final class RelayUnavailableException extends IOException {
        RelayUnavailableException() {
            super("proxy relay unavailable");
        }
    }

    private static void installAuthenticator() {
        if (!AUTH_INSTALLED.compareAndSet(false, true)) return;
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                // The real security boundary is requestor-type + the ephemeral
                // relay port + a live token: the token only works against our
                // own 127.0.0.1 listener. Accept any loopback *name* Android may
                // report for the relay endpoint (a literal IP InetSocketAddress
                // can surface as "localhost" once resolved), so an authed native
                // request never silently fails the host check and falls back to
                // a direct, un-proxied connection (F9).
                if (getRequestorType() == RequestorType.PROXY
                    && isLoopbackHost(getRequestingHost())
                    && getRequestingPort() == relayPort
                    && relayToken != null) {
                    return new PasswordAuthentication(relayUser, relayToken.toCharArray());
                }
                // Not our loopback relay — never hand out credentials.
                return null;
            }
        });
    }

    private static boolean isLoopbackHost(String h) {
        return "127.0.0.1".equals(h)
            || "::1".equals(h)
            || "localhost".equalsIgnoreCase(h);
    }
}
