package common

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"

	"k8s.io/klog/v2"

	"github.com/pkg/errors"

	"github.com/pingcap/test-infra/common/model"
	respM "github.com/pingcap/test-infra/common/model/tcms/response"
)

type TCMS interface {
	GetCase(id model.ID) (*model.TestCase, error)
	GetPlan(id model.ID) (*model.TestPlan, error)
	GetPlanByPlanExecution(id model.ID) (*model.TestPlan, error)
	ListProceedingPlanExecutions() ([]*model.TestPlanExecution, error)

	GetCaseExecution(id model.ID) (*model.TestCaseExecution, error)
	PutCaseExecution(exec *model.TestCaseExecution) error
	GetPlanExecution(id model.ID) (*model.TestPlanExecution, error)
	PutPlanExecution(exec *model.TestPlanExecution) error

	ListResourceCohorts(respool string) ([]*model.ResourceCohort, error)
	ListResourcePools() ([]*model.ResourcePool, error)

	CreateTestBedInfo(tb *model.TestBedInfo) error
	PutTestBedInfo(tb *model.TestBedInfo) error
	GetTestbedInfo(testbed string) (*model.TestBedInfo, error)
	SyncTestBed(planExecID model.ID, testbed string) error
	ReleaseTestBed(planExecID model.ID, testbed string) error
}

type transport struct {
	accessToken string
}

func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) {
	// set auth header
	r.Header.Set("Authorization", "Bearer "+t.accessToken)
	return http.DefaultTransport.RoundTrip(r)
}

func NewTCMS(endpoint string, accessToken string) TCMS {
	client := &http.Client{
		Transport: &transport{
			accessToken: accessToken,
		},
	}
	return &tcms{endpoint: endpoint, http: client}
}

type tcms struct {
	endpoint string
	http     *http.Client
}

func (t *tcms) mkURL(path string) string {
	return t.endpoint + path
}

func (t *tcms) GetCase(id model.ID) (*model.TestCase, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/cases/%v", id)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get case #%v: [%d] %s", id, resp.StatusCode, string(bodyBytes))
	}
	var out model.TestCase
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	return &out, nil
}

func (t *tcms) GetPlan(id model.ID) (*model.TestPlan, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/plans/%v", id)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get plan #%v: [%d] %s", id, resp.StatusCode, string(bodyBytes))
	}
	var out model.TestPlan
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	return &out, nil
}

func (t *tcms) GetPlanByPlanExecution(id model.ID) (*model.TestPlan, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/plan-executions/%v/plan", id)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get plan by plan-executions #%v: [%d] %s", id, resp.StatusCode, string(bodyBytes))
	}
	var out model.TestPlan
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	return &out, nil
}

func (t *tcms) GetCaseExecution(id model.ID) (*model.TestCaseExecution, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/case-executions/%v", id)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get case-execution #%v: [%d] %s", id, resp.StatusCode, string(bodyBytes))
	}
	var out model.TestCaseExecution
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	return &out, nil
}

func (t *tcms) PutCaseExecution(exec *model.TestCaseExecution) error {
	method := http.MethodPost
	apiURL := t.mkURL("/api/v1/case-executions")
	if exec.ID > 0 {
		method = http.MethodPut
		apiURL += "/" + fmt.Sprint(exec.ID)
	}
	body := new(bytes.Buffer)
	err := json.NewEncoder(body).Encode(exec)
	if err != nil {
		return errors.WithStack(err)
	}
	klog.InfoSDepth(1, "set case-execution", "api", apiURL, "data", body.String())
	req, err := http.NewRequest(method, apiURL, body)
	if err != nil {
		return errors.WithStack(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.WithStack(err)
	}
	if resp.StatusCode != 200 {
		return errors.Errorf("set case-execution #%v: [%d] %s", exec.ID, resp.StatusCode, string(bodyBytes))
	}
	err = json.Unmarshal(bodyBytes, exec)
	if err != nil {
		return errors.WithStack(err)
	}
	return nil
}

func (t *tcms) GetPlanExecution(id model.ID) (*model.TestPlanExecution, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/plan-executions/%v", id)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get plan-execution #%v: [%d] %s", id, resp.StatusCode, string(bodyBytes))
	}
	var out model.TestPlanExecution
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	return &out, nil
}

func (t *tcms) PutPlanExecution(exec *model.TestPlanExecution) error {
	method := http.MethodPut
	apiURL := t.mkURL("/api/v1/plan-executions/" + fmt.Sprint(exec.ID))
	body := new(bytes.Buffer)
	err := json.NewEncoder(body).Encode(exec)
	if err != nil {
		return errors.WithStack(err)
	}
	klog.InfoSDepth(1, "set plan-execution", "api", apiURL, "data", body.String())
	req, err := http.NewRequest(method, apiURL, body)
	if err != nil {
		return errors.WithStack(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.WithStack(err)
	}
	if resp.StatusCode != 200 {
		return errors.Errorf("set plan-execution #%v: [%d] %s", exec.ID, resp.StatusCode, string(bodyBytes))
	}
	err = json.Unmarshal(bodyBytes, exec)
	if err != nil {
		return errors.WithStack(err)
	}
	return nil
}

const PullLimit = 100

func (t *tcms) ListProceedingPlanExecutions() ([]*model.TestPlanExecution, error) {
	planExecutions, err := t.listPlanExecutionByStatus(model.ExecutionStatusPending)
	if err != nil {
		return nil, err
	}
	result := planExecutions
	planExecutions, err = t.listPlanExecutionByStatus(model.ExecutionStatusPrepareBackoff)
	if err != nil {
		klog.ErrorS(err, "pull backoff execs fail")
	} else {
		result = append(result, planExecutions...)
	}
	planExecutions, err = t.listPlanExecutionByStatus(model.ExecutionStatusCancelling)
	if err != nil {
		klog.ErrorS(err, "pull cancelling execs fail")
	} else {
		result = append(result, planExecutions...)
	}
	return result, nil
}

func (t *tcms) listPlanExecutionByStatus(status model.ExecutionStatus) ([]*model.TestPlanExecution, error) {
	result := []*model.TestPlanExecution{}
	offset := 0
	for {
		resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/plan-executions?status=%s&offset=%d&count=%d", string(status), offset, PullLimit)))
		if err != nil {
			return nil, errors.WithStack(err)
		}
		defer resp.Body.Close()
		bodyBytes, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		if resp.StatusCode != http.StatusOK {
			return nil, errors.Errorf("get %s plan-executions: [%d] %s", status, resp.StatusCode, string(bodyBytes))
		}
		var out struct {
			Total int                        `json:"total"`
			Data  []*model.TestPlanExecution `json:"data"`
		}
		if err := json.Unmarshal(bodyBytes, &out); err != nil {
			return nil, errors.WithStack(err)
		}
		if len(out.Data) == 0 {
			break
		} else {
			result = append(result, out.Data...)
			offset += PullLimit
		}
	}
	return result, nil
}

func (t *tcms) ListResourceCohorts(respool string) ([]*model.ResourceCohort, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/resource-cohorts?resourcepool=%s", respool)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("list resource cohorts: [%d] %s", resp.StatusCode, string(bodyBytes))
	}
	result := respM.ResourceCohortListResponse{}
	if err := json.Unmarshal(bodyBytes, &result); err != nil {
		return nil, errors.WithStack(err)
	}
	return result.Data, nil
}

func (t *tcms) ListResourcePools() ([]*model.ResourcePool, error) {
	resp, err := t.http.Get(t.mkURL("/api/v1/resourcepool"))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("list resourcepool: [%d] %s", resp.StatusCode, string(bodyBytes))
	}
	result := respM.ResourcePoolListResponse{}
	if err := json.Unmarshal(bodyBytes, &result); err != nil {
		return nil, errors.WithStack(err)
	}
	return result.Data, nil
}

func (t *tcms) CreateTestBedInfo(tb *model.TestBedInfo) error {
	method := http.MethodPost
	apiURL := t.mkURL("/api/v1/testbeds/info")
	body := new(bytes.Buffer)
	err := json.NewEncoder(body).Encode(tb)
	if err != nil {
		return errors.WithStack(err)
	}
	klog.InfoSDepth(1, "create testbed info in tcms", "api", apiURL, "data", body.String())
	req, err := http.NewRequest(method, apiURL, body)
	if err != nil {
		return errors.WithStack(err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return errors.Errorf("create testbed info %s in tcms error: %s", tb.Name, resp.Status)
	}
	err = json.Unmarshal(bodyBytes, tb)
	if err != nil {
		return errors.WithStack(err)
	}
	return nil
}

func (t *tcms) PutTestBedInfo(tb *model.TestBedInfo) error {
	path := fmt.Sprintf("/api/v1/plan-executions/%d/testbeds/%s/info/status?status=%s",
		tb.PlanExecutionID, tb.Name, tb.Status)
	if tb.InitiatedAt != nil {
		path += fmt.Sprintf("&initiated_at=%d", tb.InitiatedAt.Unix())
	}
	if tb.SetUpLogURL != "" {
		path += fmt.Sprintf("&setup_log_url=%s", tb.SetUpLogURL)
	}
	method := http.MethodPut
	apiURL := t.mkURL(path)

	klog.InfoSDepth(1, "set testbed", "api", apiURL, "setupURL", tb.SetUpLogURL)
	req, err := http.NewRequest(method, apiURL, nil)
	if err != nil {
		return errors.WithStack(err)
	}
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return errors.Errorf("update testbed %s status to %s in tcms error: %s", tb.Name, tb.Status, resp.Status)
	}
	err = json.Unmarshal(bodyBytes, tb)
	if err != nil {
		return errors.WithStack(err)
	}
	return nil
}

func (t *tcms) GetTestbedInfo(testbed string) (*model.TestBedInfo, error) {
	resp, err := t.http.Get(t.mkURL(fmt.Sprintf("/api/v1/testbeds/info?names=%s", testbed)))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.Errorf("get testbed info %s error: [%d] %s", testbed, resp.StatusCode, string(bodyBytes))
	}
	var out struct {
		Total int                  `json:"total"`
		Data  []*model.TestBedInfo `json:"data"`
	}
	if err := json.Unmarshal(bodyBytes, &out); err != nil {
		return nil, errors.WithStack(err)
	}
	if len(out.Data) == 0 {
		return nil, errors.Errorf("no such testbed %s", testbed)
	}
	return out.Data[0], nil
}

func (t *tcms) SyncTestBed(planExecID model.ID, testbed string) error {
	method := http.MethodGet
	apiURL := t.mkURL(fmt.Sprintf("/api/v1/plan-executions/%d/testbeds/%s", planExecID, testbed))

	klog.InfoSDepth(1, "sync testbed", "api", apiURL)
	req, err := http.NewRequest(method, apiURL, nil)
	if err != nil {
		return errors.WithStack(err)
	}
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return errors.Errorf("sync testbed '%s' error: %s", testbed, resp.Status)
	}
	return nil
}

func (t *tcms) ReleaseTestBed(planExecID model.ID, testbed string) error {
	method := http.MethodPut
	apiURL := t.mkURL(fmt.Sprintf("/api/v1/plan-executions/%d/testbeds/%s/release", planExecID, testbed))

	klog.InfoSDepth(1, "release testbed", "api", apiURL)
	req, err := http.NewRequest(method, apiURL, nil)
	if err != nil {
		return errors.WithStack(err)
	}
	resp, err := t.http.Do(req)
	if err != nil {
		return errors.WithStack(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return errors.Errorf("release testbed '%s' error: %s", testbed, resp.Status)
	}
	return nil
}
