// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package dataprovider

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

	"github.com/alexedwards/argon2id"
	"golang.org/x/crypto/bcrypt"

	"github.com/drakkan/sftpgo/v2/internal/logger"
	"github.com/drakkan/sftpgo/v2/internal/util"
)

// APIKeyScope defines the supported API key scopes
type APIKeyScope int

// Supported API key scopes
const (
	// the API key will be used for an admin
	APIKeyScopeAdmin APIKeyScope = iota + 1
	// the API key will be used for a user
	APIKeyScopeUser
)

// APIKey defines a SFTPGo API key.
// API keys can be used as authentication alternative to short lived tokens
// for REST API
type APIKey struct {
	// Database unique identifier
	ID int64 `json:"-"`
	// Unique key identifier, used for key lookups.
	// The generated key is in the format `KeyID.hash(Key)` so we can split
	// and lookup by KeyID and then verify if the key matches the recorded hash
	KeyID string `json:"id"`
	// User friendly key name
	Name string `json:"name"`
	// we store the hash of the key, this is just like a password
	Key       string      `json:"key,omitempty"`
	Scope     APIKeyScope `json:"scope"`
	CreatedAt int64       `json:"created_at"`
	UpdatedAt int64       `json:"updated_at"`
	// 0 means never used
	LastUseAt int64 `json:"last_use_at,omitempty"`
	// 0 means never expire
	ExpiresAt   int64  `json:"expires_at,omitempty"`
	Description string `json:"description,omitempty"`
	// Username associated with this API key.
	// If empty and the scope is APIKeyScopeUser the key is valid for any user
	User string `json:"user,omitempty"`
	// Admin username associated with this API key.
	// If empty and the scope is APIKeyScopeAdmin the key is valid for any admin
	Admin string `json:"admin,omitempty"`
	// these fields are for internal use
	userID   int64
	adminID  int64
	plainKey string
}

func (k *APIKey) getACopy() APIKey {
	return APIKey{
		ID:          k.ID,
		KeyID:       k.KeyID,
		Name:        k.Name,
		Key:         k.Key,
		Scope:       k.Scope,
		CreatedAt:   k.CreatedAt,
		UpdatedAt:   k.UpdatedAt,
		LastUseAt:   k.LastUseAt,
		ExpiresAt:   k.ExpiresAt,
		Description: k.Description,
		User:        k.User,
		Admin:       k.Admin,
		userID:      k.userID,
		adminID:     k.adminID,
	}
}

// RenderAsJSON implements the renderer interface used within plugins
func (k *APIKey) RenderAsJSON(reload bool) ([]byte, error) {
	if reload {
		apiKey, err := provider.apiKeyExists(k.KeyID)
		if err != nil {
			providerLog(logger.LevelError, "unable to reload api key before rendering as json: %v", err)
			return nil, err
		}
		apiKey.HideConfidentialData()
		return json.Marshal(apiKey)
	}
	k.HideConfidentialData()
	return json.Marshal(k)
}

// HideConfidentialData hides API key confidential data
func (k *APIKey) HideConfidentialData() {
	k.Key = ""
}

func (k *APIKey) hashKey() error {
	if k.Key != "" && !util.IsStringPrefixInSlice(k.Key, internalHashPwdPrefixes) {
		if config.PasswordHashing.Algo == HashingAlgoBcrypt {
			hashed, err := bcrypt.GenerateFromPassword([]byte(k.Key), config.PasswordHashing.BcryptOptions.Cost)
			if err != nil {
				return err
			}
			k.Key = string(hashed)
		} else {
			hashed, err := argon2id.CreateHash(k.Key, argon2Params)
			if err != nil {
				return err
			}
			k.Key = hashed
		}
	}
	return nil
}

func (k *APIKey) generateKey() {
	if k.KeyID != "" || k.Key != "" {
		return
	}
	k.KeyID = util.GenerateUniqueID()
	k.Key = util.GenerateUniqueID()
	k.plainKey = k.Key
}

// DisplayKey returns the key to show to the user
func (k *APIKey) DisplayKey() string {
	return fmt.Sprintf("%v.%v", k.KeyID, k.plainKey)
}

func (k *APIKey) validate() error {
	if k.Name == "" {
		return util.NewValidationError("name is mandatory")
	}
	if k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {
		return util.NewValidationError(fmt.Sprintf("invalid scope: %v", k.Scope))
	}
	k.generateKey()
	if err := k.hashKey(); err != nil {
		return err
	}
	if k.User != "" && k.Admin != "" {
		return util.NewValidationError("an API key can be related to a user or an admin, not both")
	}
	if k.Scope == APIKeyScopeAdmin {
		k.User = ""
	}
	if k.Scope == APIKeyScopeUser {
		k.Admin = ""
	}
	if k.User != "" {
		_, err := provider.userExists(k.User, "")
		if err != nil {
			return util.NewValidationError(fmt.Sprintf("unable to check API key user %v: %v", k.User, err))
		}
	}
	if k.Admin != "" {
		_, err := provider.adminExists(k.Admin)
		if err != nil {
			return util.NewValidationError(fmt.Sprintf("unable to check API key admin %v: %v", k.Admin, err))
		}
	}
	return nil
}

// Authenticate tries to authenticate the provided plain key
func (k *APIKey) Authenticate(plainKey string) error {
	if k.ExpiresAt > 0 && k.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
		return fmt.Errorf("API key %q is expired, expiration timestamp: %v current timestamp: %v", k.KeyID,
			k.ExpiresAt, util.GetTimeAsMsSinceEpoch(time.Now()))
	}
	if config.PasswordCaching {
		found, match := cachedAPIKeys.Check(k.KeyID, plainKey, k.Key)
		if found {
			if !match {
				return ErrInvalidCredentials
			}
			return nil
		}
	}
	if strings.HasPrefix(k.Key, bcryptPwdPrefix) {
		if err := bcrypt.CompareHashAndPassword([]byte(k.Key), []byte(plainKey)); err != nil {
			return ErrInvalidCredentials
		}
	} else if strings.HasPrefix(k.Key, argonPwdPrefix) {
		match, err := argon2id.ComparePasswordAndHash(plainKey, k.Key)
		if err != nil || !match {
			return ErrInvalidCredentials
		}
	}

	cachedAPIKeys.Add(k.KeyID, plainKey, k.Key)
	return nil
}
