package clustercontext

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

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

type clinicMetadataClient interface {
	GetClusterMetadata(context.Context, string) (*clinic_internal_api.ClusterMetadata, error)
	GetOrgClusterMetadata(context.Context, uint64, string) (*clinic_internal_api.ClusterMetadata, error)
	ListOrgClusterMetadata(context.Context, uint64, url.Values) (*clinic_internal_api.ClusterMetadataPage, error)
}

type clinicRepository struct {
	client clinicMetadataClient
}

// NewClinicRepository uses Clinic's internal canonical metadata API as the
// single source of business and persistence logic.
func NewClinicRepository(client clinicMetadataClient) Repository {
	if client == nil {
		return NewUnavailableRepository()
	}
	return &clinicRepository{client: client}
}

func (r *clinicRepository) GetByClusterID(ctx context.Context, clusterID string) (*Cluster, error) {
	result, err := r.client.GetClusterMetadata(ctx, clusterID)
	if err != nil {
		return nil, classifyClinicError(err)
	}
	return fromClinicMetadata(result), nil
}

func (r *clinicRepository) GetByOrgAndClusterID(ctx context.Context, orgID uint64, clusterID string) (*Cluster, error) {
	result, err := r.client.GetOrgClusterMetadata(ctx, orgID, clusterID)
	if err != nil {
		return nil, classifyClinicError(err)
	}
	return fromClinicMetadata(result), nil
}

func (r *clinicRepository) ListByOrg(ctx context.Context, orgID uint64, opts ListOptions) (*Page, error) {
	query := make(url.Values, 4)
	query.Set("page", strconv.Itoa(opts.Page))
	query.Set("pageSize", strconv.Itoa(opts.PageSize))
	query.Set("showDeleted", strconv.FormatBool(opts.ShowDeleted))
	if opts.Query != "" {
		query.Set("query", opts.Query)
	}
	result, err := r.client.ListOrgClusterMetadata(ctx, orgID, query)
	if err != nil {
		return nil, classifyClinicError(err)
	}
	items := make([]Cluster, 0, len(result.Items))
	for i := range result.Items {
		item := fromClinicMetadata(&result.Items[i])
		if item != nil {
			items = append(items, *item)
		}
	}
	return &Page{Items: items, Page: result.Page, PageSize: result.PageSize, Total: result.Total}, nil
}

func fromClinicMetadata(source *clinic_internal_api.ClusterMetadata) *Cluster {
	if source == nil {
		return nil
	}
	return &Cluster{
		ClusterID:    source.ClusterID,
		OrgID:        source.OrgID,
		Name:         source.Name,
		Status:       source.Status,
		Deleted:      source.Deleted,
		Version:      source.Version,
		Provider:     source.Provider,
		Region:       source.Region,
		DeployType:   source.DeployType,
		DeployTypeV2: source.DeployTypeV2,
		CreatedAt:    source.CreatedAt,
		DiagnosticContext: DiagnosticContext{
			ProjectID:  source.DiagnosticContext.ProjectID,
			ParentID:   source.DiagnosticContext.ParentID,
			ShootName:  source.DiagnosticContext.ShootName,
			KeyspaceID: source.DiagnosticContext.KeyspaceID,
		},
	}
}

func classifyClinicError(err error) error {
	switch {
	case stderrors.Is(err, context.DeadlineExceeded):
		return fmt.Errorf("Clinic metadata request: %w: %v", ErrTimeout, err)
	case stderrors.Is(err, context.Canceled):
		return err
	}
	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			return fmt.Errorf("Clinic metadata request: %w: %v", ErrTimeout, err)
		}
		return fmt.Errorf("Clinic metadata request: %w: %v", ErrUnavailable, err)
	}
	var upstreamError *clinic_internal_api.UpstreamError
	if stderrors.As(err, &upstreamError) {
		switch upstreamError.StatusCode {
		case http.StatusNotFound:
			return ErrNotFound
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			return fmt.Errorf("Clinic metadata request: %w", ErrTimeout)
		case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable:
			return fmt.Errorf("Clinic metadata request: %w", ErrUnavailable)
		default:
			return err
		}
	}
	return err
}
