package service

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/internal/utils"
)

const (
	CoreGroup   = "core.pingcap.com"
	CoreVersion = "v1alpha1"

	// tidbInternalAPIPathDeleteCloudAdmin is the TiDB HTTP API path for deleting the cloud_admin user.
	tidbInternalAPIPathDeleteCloudAdmin = "/internal/v1/users/cloud_admin"

	K8sComponentLabelKey = "app.kubernetes.io/component"
	K8sInstanceLabelKey  = "app.kubernetes.io/instance"

	PingcapComponentLabelKey = "pingcap.com/component"
	PingcapGroupLabelKey     = "pingcap.com/group"

	clusterName     = "db"
	namespacePrefix = "tidb"

	tikvHeadlessService = "db-cluster"

	operationIDAnnotationKey = "cluster-agent.tidbcloud.com/operation-id"

	maxTiDBHTTPErrorBodyBytes = 4096

	systemKeyspace = "SYSTEM"
)

// tidbInternalAPIPathResetPassword returns the TiDB HTTP API path for resetting a user's password.
func tidbInternalAPIPathResetPassword(username string) string {
	return fmt.Sprintf("/internal/v1/users/%s/reset-password", username)
}

// TiDBHTTPError represents an unexpected HTTP status from the TiDB API.
type TiDBHTTPError struct {
	StatusCode int
	Status     string
	Body       string
	Err        error
}

func (e *TiDBHTTPError) Error() string {
	return fmt.Sprintf("TiDB HTTP API returned unexpected status %s: %s", e.Status, e.Body)
}

func (e *TiDBHTTPError) Unwrap() error {
	return e.Err
}

const (
	TiDBComponentType              = "tidb"
	PrivilegedTiDBComponentType    = "privileged-tidb"
	TiKVComponentType              = "tikv"
	PDComponentType                = "pd"
	TSOComponentType               = "tso"
	SchedulingComponentType        = "scheduling"
	TiDBWorkerComponentType        = "tidb-worker"
	TiKVWorkerComponentType        = "tikv-worker"
	CoprocessorWorkerComponentType = "coprocessor-worker"
	TiFlashWriteComponentType      = "tiflash-write"
	TiFlashComputeComponentType    = "tiflash-compute"
	TiProxyComponentType           = "tiproxy"
)

var (
	// Notice: when you add a new component here, you need add coresponding "delete" verb for the cluster-agent Role.
	DeleteGroupInstancesApi_AllowComponents = []string{"ticdc"}

	ComponentGroupKindMap = map[string]string{
		"pd":      "PDGroup",
		"tikv":    "TiKVGroup",
		"tidb":    "TiDBGroup",
		"tiflash": "TiFlashGroup",
		"tiproxy": "TiProxyGroup",
		"ticdc":   "TiCDCGroup",
	}
	ComponentGroupInstanceKindMap = lo.MapValues(ComponentGroupKindMap, func(v string, k string) string {
		return strings.TrimSuffix(v, "Group")
	})

	ComponentGroupInstanceListKindMap = lo.MapValues(ComponentGroupInstanceKindMap, func(v string, k string) string {
		return v + "List"
	})

	// dns1123LabelPattern matches a single DNS label under RFC 1123 naming rules.
	dns1123LabelPattern = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
)

func buildNamespace(clusterId string) string {
	return fmt.Sprintf("%s%s", namespacePrefix, clusterId)
}

func buildK8sMetadata(obj client.Object) *apiv1.K8SMetadata {
	out := &apiv1.K8SMetadata{
		Name:        obj.GetName(),
		Namespace:   obj.GetNamespace(),
		Labels:      obj.GetLabels(),
		Annotations: obj.GetAnnotations(),
	}
	return out
}

// getOrdinalIdForPod return ordinal id of this pod.
// pod.GenerateName like 'db-1802962504435896320-c7082f84-tidb-'
// pod.Name like 'db-1802962504435896320-c7082f84-tidb-0'
//
//nolint:unused // Reserved for future use
func getOrdinalIdForPod(pod *corev1.Pod) (int32, error) {
	ordinalStr := strings.TrimPrefix(pod.Name, pod.GenerateName)
	ordinal, err := strconv.ParseInt(ordinalStr, 10, 32)
	if err != nil {
		return 0, fmt.Errorf("failed to parse pod ordinal(%s): %w", ordinalStr, err)
	}
	return int32(ordinal), nil
}

func convertComponentTypeFromAPI(compType string) string {
	// use lowercase and replace "_" with "-" to be compatible with both formats
	compType = strings.ToLower(strings.ReplaceAll(compType, "_", "-"))

	// delete pd-/ pd_ prefix for pd related components
	compType = strings.TrimPrefix(compType, "pd-")
	compType = strings.TrimPrefix(compType, "pd_")

	return compType
}

func calcNextOrdinal(replicas int32, deletedSlots []int32) (currentOrdinals []int32, nextOrdinal int32) {
	for ; int32(len(currentOrdinals)) < replicas; nextOrdinal++ {
		if !lo.Contains(deletedSlots, nextOrdinal) {
			currentOrdinals = append(currentOrdinals, nextOrdinal)
		}
		continue
	}
	for lo.Contains(deletedSlots, nextOrdinal) {
		nextOrdinal++
	}
	return currentOrdinals, nextOrdinal
}

func GetTLSConfig(ctx context.Context, cli client.Client, namespace, secretName string) (*tls.Config, error) {
	var secret corev1.Secret
	if err := cli.Get(ctx, client.ObjectKey{
		Namespace: namespace, Name: secretName}, &secret); err != nil {
		return nil, fmt.Errorf("unable to load certificates from secret %s/%s: %w", namespace, secretName, err)
	}

	return LoadTlsConfigFromSecret(&secret)
}

func LoadTlsConfigFromSecret(secret *corev1.Secret) (*tls.Config, error) {
	rootCAs := x509.NewCertPool()
	var tlsCert tls.Certificate

	rootCAs.AppendCertsFromPEM(secret.Data[corev1.ServiceAccountRootCAKey])

	clientCert, certExists := secret.Data[corev1.TLSCertKey]
	clientKey, keyExists := secret.Data[corev1.TLSPrivateKeyKey]
	if !certExists || !keyExists {
		return nil, fmt.Errorf("cert or key does not exist in secret %s/%s", secret.Namespace, secret.Name)
	}
	tlsCert, err := tls.X509KeyPair(clientCert, clientKey)
	if err != nil {
		return nil, fmt.Errorf("unable to load certificates from secret %s/%s: %w", secret.Namespace, secret.Name, err)
	}

	return &tls.Config{
		RootCAs:      rootCAs,
		Certificates: []tls.Certificate{tlsCert},
		MinVersion:   tls.VersionTLS12,
	}, nil
}

func buildHTTPSURL(host string, port int, path string) (string, error) {
	if err := validateInternalHost(host); err != nil {
		return "", fmt.Errorf("invalid host %q: %w", host, err)
	}
	if port <= 0 || port > 65535 {
		return "", fmt.Errorf("invalid port %d", port)
	}
	if path == "" || !strings.HasPrefix(path, "/") {
		return "", fmt.Errorf("invalid path %q", path)
	}

	endpoint := &url.URL{
		Scheme: "https",
		Host:   net.JoinHostPort(host, strconv.Itoa(port)),
		Path:   path,
	}
	return endpoint.String(), nil
}

func validateInternalHost(host string) error {
	if host == "" {
		return fmt.Errorf("host is empty")
	}
	if strings.TrimSpace(host) != host {
		return fmt.Errorf("host contains spaces")
	}
	if strings.Contains(host, "://") {
		return fmt.Errorf("host must not include scheme")
	}
	if strings.ContainsAny(host, `/\?#@`) {
		return fmt.Errorf("host contains invalid separators")
	}
	if strings.Contains(host, ":") {
		return fmt.Errorf("host must not include port")
	}

	labels := strings.Split(host, ".")
	for _, label := range labels {
		if label == "" {
			return fmt.Errorf("host contains empty label")
		}
		if len(label) > 63 {
			return fmt.Errorf("host label %q is too long", label)
		}
		if !dns1123LabelPattern.MatchString(label) {
			return fmt.Errorf("host label %q is invalid", label)
		}
	}

	return nil
}

// sqlAddrToHTTPBaseURL converts a SQL address (host:4000) to an HTTP base URL (https://host:10080).
func sqlAddrToHTTPBaseURL(sqlAddr string) (string, error) {
	host, _, err := net.SplitHostPort(sqlAddr)
	if err != nil {
		return "", fmt.Errorf("failed to parse address %q: %w", sqlAddr, err)
	}
	return "https://" + net.JoinHostPort(host, "10080"), nil
}

// buildTiDBInternalAPIURL builds a validated TiDB internal API endpoint URL from base URL and API path.
func buildTiDBInternalAPIURL(baseURL, path string) (string, error) {
	parsedBaseURL, err := url.Parse(baseURL)
	if err != nil {
		return "", fmt.Errorf("failed to parse base URL %q: %w", baseURL, err)
	}
	if parsedBaseURL.Scheme != "https" {
		return "", fmt.Errorf("unsupported base URL scheme %q", parsedBaseURL.Scheme)
	}
	if parsedBaseURL.Path != "" && parsedBaseURL.Path != "/" {
		return "", fmt.Errorf("base URL %q must not include path", baseURL)
	}
	if parsedBaseURL.RawQuery != "" || parsedBaseURL.Fragment != "" {
		return "", fmt.Errorf("base URL %q must not include query or fragment", baseURL)
	}

	host := parsedBaseURL.Hostname()
	if host == "" {
		return "", fmt.Errorf("base URL %q has empty host", baseURL)
	}
	portStr := parsedBaseURL.Port()
	if portStr == "" {
		return "", fmt.Errorf("base URL %q has empty port", baseURL)
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return "", fmt.Errorf("invalid port in base URL %q: %w", baseURL, err)
	}

	return buildHTTPSURL(host, port, path)
}

// resolveTiDBAddr resolves the SQL address of a TiDB instance by cluster ID.
func resolveTiDBAddr(ctx context.Context, cli client.Client, clusterID string) (string, error) {
	resolver := utils.NewTiDBAddrResolver(cli)
	return resolver.ResolveByLogicClusterID(ctx, clusterID)
}

// resolveWorkerTiDBAddr resolves the SQL address of a worker TiDB instance.
func resolveWorkerTiDBAddr(ctx context.Context, cli client.Client) (string, error) {
	return utils.ResolveWorkerTiDBAddr(ctx, cli)
}

// doTiDBHTTPRequest executes an HTTP request and checks the response status code.
// Returns an error if the request fails or the status code does not match expectedStatus.
func doTiDBHTTPRequest(client *http.Client, req *http.Request, expectedStatus int) error {
	resp, err := client.Do(req) //nolint:gosec // req URL is built by buildTiDBInternalAPIURL/buildHTTPSURL from internally resolved TiDB addresses
	if err != nil {
		return fmt.Errorf("failed to call TiDB HTTP API: %w", err)
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != expectedStatus {
		bodyBytes, readErr := io.ReadAll(io.LimitReader(resp.Body, maxTiDBHTTPErrorBodyBytes))
		body := string(bodyBytes)
		if readErr != nil {
			if body == "" {
				body = fmt.Sprintf("failed to read response body: %v", readErr)
			} else {
				body = fmt.Sprintf("%s (failed to finish reading response body: %v)", body, readErr)
			}
		}
		return &TiDBHTTPError{
			StatusCode: resp.StatusCode,
			Status:     resp.Status,
			Body:       body,
			Err:        readErr,
		}
	}
	return nil
}
