package store

import (
	"strconv"
	"time"

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

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/services/tcms/db"
	"github.com/pingcap/test-infra/services/tcms/service/common"
	. "github.com/pingcap/test-infra/services/tcms/service/common"
	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"
)

var (
	maxUserTokenCountStr = strconv.Itoa(MAX_USER_TOKEN_COUNT)
)

func (s *tokenService) init(db *sql.Database) {
	s.db = db
}

type tokenService struct {
	db *sql.Database
}

func (s *tokenService) Validate(ctx utils.Context, t string) (*model.PersonalToken, error) {
	hashedToken, err := common.TrimAndHashToken(t)
	if err != nil {
		return nil, err
	}

	pat, err := s.get(ctx, hashedToken)
	if err != nil {
		ctx.Log().Errorf(err, "get personal access token failed")
		return nil, err
	} else if pat == nil {
		return nil, serror.ErrInvalidToken
	}

	// if pat.ExpiresAt != nil && int64(*pat.ExpiresAt) <= time.Now().Unix() {
	// 	return nil, serror.ErrInvalidToken
	// }

	info := &model.PersonalToken{
		ID:     pat.ID,
		UserID: pat.UserID,
		Scopes: pat.Scopes,
	}
	return info, nil
}

func (s *tokenService) List(ctx utils.Context, userID model.ID) ([]*model.PersonalToken, error) {
	tokens := []*db.PersonalToken{}
	err := s.db.SelectContext(ctx.Request().Context(), &tokens, "SELECT id, name, last_used_at, scopes, created_at, updated_at FROM personal_tokens WHERE user_id = ? ORDER BY created_at desc", userID)
	if err != nil {
		return nil, err
	}
	res := make([]*model.PersonalToken, len(tokens))
	for i, t := range tokens {
		var lastUsedAt *time.Time
		if t.LastUsedAt != nil {
			t := time.Unix(*t.LastUsedAt, 0)
			lastUsedAt = &t
		}
		res[i] = &model.PersonalToken{
			ID:         t.ID,
			Name:       t.Name,
			Scopes:     t.Scopes,
			LastUsedAt: lastUsedAt,
			CreatedAt:  &t.CreatedAt,
			UpdatedAt:  &t.UpdatedAt,
		}
	}
	return res, nil
}

func (s *tokenService) Create(ctx utils.Context, t *model.PersonalToken) (string, error) {
	canCreate := false
	err := s.db.Get(&canCreate, "SELECT COUNT(*)<"+maxUserTokenCountStr+" FROM personal_tokens WHERE user_id = ?", t.UserID)
	if err != nil {
		return "", err
	} else if !canCreate {
		return "", serror.ErrCreatePersonalTokenLimited
	}

	token, hashedToken := GenerateToken()

	_, err = s.db.Exec(`INSERT INTO personal_tokens (user_id, name, hashed_token, scopes) 
			VALUES (?, ?, ?, ?)`, t.UserID, t.Name, hashedToken, t.Scopes)
	if err != nil {
		return "", err
	}

	return TOKEN_PREFIX + token, err
}

func (s *tokenService) UpdateLastUsedAt(id model.ID) error {
	now := time.Now().Unix()
	_, err := s.db.Exec("UPDATE personal_tokens SET last_used_at=? WHERE id=? AND last_used_at < ?", now, id, now)
	return err
}

func (s *tokenService) Delete(ctx utils.Context, userID model.ID, ids []model.ID) ([]string, error) {
	where := goqu.Ex{"user_id": userID}
	if len(ids) > 0 {
		where["id"] = ids
	}

	// query the tokens that should be deleted
	var tokenIDs []model.ID
	var hashedTokens []string
	query, args, _ := sql.Builder.Select("id", "hashed_token").From("personal_tokens").Where(where).ToSQL()
	rows, err := s.db.Queryx(query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var id model.ID
		var token string
		if err := rows.Scan(&id, &token); err != nil {
			return nil, err
		}
		tokenIDs = append(tokenIDs, id)
		hashedTokens = append(hashedTokens, token)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(tokenIDs) == 0 {
		return nil, nil
	}

	query, args, _ = sqlx.In("DELETE FROM personal_tokens WHERE id IN (?)", tokenIDs)
	_, err = s.db.Exec(query, args...)
	if err != nil {
		return nil, err
	}

	return hashedTokens, err
}

func (s *tokenService) get(ctx utils.Context, hashedToken string) (*db.PersonalToken, error) {
	t := &db.PersonalToken{}
	err := s.db.GetContext(ctx.Request().Context(), t, "SELECT id, user_id, scopes FROM personal_tokens WHERE hashed_token=?", hashedToken)
	if err != nil {
		if sql.IsErrNoRows(err) {
			return nil, nil
		}
		return nil, err
	}
	return t, err
}
