package service

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	clinictracing "github.com/pingcap-inc/clinic/clinic_data_service/pkg/tracing"
)

const (
	topRUSummaryPath     = "/internal/diagnosis/api/v1/topru/summary"
	topRUSummaryPlanPath = "/internal/diagnosis/api/v1/topru/summaryplan"
	topRUUserStatsPath   = "/internal/diagnosis/api/v1/topru/user_stats"
	maxTopRUResponse     = 16 << 20
)

var (
	ErrInvalidTopRUTarget   = errors.New("invalid Top RU target")
	ErrInvalidTopRUResponse = errors.New("invalid Top RU response")
	ErrTopRUUnavailable     = errors.New("Top RU service unavailable")
)

type TopRUOperation string

const (
	TopRUOperationSummary     TopRUOperation = "summary"
	TopRUOperationSummaryPlan TopRUOperation = "summary_plan"
	TopRUOperationUserStats   TopRUOperation = "user_stats"
)

type topRUTargetResolver interface {
	GetTopRUTarget(context.Context, uint64, string) (*clinic_internal_api.TopRUTarget, error)
}

// TopRUUpstreamError contains only the upstream status. The upstream body is
// deliberately discarded so O11y implementation details cannot escape the
// public Data Proxy error contract.
type TopRUUpstreamError struct {
	StatusCode int
}

func (e *TopRUUpstreamError) Error() string {
	return fmt.Sprintf("Top RU upstream returned HTTP status %d", e.StatusCode)
}

type TopRUResult struct {
	Body         []byte
	Provider     string
	Region       string
	DeployTypeV2 string
}

// TopRUService resolves a server-owned O11y target through Clinic and calls
// only the allow-listed Top RU operations. O11y remains the sole owner of
// aggregation semantics.
type TopRUService struct {
	targetResolver topRUTargetResolver
	httpClient     *http.Client
}

func NewTopRUService(targetResolver topRUTargetResolver) *TopRUService {
	return newTopRUService(targetResolver, &http.Client{
		Timeout:   2 * time.Minute,
		Transport: clinictracing.NewHTTPTransport(nil),
	})
}

func newTopRUService(targetResolver topRUTargetResolver, httpClient *http.Client) *TopRUService {
	if httpClient == nil {
		httpClient = &http.Client{
			Timeout:   2 * time.Minute,
			Transport: clinictracing.NewHTTPTransport(nil),
		}
	}
	clientCopy := *httpClient
	clientCopy.CheckRedirect = func(*http.Request, []*http.Request) error {
		return http.ErrUseLastResponse
	}
	return &TopRUService{targetResolver: targetResolver, httpClient: &clientCopy}
}

func (s *TopRUService) Query(
	ctx context.Context,
	orgID uint64,
	clusterID string,
	operation TopRUOperation,
	query url.Values,
) (*TopRUResult, error) {
	if s == nil || s.targetResolver == nil {
		return nil, ErrTopRUUnavailable
	}
	target, err := s.targetResolver.GetTopRUTarget(ctx, orgID, clusterID)
	if err != nil {
		return nil, fmt.Errorf("resolve Top RU target: %w", err)
	}
	requestURL, err := buildTopRUURL(target, operation, query)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("create Top RU request: %w", err)
	}
	req.Header.Set("Accept", "application/json")
	setTopRUHeader(req.Header, "X-Env", target.Headers.Env)
	setTopRUHeader(req.Header, "X-Biz-Type", target.Headers.BizType)
	setTopRUHeader(req.Header, "X-Provider", target.Headers.Provider)
	setTopRUHeader(req.Header, "X-Region", target.Headers.Region)
	setTopRUHeader(req.Header, "X-Org-Id", target.Headers.OrgID)
	setTopRUHeader(req.Header, "X-Project-Id", target.Headers.ProjectID)
	setTopRUHeader(req.Header, "X-Cluster-Id", target.Headers.ClusterID)
	setTopRUHeader(req.Header, "X-Deploy-Type", target.Headers.DeployType)

	resp, err := s.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("call Top RU upstream: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, maxTopRUResponse))
		return nil, &TopRUUpstreamError{StatusCode: resp.StatusCode}
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxTopRUResponse+1))
	if err != nil {
		return nil, fmt.Errorf("read Top RU response: %w", err)
	}
	if len(body) > maxTopRUResponse {
		return nil, fmt.Errorf("%w: response exceeds %d bytes", ErrInvalidTopRUResponse, maxTopRUResponse)
	}
	if !isSingleJSONValue(body) {
		return nil, fmt.Errorf("%w: response is not one JSON value", ErrInvalidTopRUResponse)
	}

	return &TopRUResult{
		Body:         body,
		Provider:     target.Headers.Provider,
		Region:       target.Headers.Region,
		DeployTypeV2: target.Headers.DeployTypeV2,
	}, nil
}

func buildTopRUURL(target *clinic_internal_api.TopRUTarget, operation TopRUOperation, query url.Values) (string, error) {
	if target == nil {
		return "", fmt.Errorf("%w: target is empty", ErrInvalidTopRUTarget)
	}
	baseURL, err := url.Parse(target.BaseURL)
	if err != nil || baseURL.Opaque != "" || (baseURL.Scheme != "http" && baseURL.Scheme != "https") ||
		baseURL.Host == "" || baseURL.Hostname() == "" {
		return "", fmt.Errorf("%w: baseUrl must be an absolute HTTP(S) URL", ErrInvalidTopRUTarget)
	}
	if baseURL.User != nil || baseURL.RawQuery != "" || baseURL.ForceQuery || baseURL.Fragment != "" ||
		(baseURL.Path != "" && baseURL.Path != "/") {
		return "", fmt.Errorf("%w: baseUrl must contain only scheme and host", ErrInvalidTopRUTarget)
	}
	if strings.EqualFold(baseURL.Hostname(), "localhost") || strings.HasSuffix(strings.ToLower(baseURL.Hostname()), ".localhost") {
		return "", fmt.Errorf("%w: localhost is not allowed", ErrInvalidTopRUTarget)
	}

	switch operation {
	case TopRUOperationSummary:
		baseURL.Path = topRUSummaryPath
	case TopRUOperationSummaryPlan:
		baseURL.Path = topRUSummaryPlanPath
	case TopRUOperationUserStats:
		baseURL.Path = topRUUserStatsPath
	default:
		return "", fmt.Errorf("unsupported Top RU operation %q", operation)
	}
	baseURL.RawPath = ""
	baseURL.RawQuery = query.Encode()
	return baseURL.String(), nil
}

func setTopRUHeader(header http.Header, name, value string) {
	if value != "" {
		header.Set(name, value)
	}
}

func isSingleJSONValue(body []byte) bool {
	decoder := json.NewDecoder(bytes.NewReader(body))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return false
	}
	var trailing any
	return decoder.Decode(&trailing) == io.EOF
}
