// Package http_client provides ports.HTTPClient implementations.
//
// WebhookClient is the proxy-aware variant used by Flow webhook actions.
// Unlike OutboundClient (direct-only, IP-pinned), WebhookClient honors
// HTTP_PROXY/HTTPS_PROXY/NO_PROXY environment variables for deployments
// that require egress through a corporate proxy.
//
// Both clients share the same url_safety SSoT for the URL allowlist and
// *url.Error redaction; they differ ONLY in the transport policy (proxy +
// dial-time IP pinning). Both enforce the same redirect contract:
// cross-host redirects and HTTPS→HTTP scheme downgrades are refused
// unconditionally (see checkRedirect for the full rationale).
//
// Why split: proxy mode and dial-time IP pinning are mutually exclusive
// on the same *http.Client — when a proxy is configured, net/http issues
// CONNECT to the proxy and the proxy resolves the final hop, so the
// originating client cannot validate the final IP at dial time. Either
// the dial-time defense or proxy support has to give for a given consumer.
//
// connector_callout.Engine talks to vendor APIs reached over public
// internet, where IP-pinned dial is the right defense; Flow webhook
// targets are admin-configured and may need to traverse a corp proxy,
// where preflight URL allowlist + unconditional cross-host/scheme-downgrade
// redirect refusal + 30s client timeout are the right defenses.
package http_client

import (
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
)

const webhookClientTimeout = 30 * time.Second

// NewWebhookClient returns a ports.HTTPClient suited for Flow webhook
// actions. allowlist provides the URL preflight + redact contract;
// must be non-nil.
func NewWebhookClient(allowlist ports.URLAllowlist) ports.HTTPClient {
	if allowlist == nil {
		panic("http_client.NewWebhookClient: allowlist is required")
	}
	w := &webhookClient{allowlist: allowlist}
	inner := &http.Client{
		Timeout: webhookClientTimeout,
		Transport: &http.Transport{
			Proxy:                 http.ProxyFromEnvironment, // honor HTTP_PROXY/HTTPS_PROXY/NO_PROXY
			TLSHandshakeTimeout:   10 * time.Second,
			ExpectContinueTimeout: 1 * time.Second,
			IdleConnTimeout:       90 * time.Second,
			ForceAttemptHTTP2:     true,
		},
		CheckRedirect: w.checkRedirect,
	}
	w.client = inner
	return w
}

// Compile-time assertion: webhookClient implements ports.HTTPClient.
var _ ports.HTTPClient = (*webhookClient)(nil)

type webhookClient struct {
	client    *http.Client
	allowlist ports.URLAllowlist
}

// Do runs a url_safety preflight on req.URL, then dispatches via the
// cached inner http.Client. The inner client applies unconditional cross-host
// and scheme-downgrade redirect refusal on every hop.
func (w *webhookClient) Do(req *http.Request) (*http.Response, error) {
	if err := w.allowlist.EnsureAllowed(req.URL.String()); err != nil {
		return nil, fmt.Errorf("webhook preflight: %w", err)
	}
	return w.client.Do(req)
}

// checkRedirect mirrors OutboundClient.checkRedirect exactly (same contract,
// different receiver). Refuses cross-host redirects and HTTPS→HTTP scheme
// downgrades unconditionally; applies the allowlist to every redirect target.
//
// Rationale: any part of the initial URL (path / query / userinfo) and any
// header on the initial request can carry credentials in conventions we cannot
// reliably enumerate (Slack-style path tokens /services/T/B/SECRET; ?api_key=;
// user:pass@host; vendor-specific header names). Go's stdlib auto-injects
// Referer on redirect, so a cross-host hop can leak any URL part to the new
// origin. The only provably-complete defense is to refuse the class.
//
// Webhook/callout targets are stable admin/pack-configured URLs; cross-host
// redirects are not a feature the use case needs. Operators requiring a
// cross-host hop should configure the final URL directly.
func (w *webhookClient) checkRedirect(req *http.Request, via []*http.Request) error {
	if len(via) >= 10 {
		return fmt.Errorf("stopped after 10 redirects")
	}
	if len(via) == 0 {
		return nil
	}
	initial := via[0].URL
	if !strings.EqualFold(req.URL.Host, initial.Host) {
		return fmt.Errorf("cross-host redirect refused: %q → %q", initial.Host, req.URL.Host)
	}
	if strings.EqualFold(initial.Scheme, "https") && !strings.EqualFold(req.URL.Scheme, "https") {
		return fmt.Errorf("https→%s scheme downgrade refused on redirect from %q", req.URL.Scheme, initial.Host)
	}
	// Apply allowlist to the redirect target URL.
	return w.allowlist.EnsureAllowed(req.URL.String())
}
