package runner

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

// RESTTemplate is the per-connection client recipe of providers whose API
// origin is not fixed at build time (self-hosted deployments such as ONES).
// Every invocation resolves one RESTClient from the connection credential
// declared by BaseURLField; the resolved client keeps every fixed-origin
// guarantee (https-only, guarded transport, no redirects, size bounds,
// origin-confined paths).
type RESTTemplate struct {
	// Auth is the provider auth contract; it must not be the none kind
	// because the base URL itself is a connection credential.
	Auth AuthSpec

	// BaseURLField is the credential field carrying the https host origin
	// (for example "https://ones.example.com"). It must be declared in Auth.
	BaseURLField string

	// Timeout bounds one request. Zero selects DefaultRequestTimeout.
	Timeout time.Duration

	// StatusClassifier overrides the default status classification.
	StatusClassifier func(status int, body []byte) ErrorCode

	// Transport overrides the guarded default transport (tests inject a
	// loopback-friendly transport here; production leaves it nil).
	Transport http.RoundTripper
}

// BuildConnectionClient resolves one client from the connection credentials.
// The base URL must be a bare https host origin — no userinfo, path, query,
// or fragment — so a connection can never steer requests onto an arbitrary
// prefix or smuggle URL credentials; anything else fails closed as a
// connection failure.
func BuildConnectionClient(template RESTTemplate, credentials Credentials) (*RESTClient, error) {
	baseURL := strings.TrimSpace(credentials[template.BaseURLField])
	if baseURL == "" {
		return nil, Errorf(
			ErrorConnectionUnavailable, "provider connection base URL is missing", nil)
	}
	origin, err := normalizeConnectionOrigin(baseURL)
	if err != nil {
		return nil, Errorf(
			ErrorConnectionUnavailable,
			"provider connection base URL is not an https host origin", err)
	}
	client, err := NewRESTClient(RESTOptions{
		BaseURL:          origin,
		Auth:             template.Auth,
		Timeout:          template.Timeout,
		StatusClassifier: template.StatusClassifier,
		Transport:        template.Transport,
	})
	if err != nil {
		return nil, Errorf(
			ErrorConnectionUnavailable, "provider connection base URL is unusable", err)
	}
	return client, nil
}

// normalizeConnectionOrigin reduces a connection base URL to the bare https
// host origin. A trailing "/" is tolerated and stripped; userinfo, a
// non-root path, a query, or a fragment is rejected.
func normalizeConnectionOrigin(raw string) (string, error) {
	parsed, err := url.Parse(raw)
	if err != nil {
		return "", err
	}
	if parsed.Scheme != "https" || parsed.Host == "" {
		return "", fmt.Errorf("origin must be an absolute https host")
	}
	if parsed.User != nil {
		return "", fmt.Errorf("origin must not carry userinfo")
	}
	if parsed.RawQuery != "" || parsed.Fragment != "" || parsed.RawFragment != "" {
		return "", fmt.Errorf("origin must not carry a query or fragment")
	}
	if parsed.Path != "" && parsed.Path != "/" {
		return "", fmt.Errorf("origin must not carry a path prefix")
	}
	return "https://" + parsed.Host, nil
}

// validateTemplate fails closed on a recipe that could never resolve a
// client: authenticated providers only, with the base URL field declared in
// the credential contract.
func validateTemplate(template RESTTemplate) error {
	if template.Auth.Kind == AuthNone {
		return fmt.Errorf("provider template requires an authenticated auth kind")
	}
	if template.BaseURLField == "" {
		return fmt.Errorf("provider template declares no base URL field")
	}
	for _, field := range template.Auth.Fields {
		if field.Name == template.BaseURLField {
			return nil
		}
	}
	return fmt.Errorf("provider template base URL field is not declared in the auth spec")
}
