package router

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

	tiproxyrouter "github.com/pingcap/tiproxy/pkg/manager/router"
	"github.com/pkg/errors"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway/client"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway/config"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway/meta"
	"github.com/tidbcloud/aws-shared-provider/pkg/gateway/metrics"
	"github.com/tidbcloud/aws-shared-provider/pkg/infra"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils"
	ctrl "sigs.k8s.io/controller-runtime"
)

var DialBackendInterval = time.Second

var _ tiproxyrouter.BackendFetcher = &backendFetcher{}

type backendFetcher struct {
	token         string
	clusterID     string
	preTidbPool   string
	router        *Router
	conf          *config.Config
	regionalInfra infra.RegionalInfra
	infraApi      infra.InfraAPI
	httpClient    *client.ManagerServerClient
}

// Be careful when returning err, because TiProxy will not try again and will return directly to the client.
func (f *backendFetcher) GetBackendList(
	ctx context.Context, refresh bool,
) (map[string]*tiproxyrouter.BackendInfo, error) {
	ips := f.getBackendAddressV4(ctx, f.token, f.clusterID, refresh)
	m := make(map[string]*tiproxyrouter.BackendInfo, len(ips))
	for _, ip := range ips {
		m[fmt.Sprintf("%s:%d", ip, 4000)] = &tiproxyrouter.BackendInfo{
			IP:         ip,
			StatusPort: 10080, //nolint: gomnd // fixed port number
		}
	}
	return m, nil
}

// getBackendAddressV4 returns the backend address from manager(or cache) for the given token.
// the TiProxy will call this function to update its backends.
// nolint: nakedret // by design
func (f *backendFetcher) getBackendAddressV4(ctx context.Context, token string, clusterID string, refresh bool) []string {
	logger := ctrl.Log.WithName("getBackendAddressV4").WithValues("token", token).
		WithValues("cluster_id", clusterID).WithValues("gateway_mode", f.conf.GatewayMode).
		WithValues("refresh", refresh)

	meta, err := meta.GetClusterOrBranchMeta(ctx, f.regionalInfra, f.infraApi, f.conf, token)
	if err != nil {
		metrics.GetBackendListCounter.WithLabelValues(clusterID, token, "check_infra_failed").Inc()
		logger.Error(errors.Cause(err), "check infra cluster status failed")
		if err.Error() == utils.ERR_CLUSTER_NOT_FOUND {
			f.router.Close()
		}
		return []string{}
	}

	poolBackend, managerReplicas, managerReplicasKnown := f.router.GetPoolBackend(meta.TiDBPool)
	replicas, replicasKnown := replicasWithManagerFallback(
		meta.InfraTiDBReplicas,
		managerReplicas,
		managerReplicasKnown,
	)
	// refresh=true indicates their are no available backends in router.
	// If expected replicas is 1, we only fetch manager when refresh=true.
	// That can avoid unnecessary assign when backend length is 0.(in zero backend mode)
	fetchManager := shouldFetchManager(poolBackend, replicas, replicasKnown, refresh, meta.TiDBPool, f.preTidbPool)

	if !fetchManager {
		metrics.GetBackendListCounter.WithLabelValues(meta.ClusterID, token, "fetch_local_cache").Inc()
		return f.router.GetBackendIPs()
	}

	logger.Info("local cache outdated, forward to manager")
	oldBackendIPs := f.router.GetBackendIPs()
	now := time.Now()

	result, err := f.assignTiDB(ctx, token, meta.TiDBPool, 1)
	if err != nil {
		metrics.GetBackendListCounter.WithLabelValues(meta.ClusterID, token, "fetch_manager_failed").Inc()
		logger.Error(err, "failed to fetch backend from manager", "duration", time.Since(now))
		return f.router.GetBackendIPs()
	}
	logger.Info("fetch manager success",
		"old backends", oldBackendIPs,
		"preTidbPool", f.preTidbPool,
		"tidbPool", meta.TiDBPool,
		"replicas", replicas,
		"duration", time.Since(now),
		"result", result,
	)

	f.preTidbPool = meta.TiDBPool
	metrics.GetBackendListCounter.WithLabelValues(meta.ClusterID, token, "fetch_manager_success").Inc()
	return f.router.GetBackendIPs()

}

func replicasWithManagerFallback(infraReplicas, managerReplicas int, managerReplicasKnown bool) (int, bool) {
	if infraReplicas > 0 {
		return infraReplicas, true
	}
	return managerReplicas, managerReplicasKnown
}

func shouldFetchManager(poolBackend []string, replicas int, replicasKnown bool, refresh bool, tidbPool, preTidbPool string) bool {
	return !replicasKnown ||
		(replicas > 1 && replicas > len(poolBackend)) ||
		(refresh && replicas == 1) ||
		tidbPool != preTidbPool
}

func (f *backendFetcher) assignTiDB(ctx context.Context, token string, tidbPoolNS string, maxAttempts int) ([]string, error) {
	urlFormat := "https://manager-server:8000/api/tidb/assign?token=%s&ns=%s&max_idle_seconds=%d"
	url := fmt.Sprintf(urlFormat, token, tidbPoolNS, f.conf.MaxIdleSeconds)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, err
	}

	var backends []string
	for i := 1; i <= maxAttempts; i++ {
		backends, err = func() ([]string, error) {
			resp, err := f.httpClient.Do(req)
			if err != nil {
				return nil, err
			}
			defer resp.Body.Close() //nolint:errcheck // no need check error

			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return nil, err
			}
			if resp.StatusCode != http.StatusOK {
				return nil, errors.Errorf("manager return %s, body: %s", resp.Status, body)
			}
			return strings.Split(string(body), ","), nil
		}()
		if err == nil {
			break
		}
		if i < maxAttempts {
			time.Sleep(DialBackendInterval)
		}
	}
	if err == nil && len(backends) == 0 {
		return nil, errors.New("get empty pod list from manager")
	}
	return backends, err
}
