// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package auth

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"time"

	"forgejo.org/models/db"
	"forgejo.org/modules/optional"
	"forgejo.org/modules/timeutil"
	"forgejo.org/modules/util"
)

type AuthorizationPurpose string

var (
	// Used to store long term authorization tokens.
	LongTermAuthorization AuthorizationPurpose = "long_term_authorization"

	// Long-term token for OAuth2/OIDC sign-ins; bounces through the IdP rather than auto-logging in.
	LongTermAuthorizationSSO AuthorizationPurpose = "long_term_authorization_sso"

	// Used to activate a user account.
	UserActivation AuthorizationPurpose = "user_activation"

	// Used to reset the password.
	PasswordReset AuthorizationPurpose = "password_reset"
)

// Used to activate the specified email address for a user.
func EmailActivation(email string) AuthorizationPurpose {
	return AuthorizationPurpose("email_activation:" + email)
}

// AuthorizationToken represents a authorization token to a user.
type AuthorizationToken struct {
	ID              int64  `xorm:"pk autoincr"`
	UID             int64  `xorm:"INDEX REFERENCES(user, id)"`
	LookupKey       string `xorm:"INDEX UNIQUE"`
	HashedValidator string
	Purpose         AuthorizationPurpose   `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
	LoginSourceID   optional.Option[int64] `xorm:"INDEX REFERENCES(login_source, id)"`
	Expiry          timeutil.TimeStamp
}

// TableName provides the real table name.
func (AuthorizationToken) TableName() string {
	return "forgejo_auth_token"
}

func init() {
	db.RegisterModel(new(AuthorizationToken))
}

// IsExpired returns if the authorization token is expired.
func (authToken *AuthorizationToken) IsExpired() bool {
	return authToken.Expiry.AsLocalTime().Before(time.Now())
}

// GenerateAuthToken generates a new authentication token for the given user.
// It returns the lookup key and validator values that should be passed to the
// user via a long-term cookie.
func GenerateAuthToken(ctx context.Context, userID int64, loginSourceID optional.Option[int64], expiry timeutil.TimeStamp, purpose AuthorizationPurpose) (lookupKey, validator string, err error) {
	// Request 64 random bytes. The first 32 bytes will be used for the lookupKey
	// and the other 32 bytes will be used for the validator.
	rBytes := util.CryptoRandomBytes(64)
	hexEncoded := hex.EncodeToString(rBytes)
	validator, lookupKey = hexEncoded[64:], hexEncoded[:64]

	_, err = db.GetEngine(ctx).Insert(&AuthorizationToken{
		UID:             userID,
		Expiry:          expiry,
		LookupKey:       lookupKey,
		HashedValidator: HashValidator(rBytes[32:]),
		Purpose:         purpose,
		LoginSourceID:   loginSourceID,
	})
	return lookupKey, validator, err
}

// FindAuthToken will find a authorization token via the lookup key.
func FindAuthToken(ctx context.Context, lookupKey string, purpose AuthorizationPurpose) (*AuthorizationToken, error) {
	var authToken AuthorizationToken
	has, err := db.GetEngine(ctx).Where("lookup_key = ? AND purpose = ?", lookupKey, purpose).Get(&authToken)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, fmt.Errorf("lookup key %q: %w", lookupKey, util.ErrNotExist)
	}
	return &authToken, nil
}

// DeleteAuthToken will delete the authorization token.
func DeleteAuthToken(ctx context.Context, authToken *AuthorizationToken) error {
	_, err := db.DeleteByBean(ctx, authToken)
	return err
}

// DeleteAuthTokenByUser will delete all authorization tokens for the user.
func DeleteAuthTokenByUser(ctx context.Context, userID int64) error {
	if userID == 0 {
		return nil
	}

	_, err := db.DeleteByBean(ctx, &AuthorizationToken{UID: userID})
	return err
}

// HashValidator will return a hexified hashed version of the validator.
func HashValidator(validator []byte) string {
	h := sha256.New()
	h.Write(validator)
	return hex.EncodeToString(h.Sum(nil))
}
