package internal_api

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"regexp"
	"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"
)

// DataProxySlowQueryTarget is the narrow Clinic-to-Data-Proxy contract for
// BYOC Slow Query reads. Data Proxy owns the allowlisted diagnosis paths and
// cannot use this response to select object storage or a warehouse engine.
type DataProxySlowQueryTarget struct {
	BaseURL       string                          `json:"baseUrl"`
	O11yClusterID string                          `json:"o11yClusterId"`
	Headers       DataProxySlowQueryTargetHeaders `json:"headers"`
	Provider      string                          `json:"provider"`
	Region        string                          `json:"region"`
	DeployType    string                          `json:"deployType"`
	DeployTypeV2  string                          `json:"deployTypeV2"`
}

// DataProxySlowQueryTargetHeaders contains only server-resolved BYOC routing
// values. Public callers must never supply or override these headers.
type DataProxySlowQueryTargetHeaders struct {
	Env           string `json:"env"`
	BizType       string `json:"bizType"`
	OrgID         string `json:"orgId"`
	RegionID      string `json:"regionId"`
	ApplicationID string `json:"applicationId"`
}

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

type DataProxySlowQueryTargetService struct {
	env                string
	byocDSEndpointHost string
	metadata           *DataProxyClusterMetadataService
	resolver           dataProxyInternalReadTargetResolver
}

func NewDataProxySlowQueryTargetService(
	cfg *config.Config,
	metadata *DataProxyClusterMetadataService,
	clusterDataURLs *ClusterDataURLsService,
) *DataProxySlowQueryTargetService {
	env := ""
	byocDSEndpoint := ""
	if cfg != nil {
		env = cfg.Env
		if cfg.CentralO11yConfig != nil {
			byocDSEndpoint = cfg.CentralO11yConfig.ByocDSEndpoint
		}
	}
	return newDataProxySlowQueryTargetService(env, byocDSEndpoint, metadata, clusterDataURLs)
}

func newDataProxySlowQueryTargetService(
	env, byocDSEndpoint string,
	metadata *DataProxyClusterMetadataService,
	resolver dataProxyInternalReadTargetResolver,
) *DataProxySlowQueryTargetService {
	return &DataProxySlowQueryTargetService{
		env:                strings.TrimSpace(env),
		byocDSEndpointHost: strings.TrimSpace(byocDSEndpoint),
		metadata:           metadata,
		resolver:           resolver,
	}
}

func (s *DataProxySlowQueryTargetService) 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 !dataProxySlowQuerySupportedBYOCCluster(cluster) {
		_ = c.Error(error2.ErrBadRequest.New("Slow Query is unsupported for this cluster"))
		return
	}
	if s.resolver == nil {
		_ = c.Error(error2.ErrServiceUnavailable.New("Slow Query target resolver is unavailable"))
		return
	}

	resolved, err := s.resolver.ResolveInternalReadTarget(c.Request.Context(), cluster)
	if err != nil {
		s.writeResolverError(c, clusterID, err)
		return
	}
	target, err := s.buildTarget(orgID, cluster, resolved)
	if err != nil {
		s.writeResolverError(c, clusterID, err)
		return
	}
	c.JSON(http.StatusOK, target)
}

func (s *DataProxySlowQueryTargetService) buildTarget(
	orgID uint64,
	cluster *db.Cluster,
	resolved *o11yurls.InternalReadTarget,
) (*DataProxySlowQueryTarget, error) {
	if s == nil || cluster == nil || resolved == nil || resolved.O11yCluster == nil {
		return nil, errors.New("resolved Slow Query target is incomplete")
	}
	baseURL, err := dataProxySlowQueryBaseURL(resolved.InternalReadURL, s.byocDSEndpointHost)
	if err != nil {
		return nil, fmt.Errorf("invalid internal read target URL: %w", err)
	}
	o11yCluster := resolved.O11yCluster
	if !dataProxyO11yClusterID.MatchString(o11yCluster.ID) {
		return nil, errors.New("resolved O11y cluster ID is invalid")
	}
	for _, value := range []struct {
		name  string
		value string
	}{
		{name: "environment", value: s.env},
		{name: "O11y tenant ID", value: o11yCluster.TenantID},
		{name: "O11y vendor", value: o11yCluster.Vendor},
		{name: "O11y region", value: o11yCluster.Region},
		{name: "O11y application ID", value: o11yCluster.ApplicationID},
	} {
		if err := validateDataProxySlowQueryRoutingValue(value.name, value.value); err != nil {
			return nil, err
		}
	}
	expectedOrgID := dataProxySlowQueryOrgID(orgID)
	if o11yCluster.TenantID != expectedOrgID {
		return nil, errors.New("resolved O11y tenant does not match the requested organization")
	}
	if o11yCluster.ApplicationID != cluster.ClusterID {
		return nil, errors.New("resolved O11y application does not match the requested cluster")
	}
	if !strings.EqualFold(o11yCluster.Vendor, cluster.Provider) || o11yCluster.Region != cluster.Region {
		return nil, errors.New("resolved O11y placement does not match the requested cluster")
	}
	if o11yCluster.BizType != "" && !strings.EqualFold(o11yCluster.BizType, o11y.ByocType) {
		return nil, errors.New("resolved O11y cluster is not a BYOC target")
	}
	deployType, deployTypeV2, ok := dataProxySlowQueryNormalizedBYOCDeployTypes(cluster)
	if !ok {
		return nil, errors.New("cluster is not a consistent BYOC target")
	}

	return &DataProxySlowQueryTarget{
		BaseURL:       baseURL,
		O11yClusterID: o11yCluster.ID,
		Headers: DataProxySlowQueryTargetHeaders{
			Env:           s.env,
			BizType:       o11y.ByocType,
			OrgID:         o11yCluster.TenantID,
			RegionID:      o11yCluster.Vendor + "-" + o11yCluster.Region,
			ApplicationID: o11yCluster.ApplicationID,
		},
		Provider:     o11yCluster.Vendor,
		Region:       o11yCluster.Region,
		DeployType:   deployType,
		DeployTypeV2: deployTypeV2,
	}, nil
}

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

func dataProxySlowQuerySupportedBYOCCluster(cluster *db.Cluster) bool {
	_, _, ok := dataProxySlowQueryNormalizedBYOCDeployTypes(cluster)
	return ok
}

func dataProxySlowQueryNormalizedBYOCDeployTypes(cluster *db.Cluster) (string, string, bool) {
	if cluster == nil {
		return "", "", false
	}
	legacy := strings.ToLower(strings.TrimSpace(cluster.DeployType))
	v2 := strings.ToLower(strings.TrimSpace(cluster.DeployTypeV2))
	switch {
	case legacy == o11y.ByocType && (v2 == "" || v2 == o11y.DeployTypeV2Byoc):
		return o11y.ByocType, o11y.DeployTypeV2Byoc, true
	case legacy == o11y.ByocHostType && (v2 == "" || v2 == o11y.DeployTypeV2ByocResourcePool):
		return o11y.ByocHostType, o11y.DeployTypeV2ByocResourcePool, true
	default:
		return "", "", false
	}
}

var dataProxyO11yClusterID = regexp.MustCompile(`^[A-Za-z0-9_-]{1,255}$`)

func dataProxySlowQueryBaseURL(rawURL, configuredHost string) (string, error) {
	if configuredHost == "" {
		return "", errors.New("configured BYOC data service endpoint is required")
	}
	if strings.TrimSpace(configuredHost) != configuredHost {
		return "", errors.New("configured BYOC data service endpoint is invalid")
	}
	expected, parseErr := url.Parse("http://" + configuredHost)
	if parseErr != nil || expected.Host == "" || expected.Hostname() == "" || expected.User != nil ||
		expected.Path != "" || expected.RawQuery != "" || expected.Fragment != "" {
		return "", errors.New("configured BYOC data service endpoint is invalid")
	}
	expectedHostname := strings.ToLower(expected.Hostname())
	normalizedExpectedHostname := strings.TrimSuffix(expectedHostname, ".")
	if normalizedExpectedHostname == "localhost" || strings.HasSuffix(normalizedExpectedHostname, ".localhost") {
		return "", errors.New("configured BYOC data service endpoint is invalid")
	}
	if ip := net.ParseIP(expectedHostname); ip != nil && ip.IsLoopback() {
		return "", errors.New("configured BYOC data service endpoint is invalid")
	}

	if rawURL == "" || strings.TrimSpace(rawURL) != rawURL {
		return "", errors.New("URL must be an absolute HTTP(S) URL")
	}
	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")
	}
	hostname := strings.ToLower(parsed.Hostname())
	if strings.TrimSuffix(hostname, ".") == "localhost" || strings.HasSuffix(strings.TrimSuffix(hostname, "."), ".localhost") {
		return "", errors.New("localhost is not allowed")
	}
	if ip := net.ParseIP(hostname); ip != nil && ip.IsLoopback() {
		return "", errors.New("loopback addresses are not allowed")
	}
	if !strings.EqualFold(parsed.Host, expected.Host) {
		return "", errors.New("resolved URL does not match the configured BYOC data service endpoint")
	}
	return (&url.URL{Scheme: scheme, Host: parsed.Host}).String(), nil
}

func validateDataProxySlowQueryRoutingValue(name, value string) error {
	if strings.TrimSpace(value) == "" || strings.TrimSpace(value) != value || strings.ContainsAny(value, "\r\n") {
		return fmt.Errorf("%s is invalid", name)
	}
	return nil
}

func dataProxySlowQueryOrgID(orgID uint64) string {
	return strconv.FormatUint(orgID, 10)
}
