package service

import (
	"errors"
	"fmt"
	"io"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// HandleComponentPodMetrics is a plain Gin handler (not wired through grpc-gateway)
// that proxies raw Prometheus metrics from a specific component pod's /metrics endpoint.
// Route: GET /clusters/:cluster_id/components/:component_type/:pod_name/metrics
// See: claude-notes/tikv-pod-metrics-proxy-proposal.md for design rationale.
func (s *AgentService) HandleComponentPodMetrics(c *gin.Context) {
	componentType := c.Param("component_type")
	clusterID := c.Param("cluster_id")
	podName := c.Param("pod_name")

	switch componentType {
	case TiKVComponentType:
		s.handleTiKVPodMetrics(c, clusterID, podName)
	default:
		c.JSON(http.StatusNotImplemented,
			gin.H{"error": fmt.Sprintf("metrics proxy not implemented for component: %s", componentType)})
	}
}

func (s *AgentService) handleTiKVPodMetrics(c *gin.Context, clusterID, podName string) {
	ctx := c.Request.Context()

	if s.tikvHTTPClient == nil {
		c.JSON(http.StatusServiceUnavailable,
			gin.H{"error": "TiKV TLS client unavailable; check /client-ssl/ mount"})
		return
	}

	ns := buildNamespace(clusterID)
	var pod corev1.Pod
	if err := s.dataplaneCli.Get(ctx, client.ObjectKey{Namespace: ns, Name: podName}, &pod); err != nil {
		if apierrors.IsNotFound(err) {
			c.JSON(http.StatusNotFound, gin.H{"error": "pod not found"})
		} else {
			tilog.Error(ctx, "failed to get pod from control plane",
				zap.String("pod", podName), zap.Error(err))
			c.JSON(http.StatusBadGateway,
				gin.H{"error": "control plane unavailable"})
		}
		return
	}

	if pod.Labels[K8sComponentLabelKey] != TiKVComponentType {
		c.JSON(http.StatusBadRequest, gin.H{"error": "pod is not a tikv component"})
		return
	}

	if pod.Status.Phase != corev1.PodRunning {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "pod is not running"})
		return
	}

	host := fmt.Sprintf("%s.%s.%s.svc", pod.Name, tikvHeadlessService, pod.Namespace)
	metricsURL, err := buildTiKVMetricsURL(host)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	s.handleTiKVPodMetricsWithURL(c, clusterID, podName, metricsURL)
}

// handleTiKVPodMetricsWithURL performs the HTTP proxy to the given metricsURL.
// Separated from handleTiKVPodMetrics to allow URL injection in tests.
func (s *AgentService) handleTiKVPodMetricsWithURL(c *gin.Context, _, podName, metricsURL string) {
	ctx := c.Request.Context()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, metricsURL, http.NoBody)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	//nolint:gosec // G107: URL is built by buildTiKVMetricsURL with internal host/path validation.
	resp, err := s.tikvHTTPClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "failed to reach tikv metrics endpoint",
			zap.String("pod", podName), zap.Error(err))
		c.JSON(http.StatusBadGateway,
			gin.H{"error": "failed to reach tikv metrics endpoint"})
		return
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		c.JSON(http.StatusBadGateway,
			gin.H{"error": fmt.Sprintf("tikv returned %d", resp.StatusCode)})
		return
	}

	c.Header("Content-Type", resp.Header.Get("Content-Type"))
	c.Status(http.StatusOK)
	if _, err = io.Copy(c.Writer, resp.Body); err != nil {
		tilog.Error(ctx, "failed to stream tikv metrics",
			zap.String("pod", podName), zap.Error(err))
	}
}

// HandleComponentPodNodeMetrics proxies raw Prometheus metrics from the node-exporter
// instance co-located with the given component pod.
// Route: GET /clusters/:cluster_id/components/:component_type/:pod_name/node-metrics
func (s *AgentService) HandleComponentPodNodeMetrics(c *gin.Context) {
	componentType := c.Param("component_type")
	clusterID := c.Param("cluster_id")
	podName := c.Param("pod_name")

	switch componentType {
	case TiKVComponentType:
		s.handleTiKVPodNodeMetrics(c, clusterID, podName)
	default:
		c.JSON(http.StatusNotImplemented,
			gin.H{"error": fmt.Sprintf("node metrics proxy not implemented for component: %s", componentType)})
	}
}

func (s *AgentService) handleTiKVPodNodeMetrics(c *gin.Context, clusterID, podName string) {
	ctx := c.Request.Context()
	ns := buildNamespace(clusterID)

	var pod corev1.Pod
	if err := s.dataplaneCli.Get(ctx, client.ObjectKey{Namespace: ns, Name: podName}, &pod); err != nil {
		if apierrors.IsNotFound(err) {
			c.JSON(http.StatusNotFound, gin.H{"error": "pod not found"})
		} else {
			tilog.Error(ctx, "failed to get pod from control plane",
				zap.String("pod", podName), zap.Error(err))
			c.JSON(http.StatusBadGateway, gin.H{"error": "control plane unavailable"})
		}
		return
	}

	if pod.Labels[K8sComponentLabelKey] != TiKVComponentType {
		c.JSON(http.StatusBadRequest, gin.H{"error": "pod is not a tikv component"})
		return
	}
	if pod.Status.Phase != corev1.PodRunning {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "pod is not running"})
		return
	}

	port, err := s.getNodeExporterPort(ctx, pod.Spec.NodeName)
	if err != nil {
		tilog.Error(ctx, "failed to resolve node-exporter port",
			zap.String("pod", podName), zap.String("node", pod.Spec.NodeName), zap.Error(err))
		if errors.Is(err, errNodeExporterListFailed) {
			c.JSON(http.StatusBadGateway, gin.H{"error": "failed to list node-exporter pods"})
		} else {
			c.JSON(http.StatusServiceUnavailable, gin.H{"error": "node-exporter unavailable"})
		}
		return
	}

	metricsURL := fmt.Sprintf("http://%s:%d/metrics", pod.Status.HostIP, port)
	s.proxyNodeExporterMetrics(c, podName, metricsURL)
}

func (s *AgentService) proxyNodeExporterMetrics(c *gin.Context, podName, metricsURL string) {
	ctx := c.Request.Context()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, metricsURL, http.NoBody)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	//nolint:gosec // G107: URL is built from validated pod.Status.HostIP + fixed /metrics path.
	resp, err := s.nodeExporterHTTPClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "failed to reach node-exporter",
			zap.String("pod", podName), zap.Error(err))
		c.JSON(http.StatusBadGateway, gin.H{"error": "failed to reach node-exporter"})
		return
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		c.JSON(http.StatusBadGateway,
			gin.H{"error": fmt.Sprintf("node-exporter returned %d", resp.StatusCode)})
		return
	}

	c.Header("Content-Type", resp.Header.Get("Content-Type"))
	c.Status(http.StatusOK)
	if _, err = io.Copy(c.Writer, resp.Body); err != nil {
		tilog.Error(ctx, "failed to stream node-exporter metrics",
			zap.String("pod", podName), zap.Error(err))
	}
}
