package history

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

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

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

// `M` is the type of object.
type HistoryService[M any] interface {
	// CreateHistory creates a history record.
	// It should record the new object everytime when the object was changed.
	Create(ctx context.Context, id model.ID, obj *M, tx *sqlx.Tx, opts *common.HistoryCreateOpts) (revision model.ID, err error)
	Get(ctx context.Context, id model.ID, opts *common.HistoryGetOpts) (obj *M, err error)
	List(ctx context.Context, id model.ID, opts common.HistoryListOpts) (records []model.HistoryRecord, err error)
}

// NewSimpleHistoryService will connect to the `{object}_histories` table in DB.
// `object` is the name of object. e.g. case, plan
func NewSimpleHistoryService[M any](db *sql.Database, object string) HistoryService[M] {
	s := &simpleHistoryService[M]{
		table:    object + "_histories",
		idcolumn: object + "_id",
		db:       db,
	}
	_, err := db.Query("SHOW CREATE TABLE " + s.table)
	if err != nil {
		panic(fmt.Sprintf("show history table of %s failed, err= %v", object, err))
	}
	return s
}

// simpleHistoryService serializes the history object into JSON.
type simpleHistoryService[M any] struct {
	// table is the name of table. e.g. case_histories
	table string

	// idcolumn is the name of id column. e.g. case_id
	idcolumn string

	db *sql.Database
}

func (s *simpleHistoryService[M]) Create(ctx context.Context, id model.ID, obj *M, tx *sqlx.Tx, opts *common.HistoryCreateOpts) (revision model.ID, err error) {
	createdAt := time.Now()
	if opts.Ts != nil {
		createdAt = *opts.Ts
	}
	revision = model.ID(createdAt.UnixMicro())

	var execer sqlx.ExecerContext = s.db
	if tx != nil {
		execer = tx
	}
	_, err = execer.ExecContext(ctx, `INSERT IGNORE INTO `+s.table+` (
		`+s.idcolumn+`,
		 user_id,
		 action,
		 object,
		 revision,
		 created_at,
		 updated_at
	) VALUES (?, ?, ?, ?, ?, ?, ?)`,
		id,
		opts.User,
		opts.Action,
		utils.ToJSON(obj),
		revision,
		createdAt,
		createdAt,
	)
	return revision, err
}

func (s *simpleHistoryService[M]) Get(ctx context.Context, id model.ID, opts *common.HistoryGetOpts) (obj *M, err error) {
	b := sql.Builder.
		Select("object").
		From(s.table).
		Prepared(true).
		Where(goqu.I(s.idcolumn).Eq(id)).
		Order(goqu.I("created_at").Desc()).
		Limit(1)
	if opts.Revision != nil {
		if opts.Ts != nil {
			return nil, fmt.Errorf("ts is uncompcatable with revision")
		}
		b = b.Where(goqu.I("revision").Eq(*opts.Revision))
	} else if opts.Ts != nil {
		b = b.Where(goqu.I("created_at").Lte(*opts.Ts))
	}
	query, args, _ := b.ToSQL()
	bytes := []byte{}
	if err = s.db.GetContext(ctx, &bytes, query, args...); err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		return nil, err
	}
	err = json.Unmarshal(bytes, &obj)
	return obj, err
}

func (s *simpleHistoryService[M]) List(ctx context.Context, id model.ID, opts common.HistoryListOpts) (records []model.HistoryRecord, err error) {
	defaults.MustSet(&opts)
	if opts.Count > 100 {
		opts.Count = 100
	}
	b := sql.Builder.
		Select(
			"revision",
			"created_at",
			"user_id",
			goqu.L("(SELECT COALESCE(users.email, '') FROM users WHERE user_id = users.id) AS user_email"),
			"action",
		).
		From(s.table).
		Prepared(true).
		Where(goqu.I(s.idcolumn).Eq(id)).
		Order(goqu.I("created_at").Desc()).
		Limit(opts.Count).
		Offset(opts.Offset)
	if opts.FromTs != nil {
		b = b.Where(goqu.I("created_at").Gte(*opts.FromTs))
	}
	if opts.ToTs != nil {
		b = b.Where(goqu.I("created_at").Lte(*opts.ToTs))
	}
	query, args, _ := b.ToSQL()
	if err = s.db.SelectContext(ctx, &records, query, args...); err != nil {
		log.Log.Errorf(err, "list history failed")
		return nil, err
	}
	return records, nil
}
