package core

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/guo-shaoge/tiflash-autoscale/pkg/config"
	"github.com/guo-shaoge/tiflash-autoscale/pkg/meta"
	"github.com/guo-shaoge/tiflash-autoscale/pkg/util/log/logger"
	"go.uber.org/zap"
	"io"
	"net"
	"net/http"
	"net/http/pprof"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type HttpServer struct {
	ctx       context.Context
	tenantMgr *TenantMgr
	podMgr    *PodMgr
	configMgr *ConfigMgr

	mux  *http.ServeMux
	port uint32

	logger *zap.Logger

	globalReqID atomic.Uint64
}

// TODO maybe config
var defTiFlashPort int = 3930

type getTopologyResult struct {
	HasError  int      `json:"hasError"`
	ErrorInfo string   `json:"errorInfo"`
	State     string   `json:"state"`
	Topology  []string `json:"topology"`
	Timestamp string   `json:"timestamp"`
}

const (
	// TODO add path to dump SharePool/VIPPool/UnassigningPool/WarmupPool
	resumeHttpPath      string = "/resume-and-get-topology"
	dumpPodsHttpPath    string = "/dumppods"
	memLimitErrRecovery        = "MemLimit"
)

// REF: https://cizixs.com/2016/08/17/golang-http-server-side/
func NewHttpServer(ctx context.Context, tenantMgr *TenantMgr, podMgr *PodMgr,
	configMgr *ConfigMgr, parentLogger *zap.Logger) *HttpServer {
	res := &HttpServer{
		ctx:       ctx,
		tenantMgr: tenantMgr,
		podMgr:    podMgr,
		configMgr: configMgr,
	}

	res.mux = http.NewServeMux()
	res.mux.HandleFunc(resumeHttpPath, func(w http.ResponseWriter, r *http.Request) {
		res.ResumeAndGetTopo(w, r)
	})
	res.mux.HandleFunc(dumpPodsHttpPath, func(w http.ResponseWriter, r *http.Request) {
		res.DumpPods(w, r)
	})

	curConfig := configMgr.GetConfig()
	res.port = *curConfig.GetClusterConfig().HttpPort
	res.logger = parentLogger.With(zap.String(logger.LogIDMod, "httpServer"))
	return res
}

func (h *HttpServer) startBgWorker(wg *sync.WaitGroup) {
	go func() {
		// http://localhost:8081/debug/pprof/goroutine?debug=2
		registerPprofHandlers(h.mux)

		err := http.ListenAndServe(":"+fmt.Sprintf("%v", h.port), h.mux)
		if err == http.ErrServerClosed {
			h.logger.Error("server closed")
			return
		}
		if err != nil {
			panic(err)
		}
	}()
}

func registerPprofHandlers(mux *http.ServeMux) {
	mux.HandleFunc("/debug/pprof/", pprof.Index)
	mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
	mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
	mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
	mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
	mux.Handle("/debug/pprof/block", pprof.Handler("block"))
	mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
	mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
	mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
	mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
}

func (h *HttpServer) ResumeAndGetTopo(w http.ResponseWriter, req *http.Request) {
	start := time.Now()

	w.Header().Set("Access-Control-Allow-Origin", "*")

	tenantName := req.FormValue("tidbclusterid")
	rgName := req.FormValue("resourcegroup")
	var err error
	if len(tenantName) == 0 {
		err = errors.New("tenant cluster id cannot be empty")
	} else if len(rgName) == 0 {
		err = errors.New("tenant resource group name cannot be empty")
	}

	errRecovery := req.FormValue("recovery")
	oriCNNum := req.FormValue("cn_cnt")
	ip, _ := getIP(req)

	traceID := req.FormValue("trace_id")
	// deprecated
	// version := req.FormValue("cn_version")
	if len(traceID) == 0 {
		reqID := h.globalReqID.Add(1)
		traceID = fmt.Sprintf("reqid-%v", reqID)
	}

	logger := h.logger.With(zap.String(logger.LogIDTrace, traceID)).With(zap.String("tenant", tenantName)).
		With(zap.String("rgName", rgName))

	curConfig := h.configMgr.GetConfig()
	tenantConfig := curConfig.GetTenantConfig(tenantName)
	clusterConfig := curConfig.GetClusterConfig()
	configMaxPod := int(*tenantConfig.TenantMaxCoreNum / *clusterConfig.CoreNumPerPod)
	configMinPod := int(*tenantConfig.TenantMinCoreNum / *clusterConfig.CoreNumPerPod)

	timeout := *clusterConfig.HttpServerTimeoutSec
	minPodNum, err := getMinPod(oriCNNum, errRecovery, configMinPod, configMaxPod, logger)

	logger.Info("http resume-and-get-topology begin", zap.String("client", ip),
		zap.String("recovery", errRecovery),
		zap.String("cn_cnt", oriCNNum),
		zap.Any("timeout", timeout),
		zap.Any("configMinPod", configMinPod),
		zap.Any("configMaxPod", configMaxPod),
		zap.Any("minPodNum", minPodNum),
		zap.Error(err))

	if err != nil {
		logger.Error("ResumeAndGetTopo failed", zap.Error(err))
		writeResp(w, true, err.Error(), []string{})
		return
	}

	topo, names, err := h.resume(tenantName, rgName, traceID, minPodNum, timeout, logger, clusterConfig)
	defer func() {
		logger.Info("http resume-and-get-topology done", zap.Any("elapsed", time.Since(start)),
			zap.Any("topo", topo), zap.Any("names", names), zap.Error(err))
	}()

	if err != nil {
		writeResp(w, true, err.Error(), []string{})
		return
	}

	writeResp(w, false, "", topo)
}

func getMinPod(oriCNNum string, errRecovery string, configMinPod int, configMaxPod int, logger *zap.Logger) (int, error) {
	minPodNum := 1

	var oriCNNumInt int
	var err error
	if errRecovery == memLimitErrRecovery {
		oriCNNumInt, err = strconv.Atoi(oriCNNum)
		if err != nil || oriCNNumInt <= 0 {
			err = fmt.Errorf("http parameter convert to int failed. cn_cnt(%s), err: %s", oriCNNum, err.Error())
			return 0, err
		}
		minPodNum = oriCNNumInt * 2
	}

	if minPodNum > configMaxPod {
		minPodNum = configMaxPod
	}
	if minPodNum < configMinPod {
		minPodNum = configMinPod
	}
	if minPodNum < 1 {
		logger.Error("min pod num is invalid, set it to 1",
			zap.Any("oriCNNum", oriCNNumInt),
			zap.Any("configMaxPod", configMaxPod),
			zap.Any("configMinPod", configMinPod))
		minPodNum = 1
	}
	return minPodNum, nil
}

func (h *HttpServer) resume(tenantName string, rgName string, traceID string,
	minPodNum int, timeout uint, logger *zap.Logger, clusterConfig *config.ClusterConfig) (topo []string, names []string, err error) {
	doneCh := make(chan bool)
	var podsMap meta.PodsMap
	go func() {
		start := time.Now()
		defer close(doneCh)
		for {
			podsMap, err = h.tenantMgr.SyncResume(tenantName, rgName, minPodNum, "httpServer", tenantName+traceID)
			if len(podsMap) > 0 {
				logger.Info("tenant SyncResume done", zap.Any("len", len(podsMap)), zap.Error(err))
				break
			}
			if time.Now().After(start.Add(time.Duration(timeout) * time.Second)) {
				logger.Info("tenant SyncResume iteration done, timeout", zap.Any("pods", len(podsMap)))
				break
			}

			if err != nil && err.Error() == SyncResumeFailBecauseAlreadyAllocating {
				logger.Info("tenant SyncResume iteration, someone is allocating, waiting")
				h.tenantMgr.WaitAllocPodsFinish(tenantName, 2*time.Second)
			} else {
				logger.Error("tenant SyncResume iteration, no pods returned and not resuming error, sleep and try again",
					zap.Error(err), zap.Any("len(podsMap)", len(podsMap)))
				time.Sleep(1 * time.Second)
			}
		}
	}()

	select {
	case <-doneCh:
	case <-time.After(time.Duration(timeout) * time.Second):
		logger.Info("tenant SyncResume timeout")
	}

	if *clusterConfig.IsMinikubeTest {
		topo, names = h.tenantMgr.GetTopo(tenantName)
	} else {
		topo, names = h.tenantMgr.GetTopoFQDN(tenantName)
	}
	return topo, names, nil
}

func writeResp(w http.ResponseWriter, hasErr bool, errInfo string, topo []string) {
	var ret getTopologyResult
	ret.HasError = 0
	ret.State = "resumed"
	ret.ErrorInfo = errInfo
	for i, addr := range topo {
		topo[i] = fmt.Sprintf("%v:%v", addr, defTiFlashPort)
	}
	ret.Topology = topo
	// Same with tidb, maybe change to real time.
	ret.Timestamp = strconv.FormatInt(time.Now().UnixNano(), 10)
	retJson, _ := json.Marshal(ret)
	io.WriteString(w, string(retJson))
}

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")
}

func (h *HttpServer) DumpPods(w http.ResponseWriter, req *http.Request) {
	resStr := h.podMgr.DumpAllPods()
	io.WriteString(w, resStr)
}

type T struct {
	name string
	pod  *meta.PodDesc
}
