// Package auth provides authentication and authorization for the Memos server.
//
// This package is used by:
// - server/router/api/v1: gRPC and Connect API interceptors
// - server/router/fileserver: HTTP file server authentication
//
// Authentication methods supported:
// - Session cookie: Browser-based authentication with sliding expiration
// - JWT token: API token authentication for programmatic access
package auth

import (
	"fmt"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/pkg/errors"

	"github.com/usememos/memos/internal/util"
)

const (
	// Issuer is the issuer claim in JWT tokens.
	// This identifies tokens as issued by Memos.
	Issuer = "memos"

	// KeyID is the key identifier used in JWT header.
	// Version "v1" allows for future key rotation while maintaining backward compatibility.
	// If signing mechanism changes, add "v2", "v3", etc. and verify both versions.
	KeyID = "v1"

	// AccessTokenAudienceName is the audience claim for JWT access tokens.
	// This ensures tokens are only used for API access, not other purposes.
	AccessTokenAudienceName = "user.access-token"

	// SessionSlidingDuration is the sliding expiration duration for user sessions.
	// Sessions remain valid if accessed within the last 14 days.
	// Each API call extends the session by updating last_accessed_time.
	SessionSlidingDuration = 14 * 24 * time.Hour

	// SessionCookieName is the HTTP cookie name used to store session information.
	// Cookie value format: {userID}-{sessionID}.
	SessionCookieName = "user_session"
)

// ClaimsMessage represents the claims structure in a JWT token.
//
// JWT Claims include:
// - name: Username (custom claim)
// - iss: Issuer = "memos"
// - aud: Audience = "user.access-token"
// - sub: Subject = user ID
// - iat: Issued at time
// - exp: Expiration time (optional, may be empty for never-expiring tokens).
type ClaimsMessage struct {
	Name string `json:"name"` // Username
	jwt.RegisteredClaims
}

// GenerateAccessToken generates a JWT access token for a user.
//
// Parameters:
// - username: The user's username (stored in "name" claim)
// - userID: The user's ID (stored in "sub" claim)
// - expirationTime: When the token expires (pass zero time for no expiration)
// - secret: Server secret used to sign the token
//
// Returns a signed JWT string or an error.
func GenerateAccessToken(username string, userID int32, expirationTime time.Time, secret []byte) (string, error) {
	return generateToken(username, userID, AccessTokenAudienceName, expirationTime, secret)
}

// generateToken generates a JWT token with the given claims.
//
// Token structure:
// Header: {"alg": "HS256", "kid": "v1", "typ": "JWT"}
// Claims: {"name": username, "iss": "memos", "aud": [audience], "sub": userID, "iat": now, "exp": expiry}
// Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret).
func generateToken(username string, userID int32, audience string, expirationTime time.Time, secret []byte) (string, error) {
	registeredClaims := jwt.RegisteredClaims{
		Issuer:   Issuer,
		Audience: jwt.ClaimStrings{audience},
		IssuedAt: jwt.NewNumericDate(time.Now()),
		Subject:  fmt.Sprint(userID),
	}
	if !expirationTime.IsZero() {
		registeredClaims.ExpiresAt = jwt.NewNumericDate(expirationTime)
	}

	// Declare the token with the HS256 algorithm used for signing, and the claims.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, &ClaimsMessage{
		Name:             username,
		RegisteredClaims: registeredClaims,
	})
	token.Header["kid"] = KeyID

	// Create the JWT string.
	tokenString, err := token.SignedString(secret)
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// GenerateSessionID generates a unique session ID.
//
// Uses UUID v4 (random) for high entropy and uniqueness.
// Session IDs are stored in user settings and used to identify browser sessions.
func GenerateSessionID() string {
	return util.GenUUID()
}

// BuildSessionCookieValue creates the session cookie value.
//
// Format: {userID}-{sessionID}
// Example: "123-550e8400-e29b-41d4-a716-446655440000"
//
// This format allows quick extraction of both user ID and session ID
// from the cookie without database lookup during authentication.
func BuildSessionCookieValue(userID int32, sessionID string) string {
	return fmt.Sprintf("%d-%s", userID, sessionID)
}

// ParseSessionCookieValue extracts user ID and session ID from cookie value.
//
// Input format: "{userID}-{sessionID}"
// Returns: (userID, sessionID, error)
//
// Example: "123-550e8400-..." → (123, "550e8400-...", nil).
func ParseSessionCookieValue(cookieValue string) (int32, string, error) {
	parts := strings.SplitN(cookieValue, "-", 2)
	if len(parts) != 2 {
		return 0, "", errors.New("invalid session cookie format")
	}

	userID, err := util.ConvertStringToInt32(parts[0])
	if err != nil {
		return 0, "", errors.Errorf("invalid user ID in session cookie: %v", err)
	}

	return userID, parts[1], nil
}
