package internal_api

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

type dataProxyInternalReadTargetResolver interface {
	ResolveInternalReadTarget(context.Context, *db.Cluster) (*o11yurls.InternalReadTarget, error)
}

// DataProxyTopRUTarget is the narrow Clinic-to-Data-Proxy contract needed to
// proxy Top RU requests. It intentionally exposes no o11y path: Data Proxy
// appends its allowlisted Top RU endpoint to BaseURL.
type DataProxyTopRUTarget struct {
	BaseURL string                      `json:"baseUrl"`
	Headers DataProxyTopRUTargetHeaders `json:"headers"`
}

type DataProxyTopRUTargetHeaders struct {
	Env          string `json:"env"`
	BizType      string `json:"bizType"`
	Provider     string `json:"provider"`
	Region       string `json:"region"`
	OrgID        string `json:"orgId"`
	ProjectID    string `json:"projectId"`
	ClusterID    string `json:"clusterId"`
	DeployType   string `json:"deployType"`
	DeployTypeV2 string `json:"deployTypeV2"`
}

type dataProxyTopRUTargetError struct {
	Error   bool   `json:"error"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

type DataProxyTopRUTargetService struct {
	env      string
	metadata *DataProxyClusterMetadataService
	resolver dataProxyInternalReadTargetResolver
}

func NewDataProxyTopRUTargetService(
	cfg *config.Config,
	metadata *DataProxyClusterMetadataService,
	clusterDataURLs *ClusterDataURLsService,
) *DataProxyTopRUTargetService {
	env := ""
	if cfg != nil {
		env = cfg.Env
	}
	return newDataProxyTopRUTargetService(env, metadata, clusterDataURLs)
}

func newDataProxyTopRUTargetService(
	env string,
	metadata *DataProxyClusterMetadataService,
	resolver dataProxyInternalReadTargetResolver,
) *DataProxyTopRUTargetService {
	return &DataProxyTopRUTargetService{
		env:      env,
		metadata: metadata,
		resolver: resolver,
	}
}

func (s *DataProxyTopRUTargetService) GetTarget(c *gin.Context) {
	orgID, ok := parseDataProxyOrgID(c)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateClusterMetadataID(clusterID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	user, ok := dataProxyInternalUser(c)
	if !ok {
		return
	}
	if s == nil || s.metadata == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("cluster metadata service is unavailable"))
		return
	}

	cluster, _, err := s.metadata.resolveOwnedCluster(user, orgID, clusterID)
	if err != nil {
		s.metadata.writeDataSourceError(c, clusterID, err)
		return
	}
	if !dataProxyTopRUSupportedCluster(cluster) {
		_ = c.Error(error2.ErrBadRequest.New("Top RU is unsupported for this cluster"))
		return
	}
	if s.resolver == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("Top RU target resolver is unavailable"))
		return
	}

	target, err := s.resolver.ResolveInternalReadTarget(c.Request.Context(), cluster)
	if err != nil {
		s.writeResolverError(c, clusterID, err)
		return
	}
	if target == nil {
		s.writeResolverError(c, clusterID, errors.New("internal read target is empty"))
		return
	}
	baseURL, err := dataProxyTopRUBaseURL(target.InternalReadURL)
	if err != nil {
		s.writeResolverError(c, clusterID, fmt.Errorf("invalid internal read target URL: %w", err))
		return
	}

	c.JSON(http.StatusOK, DataProxyTopRUTarget{
		BaseURL: baseURL,
		Headers: DataProxyTopRUTargetHeaders{
			Env:          s.env,
			BizType:      o11y.InternalReadBizType(s.env, cluster.DeployType),
			Provider:     cluster.Provider,
			Region:       cluster.Region,
			OrgID:        strconv.FormatUint(orgID, 10),
			ProjectID:    cluster.ProjectID,
			ClusterID:    cluster.ClusterID,
			DeployType:   cluster.DeployType,
			DeployTypeV2: cluster.DeployTypeV2,
		},
	})
}

func (s *DataProxyTopRUTargetService) writeResolverError(c *gin.Context, clusterID string, err error) {
	status := http.StatusBadGateway
	code := "BadGateway"
	message := "failed to resolve Top RU target"
	switch {
	case errors.Is(err, context.DeadlineExceeded):
		status = http.StatusGatewayTimeout
		code = "DeadlineExceeded"
		message = "Top RU target resolution timed out"
	case errors.Is(err, errClusterDataURLsResolverUnavailable):
		status = http.StatusServiceUnavailable
		code = "Unavailable"
		message = "Top RU target resolver is unavailable"
	}
	log.Error("resolve Data Proxy Top RU target failed",
		zap.String("cluster_id", clusterID), zap.Int("status", status), zap.Error(err))
	c.JSON(status, dataProxyTopRUTargetError{Error: true, Code: code, Message: message})
}

func dataProxyTopRUSupportedCluster(cluster *db.Cluster) bool {
	if cluster == nil {
		return false
	}
	deployTypeV2 := strings.ToLower(strings.TrimSpace(cluster.DeployTypeV2))
	switch deployTypeV2 {
	case o11y.DeployTypeV2Essential,
		o11y.DeployTypeV2Premium,
		o11y.DeployTypeV2PremiumReserved,
		o11y.DeployTypeV2EssentialV2:
		return true
	}
	if deployTypeV2 != "" {
		return false
	}
	switch strings.ToLower(strings.TrimSpace(cluster.DeployType)) {
	case o11y.PremiumType:
		// Keep legacy Premium records usable only when deploy_type_v2 has not
		// been backfilled. Shared records remain fail-closed because that value
		// cannot distinguish Starter from Essential.
		return true
	default:
		return false
	}
}

func dataProxyTopRUBaseURL(rawURL string) (string, error) {
	parsed, err := url.Parse(rawURL)
	if err != nil {
		return "", err
	}
	scheme := strings.ToLower(parsed.Scheme)
	if parsed.Opaque != "" || (scheme != "http" && scheme != "https") || parsed.Host == "" || parsed.Hostname() == "" {
		return "", errors.New("URL must be an absolute HTTP(S) URL")
	}
	if parsed.User != nil {
		return "", errors.New("URL userinfo is not allowed")
	}
	if parsed.RawQuery != "" || parsed.ForceQuery {
		return "", errors.New("URL query is not allowed")
	}
	if parsed.Fragment != "" || parsed.RawFragment != "" {
		return "", errors.New("URL fragment is not allowed")
	}

	return (&url.URL{Scheme: scheme, Host: parsed.Host}).String(), nil
}
