package store

import (
	"context"
	"fmt"
	"strings"
	"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"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
	"github.com/pingcap/test-infra/services/tcms/utils/sql"
)

const SvcTaskTableName = "svc_tasks"

var svcTaskFields = []string{
	"create_at", "update_at", "create_by_id", "update_by_id", "name", "description", "inputs", "outputs", "implementation",
}
var svc_task_insert_query string
var svc_task_update_query string

func init() {
	fieldStr := strings.Join(svcTaskFields, ", ")
	var holders []string
	for _, field := range svcTaskFields {
		holders = append(holders, ":"+field)
	}

	holderStr := strings.Join(holders, ",")
	svc_task_insert_query = fmt.Sprintf("insert into %s (%s) values (%s)", SvcTaskTableName, fieldStr, holderStr)

	var mutations []string
	for _, field := range svcTaskFields {
		if field == "create_at" || field == "create_by_id" {
			continue
		}
		mutations = append(mutations, fmt.Sprintf("%s = :%s", field, field))
	}
	mutateStr := strings.Join(mutations, ",")
	svc_task_update_query = fmt.Sprintf("update %s set %s where id = :id", SvcTaskTableName, mutateStr)
}

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

func fields2interfaces(arg []string) []interface{} {
	rt := make([]interface{}, 0, len(arg)+1)
	rt = append(rt, "id")
	for _, s := range arg {
		rt = append(rt, s)
	}
	return rt
}

func (r *svcTaskService) List(ctx context.Context, opts taskruns.ListSvcTaskOpts) (*taskruns.SvcTaskList, error) {
	rt := &taskruns.SvcTaskList{}
	ct := utils.GetUtilContext(ctx)
	stmt := sql.Builder.From(SvcTaskTableName)

	stmt = stmt.Select(fields2interfaces(svcTaskFields)...).
		Prepared(true).
		Order(goqu.I("id").Desc()).
		Offset(opts.Offset).
		Limit(opts.Count)
	query, args, err := stmt.ToSQL()
	if err != nil {
		return nil, err
	}
	var dbos []SvcTaskDBO
	if err := r.db.SelectContext(ctx, &dbos, query, args...); err != nil {
		ct.Log().Errorf(err, "list cases failed")
		return nil, err
	}
	rt.Items = make([]taskruns.SvcTask, 0, len(dbos))
	for _, dbo := range dbos {
		apiObj, err := dbo.To()
		if err != nil {
			return nil, err
		}
		rt.Items = append(rt.Items, *apiObj)
	}
	rt.FillHeader()
	return rt, nil
}

func (r *svcTaskService) Get(ctx context.Context, id model.ID, opts taskruns.GetSvcTaskOpts) (*taskruns.SvcTask, error) {
	obj, err := getSvcTaskByID(ctx, id, r.db)
	if err != nil && sql.IsErrNoRows(err) {
		return nil, serror.New(404, err)
	}
	return obj, err
}

func (r *svcTaskService) GetByName(ctx context.Context, name string, opts taskruns.GetSvcTaskOpts) (*taskruns.SvcTask, error) {
	dbo := SvcTaskDBO{}
	err := r.db.GetContext(ctx, &dbo, "SELECT * FROM "+SvcTaskTableName+" WHERE name = ?", name)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, serror.New(404, err)
		}
		return nil, err
	}
	return dbo.To()
}

func (r *svcTaskService) Update(ctx context.Context, obj taskruns.SvcTask, opts taskruns.UpdateSvcTaskOpts) (*taskruns.SvcTask, error) {
	obj.MetaData.UpdateAt = r.now()
	if user := utils.GetUtilContext(ctx).User(); user != nil {
		obj.MetaData.UpdateByID = user.ID
	}
	var rt *taskruns.SvcTask
	err := r.db.Transaction(ctx, func(tx *sqlx.Tx) error {
		dbo := &SvcTaskDBO{}
		err := dbo.From(obj)
		if err != nil {
			return err
		}
		updated, err := tx.NamedExecContext(ctx, svc_task_update_query, dbo)
		if err != nil {
			return err
		}
		rows, err := updated.RowsAffected()
		if err != nil {
			return err
		}
		if rows != 1 {
			return fmt.Errorf("no such rows")
		}
		obj, err := getSvcTaskByID(ctx, obj.MetaData.ID, tx)
		if err != nil {
			return err
		}
		rt = obj
		return nil
	})
	if err != nil {
		return nil, err
	}
	rt.FillHeader()
	return rt, nil
}

func (r *svcTaskService) Create(ctx context.Context, obj taskruns.SvcTask, opts taskruns.CreateSvcTaskOpts) (*taskruns.SvcTask, error) {
	now := r.now()
	obj.MetaData.CreateAt = now
	obj.MetaData.UpdateAt = now
	var userID model.ID
	if user := utils.GetUtilContext(ctx).User(); user != nil {
		obj.MetaData.UpdateByID = user.ID
	}
	obj.MetaData.UpdateByID = userID
	obj.MetaData.CreateByID = userID
	var rt *taskruns.SvcTask
	if obj.MetaData.ID != 0 {
		return nil, fmt.Errorf("ID must not be provided")
	}
	err := r.db.Transaction(ctx, func(tx *sqlx.Tx) error {
		dbo := &SvcTaskDBO{}
		err := dbo.From(obj)
		if err != nil {
			return err
		}
		inserted, err := tx.NamedExecContext(ctx, svc_task_insert_query, dbo)
		if err != nil {
			return err
		}
		id, err := inserted.LastInsertId()
		if err != nil {
			return err
		}
		obj, err := getSvcTaskByID(ctx, model.ID(id), tx)
		if err != nil {
			return err
		}
		rt = obj
		return nil
	})
	if err != nil {
		return nil, err
	}
	return rt, nil
}

func (r *svcTaskService) Delete(ctx context.Context, id model.ID, opts taskruns.DeleteSvcTaskOpts) (rt *taskruns.SvcTask, err error) {
	err = r.db.Transaction(ctx, func(tx *sqlx.Tx) error {
		rt, err = getSvcTaskByID(ctx, id, tx)
		if err != nil {
			return serror.New(404, err)
		}
		_, err := tx.ExecContext(ctx, "delete from "+SvcTaskTableName+" where id = ?", id)
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return
}

func getSvcTaskByID(ctx context.Context, id model.ID, q sqlx.QueryerContext) (rt *taskruns.SvcTask, err error) {
	dbo := SvcTaskDBO{}
	err = sqlx.GetContext(ctx, q, &dbo, "SELECT * FROM "+SvcTaskTableName+" WHERE id = ?", int(id))
	if err != nil {
		return
	}
	return dbo.To()
}

func NewSvcTaskService(db *sql.Database) *svcTaskService {
	return &svcTaskService{db: db, now: time.Now}
}
