package entity

import (
	"bytes"
	"errors"
	"fmt"
	"image"
	"image/png"
	"time"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"

	"github.com/photoprism/photoprism/pkg/authn"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/rnd"
)

// Passcode represents a two-factor authentication key.
type Passcode struct {
	UID          string     `gorm:"type:VARBINARY(255);primary_key;" json:"UID"`
	KeyType      string     `gorm:"size:64;default:'';primary_key;" json:"Type" yaml:"Type"`
	KeyURL       string     `gorm:"size:2048;default:'';column:key_url;" json:"-" yaml:"-"`
	key          *otp.Key   `gorm:"-" yaml:"-"`
	RecoveryCode string     `gorm:"size:255;default:'';" json:"-" yaml:"-"`
	VerifiedAt   *time.Time `json:"VerifiedAt" yaml:"-"`
	ActivatedAt  *time.Time `json:"ActivatedAt" yaml:"-"`
	CreatedAt    time.Time  `json:"CreatedAt" yaml:"-"`
	UpdatedAt    time.Time  `json:"UpdatedAt" yaml:"-"`
}

// TableName returns the entity table name.
func (Passcode) TableName() string {
	return "passcodes"
}

// NewPasscode returns a new two-factor authentication key or nil if no valid entity UID was provided.
func NewPasscode(uid string, keyUrl, recoveryCode string) (*Passcode, error) {
	// Create new authentication key.
	m := &Passcode{
		UID:          uid,
		KeyURL:       keyUrl,
		RecoveryCode: clean.Token(recoveryCode),
		CreatedAt:    Now(),
		UpdatedAt:    Now(),
		VerifiedAt:   nil,
		ActivatedAt:  nil,
	}

	// Return an error if the uid or key are invalid.
	if rnd.InvalidUID(uid, 0) {
		return m, errors.New("auth: invalid uid")
	} else if keyUrl == "" {
		return m, errors.New("auth: invalid url")
	} else if err := m.SetKeyURL(keyUrl); err != nil {
		return m, err
	}

	return m, nil
}

// FindPasscode returns the matching key or nil if it was not found.
func FindPasscode(find Passcode) *Passcode {
	m := &Passcode{}

	keyType := authn.Key(find.KeyType)

	// Build query.
	stmt := UnscopedDb()
	if rnd.IsUID(find.UID, 0) {
		stmt = stmt.Where("uid = ? AND key_type = ?", find.UID, keyType.String())
	} else {
		return nil
	}

	// Find matching record.
	if err := stmt.First(m).Error; err != nil {
		return nil
	}

	return m
}

// Create new entity in the database.
func (m *Passcode) Create() (err error) {
	return Db().Create(m).Error
}

// Save updates the record in the database or inserts a new record if it does not exist yet.
func (m *Passcode) Save() (err error) {
	return UnscopedDb().Save(m).Error
}

// Delete deletes the entity record.
func (m *Passcode) Delete() (err error) {
	if m == nil {
		return fmt.Errorf("entity is nil")
	} else if m.UID == "" {
		return fmt.Errorf("uid not set")
	}

	err = UnscopedDb().Delete(m).Error

	return err
}

// Updates multiple properties in the database.
func (m *Passcode) Updates(values interface{}) error {
	return UnscopedDb().Model(m).Updates(values).Error
}

// SetUID assigns a valid entity UID.
func (m *Passcode) SetUID(uid string) *Passcode {
	if rnd.IsUID(uid, 0) {
		m.UID = uid
	}

	return m
}

// InvalidUID checks if the entity UID is invalid.
func (m *Passcode) InvalidUID() bool {
	if m == nil {
		return true
	}

	return !rnd.IsUID(m.UID, 0)
}

// Key returns the parsed two-factor authentication key or nil if the KeyURL is invalid.
func (m *Passcode) Key() *otp.Key {
	if m == nil {
		return nil
	} else if m.key != nil {
		return m.key
	}

	key, err := otp.NewKeyFromURL(m.KeyURL)

	if err != nil {
		return nil
	}

	m.key = key

	return m.key
}

// SetKey sets a new two-factor authentication key.
func (m *Passcode) SetKey(key *otp.Key) error {
	if key == nil {
		return errors.New("auth: key is nil")
	}

	if keyType := key.Type(); authn.KeyTOTP.NotEqual(keyType) {
		return fmt.Errorf("auth: invalid key type %s", clean.Log(keyType))
	} else if key.Secret() == "" {
		return errors.New("auth: invalid key secret")
	}

	m.KeyType = key.Type()
	m.KeyURL = key.URL()
	m.key = key

	return nil
}

// SetKeyURL sets a new two-factor authentication key based on the URL provided.
func (m *Passcode) SetKeyURL(keyUrl string) error {
	key, err := otp.NewKeyFromURL(keyUrl)

	if err != nil {
		return fmt.Errorf("auth: %s", err)
	} else if key == nil {
		return errors.New("auth: failed to parse url")
	}

	return m.SetKey(key)
}

// Secret returns the key secret or an empty string if none is set.
func (m *Passcode) Secret() string {
	if m == nil {
		return ""
	}

	key := m.Key()

	if key == nil {
		return ""
	}

	return key.Secret()
}

// Type returns the normalized key type.
func (m *Passcode) Type() authn.KeyType {
	if m == nil {
		return ""
	}

	return authn.Key(m.KeyType)
}

// GenerateCode returns a TOTP passcode for the current instant (primarily used in tests).
func (m *Passcode) GenerateCode() (code string, err error) {
	if m == nil {
		return "", errors.New("passcode is nil")
	}

	// Get authentication key.
	key := m.Key()

	if key == nil {
		return "", authn.ErrInvalidPasscodeKey
	}

	// Generate code depending on key type.
	switch m.Type() {
	case authn.KeyTOTP:
		code, err = totp.GenerateCodeCustom(
			key.Secret(),
			time.Now().UTC(),
			totp.ValidateOpts{
				Period:    uint(key.Period()),
				Skew:      1,
				Digits:    key.Digits(),
				Algorithm: key.Algorithm(),
			},
		)
	default:
		return "", authn.ErrInvalidPasscodeType
	}

	// Return result.
	return code, err
}

// Valid checks whether the provided passcode or recovery code is valid.
func (m *Passcode) Valid(code string) (valid bool, recovery bool, err error) {
	// Validate arguments.
	if m == nil {
		return false, false, errors.New("passcode is nil")
	} else if code == "" {
		return false, false, authn.ErrPasscodeRequired
	} else if len(code) > 255 {
		return false, false, authn.ErrInvalidPasscodeFormat
	}

	// Get authentication key.
	key := m.Key()

	if key == nil {
		return false, false, authn.ErrInvalidPasscodeKey
	}

	// Check if recovery code has been used.
	if m.RecoveryCode == code {
		return true, true, nil
	}

	// Verify passcode.
	switch m.Type() {
	case authn.KeyTOTP:
		valid, err = totp.ValidateCustom(
			code,
			key.Secret(),
			time.Now().UTC(),
			totp.ValidateOpts{
				Period:    uint(key.Period()),
				Skew:      1,
				Digits:    key.Digits(),
				Algorithm: key.Algorithm(),
			},
		)
	default:
		return false, false, authn.ErrInvalidPasscodeType
	}

	// Check if an error has been returned.
	if err != nil {
		return valid, false, err
	}

	// Set verified timestamp if nil.
	if valid && m.VerifiedAt == nil {
		m.VerifiedAt = TimeStamp()
		err = m.Updates(Values{"verified_at": m.VerifiedAt})
	}

	// Return result.
	return valid, false, err
}

// Activate activates the passcode.
func (m *Passcode) Activate() (err error) {
	if m == nil {
		return errors.New("passcode is nil")
	}

	if m.VerifiedAt == nil {
		return authn.ErrPasscodeNotVerified
	} else if m.ActivatedAt != nil {
		return authn.ErrPasscodeAlreadyActivated
	} else {
		m.ActivatedAt = TimeStamp()
		err = m.Updates(Values{"activated_at": m.ActivatedAt})
	}

	return err
}

// Image returns an image with a QR Code that can be used to initialize compatible authenticator apps.
func (m *Passcode) Image(size int) (image.Image, error) {
	if m == nil {
		return nil, errors.New("key is nil")
	}

	key := m.Key()

	if key == nil {
		return nil, authn.ErrPasscodeNotSetUp
	}

	return key.Image(size, size)
}

// Png returns a PNG image buffer with a QR Code that can be used to initialize compatible authenticator apps.
func (m *Passcode) Png(size int) *bytes.Buffer {
	if m == nil {
		return nil
	}

	img, err := m.Image(size)

	if err != nil {
		return nil
	}

	var buf bytes.Buffer

	err = png.Encode(&buf, img)

	if err != nil {
		return nil
	}

	return &buf
}
