package handler

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/pingcap/log"
	clientgo "github.com/tidbcloud/tiflash-autoscale/manager/pkg/client-go"
	"github.com/tidbcloud/tiflash-autoscale/manager/pkg/config"
	"github.com/tidbcloud/tiflash-autoscale/manager/pkg/core"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
)

type Server struct {
	ctx         context.Context
	pools       *core.Pools
	tenantsPool *core.TenantsPool
	conf        *config.Config
}

func NewServer(ctx context.Context, conf *config.Config, pools *core.Pools, tenantsPool *core.TenantsPool) *Server {
	return &Server{
		ctx,
		pools,
		tenantsPool,
		conf,
	}
}

func (s *Server) GetCluster(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	tenantId := r.URL.Query().Get(clientgo.TenantIdKey)
	tenantToken := r.URL.Query().Get(clientgo.TenantTokenKey)
	if tenantId == "" && tenantToken == "" {
		resp := clientgo.Response{
			Error: clientgo.NewError(http.StatusBadRequest, "tenant_id or tenant_token are required"),
		}
		writeResponse(w, http.StatusBadRequest, resp.String())
		return
	}

	var poolName string
	if tenantToken != "" {
		poolName = s.tenantsPool.GetTenantPool(tenantToken)
	} else if tenantId != "" {
		poolName = s.tenantsPool.GetTenantPool(tenantId)
	} else {
		poolName = s.tenantsPool.GetTenantPool(tenantId)
	}

	pods, ok := s.pools.GetPool(r.Context(), poolName)
	if !ok {
		resp := clientgo.Response{
			Error: clientgo.NewError(http.StatusNotFound, "pool not found"),
		}
		writeResponse(w, http.StatusBadRequest, resp.String())
		return
	}

	ps, err := k8sPods2ClientPods(pods, s.conf.Tiflash.ServiceDNS, poolName)
	if err != nil {
		resp := clientgo.Response{
			Error: clientgo.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to convert pods: %v", err)),
		}
		writeResponse(w, http.StatusInternalServerError, resp.String())
		return
	}

	resp := clientgo.Response{
		Cluster: &clientgo.Cluster{
			Pool: poolName,
			Port: s.conf.Tiflash.Port,
			Pods: ps,
		},
	}

	writeResponse(w, http.StatusOK, resp.String())
}

func k8sPods2ClientPods(pods []*corev1.Pod, dnsFormat, pool string) ([]*clientgo.Pod, error) {
	clientPods := make([]*clientgo.Pod, len(pods))
	for i, pod := range pods {
		dns, err := getPodSvcDNS(pod, dnsFormat, pool)
		if err != nil {
			return nil, err
		}

		clientPods[i] = &clientgo.Pod{
			Name:    pod.Name,
			Address: dns,
		}
	}
	return clientPods, nil
}

func getPodSvcDNS(pod *corev1.Pod, serviceDNSFormat string, poolName string) (string, error) {
	index, ok := pod.Labels["apps.kubernetes.io/pod-index"]
	if !ok {
		return "", fmt.Errorf("pod index not found")
	}

	return strings.ReplaceAll(strings.ReplaceAll(serviceDNSFormat, "{POOL}", poolName), "{INDEX}", index), nil
}

func (s *Server) Healthz(w http.ResponseWriter, r *http.Request) {
	writeResponse(w, http.StatusOK, "OK")
}

func (s *Server) DumpClusters(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	clusters := s.pools.GetPools(r.Context())
	if len(clusters) == 0 {
		resp := clientgo.DumpResponse{
			Error:     clientgo.NewError(http.StatusNotFound, "no clusters found"),
			Timestamp: time.Now().UnixNano(),
		}
		writeResponse(w, http.StatusNotFound, resp.String())
		return
	}
	var clientClusters []*clientgo.Cluster
	for poolName, pods := range clusters {
		ps, err := k8sPods2ClientPods(pods, s.conf.Tiflash.ServiceDNS, poolName)
		if err != nil {
			resp := clientgo.DumpResponse{
				Error:     clientgo.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to convert pods: %v", err)),
				Timestamp: time.Now().UnixNano(),
			}
			writeResponse(w, http.StatusInternalServerError, resp.String())
			return
		}

		clientClusters = append(clientClusters, &clientgo.Cluster{
			Pool: poolName,
			Port: s.conf.Tiflash.Port,
			Pods: ps,
		})
	}
	resp := clientgo.DumpResponse{
		Clusters:  clientClusters,
		Timestamp: time.Now().UnixNano(),
	}
	writeResponse(w, http.StatusOK, resp.String())
}

func (s *Server) GetTopology(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	tenantName := r.FormValue("tidbclusterid")
	rgName := r.FormValue("resourcegroup")
	if len(tenantName) == 0 {
		resp := clientgo.NewTopologyResult(errors.New("tenant cluster id cannot be empty"), nil)
		writeResponse(w, http.StatusBadRequest, resp.String())
		return
	}

	ip, _ := getIP(r)
	traceID := r.FormValue("trace_id")

	log.Info("GetTopology called",
		zap.String("tenantName", tenantName),
		zap.String("rgName", rgName),
		zap.String("traceID", r.FormValue("trace_id")),
		zap.String("clientIP", ip),
	)

	ctx := context.WithValue(r.Context(), "trace_id", traceID)
	ctx = context.WithValue(ctx, "client_ip", ip)

	poolName := s.tenantsPool.GetTenantPool(tenantName)
	pods, ok := s.pools.GetPool(ctx, poolName)
	if !ok {
		resp := clientgo.NewTopologyResult(errors.New("pool not found"), nil)
		writeResponse(w, http.StatusBadRequest, resp.String())
		return
	}

	var topo []string
	for _, pod := range pods {
		if pod.Status.PodIP == "" {
			log.Warn("Pod IP is empty", zap.String("podName", pod.Name), zap.String("poolName", poolName))
			continue
		}

		address, err := getPodSvcDNS(pod, s.conf.Tiflash.ServiceDNS, poolName)
		if err != nil {
			log.Warn("Failed to convert pods", zap.String("poolName", poolName), zap.Error(err))
			continue
		}

		topo = append(topo, fmt.Sprintf("%s:%v", address, s.conf.Tiflash.Port))
	}
	resp := clientgo.NewTopologyResult(nil, topo)
	writeResponse(w, http.StatusOK, resp.String())
}

func writeResponse(w http.ResponseWriter, status int, message string) {
	w.WriteHeader(status)
	io.WriteString(w, message)
}

func getIP(r *http.Request) (string, error) {
	ips := r.Header.Get("X-Forwarded-For")
	splitIps := strings.Split(ips, ",")

	if len(splitIps) > 0 {
		// get last IP in list since ELB prepends other user defined IPs, meaning the last one is the actual client IP.
		netIP := net.ParseIP(splitIps[len(splitIps)-1])
		if netIP != nil {
			return netIP.String(), nil
		}
	}

	ip, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return "", err
	}

	netIP := net.ParseIP(ip)
	if netIP != nil {
		ip := netIP.String()
		if ip == "::1" {
			return "127.0.0.1", nil
		}
		return ip, nil
	}

	return "", errors.New("IP not found")
}
