// Package auth provides authentication and authorization functionality
package auth

import (
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"fmt"

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

// HashAlgorithm identifies the hashing algorithm used for API keys
type HashAlgorithm string

const (
	// HashAlgorithmSHA256 is the legacy SHA256 hashing (deprecated, for backward compatibility)
	HashAlgorithmSHA256 HashAlgorithm = "sha256"
	// HashAlgorithmArgon2id is the modern Argon2id hashing (recommended)
	HashAlgorithmArgon2id HashAlgorithm = "argon2id"
)

// Argon2id parameters (OWASP recommended minimum)
const (
	argon2idTime    = 1         // Iterations
	argon2idMemory  = 64 * 1024 // 64 MB
	argon2idThreads = 4         // Parallelism
	argon2idKeyLen  = 32        // 256-bit output
	argon2idSaltLen = 16        // 128-bit salt
)

// HashedKey represents a hashed API key with its algorithm and salt
type HashedKey struct {
	Hash      string        `json:"hash"`
	Salt      string        `json:"salt,omitempty"`
	Algorithm HashAlgorithm `json:"algorithm"`
}

// HashAPIKeyArgon2id creates an Argon2id hash of an API key
// This is the recommended hashing method for new keys
func HashAPIKeyArgon2id(key string) (*HashedKey, error) {
	// Generate random salt
	salt := make([]byte, argon2idSaltLen)
	if _, err := rand.Read(salt); err != nil {
		return nil, fmt.Errorf("failed to generate salt: %w", err)
	}

	// Hash with Argon2id
	hash := argon2.IDKey(
		[]byte(key),
		salt,
		argon2idTime,
		argon2idMemory,
		argon2idThreads,
		argon2idKeyLen,
	)

	return &HashedKey{
		Hash:      hex.EncodeToString(hash),
		Salt:      hex.EncodeToString(salt),
		Algorithm: HashAlgorithmArgon2id,
	}, nil
}

// HashAPIKey creates a legacy SHA256 hash of an API key
// Deprecated: Use HashAPIKeyArgon2id for new keys
func HashAPIKey(key string) string {
	hash := sha256.Sum256([]byte(key))
	return hex.EncodeToString(hash[:])
}

// VerifyAPIKey verifies an API key against a stored hash
// Supports both Argon2id (preferred) and SHA256 (legacy) for backward compatibility
func VerifyAPIKey(key string, stored *HashedKey) (bool, error) {
	switch stored.Algorithm {
	case HashAlgorithmArgon2id:
		return verifyArgon2id(key, stored)
	case HashAlgorithmSHA256, "": // Empty string treated as legacy SHA256
		return verifySHA256(key, stored.Hash), nil
	default:
		return false, fmt.Errorf("unsupported hash algorithm: %s", stored.Algorithm)
	}
}

// verifyArgon2id verifies a key against an Argon2id hash
func verifyArgon2id(key string, stored *HashedKey) (bool, error) {
	// Decode salt
	salt, err := hex.DecodeString(stored.Salt)
	if err != nil {
		return false, fmt.Errorf("invalid salt encoding: %w", err)
	}

	// Compute hash with same parameters
	computedHash := argon2.IDKey(
		[]byte(key),
		salt,
		argon2idTime,
		argon2idMemory,
		argon2idThreads,
		argon2idKeyLen,
	)

	// Decode stored hash
	storedHash, err := hex.DecodeString(stored.Hash)
	if err != nil {
		return false, fmt.Errorf("invalid hash encoding: %w", err)
	}

	// Constant-time comparison to prevent timing attacks
	return subtle.ConstantTimeCompare(computedHash, storedHash) == 1, nil
}

// verifySHA256 verifies a key against a legacy SHA256 hash
func verifySHA256(key, storedHash string) bool {
	computedHash := HashAPIKey(key)
	return subtle.ConstantTimeCompare([]byte(computedHash), []byte(storedHash)) == 1
}

// GenerateAPIKey generates a new random API key
// Returns the plaintext key (to be shown once) and its Argon2id hash
func GenerateAPIKey() (plaintext string, hashed *HashedKey, err error) {
	// Generate 32 bytes of randomness (256 bits)
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", nil, fmt.Errorf("failed to generate API key: %w", err)
	}

	plaintext = hex.EncodeToString(buf)
	hashed, err = HashAPIKeyArgon2id(plaintext)
	if err != nil {
		return "", nil, err
	}

	return plaintext, hashed, nil
}

// GenerateAPISalt creates a random salt for API key hashing
// Primarily used for testing and migration scenarios
func GenerateAPISalt() ([]byte, error) {
	salt := make([]byte, argon2idSaltLen)
	if _, err := rand.Read(salt); err != nil {
		return nil, fmt.Errorf("failed to generate salt: %w", err)
	}
	return salt, nil
}
