package cbr

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

const (
	defaultUserAgent = "Mozilla/5.0 (compatible; SendicoFX/1.0; +https://app.sendico.io)"
	defaultAccept    = "application/xml,text/xml;q=0.9,*/*;q=0.8"
)

var (
	errNilRequestURL       = errors.New("http_client: request URL is nil")
	errRelativeRequestURL  = errors.New("http_client: request URL must be absolute")
	errUnexpectedURLScheme = errors.New("http_client: unexpected URL scheme")
	errUnexpectedURLHost   = errors.New("http_client: unexpected URL host")
)

// httpClient wraps http.Client to ensure CBR requests always carry required headers.
type httpClient struct {
	client        *http.Client
	headers       http.Header
	logger        mlogger.Logger
	allowedScheme string
	allowedHost   string
}

type httpClientOptions struct {
	userAgent     string
	accept        string
	referer       string
	allowedScheme string
	allowedHost   string
}

func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOptions) *httpClient {
	userAgent := opts.userAgent
	if strings.TrimSpace(userAgent) == "" {
		userAgent = defaultUserAgent
	}

	accept := opts.accept
	if strings.TrimSpace(accept) == "" {
		accept = defaultAccept
	}

	referer := opts.referer
	if strings.TrimSpace(referer) == "" {
		referer = defaultCBRBaseURL
	}

	allowedScheme := strings.ToLower(strings.TrimSpace(opts.allowedScheme))
	allowedHost := strings.ToLower(strings.TrimSpace(opts.allowedHost))

	if allowedScheme == "" || allowedHost == "" {
		if parsed, err := url.Parse(referer); err == nil {
			if allowedScheme == "" {
				allowedScheme = strings.ToLower(parsed.Scheme)
			}
			if allowedHost == "" {
				allowedHost = strings.ToLower(parsed.Host)
			}
		}
	}
	httpLogger := logger.Named("http_client")

	headers := make(http.Header, 3)
	headers.Set("User-Agent", userAgent)
	headers.Set("Accept", accept)
	headers.Set("Referer", referer)

	httpLogger.Info("HTTP client initialized", zap.String("user_agent", userAgent),
		zap.String("accept", accept), zap.String("referrer", referer))

	return &httpClient{
		client:        client,
		headers:       headers,
		logger:        httpLogger,
		allowedScheme: allowedScheme,
		allowedHost:   allowedHost,
	}
}

func (h *httpClient) NewRequest(ctx context.Context, method, endpoint string) (*http.Request, error) {
	return http.NewRequestWithContext(ctx, method, endpoint, nil)
}

func (h *httpClient) Do(req *http.Request) (*http.Response, error) {
	enriched := req.Clone(req.Context())
	for key, values := range h.headers {
		if enriched.Header.Get(key) != "" {
			continue
		}

		for _, value := range values {
			enriched.Header.Add(key, value)
		}
	}

	if err := h.validateRequestTarget(enriched.URL); err != nil {
		h.logger.Warn("HTTP request blocked by target validation", zap.Error(err), zap.String("method", req.Method))
		return nil, err
	}

	//nolint:gosec // request URL is constrained in validateRequestTarget before any outbound call.
	r, err := h.client.Do(enriched)
	if err != nil {
		h.logger.Warn("HTTP request failed", zap.Error(err), zap.String("method", req.Method),
			zap.String("url", req.URL.String()))
	}
	return r, err
}

func (h *httpClient) headerValue(name string) string {
	return h.headers.Get(name)
}

func (h *httpClient) validateRequestTarget(requestURL *url.URL) error {
	if requestURL == nil {
		return errNilRequestURL
	}

	if !requestURL.IsAbs() {
		return errRelativeRequestURL
	}

	scheme := strings.ToLower(requestURL.Scheme)
	host := strings.ToLower(requestURL.Host)

	if h.allowedScheme != "" && scheme != h.allowedScheme {
		return fmt.Errorf("%w: %q", errUnexpectedURLScheme, requestURL.Scheme)
	}

	if h.allowedHost != "" && host != h.allowedHost {
		return fmt.Errorf("%w: %q", errUnexpectedURLHost, requestURL.Host)
	}

	return nil
}
