package service

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/cluster-agent/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	tiProxyHealthAPIPath = "/api/debug/health"
	tiProxyHealthAPIPort = "3080"
)

type TiProxyHealthAPIProber interface {
	Probe(ctx context.Context, podIP string, tlsConfig *tls.Config) tiProxyHealthAPIProbe
}

type tiProxyHealthAPIProbe struct {
	status     apiv1.TiProxyHealthAPIProbeStatus
	statusCode int32
	message    string
}

type httpsTiProxyHealthAPIProber struct {
	client *http.Client
}

func (p *httpsTiProxyHealthAPIProber) Probe(ctx context.Context, podIP string, tlsConfig *tls.Config) tiProxyHealthAPIProbe {
	endpoint, err := buildTiProxyHealthAPIURL(podIP)
	if err != nil {
		return tiProxyHealthAPIProbe{
			status:  apiv1.TiProxyHealthAPIProbeStatus_UNKNOWN,
			message: err.Error(),
		}
	}

	// Probe TiProxy's HTTPS management API, not the plain HTTP load-balancer
	// health check path.
	// Use an intentionally malformed PUT body as a no-op capability probe. New
	// TiProxy rejects it before storing the manual health override, while old
	// TiProxy returns not-found or method-not-allowed because the API is missing.
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, strings.NewReader("{"))
	if err != nil {
		return tiProxyHealthAPIProbe{
			status:  apiv1.TiProxyHealthAPIProbeStatus_UNKNOWN,
			message: err.Error(),
		}
	}
	req.Header.Set("Content-Type", "application/json")

	httpClient := p.client
	if httpClient == nil {
		httpClient = buildTiProxyHealthAPIProbeClient(tlsConfig)
	}
	resp, err := httpClient.Do(req) //nolint:gosec // endpoint is built from a validated PodIP and fixed path/port.
	if err != nil {
		return tiProxyHealthAPIProbe{
			status:  apiv1.TiProxyHealthAPIProbeStatus_UNKNOWN,
			message: err.Error(),
		}
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode == http.StatusBadRequest {
		return tiProxyHealthAPIProbe{status: apiv1.TiProxyHealthAPIProbeStatus_SUPPORTED, statusCode: int32(resp.StatusCode)}
	}
	if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed {
		return tiProxyHealthAPIProbe{status: apiv1.TiProxyHealthAPIProbeStatus_UNSUPPORTED, statusCode: int32(resp.StatusCode)}
	}
	return tiProxyHealthAPIProbe{
		status:     apiv1.TiProxyHealthAPIProbeStatus_UNKNOWN,
		statusCode: int32(resp.StatusCode),
		message:    fmt.Sprintf("unexpected status %s", resp.Status),
	}
}

func buildTiProxyHealthAPIProbeClient(tlsConfig *tls.Config) *http.Client {
	config := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}
	if tlsConfig != nil {
		config = tlsConfig.Clone()
	}
	// The target is a Kubernetes PodIP from the informer/client cache and
	// TiProxy's serving cert is not guaranteed to be valid for that IP.
	config.InsecureSkipVerify = true

	return &http.Client{
		Timeout: 5 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: config,
		},
	}
}

func buildTiProxyHealthAPIURL(podIP string) (string, error) {
	if net.ParseIP(podIP) == nil {
		return "", fmt.Errorf("invalid pod ip %q", podIP)
	}
	return (&url.URL{
		Scheme: "https",
		Host:   net.JoinHostPort(podIP, tiProxyHealthAPIPort),
		Path:   tiProxyHealthAPIPath,
	}).String(), nil
}

func (s *AgentService) ProbeTiProxyHealthAPI(ctx context.Context, req *apiv1.ProbeTiProxyHealthAPIReq) (*apiv1.ProbeTiProxyHealthAPIResp, error) {
	if req.GetClusterId() == "" {
		return nil, fmt.Errorf("cluster_id is required")
	}
	if len(req.GetPodNames()) == 0 {
		return nil, fmt.Errorf("pod_names is required")
	}

	namespace := buildNamespace(req.GetClusterId())
	tlsConfig, err := s.getTiProxyHealthAPIProbeTLSConfig(ctx, namespace)
	if err != nil {
		return nil, err
	}

	results := make([]*apiv1.TiProxyHealthAPIProbeResult, 0, len(req.GetPodNames()))
	for _, podName := range req.GetPodNames() {
		if podName == "" {
			return nil, fmt.Errorf("pod_names contains empty pod name")
		}

		pod, err := s.getTiProxyPod(ctx, req.GetClusterId(), podName)
		if err != nil {
			return nil, err
		}
		if pod.Status.PodIP == "" {
			return nil, fmt.Errorf("pod %s/%s does not have pod ip", pod.Namespace, pod.Name)
		}

		probe := s.tiProxyHealthAPIProber.Probe(ctx, pod.Status.PodIP, tlsConfig)
		results = append(results, &apiv1.TiProxyHealthAPIProbeResult{
			PodName:    pod.Name,
			Status:     probe.status,
			StatusCode: probe.statusCode,
			Message:    probe.message,
		})
	}
	return &apiv1.ProbeTiProxyHealthAPIResp{Results: results}, nil
}

func (s *AgentService) getTiProxyPod(ctx context.Context, clusterID, podName string) (*corev1.Pod, error) {
	var pod corev1.Pod
	key := client.ObjectKey{Namespace: buildNamespace(clusterID), Name: podName}
	if err := s.dataplaneCli.Get(ctx, key, &pod); err != nil {
		return nil, fmt.Errorf("get pod %s/%s: %w", key.Namespace, key.Name, err)
	}
	if pod.Labels[K8sComponentLabelKey] != TiProxyComponentType {
		return nil, fmt.Errorf("pod %s/%s is not a tiproxy pod", pod.Namespace, pod.Name)
	}
	return &pod, nil
}

func (s *AgentService) getTiProxyHealthAPIProbeTLSConfig(ctx context.Context, namespace string) (*tls.Config, error) {
	var secret corev1.Secret
	key := client.ObjectKey{Namespace: namespace, Name: tlsClientSecret}
	if err := s.dataplaneCli.Get(ctx, key, &secret); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("get TiProxy health API client certificate secret %s/%s: %w", key.Namespace, key.Name, err)
	}

	tlsConfig, err := LoadTlsConfigFromSecret(&secret)
	if err != nil {
		return nil, fmt.Errorf("load TiProxy health API client certificate from secret %s/%s: %w", key.Namespace, key.Name, err)
	}
	return tlsConfig, nil
}
