package services

import (
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/mail"
	"os"
	"sort"
	"strings"
	"time"

	"go.etcd.io/bbolt"
	"golang.org/x/crypto/argon2"
)

var (
	usersBucket       = []byte("users")
	userEmailsBucket  = []byte("user_emails")
	sessionsBucket    = []byte("sessions")
	invitesBucket     = []byte("invites")
	collectionsBucket = []byte("collections")
	apiTokensBucket   = []byte("api_tokens")
)

// apiTokenPrefix marks raw API tokens so clients and logs can recognise them.
const apiTokenPrefix = "wbx_"

var (
	ErrTokenInvalid  = errors.New("api token is invalid")
	ErrTokenNotFound = errors.New("api token not found")
)

const (
	UserRoleAdmin = "admin"
	UserRoleUser  = "user"

	UserStatusActive   = "active"
	UserStatusDisabled = "disabled"
)

var (
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrRegistrationClosed = errors.New("registration is closed")
	ErrInviteInvalid      = errors.New("invite is invalid")
	ErrUserDisabled       = errors.New("user is disabled")
)

type AuthService struct {
	db      *bbolt.DB
	baseURL string
}

type User struct {
	ID             string     `json:"id"`
	Username       string     `json:"username"`
	Email          string     `json:"email"`
	PasswordHash   string     `json:"passwordHash"`
	Role           string     `json:"role"`
	Status         string     `json:"status"`
	StorageQuotaMB *float64   `json:"storageQuotaMb,omitempty"`
	Policy         UserPolicy `json:"policy,omitempty"`
	CreatedAt      time.Time  `json:"createdAt"`
	UpdatedAt      time.Time  `json:"updatedAt"`
}

type UserPolicy struct {
	MaxUploadMB         *float64 `json:"maxUploadMb,omitempty"`
	DailyUploadMB       *float64 `json:"dailyUploadMb,omitempty"`
	StorageQuotaMB      *float64 `json:"storageQuotaMb,omitempty"`
	MaxDays             *int     `json:"maxDays,omitempty"`
	DailyBoxes          *int     `json:"dailyBoxes,omitempty"`
	ActiveBoxes         *int     `json:"activeBoxes,omitempty"`
	ShortWindowRequests *int     `json:"shortWindowRequests,omitempty"`
	StorageBackendID    *string  `json:"storageBackendId,omitempty"`
}

type PublicUser struct {
	ID             string
	Username       string
	Email          string
	Role           string
	Status         string
	StorageQuotaMB *float64
	Policy         UserPolicy
	CreatedAt      time.Time
}

type Session struct {
	ID        string    `json:"id"`
	UserID    string    `json:"userId"`
	TokenHash string    `json:"tokenHash"`
	ExpiresAt time.Time `json:"expiresAt"`
	CreatedAt time.Time `json:"createdAt"`
}

type Invite struct {
	ID           string     `json:"id"`
	UserID       string     `json:"userId,omitempty"`
	Email        string     `json:"email"`
	Role         string     `json:"role"`
	TokenHash    string     `json:"tokenHash"`
	CreatedBy    string     `json:"createdBy"`
	CreatedAt    time.Time  `json:"createdAt"`
	ExpiresAt    time.Time  `json:"expiresAt"`
	UsedAt       *time.Time `json:"usedAt,omitempty"`
	UsedByUserID string     `json:"usedByUserId,omitempty"`
}

type Collection struct {
	ID        string    `json:"id"`
	UserID    string    `json:"userId"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

// APIToken is a long-lived personal access token. Only the SHA-256 hash of the
// secret is stored; the plaintext is shown to the user exactly once at creation.
type APIToken struct {
	ID         string     `json:"id"`
	UserID     string     `json:"userId"`
	Name       string     `json:"name"`
	TokenHash  string     `json:"tokenHash"`
	CreatedAt  time.Time  `json:"createdAt"`
	LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
}

// APITokenResult carries the one-time plaintext alongside the stored token.
type APITokenResult struct {
	Token     APIToken
	Plaintext string
}

type InviteResult struct {
	Invite Invite
	URL    string
	Token  string
}

func NewAuthService(db *bbolt.DB, baseURL string) (*AuthService, error) {
	service := &AuthService{db: db, baseURL: strings.TrimRight(baseURL, "/")}
	err := db.Update(func(tx *bbolt.Tx) error {
		for _, bucket := range [][]byte{usersBucket, userEmailsBucket, sessionsBucket, invitesBucket, collectionsBucket, apiTokensBucket} {
			if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return service, nil
}

func (s *AuthService) BootstrapAvailable() (bool, error) {
	count := 0
	err := s.db.View(func(tx *bbolt.Tx) error {
		return tx.Bucket(usersBucket).ForEach(func(_, _ []byte) error {
			count++
			return nil
		})
	})
	return count == 0, err
}

func (s *AuthService) CreateBootstrapUser(username, email, password string) (User, error) {
	available, err := s.BootstrapAvailable()
	if err != nil {
		return User{}, err
	}
	if !available {
		return User{}, ErrRegistrationClosed
	}
	return s.createUser(username, email, password, UserRoleAdmin)
}

func (s *AuthService) Login(email, password string) (User, string, error) {
	user, err := s.UserByEmail(email)
	if err != nil {
		return User{}, "", ErrInvalidCredentials
	}
	if user.Status != UserStatusActive {
		return User{}, "", ErrUserDisabled
	}
	if !VerifyPasswordHash(user.PasswordHash, password) {
		return User{}, "", ErrInvalidCredentials
	}

	token := randomID(32)
	session := Session{
		ID:        randomID(12),
		UserID:    user.ID,
		TokenHash: tokenHash(token),
		CreatedAt: time.Now().UTC(),
		ExpiresAt: time.Now().UTC().Add(30 * 24 * time.Hour),
	}
	err = s.db.Update(func(tx *bbolt.Tx) error {
		data, err := json.Marshal(session)
		if err != nil {
			return err
		}
		return tx.Bucket(sessionsBucket).Put([]byte(session.ID), data)
	})
	return user, session.ID + "." + token, err
}

func (s *AuthService) UserForSession(raw string) (User, Session, error) {
	sessionID, token, ok := strings.Cut(raw, ".")
	if !ok || sessionID == "" || token == "" {
		return User{}, Session{}, os.ErrNotExist
	}

	var session Session
	err := s.db.View(func(tx *bbolt.Tx) error {
		data := tx.Bucket(sessionsBucket).Get([]byte(sessionID))
		if data == nil {
			return os.ErrNotExist
		}
		return json.Unmarshal(data, &session)
	})
	if err != nil {
		return User{}, Session{}, err
	}
	if time.Now().UTC().After(session.ExpiresAt) || subtle.ConstantTimeCompare([]byte(tokenHash(token)), []byte(session.TokenHash)) != 1 {
		return User{}, Session{}, os.ErrPermission
	}
	user, err := s.UserByID(session.UserID)
	if err != nil {
		return User{}, Session{}, err
	}
	if user.Status != UserStatusActive {
		return User{}, Session{}, ErrUserDisabled
	}
	return user, session, nil
}

func (s *AuthService) Logout(raw string) error {
	sessionID, _, ok := strings.Cut(raw, ".")
	if !ok || sessionID == "" {
		return nil
	}
	return s.db.Update(func(tx *bbolt.Tx) error {
		return tx.Bucket(sessionsBucket).Delete([]byte(sessionID))
	})
}

// CreateAPIToken mints a new personal access token for the user. The returned
// plaintext is the only time the secret is available; only its hash is stored.
func (s *AuthService) CreateAPIToken(userID, name string) (APITokenResult, error) {
	if userID == "" {
		return APITokenResult{}, fmt.Errorf("user is required")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		name = "Untitled token"
	}
	if len(name) > 80 {
		name = name[:80]
	}

	secret := randomID(32)
	token := APIToken{
		ID:        randomID(12),
		UserID:    userID,
		Name:      name,
		TokenHash: apiTokenHash(secret),
		CreatedAt: time.Now().UTC(),
	}
	if err := s.saveAPIToken(token); err != nil {
		return APITokenResult{}, err
	}
	plaintext := apiTokenPrefix + token.ID + "." + secret
	return APITokenResult{Token: token, Plaintext: plaintext}, nil
}

// ListAPITokens returns the user's tokens, newest first.
func (s *AuthService) ListAPITokens(userID string) ([]APIToken, error) {
	tokens := make([]APIToken, 0)
	err := s.db.View(func(tx *bbolt.Tx) error {
		return tx.Bucket(apiTokensBucket).ForEach(func(_, data []byte) error {
			var token APIToken
			if err := json.Unmarshal(data, &token); err != nil {
				return err
			}
			if token.UserID == userID {
				tokens = append(tokens, token)
			}
			return nil
		})
	})
	if err != nil {
		return nil, err
	}
	sort.Slice(tokens, func(i, j int) bool {
		return tokens[i].CreatedAt.After(tokens[j].CreatedAt)
	})
	return tokens, nil
}

// DeleteAPIToken removes a token, but only if it belongs to the given user.
func (s *AuthService) DeleteAPIToken(userID, tokenID string) error {
	if userID == "" || tokenID == "" {
		return ErrTokenNotFound
	}
	return s.db.Update(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket(apiTokensBucket)
		data := bucket.Get([]byte(tokenID))
		if data == nil {
			return ErrTokenNotFound
		}
		var token APIToken
		if err := json.Unmarshal(data, &token); err != nil {
			return err
		}
		if token.UserID != userID {
			return ErrTokenNotFound
		}
		return bucket.Delete([]byte(tokenID))
	})
}

// UserForAPIToken resolves a raw bearer token to its owning user. It records
// last-used time on a best-effort basis. The user must exist and be enabled.
func (s *AuthService) UserForAPIToken(raw string) (User, error) {
	raw = strings.TrimSpace(raw)
	raw = strings.TrimPrefix(raw, apiTokenPrefix)
	tokenID, secret, ok := strings.Cut(raw, ".")
	if !ok || tokenID == "" || secret == "" {
		return User{}, ErrTokenInvalid
	}

	var token APIToken
	err := s.db.View(func(tx *bbolt.Tx) error {
		data := tx.Bucket(apiTokensBucket).Get([]byte(tokenID))
		if data == nil {
			return ErrTokenInvalid
		}
		return json.Unmarshal(data, &token)
	})
	if err != nil {
		return User{}, ErrTokenInvalid
	}
	if subtle.ConstantTimeCompare([]byte(apiTokenHash(secret)), []byte(token.TokenHash)) != 1 {
		return User{}, ErrTokenInvalid
	}

	user, err := s.UserByID(token.UserID)
	if err != nil {
		return User{}, ErrTokenInvalid
	}
	if user.Status != UserStatusActive {
		return User{}, ErrUserDisabled
	}

	now := time.Now().UTC()
	token.LastUsedAt = &now
	_ = s.saveAPIToken(token)

	return user, nil
}

func (s *AuthService) saveAPIToken(token APIToken) error {
	return s.db.Update(func(tx *bbolt.Tx) error {
		data, err := json.Marshal(token)
		if err != nil {
			return err
		}
		return tx.Bucket(apiTokensBucket).Put([]byte(token.ID), data)
	})
}

func (s *AuthService) CreateInvite(email, role, createdBy string, expiresIn time.Duration) (InviteResult, error) {
	email, err := normalizeEmail(email)
	if err != nil {
		return InviteResult{}, err
	}
	if role == "" {
		role = UserRoleUser
	}
	if role != UserRoleAdmin && role != UserRoleUser {
		role = UserRoleUser
	}
	if expiresIn <= 0 {
		expiresIn = 7 * 24 * time.Hour
	}

	token := randomID(32)
	invite := Invite{
		ID:        randomID(12),
		Email:     email,
		Role:      role,
		TokenHash: tokenHash(token),
		CreatedBy: createdBy,
		CreatedAt: time.Now().UTC(),
		ExpiresAt: time.Now().UTC().Add(expiresIn),
	}
	err = s.saveInvite(invite)
	if err != nil {
		return InviteResult{}, err
	}
	return InviteResult{
		Invite: invite,
		Token:  token,
		URL:    fmt.Sprintf("%s/invite/%s", s.baseURL, token),
	}, nil
}

func (s *AuthService) AcceptInvite(token, username, password string) (User, error) {
	invite, err := s.InviteByToken(token)
	if err != nil {
		return User{}, err
	}
	if invite.UsedAt != nil || time.Now().UTC().After(invite.ExpiresAt) {
		return User{}, ErrInviteInvalid
	}

	var user User
	if invite.UserID != "" {
		user, err = s.UserByID(invite.UserID)
		if err != nil {
			return User{}, err
		}
		if err := s.SetPassword(user.ID, password); err != nil {
			return User{}, err
		}
		user, _ = s.UserByID(user.ID)
	} else {
		user, err = s.createUser(username, invite.Email, password, invite.Role)
		if err != nil {
			return User{}, err
		}
	}

	now := time.Now().UTC()
	invite.UsedAt = &now
	invite.UsedByUserID = user.ID
	if err := s.saveInvite(invite); err != nil {
		return User{}, err
	}
	return user, nil
}

func (s *AuthService) InviteByToken(token string) (Invite, error) {
	hash := tokenHash(token)
	var match Invite
	err := s.db.View(func(tx *bbolt.Tx) error {
		return tx.Bucket(invitesBucket).ForEach(func(_, value []byte) error {
			var invite Invite
			if err := json.Unmarshal(value, &invite); err != nil {
				return err
			}
			if subtle.ConstantTimeCompare([]byte(hash), []byte(invite.TokenHash)) == 1 {
				match = invite
			}
			return nil
		})
	})
	if err != nil {
		return Invite{}, err
	}
	if match.ID == "" {
		return Invite{}, ErrInviteInvalid
	}
	return match, nil
}

func (s *AuthService) CreatePasswordResetInvite(userID, createdBy string) (InviteResult, error) {
	user, err := s.UserByID(userID)
	if err != nil {
		return InviteResult{}, err
	}
	result, err := s.CreateInvite(user.Email, user.Role, createdBy, 24*time.Hour)
	if err != nil {
		return InviteResult{}, err
	}
	result.Invite.UserID = user.ID
	if err := s.saveInvite(result.Invite); err != nil {
		return InviteResult{}, err
	}
	return result, nil
}

func (s *AuthService) ListUsers() ([]User, error) {
	users := make([]User, 0)
	err := s.db.View(func(tx *bbolt.Tx) error {
		return tx.Bucket(usersBucket).ForEach(func(_, value []byte) error {
			var user User
			if err := json.Unmarshal(value, &user); err != nil {
				return err
			}
			users = append(users, user)
			return nil
		})
	})
	sort.Slice(users, func(i, j int) bool {
		return users[i].CreatedAt.After(users[j].CreatedAt)
	})
	return users, err
}

func (s *AuthService) DisableUser(userID string, disabled bool) error {
	user, err := s.UserByID(userID)
	if err != nil {
		return err
	}
	if disabled {
		user.Status = UserStatusDisabled
	} else {
		user.Status = UserStatusActive
	}
	user.UpdatedAt = time.Now().UTC()
	return s.saveUser(user)
}

func (s *AuthService) SetPassword(userID, password string) error {
	if len(password) < 8 {
		return fmt.Errorf("password must be at least 8 characters")
	}
	user, err := s.UserByID(userID)
	if err != nil {
		return err
	}
	user.PasswordHash = HashPassword(password)
	user.UpdatedAt = time.Now().UTC()
	return s.saveUser(user)
}

func (s *AuthService) SetUserStorageQuota(userID string, quotaMB *float64) error {
	if quotaMB != nil && *quotaMB <= 0 {
		return fmt.Errorf("storage quota must be positive")
	}
	user, err := s.UserByID(userID)
	if err != nil {
		return err
	}
	user.StorageQuotaMB = quotaMB
	user.UpdatedAt = time.Now().UTC()
	return s.saveUser(user)
}

func (s *AuthService) SetUserPolicy(userID string, policy UserPolicy) error {
	if err := validateUserPolicy(policy); err != nil {
		return err
	}
	user, err := s.UserByID(userID)
	if err != nil {
		return err
	}
	user.Policy = policy
	user.StorageQuotaMB = policy.StorageQuotaMB
	user.UpdatedAt = time.Now().UTC()
	return s.saveUser(user)
}

func (s *AuthService) SetUserStorageBackend(userID, backendID string) error {
	user, err := s.UserByID(userID)
	if err != nil {
		return err
	}
	backendID = strings.TrimSpace(backendID)
	if backendID == "" {
		user.Policy.StorageBackendID = nil
	} else {
		user.Policy.StorageBackendID = &backendID
	}
	user.UpdatedAt = time.Now().UTC()
	return s.saveUser(user)
}

func (s *AuthService) ClearStorageBackendOverrides(backendID string) (int, error) {
	backendID = strings.TrimSpace(backendID)
	if backendID == "" {
		return 0, nil
	}
	cleared := 0
	err := s.db.Update(func(tx *bbolt.Tx) error {
		users := tx.Bucket(usersBucket)
		return users.ForEach(func(key, value []byte) error {
			var user User
			if err := json.Unmarshal(value, &user); err != nil {
				return err
			}
			if user.Policy.StorageBackendID == nil || *user.Policy.StorageBackendID != backendID {
				return nil
			}
			user.Policy.StorageBackendID = nil
			user.UpdatedAt = time.Now().UTC()
			next, err := json.Marshal(user)
			if err != nil {
				return err
			}
			if err := users.Put(key, next); err != nil {
				return err
			}
			cleared++
			return nil
		})
	})
	return cleared, err
}

func (s *AuthService) UpdateUserAdminFields(userID, username, email, role, status string, policy UserPolicy) (User, error) {
	if err := validateUserPolicy(policy); err != nil {
		return User{}, err
	}
	username = strings.TrimSpace(username)
	if username == "" {
		return User{}, fmt.Errorf("username is required")
	}
	email, err := normalizeEmail(email)
	if err != nil {
		return User{}, err
	}
	if role != UserRoleAdmin && role != UserRoleUser {
		return User{}, fmt.Errorf("invalid role")
	}
	if status != UserStatusActive && status != UserStatusDisabled {
		return User{}, fmt.Errorf("invalid status")
	}

	var updated User
	err = s.db.Update(func(tx *bbolt.Tx) error {
		users := tx.Bucket(usersBucket)
		emails := tx.Bucket(userEmailsBucket)
		data := users.Get([]byte(userID))
		if data == nil {
			return os.ErrNotExist
		}
		var user User
		if err := json.Unmarshal(data, &user); err != nil {
			return err
		}
		if existing := emails.Get([]byte(email)); existing != nil && string(existing) != user.ID {
			return fmt.Errorf("email is already registered")
		}
		if user.Email != email {
			if err := emails.Delete([]byte(user.Email)); err != nil {
				return err
			}
			if err := emails.Put([]byte(email), []byte(user.ID)); err != nil {
				return err
			}
		}
		user.Username = username
		user.Email = email
		user.Role = role
		user.Status = status
		user.Policy = policy
		user.StorageQuotaMB = policy.StorageQuotaMB
		user.UpdatedAt = time.Now().UTC()
		next, err := json.Marshal(user)
		if err != nil {
			return err
		}
		if err := users.Put([]byte(user.ID), next); err != nil {
			return err
		}
		updated = user
		return nil
	})
	return updated, err
}

func (s *AuthService) UserByID(id string) (User, error) {
	var user User
	err := s.db.View(func(tx *bbolt.Tx) error {
		data := tx.Bucket(usersBucket).Get([]byte(id))
		if data == nil {
			return os.ErrNotExist
		}
		return json.Unmarshal(data, &user)
	})
	return user, err
}

func (s *AuthService) UserByEmail(email string) (User, error) {
	email, err := normalizeEmail(email)
	if err != nil {
		return User{}, err
	}
	var userID string
	err = s.db.View(func(tx *bbolt.Tx) error {
		data := tx.Bucket(userEmailsBucket).Get([]byte(email))
		if data == nil {
			return os.ErrNotExist
		}
		userID = string(data)
		return nil
	})
	if err != nil {
		return User{}, err
	}
	return s.UserByID(userID)
}

func (s *AuthService) CreateCollection(userID, name string) (Collection, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return Collection{}, fmt.Errorf("collection name is required")
	}
	collection := Collection{
		ID:        randomID(10),
		UserID:    userID,
		Name:      name,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}
	return collection, s.saveCollection(collection)
}

func (s *AuthService) ListCollections(userID string) ([]Collection, error) {
	collections := make([]Collection, 0)
	err := s.db.View(func(tx *bbolt.Tx) error {
		return tx.Bucket(collectionsBucket).ForEach(func(_, value []byte) error {
			var collection Collection
			if err := json.Unmarshal(value, &collection); err != nil {
				return err
			}
			if collection.UserID == userID {
				collections = append(collections, collection)
			}
			return nil
		})
	})
	sort.Slice(collections, func(i, j int) bool {
		return strings.ToLower(collections[i].Name) < strings.ToLower(collections[j].Name)
	})
	return collections, err
}

func (s *AuthService) CollectionOwnedBy(collectionID, userID string) bool {
	if collectionID == "" {
		return true
	}
	collection, err := s.CollectionByID(collectionID)
	return err == nil && collection.UserID == userID
}

func (s *AuthService) CollectionByID(id string) (Collection, error) {
	var collection Collection
	err := s.db.View(func(tx *bbolt.Tx) error {
		data := tx.Bucket(collectionsBucket).Get([]byte(id))
		if data == nil {
			return os.ErrNotExist
		}
		return json.Unmarshal(data, &collection)
	})
	return collection, err
}

func (s *AuthService) PublicUser(user User) PublicUser {
	return PublicUser{
		ID:             user.ID,
		Username:       user.Username,
		Email:          user.Email,
		Role:           user.Role,
		Status:         user.Status,
		StorageQuotaMB: user.StorageQuotaMB,
		Policy:         user.Policy,
		CreatedAt:      user.CreatedAt,
	}
}

func (s *AuthService) createUser(username, email, password, role string) (User, error) {
	username = strings.TrimSpace(username)
	if username == "" {
		return User{}, fmt.Errorf("username is required")
	}
	email, err := normalizeEmail(email)
	if err != nil {
		return User{}, err
	}
	if len(password) < 8 {
		return User{}, fmt.Errorf("password must be at least 8 characters")
	}
	if role != UserRoleAdmin && role != UserRoleUser {
		role = UserRoleUser
	}

	now := time.Now().UTC()
	user := User{
		ID:           randomID(12),
		Username:     username,
		Email:        email,
		PasswordHash: HashPassword(password),
		Role:         role,
		Status:       UserStatusActive,
		CreatedAt:    now,
		UpdatedAt:    now,
	}
	return user, s.db.Update(func(tx *bbolt.Tx) error {
		if existing := tx.Bucket(userEmailsBucket).Get([]byte(email)); existing != nil {
			return fmt.Errorf("email is already registered")
		}
		data, err := json.Marshal(user)
		if err != nil {
			return err
		}
		if err := tx.Bucket(usersBucket).Put([]byte(user.ID), data); err != nil {
			return err
		}
		return tx.Bucket(userEmailsBucket).Put([]byte(email), []byte(user.ID))
	})
}

func (s *AuthService) saveUser(user User) error {
	data, err := json.Marshal(user)
	if err != nil {
		return err
	}
	return s.db.Update(func(tx *bbolt.Tx) error {
		return tx.Bucket(usersBucket).Put([]byte(user.ID), data)
	})
}

func (s *AuthService) saveInvite(invite Invite) error {
	data, err := json.Marshal(invite)
	if err != nil {
		return err
	}
	return s.db.Update(func(tx *bbolt.Tx) error {
		return tx.Bucket(invitesBucket).Put([]byte(invite.ID), data)
	})
}

func (s *AuthService) saveCollection(collection Collection) error {
	data, err := json.Marshal(collection)
	if err != nil {
		return err
	}
	return s.db.Update(func(tx *bbolt.Tx) error {
		return tx.Bucket(collectionsBucket).Put([]byte(collection.ID), data)
	})
}

func normalizeEmail(email string) (string, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	if email == "" {
		return "", fmt.Errorf("email is required")
	}
	if _, err := mail.ParseAddress(email); err != nil {
		return "", fmt.Errorf("email is invalid")
	}
	return email, nil
}

func tokenHash(token string) string {
	sum := sha256.Sum256([]byte("warpbox-session:" + token))
	return hex.EncodeToString(sum[:])
}

func apiTokenHash(secret string) string {
	sum := sha256.Sum256([]byte("warpbox-api-token:" + secret))
	return hex.EncodeToString(sum[:])
}

func HashPassword(password string) string {
	salt := make([]byte, 16)
	if _, err := rand.Read(salt); err != nil {
		salt = []byte(randomID(16))[:16]
	}
	hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
	return "argon2id$v=19$m=65536,t=1,p=4$" + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(hash)
}

func VerifyPasswordHash(encoded, password string) bool {
	parts := strings.Split(encoded, "$")
	if len(parts) != 5 || parts[0] != "argon2id" {
		return false
	}
	salt, err := base64.RawStdEncoding.DecodeString(parts[3])
	if err != nil {
		return false
	}
	expected, err := base64.RawStdEncoding.DecodeString(parts[4])
	if err != nil {
		return false
	}
	actual := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, uint32(len(expected)))
	return subtle.ConstantTimeCompare(actual, expected) == 1
}

func validateUserPolicy(policy UserPolicy) error {
	if policy.MaxUploadMB != nil && *policy.MaxUploadMB < 0 && *policy.MaxUploadMB != -1 {
		return fmt.Errorf("max upload override must be positive or -1 for unlimited")
	}
	if policy.DailyUploadMB != nil && ((*policy.DailyUploadMB < 0 && *policy.DailyUploadMB != -1) || *policy.DailyUploadMB == 0) {
		return fmt.Errorf("daily upload override must be positive or -1 for unlimited")
	}
	if policy.StorageQuotaMB != nil && *policy.StorageQuotaMB < 0 && *policy.StorageQuotaMB != -1 {
		return fmt.Errorf("storage quota override must be 0/positive or -1 for unlimited")
	}
	if policy.MaxDays != nil && *policy.MaxDays <= 0 && *policy.MaxDays != -1 {
		return fmt.Errorf("expiration override must be positive or -1 for unlimited")
	}
	if policy.DailyBoxes != nil && *policy.DailyBoxes <= 0 && *policy.DailyBoxes != -1 {
		return fmt.Errorf("daily box override must be positive or -1 for unlimited")
	}
	if policy.ActiveBoxes != nil && *policy.ActiveBoxes <= 0 && *policy.ActiveBoxes != -1 {
		return fmt.Errorf("active box override must be positive or -1 for unlimited")
	}
	if policy.ShortWindowRequests != nil && *policy.ShortWindowRequests <= 0 && *policy.ShortWindowRequests != -1 {
		return fmt.Errorf("short-window request override must be positive or -1 for unlimited")
	}
	return nil
}
