package apikey

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base32"
	"encoding/hex"
	"fmt"
	"io"
	"strings"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"github.com/tidbcloud/central-sdk/db/tenant"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const BearerAPIKeyPrefix = "APIKEY_"

type Model struct {
	log *zap.Logger
}

type UserContext struct {
	Model
	tenantAndUser *tenant.User
	log           *zap.Logger
}

// User returns a UserContext
func (m Model) User(user *tenant.User) UserContext {
	return UserContext{
		m,
		user,
		m.log.With(zap.String("userID", user.Email), zap.String("model", "apiKey")),
	}
}

func hashKey(plainKey string) string {
	keyHashBytes := sha256.Sum256([]byte(plainKey))
	return hex.EncodeToString(keyHashBytes[:])
}

// CreateAPIKey creates an API key and returns API key in plain text. Currently we will create a bearer API key.
func CreateAPIKey(tenantID uint64, creatorUserID uint64, keyName string) (string, error) {
	if len(keyName) == 0 {
		return "", errors.New("key name must be specified")
	}

	encoder := base32.StdEncoding.WithPadding(base32.NoPadding)
	apiKeyBytes := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, apiKeyBytes); err != nil {
		return "", errors.Annotate(err, "failed to generate api key")
	}
	apiKey := encoder.EncodeToString(apiKeyBytes)

	apiKeyEntity := entity.APIKey{
		TenantID:    entity.ID(tenantID),
		CreatedByID: entity.ID(creatorUserID),
		Name:        keyName,
		KeyHash:     hashKey(apiKey),
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		var user entity.User
		if err := entity.NewUserQuerySet(tx).IDEq(apiKeyEntity.CreatedByID).One(&user); err != nil {
			return errors.Annotate(err, "failed to query creator id")
		}
		if err := (&apiKeyEntity).Create(tx); err != nil {
			// TODO: Very little chance that it is caused by key duplicate.
			return errors.Annotate(err, "failed to create api key record")
		}

		return dbutil.AddAccessLogNow(tx, apiKeyEntity.TenantID, user.Email, fmt.Sprintf("create api key: %s", keyName))
	})

	if err != nil {
		return "", err
	}

	return BearerAPIKeyPrefix + apiKey, nil
}

// CreateAPIKey creates an API key and returns API key in plain text.
func (c UserContext) CreateAPIKey(keyName string) (string, error) {
	return CreateAPIKey(c.tenantAndUser.TenantID.Uint64(), c.tenantAndUser.User.ID.Uint64(), keyName)
}

// AuthenticateAPIKey receives an API key in plain text and checks whether API key is valid.
// Note that this function does not do authorization, i.e. it does not verify whether API key matches the tenant.
func AuthenticateAPIKey(plainAPIKey string) (*entity.APIKey, error) {
	if !strings.HasPrefix(plainAPIKey, BearerAPIKeyPrefix) {
		return nil, nil
	}
	plainAPIKey = plainAPIKey[len(BearerAPIKeyPrefix):]
	var h entity.APIKey

	err := db.Transaction(func(tx *gorm.DB) error {
		if err := entity.
			NewAPIKeyQuerySet(tx).
			KeyHashEq(hashKey(plainAPIKey)).
			One(&h); err != nil {
			if gorm.ErrRecordNotFound == err {
				return nil
			}
			return err
		}
		return nil
	})

	if err != nil {
		return nil, err
	}
	return &h, nil
}
