package clustercontext

import (
	"context"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"
	"strconv"

	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/tidbmgmthttp"
)

type userVisibilityClient interface {
	ListTidbs(context.Context, tidbmgmthttp.ListRequest) (*tidbmgmthttp.ListResponse, error)
}

type userScopedRepository struct {
	metadataClient   clinicMetadataClient
	visibilityClient userVisibilityClient
}

// NewUserScopedRepository combines HTTP API authorization with Clinic's canonical
// metadata source, so the public response does not lose established fields.
func NewUserScopedRepository(metadataClient clinicMetadataClient, visibilityClient userVisibilityClient) UserRepository {
	if metadataClient == nil || visibilityClient == nil {
		return unavailableUserRepository{}
	}
	return &userScopedRepository{metadataClient: metadataClient, visibilityClient: visibilityClient}
}

func (r *userScopedRepository) ListByOrgForUser(ctx context.Context, orgID uint64, opts ListOptions, authorization string, traceHeaders map[string][]string) (*Page, error) {
	skip64 := int64(opts.Page-1) * int64(opts.PageSize)
	if skip64 > int64(^uint32(0)>>1) {
		return nil, fmt.Errorf("page offset exceeds supported range")
	}
	visible, err := r.visibilityClient.ListTidbs(ctx, tidbmgmthttp.ListRequest{
		OrgID:         orgID,
		PageSize:      opts.PageSize,
		Skip:          int(skip64),
		Authorization: authorization,
		TraceHeaders:  http.Header(traceHeaders),
	})
	if err != nil {
		return nil, classifyUserVisibilityError(err)
	}
	if visible.TotalSize < 0 {
		return nil, fmt.Errorf("%w: HTTP API returned a negative total", ErrBadGateway)
	}

	items := make([]Cluster, 0, len(visible.Tidbs))
	for _, tidb := range visible.Tidbs {
		if err := validateResourceID(tidb.TidbID); err != nil {
			return nil, fmt.Errorf("%w: HTTP API returned invalid cluster ID", ErrBadGateway)
		}
		metadata, err := r.metadataClient.GetOrgClusterMetadata(ctx, orgID, tidb.TidbID)
		if err != nil {
			return nil, classifyClinicError(err)
		}
		item := fromClinicMetadata(metadata)
		if item == nil || item.ClusterID != tidb.TidbID || item.OrgID != strconv.FormatUint(orgID, 10) {
			return nil, fmt.Errorf("%w: canonical metadata does not match HTTP API visibility result", ErrBadGateway)
		}
		items = append(items, *item)
	}
	return &Page{Items: items, Page: opts.Page, PageSize: opts.PageSize, Total: visible.TotalSize}, nil
}

type unavailableUserRepository struct{}

func (unavailableUserRepository) ListByOrgForUser(context.Context, uint64, ListOptions, string, map[string][]string) (*Page, error) {
	return nil, ErrUnavailable
}

func classifyUserVisibilityError(err error) error {
	if stderrors.Is(err, context.DeadlineExceeded) {
		return fmt.Errorf("TiDB management HTTP API request: %w: %v", ErrTimeout, err)
	}
	if stderrors.Is(err, context.Canceled) {
		return err
	}
	var upstream *tidbmgmthttp.UpstreamError
	if stderrors.As(err, &upstream) {
		switch upstream.StatusCode {
		case http.StatusUnauthorized:
			return ErrUnauthenticated
		case http.StatusForbidden:
			return ErrPermissionDenied
		case http.StatusTooManyRequests:
			return ErrResourceExhausted
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			return ErrTimeout
		case http.StatusBadRequest:
			return ErrBadGateway
		default:
			return ErrUnavailable
		}
	}
	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			return fmt.Errorf("TiDB management HTTP API request: %w: %v", ErrTimeout, err)
		}
		return fmt.Errorf("TiDB management HTTP API request: %w: %v", ErrUnavailable, err)
	}
	return fmt.Errorf("TiDB management HTTP API response: %w", ErrBadGateway)
}

func validateResourceID(value string) error {
	if value == "" || len(value) > 255 {
		return fmt.Errorf("invalid cluster ID")
	}
	for _, r := range value {
		if r == '/' || r < 0x20 || r == 0x7f {
			return fmt.Errorf("invalid cluster ID")
		}
	}
	return nil
}
