package auth

import (
	"context"
	"errors"

	"github.com/owncord/server/db"
)

// tokenStore is the DB surface bearer-token resolution needs. *db.DB satisfies
// it directly; tests use a fake. Kept as a tiny interface (like db.Auditor) so
// the security-critical resolution logic is unit-testable without a real DB.
type tokenStore interface {
	GetSessionByTokenHash(ctx context.Context, tokenHash string) (*db.Session, error)
	GetActiveAPIToken(ctx context.Context, tokenHash string) (*db.APIToken, error)
	GetUserByID(ctx context.Context, id int64) (*db.User, error)
	GetRoleByID(ctx context.Context, id int64) (*db.Role, error)
}

// Sentinel outcomes, so each caller can reproduce its existing 401/403 responses
// exactly. A DB outage is NOT one of these — it surfaces as a wrapped error.
var (
	ErrTokenNotFound = errors.New("auth: no matching session or api token")
	ErrTokenExpired  = errors.New("auth: session expired")
	ErrUserNotFound  = errors.New("auth: user not found")
	ErrRoleNotFound  = errors.New("auth: role not found")
)

// ResolveTokenHash resolves a hashed bearer token to its principal (user + role).
//
// It matches a login session FIRST — so every existing session code path is
// preserved byte-for-byte — and only falls through to an API token when no
// session row matches. The returned *db.Session is nil for an API-token
// principal (downstream consumers already guard a nil session).
//
// A DB error is returned WRAPPED (never a sentinel) so callers can distinguish
// an outage from a bad token and never fall through to API-token lookup on an
// outage. On ErrTokenExpired the matched (expired) session is returned so the
// caller can schedule its cleanup by hash, exactly as the api middleware does today.
func ResolveTokenHash(ctx context.Context, store tokenStore, hash string) (*db.User, *db.Role, *db.Session, error) {
	sess, err := store.GetSessionByTokenHash(ctx, hash)
	if err != nil {
		return nil, nil, nil, err // DB outage — do not fall through to API tokens
	}

	var userID int64
	switch {
	case sess != nil:
		if IsSessionExpired(sess.ExpiresAt) {
			return nil, nil, sess, ErrTokenExpired
		}
		userID = sess.UserID
	default:
		tok, err := store.GetActiveAPIToken(ctx, hash)
		if err != nil {
			return nil, nil, nil, err
		}
		if tok == nil {
			return nil, nil, nil, ErrTokenNotFound
		}
		userID = tok.UserID
	}

	user, err := store.GetUserByID(ctx, userID)
	if err != nil {
		return nil, nil, nil, err
	}
	if user == nil {
		return nil, nil, nil, ErrUserNotFound
	}

	// A dangling role_id returns (nil, nil): the nil check is load-bearing so a
	// nil role never reaches the context and every downstream permission check.
	role, err := store.GetRoleByID(ctx, user.RoleID)
	if err != nil {
		return nil, nil, nil, err
	}
	if role == nil {
		return nil, nil, nil, ErrRoleNotFound
	}
	return user, role, sess, nil
}
