package auth

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"strings"

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

// Params holds the argon2id cost parameters.
// The PHC string format self-describes these values, so each stored hash
// carries its own params — cost upgrades are non-breaking.
type Params struct {
	Memory      uint32 // KiB
	Iterations  uint32
	Parallelism uint8
	SaltLength  uint32
	KeyLength   uint32
}

// DefaultParams matches the OWASP 2024 Password Storage Cheat Sheet baseline
// for argon2id (D-08). Do not reduce these in production.
var DefaultParams = Params{
	Memory:      64 * 1024, // 64 MiB
	Iterations:  1,
	Parallelism: 4,
	SaltLength:  16,
	KeyLength:   32,
}

// TestParams uses reduced cost so that `go test` wall-time stays under 5 seconds
// (D-26, Pitfall 4). The same code path is exercised; only the cost differs.
var TestParams = Params{
	Memory:      8 * 1024, // 8 MiB
	Iterations:  1,
	Parallelism: 2,
	SaltLength:  16,
	KeyLength:   32,
}

// init runs a self-test hash/verify round-trip to catch param-drift or build
// regressions at package import time (D-08).
func init() {
	phc, err := Hash("self-test-password-12chars", TestParams)
	if err != nil {
		panic("argon2 self-test Hash failed: " + err.Error())
	}
	ok, err := Verify(phc, "self-test-password-12chars")
	if err != nil || !ok {
		panic("argon2 self-test Verify failed: hash/verify round-trip mismatch")
	}
}

// Hash derives an argon2id key from password and encodes it as a PHC-formatted
// string. A fresh random salt is generated on every call (never reused).
//
// Length contract (D-25): callers MUST reject len(password) > 128 before calling
// Hash to prevent a long-password DoS. This function does not enforce the ceiling
// itself so that it remains a pure primitive usable in tests.
func Hash(password string, p Params) (string, error) {
	salt := make([]byte, p.SaltLength)
	if _, err := rand.Read(salt); err != nil {
		return "", fmt.Errorf("auth: failed to generate salt: %w", err)
	}
	h := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)
	return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
		argon2.Version,
		p.Memory,
		p.Iterations,
		p.Parallelism,
		base64.RawStdEncoding.EncodeToString(salt),
		base64.RawStdEncoding.EncodeToString(h),
	), nil
}

// Verify checks whether password matches the argon2id PHC string encoded.
// Returns (true, nil) on match, (false, nil) on mismatch, and (false, sentinel)
// when encoded is structurally invalid or uses an incompatible argon2 version.
// The comparison is constant-time to prevent timing attacks (T-2-13).
func Verify(encoded, password string) (bool, error) {
	parts := strings.Split(encoded, "$")
	// A valid PHC string split on "$" yields 6 elements:
	//   [0]=""  [1]="argon2id"  [2]="v=19"  [3]="m=...,t=...,p=..."  [4]=<salt>  [5]=<hash>
	if len(parts) != 6 || parts[1] != "argon2id" {
		return false, ErrInvalidHash
	}

	var version int
	if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
		return false, ErrInvalidHash
	}
	if version != argon2.Version {
		return false, ErrIncompatibleVersion
	}

	var mem, iter uint32
	var par uint8
	if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &mem, &iter, &par); err != nil {
		return false, ErrInvalidHash
	}

	salt, err := base64.RawStdEncoding.Strict().DecodeString(parts[4])
	if err != nil {
		return false, ErrInvalidHash
	}
	want, err := base64.RawStdEncoding.Strict().DecodeString(parts[5])
	if err != nil {
		return false, ErrInvalidHash
	}

	got := argon2.IDKey([]byte(password), salt, iter, mem, par, uint32(len(want)))
	// NOTE: constant-time compare is required here to prevent timing attacks (T-2-13).
	return subtle.ConstantTimeCompare(want, got) == 1, nil
}
