package migrationdedicated

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/avast/retry-go"
	"github.com/google/go-github/v63/github"
	"github.com/samber/lo"

	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
)

var baseO11yClusterID = map[string]string{
	"dev":     "39190b22-bb48-4167-a485-eec9917ac85a",
	"staging": "ea5eef3e-2c03-462d-9594-8bdc2299168c",
	"prod":    "90c5d4fa-ba99-4c40-8b5f-041f6dd28501",
}

// Test_101_XXX to Test_200_XXX

// func (su *TestDedicatedMigrationSuite) TestWaitLatestRunCompletedByFileName() {
// 	ctx := context.Background()
// 	workflowFileName := "migrate-project-mig-e2e.yaml"
// 	err := su.waitLatestRunCompletedByFileName(ctx, workflowFileName)
// 	su.Require().NoError(err)
// }

func (su *TestDedicatedMigrationSuite) waitLatestRunCompletedByFileName(
	ctx context.Context,
	repo, workflowFileName string,
) error {
	opts := new(github.ListWorkflowRunsOptions)
	opts.Actor = "tidbcloud-bot"
	opts.Branch = "main"
	opts.Event = "workflow_dispatch"
	runs, _, err := su.ghCli.Actions.ListWorkflowRunsByFileName(ctx, "tidbcloud", repo, workflowFileName, opts)
	if err != nil {
		return err
	}
	if runs == nil {
		return errors.New("call ListWorkflowRunsByFileName got nil runs")
	}
	if len(runs.WorkflowRuns) == 0 {
		return errors.New("call ListWorkflowRunsByFileName got empty runs")
	}
	runID := runs.WorkflowRuns[0].GetID()
	// currTime := time.Now()
	// for _, v := range runs.WorkflowRuns {
	// 	if v.CreatedAt != nil && v.CreatedAt.GetTime().After(currTime.Add(-1*time.Hour)) {
	// 		runID = v.GetID()
	// 		break
	// 	}
	// }
	su.T().Logf("workflow %s latest run is %v", workflowFileName, runID)
	if runID == 0 {
		return errors.New("cannot found latest run id")
	}
	for range 60 {
		time.Sleep(30 * time.Second)
		run, _, err := su.ghCli.Actions.GetWorkflowRunByID(ctx, "tidbcloud", repo, runID)
		if err != nil {
			return err
		}
		su.T().
			Logf("workflow %s run %v status is %s, conclusion is %s", workflowFileName, runID, lo.FromPtr(run.Status), lo.FromPtr(run.Conclusion))
		switch lo.FromPtr(run.Status) {
		case "completed":
			// conclusion is `failure` or `success`
			if lo.FromPtr(run.Conclusion) != "success" {
				return fmt.Errorf(
					"workflow %s run %v completed with conculsion %s",
					workflowFileName,
					runID,
					lo.FromPtr(run.Conclusion),
				)
			}
			return nil
		case "failure", "timed_out":
			return fmt.Errorf("workflow %s run %v failed", workflowFileName, runID)
		default:
			continue
		}
	}
	return fmt.Errorf(
		"workflow %s run %v cannot completed in 30 minutes, check the status in %s repo",
		workflowFileName,
		runID,
		repo,
	)
}

func (su *TestDedicatedMigrationSuite) Test_101_LockProject() {
	if su.skipMigration {
		su.T().Log("Skip trigger migration case")
		return
	}

	// input := []byte(fmt.Sprintf(`{"ref":"main", "inputs":{"tenant":"%s", "project":"%s", "env":"%s"}}`, su.orgID, su.projectID, su.env)) //nolint: gocritic // we use back quote to keep the format
	// err := su.triggerWorkflowOnAppDeliveryRepo("lock-project-mig-e2e.yaml", input)
	ctx := context.Background()
	workflowFileName := "lock-project-mig-e2e.yaml"
	event := github.CreateWorkflowDispatchEventRequest{
		Ref: "main",
		Inputs: map[string]any{
			"tenant":  su.orgID,
			"project": su.projectID,
			"env":     su.env,
		},
	}
	_, err := su.ghCli.Actions.CreateWorkflowDispatchEventByFileName(
		ctx,
		"tidbcloud",
		"app-delivery",
		workflowFileName,
		event,
	)
	su.Require().NoError(err)
	time.Sleep(5 * time.Second)
	err = su.waitLatestRunCompletedByFileName(ctx, "app-delivery", workflowFileName)
	su.Require().NoError(err)
	time.Sleep(180 * time.Second)
}

func (su *TestDedicatedMigrationSuite) Test_102_WaitForAllowListTakeEffect() {
	if su.skipMigration {
		su.T().Log("Skip trigger migration case")
		return
	}

	su.T().Logf("Waiting for lock list to take effect")
	retryErr := retry.Do(
		func() error {
			err := su.portalCli.GetClusterWithError(su.orgID, su.projectID, su.clusterID)
			su.T().Logf("GetCluster with error: %v", err)
			if err == nil {
				return errors.New("error is nil")
			}
			if strings.Contains(err.Error(), "503") {
				return nil
			}
			return errors.New("error is not HTTP 503")
		},
		retry.Attempts(120),
		retry.DelayType(retry.FixedDelay),
		retry.Delay(5*time.Second),
	)
	su.Require().NoError(retryErr)
}

// func (su *TestDedicatedMigrationSuite) TestListMigPodCompeletionTimeByProject() {
// 	pods, err := su.listMigPodCompeleteTimeByProject()
// 	su.T().Log(pods)
// 	su.Require().NoError(err)
// }

func (su *TestDedicatedMigrationSuite) listMigPodCompeletionTimeByProject() (map[string]uint64, error) {
	// mig pod is deploy in base-us-west-2 k8s
	o11yHost := "https://www.ds.us-west-2.aws.observability.tidbcloud.com"
	query := make(url.Values)
	query.Add(
		"query",
		fmt.Sprintf(
			`label_keep(kube_pod_completion_time{namespace="infra",pod=~"mig-project-%s.*"},"pod")`,
			su.projectID,
		), //nolint: gocritic // we use back quote to keep the format
	)
	query.Add("step", "10m")
	u := fmt.Sprintf("%s/internal/metrics/%s/api/v1/query?%s",
		o11yHost,
		baseO11yClusterID[su.env],
		query.Encode())
	su.T().Log(u)
	req, err := http.NewRequest(http.MethodGet, u, http.NoBody)
	if err != nil {
		return nil, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		su.T().Logf("do request failed, err:%v", err)
		return nil, err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		su.T().Logf("read body failed, err:%v", err)
		return nil, err
	}
	if resp.StatusCode > 300 {
		return nil, errors.New(string(data))
	}
	result := new(InstanceQueryResp)
	if err := json.Unmarshal(data, result); err != nil {
		return nil, err
	}
	out := map[string]uint64{}
	for _, v := range result.Data.Result {
		value, ok := v.Value[1].(string)
		if !ok {
			return nil, errors.New("cannot parse instance query result")
		}
		ts, err := strconv.ParseUint(value, 10, 64)
		if err != nil {
			return nil, err
		}
		pod := v.Metric["pod"]
		out[pod] = ts
	}
	return out, err
}

// func (su *TestDedicatedMigrationSuite) TestGetPodStatusPhase() {
// 	pod := "mig-e2e-1369847559694040571-fbef385-b2ft7"
// 	phase, err := su.getPodStatusPhase(pod)
// 	su.T().Log(phase)
// 	su.Require().NoError(err)
// }

func (su *TestDedicatedMigrationSuite) getMigPodStatusPhase(pod string) (string, error) {
	// mig pod is deploy in base-us-west-2 k8s
	o11yHost := "https://www.ds.us-west-2.aws.observability.tidbcloud.com"
	query := make(url.Values)
	query.Add(
		"query",
		fmt.Sprintf( //nolint: gocritic // we use back quote to keep the format
			`label_keep(kube_pod_status_phase{namespace="infra",pod="%s"}>0,"phase")`,
			pod,
		),
	)
	query.Add("step", "10m")
	u := fmt.Sprintf("%s/internal/metrics/%s/api/v1/query?%s",
		o11yHost,
		baseO11yClusterID[su.env],
		query.Encode())
	su.T().Log(u)
	req, err := http.NewRequest(http.MethodGet, u, http.NoBody)
	if err != nil {
		return "", err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		su.T().Logf("do request failed, err:%v", err)
		return "", err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		su.T().Logf("read body failed, err:%v", err)
		return "", err
	}
	if resp.StatusCode > 300 {
		return "", errors.New(string(data))
	}
	result := new(InstanceQueryResp)
	if err := json.Unmarshal(data, result); err != nil {
		return "", err
	}
	if len(result.Data.Result) == 0 {
		return "", fmt.Errorf("metrics kube_pod_status_phase for pod %s not found", pod)
	}
	return result.Data.Result[0].Metric["phase"], nil
}

// func (su *TestDedicatedMigrationSuite) TestWaitLatestMigPodSucceeded() {
// 	err := su.waitLatestMigPodSucceeded(nil)
// 	su.Require().NoError(err)
// }

func (su *TestDedicatedMigrationSuite) waitLatestMigPodSucceeded(podsBeforeMig map[string]uint64) error {
	latestPod := ""
outer:
	for range 10 {
		pods, err := su.listMigPodCompeletionTimeByProject()
		if err != nil {
			return err
		}
		for pod := range pods {
			if _, ok := podsBeforeMig[pod]; !ok {
				latestPod = pod
				break outer
			}
		}
		su.T().Log("mig pod is not completed")
		time.Sleep(60 * time.Second)
	}
	if latestPod == "" {
		return fmt.Errorf("cannot found the latest mig pod for project %s", su.projectID)
	}

	var lastObservedPhase string
	for range 3 {
		phase, err := su.getMigPodStatusPhase(latestPod)
		if err != nil {
			return err
		}
		lastObservedPhase = phase
		if phase == "Succeeded" {
			return nil
		}
		if phase == "Running" {
			time.Sleep(15 * time.Second)
			continue
		}
		break
	}
	return fmt.Errorf("mig pod %s is completed with status %s", latestPod, lastObservedPhase)
}

func (su *TestDedicatedMigrationSuite) Test_103_MigrateProject() {
	if su.skipMigration {
		su.T().Log("Skip trigger migration case")
		return
	}

	// input := []byte(fmt.Sprintf(`{"ref":"main", "inputs":{"tenant":"%s", "project":"%s", "env":"%s"}}`, su.orgID, su.projectID, su.env)) //nolint: gocritic // we use back quote to keep the format
	// err := su.triggerWorkflowOnAppDeliveryRepo("migrate-project-mig-e2e.yaml", input)
	ctx := context.Background()
	completedPods, err := su.listMigPodCompeletionTimeByProject()
	su.Require().NoError(err)
	workflowFileName := "migrate-project-mig-e2e.yaml"
	event := github.CreateWorkflowDispatchEventRequest{
		Ref: "main",
		Inputs: map[string]any{
			"tenant":  su.orgID,
			"project": su.projectID,
			"env":     su.env,
		},
	}
	_, err = su.ghCli.Actions.CreateWorkflowDispatchEventByFileName(
		ctx,
		"tidbcloud",
		"app-delivery",
		workflowFileName,
		event,
	)
	su.Require().NoError(err)
	time.Sleep(5 * time.Second)
	err = su.waitLatestRunCompletedByFileName(ctx, "app-delivery", workflowFileName)
	su.Require().NoError(err)
	time.Sleep(120 * time.Second)
	err = su.waitLatestMigPodSucceeded(completedPods)
	su.Require().NoError(err)
}

func (su *TestDedicatedMigrationSuite) Test_104_RecordMigratedClusterMetering() {
	if su.skipMigration {
		su.T().Log("Skip record cluster metering case")
		return
	}

	if su.env == "prod" {
		su.T().Log("Skip test for prod environment")
		return
	}

	// record migrated cluster metering before unlock project
	var clusterMetering schema.ClusterMetering
	err := su.dedicatedGlobalDB.Model(&schema.ClusterMetering{}).Unscoped().
		Where("`cluster_id` = ?", su.clusterID).
		Order("start_ts DESC").
		First(&clusterMetering).Error
	su.Require().NoError(err)
	su.migratedClusterMetering = clusterMetering
	su.Require().Equal(strconv.FormatUint(su.migratedClusterMetering.TenantID, 10), su.orgID)
	su.Require().Equal(strconv.FormatUint(su.migratedClusterMetering.ProjectID, 10), su.projectID)
	su.T().Logf("migrated cluster_metering id: %d", su.migratedClusterMetering.ID)

	// check if there is only one migrated cluster metering record
	var count int64
	err = su.dedicatedGlobalDB.Model(&schema.ClusterMetering{}).Unscoped().
		Where("`cluster_id` = ?", su.clusterID).
		Count(&count).Error
	su.Require().NoError(err)
	su.Require().Equal(int64(1), count, "cluster metering record count is not 1 before unlock project")
}

func (su *TestDedicatedMigrationSuite) Test_105_UnlockProject() {
	if su.skipMigration {
		su.T().Log("Skip trigger migration case")
		return
	}

	// input := []byte(fmt.Sprintf(`{"ref":"main", "inputs":{"tenant":"%s", "project":"%s", "env":"%s"}}`, su.orgID, su.projectID, su.env)) //nolint: gocritic // we use back quote to keep the format
	// err := su.triggerWorkflowOnAppDeliveryRepo("unlock-project-mig-e2e.yaml", input)
	ctx := context.Background()
	workflowFileName := "unlock-project-mig-e2e.yaml"
	event := github.CreateWorkflowDispatchEventRequest{
		Ref: "main",
		Inputs: map[string]any{
			"tenant":  su.orgID,
			"project": su.projectID,
			"env":     su.env,
		},
	}
	_, err := su.ghCli.Actions.CreateWorkflowDispatchEventByFileName(
		ctx,
		"tidbcloud",
		"app-delivery",
		workflowFileName,
		event,
	)
	su.Require().NoError(err)
	time.Sleep(5 * time.Second)
	err = su.waitLatestRunCompletedByFileName(ctx, "app-delivery", workflowFileName)
	su.Require().NoError(err)
	time.Sleep(180 * time.Second)
}

func (su *TestDedicatedMigrationSuite) Test_106_WaitForAllowListTakeEffect() {
	if su.skipMigration {
		su.T().Log("Skip trigger migration case")
		return
	}

	su.T().Logf("Waiting for lock list removal to take effect")
	retryErr := retry.Do(
		func() error {
			err := su.portalCli.GetClusterWithError(su.orgID, su.projectID, su.clusterID)
			su.T().Logf("GetCluster with error: %v", err)
			if err == nil {
				return nil
			}
			if strings.Contains(err.Error(), "503") {
				return errors.New("error is HTTP 503")
			}
			return errors.New("error is not nil")
		},
		retry.Attempts(120),
		retry.DelayType(retry.FixedDelay),
		retry.Delay(5*time.Second),
	)
	su.Require().NoError(retryErr)

	// wait for all pods take effect
	time.Sleep(60 * time.Second)
}
