package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"path/filepath"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

type serverConfig struct {
	defaultNamespace string
	defaultConfigMap string
	defaultImageKey  string
}

type updateImageRequest struct {
	Image         string `json:"image"`
	Namespace     string `json:"namespace,omitempty"`
	ConfigMapName string `json:"configmap_name,omitempty"`
	ImageKey      string `json:"image_key,omitempty"`
}

type imageResponse struct {
	Namespace  string  `json:"namespace"`
	ConfigMap  string  `json:"configmap_name"`
	ImageKey   string  `json:"image_key"`
	ImageValue *string `json:"image_value"`
}

type latestPodImageResponse struct {
	Namespace string `json:"namespace"`
	PodName   string `json:"pod_name"`
	Image     string `json:"image"`
}

type errorResponse struct {
	Error string `json:"error"`
}

type app struct {
	client kubernetes.Interface
	cfg    serverConfig
}

const allowedUpdateNamespace = "canary-vip-tidb-pool"

func main() {
	kubeCfg, err := buildKubeConfig()
	if err != nil {
		log.Fatalf("failed to create kube config: %v", err)
	}

	client, err := kubernetes.NewForConfig(kubeCfg)
	if err != nil {
		log.Fatalf("failed to create kube client: %v", err)
	}

	application := &app{
		client: client,
		cfg: serverConfig{
			defaultNamespace: envOrDefault("TARGET_NAMESPACE", "pool"),
			defaultConfigMap: envOrDefault("TARGET_CONFIGMAP", "pool-config"),
			defaultImageKey:  envOrDefault("IMAGE_KEY", "image"),
		},
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", application.handleHealthz)
	mux.HandleFunc("/api/v1/configmap/image", application.handleConfigMapImage)
	mux.HandleFunc("/api/v1/canary-vip-tidb-pool/latest-pod-image", application.handleLatestPodImage)

	addr := ":" + envOrDefault("PORT", "8080")
	log.Printf("tidb-image-api listening on %s", addr)
	if err := http.ListenAndServe(addr, mux); err != nil {
		log.Fatalf("server exited: %v", err)
	}
}

func (a *app) handleHealthz(w http.ResponseWriter, _ *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func (a *app) handleConfigMapImage(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		a.handleGetImage(w, r)
	case http.MethodPut:
		a.handleUpdateImage(w, r)
	default:
		w.Header().Set("Allow", "GET, PUT")
		writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
	}
}

func (a *app) handleLatestPodImage(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		w.Header().Set("Allow", "GET")
		writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
		return
	}

	podList, err := a.client.CoreV1().Pods(allowedUpdateNamespace).List(r.Context(), metav1.ListOptions{})
	if err != nil {
		if apierrors.IsNotFound(err) {
			writeJSON(w, http.StatusNotFound, errorResponse{Error: "namespace not found"})
			return
		}
		writeJSON(w, http.StatusInternalServerError, errorResponse{Error: err.Error()})
		return
	}
	if len(podList.Items) == 0 {
		writeJSON(w, http.StatusNotFound, errorResponse{Error: "no pods found in canary-vip-tidb-pool"})
		return
	}

	var latest *corev1.Pod
	for i := range podList.Items {
		if podList.Items[i].Status.Phase != corev1.PodRunning {
			continue
		}
		if latest == nil || podList.Items[i].CreationTimestamp.Time.After(latest.CreationTimestamp.Time) {
			latest = &podList.Items[i]
		}
	}
	if latest == nil {
		writeJSON(w, http.StatusNotFound, errorResponse{Error: "no running pods found in canary-vip-tidb-pool"})
		return
	}
	if len(latest.Spec.Containers) == 0 {
		writeJSON(w, http.StatusNotFound, errorResponse{Error: "latest pod has no containers"})
		return
	}

	writeJSON(w, http.StatusOK, latestPodImageResponse{
		Namespace: allowedUpdateNamespace,
		PodName:   latest.Name,
		Image:     latest.Spec.Containers[0].Image,
	})
}

func (a *app) handleGetImage(w http.ResponseWriter, r *http.Request) {
	ns, cmName, imageKey := a.resolveTarget(
		r.URL.Query().Get("namespace"),
		r.URL.Query().Get("configmap_name"),
		r.URL.Query().Get("image_key"),
	)

	cm, err := a.client.CoreV1().ConfigMaps(ns).Get(r.Context(), cmName, metav1.GetOptions{})
	if err != nil {
		a.writeKubeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, buildImageResponse(cm, ns, cmName, imageKey))
}

func (a *app) handleUpdateImage(w http.ResponseWriter, r *http.Request) {
	var req updateImageRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid json body"})
		return
	}
	if req.Image == "" {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "image is required"})
		return
	}
	if req.ConfigMapName == "" {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "configmap_name is required"})
		return
	}
	if req.ImageKey == "" {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "image_key is required"})
		return
	}
	if req.Namespace != "" && req.Namespace != allowedUpdateNamespace {
		writeJSON(w, http.StatusBadRequest, errorResponse{Error: "namespace must be canary-vip-tidb-pool"})
		return
	}

	ns, cmName, imageKey := a.resolveTarget(req.Namespace, req.ConfigMapName, req.ImageKey)
	patchBody, err := json.Marshal(map[string]map[string]string{
		"data": {imageKey: req.Image},
	})
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "failed to build patch"})
		return
	}

	if _, err = a.client.CoreV1().ConfigMaps(ns).Patch(
		r.Context(), cmName, types.MergePatchType, patchBody, metav1.PatchOptions{},
	); err != nil {
		a.writeKubeError(w, err)
		return
	}

	cm, err := a.client.CoreV1().ConfigMaps(ns).Get(r.Context(), cmName, metav1.GetOptions{})
	if err != nil {
		a.writeKubeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, buildImageResponse(cm, ns, cmName, imageKey))
}

func buildImageResponse(cm *corev1.ConfigMap, namespace, configMapName, imageKey string) imageResponse {
	var imageVal *string
	if cm != nil && cm.Data != nil {
		if val, ok := cm.Data[imageKey]; ok {
			v := val
			imageVal = &v
		}
	}

	return imageResponse{
		Namespace:  namespace,
		ConfigMap:  configMapName,
		ImageKey:   imageKey,
		ImageValue: imageVal,
	}
}

func (a *app) resolveTarget(namespace, configMapName, imageKey string) (string, string, string) {
	ns := namespace
	if ns == "" {
		ns = a.cfg.defaultNamespace
	}
	cm := configMapName
	if cm == "" {
		cm = a.cfg.defaultConfigMap
	}
	key := imageKey
	if key == "" {
		key = a.cfg.defaultImageKey
	}
	return ns, cm, key
}

func (a *app) writeKubeError(w http.ResponseWriter, err error) {
	switch {
	case apierrors.IsNotFound(err):
		writeJSON(w, http.StatusNotFound, errorResponse{Error: "configmap not found"})
	case errors.Is(err, context.DeadlineExceeded):
		writeJSON(w, http.StatusGatewayTimeout, errorResponse{Error: "kubernetes api timeout"})
	default:
		writeJSON(w, http.StatusInternalServerError, errorResponse{Error: err.Error()})
	}
}

func writeJSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(payload); err != nil {
		log.Printf("failed to write response: %v", err)
	}
}

func buildKubeConfig() (*rest.Config, error) {
	// In EKS pod, in-cluster config is used. For local debug, fallback to kubeconfig.
	if cfg, err := rest.InClusterConfig(); err == nil {
		return cfg, nil
	}

	kubeConfigPath := os.Getenv("KUBECONFIG")
	if kubeConfigPath == "" {
		home := homedir.HomeDir()
		if home == "" {
			return nil, errors.New("cannot resolve kubeconfig path")
		}
		kubeConfigPath = filepath.Join(home, ".kube", "config")
	}
	return clientcmd.BuildConfigFromFlags("", kubeConfigPath)
}

func envOrDefault(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}
