// Package auth validates the Better Auth sessions issued by the backend.
// The session cookie value is "<token>.<signature>" (HMAC-signed with the
// backend's BETTER_AUTH_SECRET); this service resolves the token part against
// the shared auth_sessions table, which is the authoritative validity check
// (the token carries 32+ characters of entropy, and a DB hit is required
// anyway for expiry/ban/role, so revocations apply here instantly).
package auth

import (
	"errors"
	"net/http"
	"net/url"
	"strings"
	"time"

	"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
)

// Cookie names set by the backend: advanced.cookiePrefix "spanglish", with
// the __Secure- prefix added in production (advanced.useSecureCookies).
var sessionCookieNames = []string{
	"__Secure-spanglish.session_token",
	"spanglish.session_token",
}

var (
	ErrNoToken      = errors.New("no session token")
	ErrInvalidToken = errors.New("invalid session")
)

// AdminRoles mirrors backend/src/routes/media.ts: photo administration is
// admin/organizer only (review decision #6).
var AdminRoles = []string{"admin", "organizer"}

type User struct {
	ID    string
	Email string
	Role  string
}

func (u User) IsAdmin() bool {
	for _, r := range AdminRoles {
		if u.Role == r {
			return true
		}
	}
	return false
}

type Verifier struct {
	db *store.DB
}

func NewVerifier(db *store.DB) *Verifier {
	return &Verifier{db: db}
}

// FromRequest returns the authenticated user, ErrNoToken when no session
// cookie or bearer token is present, or ErrInvalidToken for anything bad.
func (v *Verifier) FromRequest(r *http.Request) (User, error) {
	token := sessionTokenFromRequest(r)
	if token == "" {
		return User{}, ErrNoToken
	}

	su, err := v.db.GetSessionUser(r.Context(), token)
	if err != nil {
		return User{}, ErrInvalidToken
	}
	if !su.ExpiresAt.After(time.Now()) {
		return User{}, ErrInvalidToken
	}
	// Same semantics as backend getAuthUser: suspended (banned) or unclaimed
	// accounts must not retain API access via a live session.
	if su.Banned || su.AccountStatus != "active" {
		return User{}, ErrInvalidToken
	}
	// Role comes from the users row, so demotions apply immediately.
	return User{ID: su.ID, Email: su.Email, Role: su.Role}, nil
}

// sessionTokenFromRequest extracts the raw session token from the Better Auth
// cookie, or from an Authorization: Bearer header (tests and CLI tooling).
func sessionTokenFromRequest(r *http.Request) string {
	for _, name := range sessionCookieNames {
		if c, err := r.Cookie(name); err == nil && c.Value != "" {
			return tokenPart(c.Value)
		}
	}
	if raw, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer "); ok && raw != "" {
		return tokenPart(raw)
	}
	return ""
}

// tokenPart strips the URL-encoding and HMAC signature from a cookie value:
// "token.base64sig" -> "token". The token itself is alphanumeric, so the
// first '.' always separates it from the signature.
func tokenPart(v string) string {
	if dec, err := url.QueryUnescape(v); err == nil {
		v = dec
	}
	if i := strings.IndexByte(v, '.'); i >= 0 {
		return v[:i]
	}
	return v
}
