// Package tidbmgmthttp provides the user-authorized TiDB management HTTP client.
package tidbmgmthttp

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

	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	clinictracing "github.com/pingcap-inc/clinic/clinic_data_service/pkg/tracing"
	"go.uber.org/zap"
)

const listTidbsPath = "/api/v2023/clusterManagement/v1beta2/tidbs"
const maxResponseBytes = 32 << 20

// UpstreamError intentionally contains no response body because upstream error
// details may expose authorization or implementation information.
type UpstreamError struct {
	StatusCode int
}

func (e *UpstreamError) Error() string {
	return fmt.Sprintf("tidb management HTTP API returned status %d", e.StatusCode)
}

type ListRequest struct {
	OrgID         uint64
	PageSize      int
	Skip          int
	Authorization string
	TraceHeaders  http.Header
}

type Tidb struct {
	TidbID string `json:"tidbId"`
}

type ListResponse struct {
	Tidbs     []Tidb `json:"tidbs"`
	TotalSize int64  `json:"totalSize"`
}

type Client struct {
	baseURL    *url.URL
	httpClient *http.Client
}

func NewClient(base string, timeout time.Duration) (*Client, error) {
	parsed, err := url.Parse(base)
	if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" ||
		parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
		return nil, fmt.Errorf("invalid TiDB management HTTP API base URL")
	}
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	return &Client{baseURL: parsed, httpClient: &http.Client{
		Timeout:   timeout,
		Transport: clinictracing.NewHTTPTransport(nil),
	}}, nil
}

func (c *Client) SetHTTPClient(client *http.Client) {
	if client != nil {
		c.httpClient = client
	}
}

func (c *Client) ListTidbs(ctx context.Context, input ListRequest) (*ListResponse, error) {
	startedAt := time.Now()
	if c == nil || c.baseURL == nil || c.httpClient == nil {
		return nil, fmt.Errorf("TiDB management HTTP API client is unavailable")
	}
	if input.OrgID == 0 || input.PageSize < 1 || input.Skip < 0 {
		return nil, fmt.Errorf("invalid TiDB management HTTP API list request")
	}
	if strings.TrimSpace(input.Authorization) == "" {
		return nil, &UpstreamError{StatusCode: http.StatusUnauthorized}
	}

	requestURL := c.baseURL.ResolveReference(&url.URL{Path: listTidbsPath})
	query := requestURL.Query()
	query.Set("orgId", strconv.FormatUint(input.OrgID, 10))
	query.Set("pageSize", strconv.Itoa(input.PageSize))
	query.Set("skip", strconv.Itoa(input.Skip))
	requestURL.RawQuery = query.Encode()
	requestLogFields := listRequestLogFields(requestURL, input)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("create TiDB management HTTP API request: %w", err)
	}
	req.Header.Set("Authorization", input.Authorization)
	copyTraceHeaders(req.Header, input.TraceHeaders)

	httpClient := *c.httpClient
	httpClient.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
	resp, err := httpClient.Do(req)
	if err != nil {
		log.Warn(ctx, "tidb_mgmt_http_request", append(requestLogFields,
			zap.String("outcome", "transport_error"),
			zap.Duration("duration", time.Since(startedAt)),
		)...)
		return nil, fmt.Errorf("call TiDB management HTTP API: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, maxResponseBytes))
		log.Info(ctx, "tidb_mgmt_http_request", append(requestLogFields,
			zap.String("outcome", "upstream_error"),
			zap.Int("upstream_status", resp.StatusCode),
			zap.Duration("duration", time.Since(startedAt)),
		)...)
		return nil, &UpstreamError{StatusCode: resp.StatusCode}
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
	if err != nil {
		log.Warn(ctx, "tidb_mgmt_http_request", append(requestLogFields,
			zap.String("outcome", "read_error"),
			zap.Int("upstream_status", resp.StatusCode),
			zap.Duration("duration", time.Since(startedAt)),
		)...)
		return nil, fmt.Errorf("read TiDB management HTTP API response: %w", err)
	}
	if len(body) > maxResponseBytes {
		log.Warn(ctx, "tidb_mgmt_http_request", append(requestLogFields,
			zap.String("outcome", "response_too_large"),
			zap.Int("upstream_status", resp.StatusCode),
			zap.Duration("duration", time.Since(startedAt)),
		)...)
		return nil, fmt.Errorf("TiDB management HTTP API response exceeds size limit")
	}
	var result ListResponse
	if err := json.Unmarshal(body, &result); err != nil {
		log.Warn(ctx, "tidb_mgmt_http_request", append(requestLogFields,
			zap.String("outcome", "decode_error"),
			zap.Int("upstream_status", resp.StatusCode),
			zap.Duration("duration", time.Since(startedAt)),
		)...)
		return nil, fmt.Errorf("decode TiDB management HTTP API response: %w", err)
	}
	if result.Tidbs == nil {
		result.Tidbs = make([]Tidb, 0)
	}
	log.Info(ctx, "tidb_mgmt_http_request", append(requestLogFields,
		zap.String("outcome", "success"),
		zap.Int("upstream_status", resp.StatusCode),
		zap.Duration("duration", time.Since(startedAt)),
		zap.Int("result_count", len(result.Tidbs)),
		zap.Int64("total_size", result.TotalSize),
	)...)
	return &result, nil
}

func listRequestLogFields(requestURL *url.URL, input ListRequest) []zap.Field {
	return []zap.Field{
		zap.String("upstream_method", http.MethodGet),
		zap.String("upstream_url", requestURL.String()),
		zap.Uint64("org_id", input.OrgID),
		zap.Int("page_size", input.PageSize),
		zap.Int("skip", input.Skip),
	}
}

func copyTraceHeaders(destination, source http.Header) {
	for _, key := range []string{"Traceparent", "X-Trace-Id", "X-B3-Traceid", "X-B3-Spanid", "X-B3-Sampled"} {
		if value := source.Get(key); value != "" {
			destination.Set(key, value)
		}
	}
}
