package egress

import (
	"bytes"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/extension/binding"
)

const (
	totalTimeout          = 30 * time.Second
	responseHeaderTimeout = 10 * time.Second

	CredentialOAuth2Bearer = "oauth2_bearer"
	CredentialAPIKey       = "api_key"
)

var ErrRequestFailed = errors.New("extension upstream request failed")

// Resolver performs the connect-time DNS lookup used by the guarded dialer.
type Resolver interface {
	LookupIP(ctx context.Context, network, host string) ([]net.IP, error)
}

// Config is the guarded transport configuration for one fixed Action origin.
// Resolver, DialContext, and TLSClientConfig are dependency seams used by tests;
// production leaves them nil. No global private-network exception exists.
// TODO(#t2465): if internal providers are supported, resolve private-network
// policy from the selected Extension Definition rather than process config.
type Config struct {
	Origin         *url.URL
	TargetNetworks []*net.IPNet
	// AllowPrivate permits global-unicast private addresses for one fixed,
	// administrator-configured provider origin. Mandatory special-address and
	// metadata denies still win, and DNS is resolved again on every dial.
	AllowPrivate    bool
	Resolver        Resolver
	DialContext     func(context.Context, string, string) (net.Conn, error)
	TLSClientConfig *tls.Config
}

// Credential is one decrypted provider credential selected by the service
// layer. It is never copied into BoundRequest or any returned Result.
type Credential struct {
	Type  string
	In    string
	Name  string
	Value string
}

// Client executes only requests under one immutable HTTPS origin.
type Client struct {
	originKey string
	basePath  string
	http      *http.Client
}

func NewClient(config Config) (*Client, error) {
	originKey, err := normalizedHTTPSOrigin(config.Origin)
	if err != nil {
		return nil, err
	}
	if config.Origin.RawQuery != "" || config.Origin.Fragment != "" {
		return nil, errors.New("extension egress origin must not include query or fragment")
	}
	dialer := newGuardedDialer(config)
	tlsConfig := config.TLSClientConfig
	if tlsConfig != nil {
		tlsConfig = tlsConfig.Clone()
	} else {
		tlsConfig = &tls.Config{}
	}
	if tlsConfig.MinVersion == 0 {
		tlsConfig.MinVersion = tls.VersionTLS12
	}
	transport := &http.Transport{
		Proxy:                  nil,
		DialContext:            dialer.DialContext,
		ForceAttemptHTTP2:      true,
		TLSClientConfig:        tlsConfig,
		TLSHandshakeTimeout:    responseHeaderTimeout,
		ResponseHeaderTimeout:  responseHeaderTimeout,
		ExpectContinueTimeout:  time.Second,
		DisableCompression:     true,
		MaxResponseHeaderBytes: 64 << 10,
	}
	basePath := strings.TrimSuffix(config.Origin.EscapedPath(), "/")
	if basePath == "" {
		basePath = "/"
	}
	return &Client{
		originKey: originKey,
		basePath:  basePath,
		http: &http.Client{
			Transport:     transport,
			Timeout:       totalTimeout,
			CheckRedirect: checkRedirectForOrigin(originKey, basePath),
		},
	}, nil
}

// Execute authenticates and executes one already-bound request. It never
// changes method, origin, path, non-secret headers, body, or response policy;
// only the selected provider credential may be injected.
func (client *Client) Execute(
	ctx context.Context,
	bound binding.BoundRequest,
	credential Credential,
) (Result, error) {
	if client == nil || client.http == nil || bound.URL == nil {
		return Result{}, errors.New("extension egress client is unavailable")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	origin, err := normalizedHTTPSOrigin(bound.URL)
	if err != nil || origin != client.originKey {
		return Result{}, ErrDestinationDenied
	}
	if !pathWithinBase(bound.URL.EscapedPath(), client.basePath) {
		return Result{}, ErrDestinationDenied
	}
	method := strings.ToUpper(bound.Method)
	switch method {
	case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
	default:
		return Result{}, errors.New("extension egress method is not allowed")
	}
	target := *bound.URL
	headers := bound.Headers.Clone()
	for name := range headers {
		if isReservedEgressHeader(name) {
			return Result{}, errors.New("extension bound request contains a reserved header")
		}
	}
	if err := injectCredential(&target, headers, credential); err != nil {
		return Result{}, err
	}
	totalCtx, cancel := context.WithTimeout(ctx, totalTimeout)
	defer cancel()
	request, err := http.NewRequestWithContext(
		totalCtx,
		method,
		target.String(),
		bytes.NewReader(bound.Body),
	)
	if err != nil {
		return Result{}, fmt.Errorf("create extension upstream request: %w", err)
	}
	request.Header = headers
	response, err := client.http.Do(request)
	if err != nil {
		// Preserve only our closed policy sentinels. They carry no URL or
		// credential data and tests/callers use errors.Is to distinguish a
		// denied destination/redirect from an ordinary upstream failure.
		switch {
		case errors.Is(err, ErrDestinationDenied):
			return Result{RequestBytes: int64(len(bound.Body))}, ErrDestinationDenied
		case errors.Is(err, ErrCrossOriginRedirect):
			return Result{RequestBytes: int64(len(bound.Body))}, ErrCrossOriginRedirect
		}
		// net/http errors may render the full request URL. A query API key is
		// injected only after binding, so returning/wrapping that error could leak
		// the credential through logs or an API adapter. Preserve only a stable
		// secret-free sentinel.
		return Result{RequestBytes: int64(len(bound.Body))}, ErrRequestFailed
	}
	defer response.Body.Close()
	return normalizeResponse(
		response,
		int64(len(bound.Body)),
		bound.ResponsePolicy.SuccessStatuses,
		bound.ResponsePolicy.ContentTypes,
	)
}

func injectCredential(target *url.URL, headers http.Header, credential Credential) error {
	if credential.Type == "" {
		if credential.Value != "" || credential.In != "" || credential.Name != "" {
			return errors.New("extension credential type is required")
		}
		return nil
	}
	if credential.Value == "" || strings.ContainsAny(credential.Value, "\r\n") {
		return errors.New("extension provider credential is invalid")
	}
	switch credential.Type {
	case CredentialOAuth2Bearer:
		if credential.In != "" || credential.Name != "" {
			return errors.New("extension OAuth credential has invalid placement")
		}
		headers.Set("Authorization", "Bearer "+credential.Value)
		return nil
	case CredentialAPIKey:
		if credential.Name == "" {
			return errors.New("extension API key name is required")
		}
		switch credential.In {
		case "header":
			if !validHeaderName(credential.Name) ||
				isReservedEgressHeader(credential.Name) ||
				headerPresent(headers, credential.Name) {
				return errors.New("extension API key header collides with bound request")
			}
			headers.Set(credential.Name, credential.Value)
			return nil
		case "query":
			values, err := url.ParseQuery(target.RawQuery)
			if err != nil {
				return errors.New("extension bound query is invalid")
			}
			if values.Has(credential.Name) {
				return errors.New("extension API key query parameter collides with bound request")
			}
			secret := url.QueryEscape(credential.Name) + "=" + url.QueryEscape(credential.Value)
			if target.RawQuery == "" {
				target.RawQuery = secret
			} else {
				target.RawQuery += "&" + secret
			}
			return nil
		default:
			return errors.New("extension API key placement is not supported")
		}
	default:
		return errors.New("extension credential type is not supported")
	}
}

func headerPresent(headers http.Header, name string) bool {
	for candidate := range headers {
		if strings.EqualFold(candidate, name) {
			return true
		}
	}
	return false
}

func isReservedEgressHeader(name string) bool {
	switch strings.ToLower(name) {
	case "authorization", "proxy-authorization", "cookie", "host",
		"content-length", "transfer-encoding", "connection",
		"proxy-connection", "keep-alive", "te", "trailer", "upgrade":
		return true
	default:
		return false
	}
}

func pathWithinBase(path, base string) bool {
	if base == "/" {
		return strings.HasPrefix(path, "/")
	}
	return path == base || strings.HasPrefix(path, base+"/")
}

func validHeaderName(name string) bool {
	if name == "" {
		return false
	}
	for i := 0; i < len(name); i++ {
		ch := name[i]
		if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
			(ch >= '0' && ch <= '9') ||
			strings.ContainsRune("!#$%&'*+-.^_`|~", rune(ch)) {
			continue
		}
		return false
	}
	return true
}
