package store

import (
	"context"
	"encoding/json"
	"time"

	"github.com/doug-martin/goqu/v9"
	"github.com/jmoiron/sqlx"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/taskruns"
	"github.com/pingcap/test-infra/services/tcms/db"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
)

type testbedSetupService struct {
	db  *sql.Database
	now func() time.Time
}

func (s *testbedSetupService) init(db *sql.Database) {
	s.db = db
	s.now = time.Now
}

func (s testbedSetupService) List(ctx utils.Context, opts taskruns.ListTestbedSetupOpts) (*taskruns.TestbedSetupList, error) {
	rt := taskruns.NewTestbedSetupList()
	c := ctx.Request().Context()
	stmt := sql.Builder.From("testbed_setups")

	if opts.ScheduleStatus != nil {
		stmt = stmt.Where(goqu.I("schedule_status").Eq(*opts.ScheduleStatus))
	}
	if opts.RunPhase != nil {
		stmt = stmt.Where(goqu.I("run_phase").Eq(*opts.RunPhase))
	}
	if len(opts.PlanExecIDs) != 0 {
		stmt = stmt.Where(goqu.I("plan_exec_id").In(opts.PlanExecIDs))
	}
	if len(opts.IDs) != 0 {
		stmt = stmt.Where(goqu.I("id").In(opts.IDs))
	}

	stmt = stmt.Select("id", "body").
		Prepared(true).
		Order(goqu.I("id").Desc()).
		Offset(opts.Offset)
	if opts.Count != 0 {
		stmt = stmt.Limit(opts.Count)
	}
	query, args, _ := stmt.ToSQL()
	var setupDBOs []db.TestbedSetup
	if err := s.db.SelectContext(c, &setupDBOs, query, args...); err != nil {
		ctx.Log().Errorf(err, "list cases failed")
		return nil, err
	}
	rt.Items = make([]taskruns.TestbedSetup, 0)
	for _, dbo := range setupDBOs {
		apiObj, err := db2api(dbo)
		if err != nil {
			return nil, err
		}
		rt.Items = append(rt.Items, *apiObj)
	}
	return &rt, nil
}

func (s testbedSetupService) Get(ctx utils.Context, id model.ID, opts taskruns.GetTestbedSetupOpts) (*taskruns.TestbedSetup, error) {
	rt, err := getTestbedByID(ctx.Request().Context(), id, s.db)
	if sql.IsErrNoRows(err) {
		return nil, nil
	}
	return rt, err
}

func (s testbedSetupService) Create(ctx utils.Context, run taskruns.TestbedSetup, opts taskruns.CreateTestbedSetupOpts) (rt *taskruns.TestbedSetup, err error) {
	run.MetaData.CreateAt = s.now()
	// todo use service would cause cycle import
	// user, err := s.userService.Get(ctx, ctx.User().ID)
	if run.Status.Schedule == nil {
		run.Status.Schedule = &taskruns.Schedule{ScheduleStatus: taskruns.ScheduleStatusPending}
	}
	if opts.DryRun {
		return &run, nil
	}
	createF := func(tx *sqlx.Tx) error {
		var err error
		rt, err = createTestbedSetup(ctx.Request().Context(), tx, run)
		return err
	}
	err = s.db.Transaction(ctx.Request().Context(), createF)
	return rt, err
}

func (s testbedSetupService) UpdateSchedule(ctx utils.Context, run taskruns.TestbedSetup, opts taskruns.GeneralUpdateOpts) (*taskruns.TestbedSetup, error) {
	return s.patchTestbedSetup(ctx, run, func(old taskruns.TestbedSetup, patch taskruns.TestbedSetup) taskruns.TestbedSetup {
		old.Status.Schedule = patch.Status.Schedule
		return old
	}, opts)
}

func (s testbedSetupService) UpdateRun(ctx utils.Context, run taskruns.TestbedSetup, opts taskruns.GeneralUpdateOpts) (*taskruns.TestbedSetup, error) {
	return s.patchTestbedSetup(ctx, run, func(old taskruns.TestbedSetup, patch taskruns.TestbedSetup) taskruns.TestbedSetup {
		old.Status.TaskRun = patch.Status.TaskRun
		return old
	}, opts)
}

func (s testbedSetupService) UpdateReport(ctx utils.Context, run taskruns.TestbedSetup, opts taskruns.GeneralUpdateOpts) (*taskruns.TestbedSetup, error) {
	return s.patchTestbedSetup(ctx, run, func(old taskruns.TestbedSetup, patch taskruns.TestbedSetup) taskruns.TestbedSetup {
		old.Status.Report = patch.Status.Report
		return old
	}, opts)
}

type patcher func(old taskruns.TestbedSetup, patch taskruns.TestbedSetup) taskruns.TestbedSetup

func (s testbedSetupService) patchTestbedSetup(ctx utils.Context, patch taskruns.TestbedSetup, patcher patcher, opts taskruns.GeneralUpdateOpts) (rt *taskruns.TestbedSetup, err error) {
	if opts.DryRun {
		return &patch, nil
	}
	updateF := func(tx *sqlx.Tx) error {
		id := patch.MetaData.ID
		c := ctx.Request().Context()
		old, err := getTestbedByID(c, id, tx)
		if err != nil {
			return err
		}
		newObj := patcher(*old, patch)
		newObj.MetaData.UpdateAt = s.now()
		err = updateTestbedSetup(c, tx, newObj)
		if err != nil {
			return err
		}
		rt, err = getTestbedByID(c, id, tx)
		return err
	}
	err = s.db.Transaction(ctx.Request().Context(), updateF)
	return rt, err
}

func createTestbedSetup(ctx context.Context, e sqlx.ExtContext, setup taskruns.TestbedSetup) (rt *taskruns.TestbedSetup, err error) {
	dbo, err := api2db(setup)
	if err != nil {
		return rt, err
	}
	var query string
	var args []interface{}
	if dbo.ID != 0 {
		query = "INSERT INTO testbed_setups(id, body) VALUES (?, ?)"
		args = []interface{}{dbo.ID, dbo.Body}
	} else {
		query = "INSERT INTO testbed_setups(body) VALUES (?)"
		args = []interface{}{dbo.Body}
	}
	exec, err := e.ExecContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	id, err := exec.LastInsertId()
	if err != nil {
		return nil, err
	}
	return getTestbedByID(ctx, model.ID(id), e)
}

func updateTestbedSetup(ctx context.Context, e sqlx.ExecerContext, setup taskruns.TestbedSetup) error {
	dbo, err := api2db(setup)
	if err != nil {
		return err
	}
	query := "UPDATE testbed_setups SET body = ? WHERE id = ?"
	args := []interface{}{dbo.Body, int(dbo.ID)}
	_, err = e.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}
	return nil
}

func getTestbedByID(ctx context.Context, id model.ID, q sqlx.QueryerContext) (rt *taskruns.TestbedSetup, err error) {
	dbo := db.TestbedSetup{}
	err = sqlx.GetContext(ctx, q, &dbo, "SELECT id, body FROM testbed_setups WHERE id = ?", int(id))
	if err != nil {
		return
	}
	rt, err = db2api(dbo)
	return rt, err
}

func db2api(dbo db.TestbedSetup) (*taskruns.TestbedSetup, error) {
	rt := taskruns.NewTestbedSetup()
	err := json.Unmarshal(dbo.Body, &rt)
	if err != nil {
		return nil, err
	}
	rt.MetaData.ID = dbo.ID
	return &rt, nil
}

func api2db(apiObj taskruns.TestbedSetup) (rt db.TestbedSetup, err error) {
	marshal, err := json.Marshal(apiObj)
	if err != nil {
		return
	}
	rt.Body = marshal
	rt.ID = apiObj.MetaData.ID
	return
}
