package admin

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"golang.org/x/crypto/argon2"
)

// Argon2id parameters. These follow OWASP's "second" recommended profile
// (64 MiB, 1 iteration, parallelism 4) — strong yet fast enough for an
// interactive admin login.
const (
	argonMemory      = 64 * 1024 // KiB
	argonTime        = 1
	argonParallelism = 4
	argonSaltLen     = 16
	argonKeyLen      = 32
)

var b64 = base64.RawStdEncoding

// HashPassword hashes a plaintext password with argon2id and returns a PHC
// formatted string: $argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>.
func HashPassword(password string) (string, error) {
	salt := make([]byte, argonSaltLen)
	if _, err := rand.Read(salt); err != nil {
		return "", fmt.Errorf("admin.HashPassword: %w", err)
	}
	key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonParallelism, argonKeyLen)
	return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
		argon2.Version, argonMemory, argonTime, argonParallelism,
		b64.EncodeToString(salt), b64.EncodeToString(key)), nil
}

// VerifyPassword reports whether password matches the given PHC-encoded
// argon2id hash, in constant time.
func VerifyPassword(encoded, password string) bool {
	params, salt, want, err := decodePHC(encoded)
	if err != nil {
		return false
	}
	got := argon2.IDKey([]byte(password), salt, params.t, params.m, params.p, uint32(len(want)))
	return subtle.ConstantTimeCompare(got, want) == 1
}

type argonParams struct {
	m uint32
	t uint32
	p uint8
}

func decodePHC(encoded string) (argonParams, []byte, []byte, error) {
	parts := strings.Split(encoded, "$")
	// ["", "argon2id", "v=19", "m=..,t=..,p=..", salt, hash]
	if len(parts) != 6 || parts[1] != "argon2id" {
		return argonParams{}, nil, nil, errors.New("admin: malformed argon2 hash")
	}
	var version int
	if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
		return argonParams{}, nil, nil, errors.New("admin: bad argon2 version")
	}
	var pr argonParams
	if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &pr.m, &pr.t, &pr.p); err != nil {
		return argonParams{}, nil, nil, errors.New("admin: bad argon2 params")
	}
	salt, err := b64.DecodeString(parts[4])
	if err != nil {
		return argonParams{}, nil, nil, errors.New("admin: bad argon2 salt")
	}
	hash, err := b64.DecodeString(parts[5])
	if err != nil {
		return argonParams{}, nil, nil, errors.New("admin: bad argon2 hash")
	}
	return pr, salt, hash, nil
}

// EncryptSecret seals plaintext with AES-256-GCM under key (32 bytes). The
// returned blob is nonce || ciphertext, suitable for storing in a VARBINARY
// column. Used to keep TOTP secrets encrypted at rest.
func EncryptSecret(key []byte, plaintext string) ([]byte, error) {
	gcm, err := newGCM(key)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, fmt.Errorf("admin.EncryptSecret nonce: %w", err)
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// DecryptSecret reverses EncryptSecret.
func DecryptSecret(key, blob []byte) (string, error) {
	gcm, err := newGCM(key)
	if err != nil {
		return "", err
	}
	ns := gcm.NonceSize()
	if len(blob) < ns {
		return "", errors.New("admin.DecryptSecret: ciphertext too short")
	}
	nonce, ct := blob[:ns], blob[ns:]
	pt, err := gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return "", fmt.Errorf("admin.DecryptSecret: %w", err)
	}
	return string(pt), nil
}

func newGCM(key []byte) (cipher.AEAD, error) {
	if len(key) != 32 {
		return nil, fmt.Errorf("admin: secret key must be 32 bytes, got %d", len(key))
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("admin: aes cipher: %w", err)
	}
	return cipher.NewGCM(block)
}
