package identity

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/registration"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

const mobileVerificationMaxFailedAttempts = 5

// AuthenticatedUser bundles the data resolved from a session bearer token.
type AuthenticatedUser struct {
	User    model.User
	Session model.UserSession
}

func (store *Store) SystemInitialized(ctx context.Context) (bool, error) {
	row := store.db.QueryRow(ctx, `
SELECT EXISTS (SELECT 1 FROM users WHERE is_admin = TRUE)
`)
	var initialized bool
	if err := row.Scan(&initialized); err != nil {
		return false, fmt.Errorf("check system initialized: %w", err)
	}
	return initialized, nil
}

func (store *Store) BootstrapInitialAdmin(ctx context.Context, email string, name string, displayName string, passwordHash string, tokenHash string, expiresAt time.Time) (AuthenticatedUser, error) {
	var authenticated AuthenticatedUser
	err := coredb.WithSystemBootstrapLockTx(ctx, store.db, func(tx coredb.Tx) error {
		var initialized bool
		if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE is_admin = TRUE)`).Scan(&initialized); err != nil {
			return fmt.Errorf("check existing admin: %w", err)
		}
		if initialized {
			return ErrSystemAlreadyInitialized
		}

		userID, err := identifiers.NewEntityID("usr")
		if err != nil {
			return fmt.Errorf("generate admin user id: %w", err)
		}
		sessionID, err := identifiers.NewEntityID("sess")
		if err != nil {
			return fmt.Errorf("generate admin session id: %w", err)
		}
		now := time.Now().UTC()

		if _, err := tx.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, '', TRUE, $5, $6, $6, $6)
`, userID, email, name, displayName, passwordHash, now); err != nil {
			if constraint, ok := uniqueViolationConstraint(err); ok {
				switch constraint {
				case "email", "users_email_key":
					return conflict("Email already registered. Try signing in instead.")
				case "name", "users_name_key":
					return conflict("Name already taken. Try another one.")
				default:
					return conflict("Account already exists.")
				}
			}
			return fmt.Errorf("insert initial admin user: %w", err)
		}

		user := modelUserFromRegistration(registration.BuildUserRecord(registration.UserRecordParams{
			ID:              userID,
			Email:           email,
			Name:            name,
			DisplayName:     displayName,
			IsAdmin:         true,
			PasswordHash:    passwordHash,
			EmailVerifiedAt: &now,
			Now:             now,
		}))
		if _, err := tx.Exec(ctx, `
INSERT INTO user_sessions (id, user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5)
`, sessionID, userID, tokenHash, expiresAt, now); err != nil {
			return fmt.Errorf("insert initial admin session: %w", err)
		}

		authenticated = AuthenticatedUser{
			User: user,
			Session: model.UserSession{
				ID:        sessionID,
				UserID:    userID,
				ExpiresAt: expiresAt,
				CreatedAt: now,
			},
		}
		return nil
	})
	if err != nil {
		return AuthenticatedUser{}, err
	}
	return authenticated, nil
}

func (store *Store) RegisterUser(ctx context.Context, email string, name string, displayName string, passwordHash string, verificationTokenHash string, verificationExpiresAt time.Time) (model.User, error) {
	initialized, err := store.SystemInitialized(ctx)
	if err != nil {
		return model.User{}, err
	}
	if !initialized {
		return model.User{}, ErrSystemNotInitialized
	}

	return withTxValue(ctx, store.db, func(tx coredb.Tx) (model.User, error) {
		user, err := insertRegularUser(ctx, tx, email, name, displayName, "", passwordHash, nil)
		if err != nil {
			return model.User{}, err
		}
		if _, err := tx.Exec(ctx, `
INSERT INTO user_email_verification_tokens (user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4)
`, user.ID, verificationTokenHash, verificationExpiresAt, user.CreatedAt); err != nil {
			return model.User{}, fmt.Errorf("insert verification token: %w", err)
		}

		return user, nil
	})
}

// CreateVerifiedUser creates a regular user whose email is already verified.
func (store *Store) CreateVerifiedUser(ctx context.Context, email string, name string, displayName string, passwordHash string) (model.User, error) {
	initialized, err := store.SystemInitialized(ctx)
	if err != nil {
		return model.User{}, err
	}
	if !initialized {
		return model.User{}, ErrSystemNotInitialized
	}

	return withTxValue(ctx, store.db, func(tx coredb.Tx) (model.User, error) {
		now := time.Now().UTC()
		user, err := insertRegularUser(ctx, tx, email, name, displayName, "", passwordHash, &now)
		if err != nil {
			return model.User{}, err
		}

		return user, nil
	})
}

func insertRegularUser(ctx context.Context, tx coredb.Tx, email string, name string, displayName string, avatarURL string, passwordHash string, emailVerifiedAt *time.Time) (model.User, error) {
	userID, err := identifiers.NewEntityID("usr")
	if err != nil {
		return model.User{}, fmt.Errorf("generate user id: %w", err)
	}
	now := time.Now().UTC()
	if _, err := tx.Exec(ctx, `
INSERT INTO users (id, email, name, display_name, avatar_url, is_admin, password_hash, email_verified_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, FALSE, $6, $7, $8, $8)
`, userID, email, name, displayName, avatarURL, passwordHash, emailVerifiedAt, now); err != nil {
		if constraint, ok := uniqueViolationConstraint(err); ok {
			switch constraint {
			case "email", "users_email_key":
				return model.User{}, conflict("Email already registered. Try signing in instead.")
			case "name", "users_name_key":
				return model.User{}, conflict("Name already taken. Try another one.")
			default:
				return model.User{}, conflict("Account already exists.")
			}
		}
		return model.User{}, fmt.Errorf("insert user: %w", err)
	}
	return modelUserFromRegistration(registration.BuildUserRecord(registration.UserRecordParams{
		ID:              userID,
		Email:           email,
		Name:            name,
		DisplayName:     displayName,
		AvatarURL:       avatarURL,
		PasswordHash:    passwordHash,
		EmailVerifiedAt: emailVerifiedAt,
		Now:             now,
	})), nil
}

func (store *Store) CreateVerificationToken(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) error {
	_, err := store.db.Exec(ctx, `
INSERT INTO user_email_verification_tokens (user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, NOW())
`, userID, tokenHash, expiresAt)
	if err != nil {
		return fmt.Errorf("insert verification token: %w", err)
	}
	return nil
}

// MarkUserEmailVerified marks the user as verified and returns the updated user record.
func (store *Store) MarkUserEmailVerified(ctx context.Context, userID string) (model.User, error) {
	now := time.Now().UTC()
	return withTxValue(ctx, store.db, func(tx coredb.Tx) (model.User, error) {
		if _, err := tx.Exec(ctx, `
UPDATE users
SET email_verified_at = COALESCE(email_verified_at, $2), updated_at = $2
WHERE id = $1
`, userID, now); err != nil {
			return model.User{}, fmt.Errorf("mark user email verified: %w", err)
		}

		user, err := loadUserWithoutPasswordHash(ctx, tx, userID)
		if err != nil {
			if coredb.IsNotFound(err) {
				return model.User{}, ErrNotFound
			}
			return model.User{}, fmt.Errorf("mark user email verified: %w", err)
		}

		return user, nil
	})
}

func (store *Store) VerifyEmailToken(ctx context.Context, token string) (model.User, error) {
	return withTxValue(ctx, store.db, func(tx coredb.Tx) (model.User, error) {
		tokenHash := secrets.HashToken(token)
		row := tx.QueryRow(ctx, `
SELECT u.id, COALESCE(u.email, ''), COALESCE(u.mobile, ''), u.name, u.display_name, u.avatar_url, u.is_admin, u.email_verified_at, u.mobile_verified_at, u.created_at, u.updated_at,
       t.user_id, t.expires_at, t.consumed_at
FROM user_email_verification_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.token_hash = $1
FOR UPDATE
`, tokenHash)
		var user model.User
		var tokenUserID string
		var expiresAt time.Time
		var consumedAt *time.Time
		if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt, &tokenUserID, &expiresAt, &consumedAt); err != nil {
			if coredb.IsNotFound(err) {
				return model.User{}, badRequest("invalid verification token")
			}
			return model.User{}, fmt.Errorf("load verification token: %w", err)
		}
		if consumedAt != nil {
			return user, nil
		}
		if time.Now().UTC().After(expiresAt) {
			return model.User{}, badRequest("verification token expired")
		}
		now := time.Now().UTC()
		if _, err := tx.Exec(ctx, `
UPDATE user_email_verification_tokens
SET consumed_at = $2
WHERE user_id = $1 AND token_hash = $3
`, tokenUserID, now, tokenHash); err != nil {
			return model.User{}, fmt.Errorf("consume verification token: %w", err)
		}
		if _, err := tx.Exec(ctx, `
UPDATE users
SET email_verified_at = COALESCE(email_verified_at, $2), updated_at = $2
WHERE id = $1
`, tokenUserID, now); err != nil {
			return model.User{}, fmt.Errorf("mark email verified: %w", err)
		}
		user.EmailVerifiedAt = &now
		user.UpdatedAt = now

		return user, nil
	})
}

func (store *Store) FindUserByEmail(ctx context.Context, email string) (model.User, error) {
	row := store.db.QueryRow(ctx, `
SELECT id, COALESCE(email, ''), COALESCE(mobile, ''), name, display_name, avatar_url, is_admin, password_hash, email_verified_at, mobile_verified_at, created_at, updated_at
FROM users
WHERE email = $1
`, email)
	var user model.User
	if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.PasswordHash, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.User{}, ErrNotFound
		}
		return model.User{}, fmt.Errorf("find user by email: %w", err)
	}
	return user, nil
}

func (store *Store) FindUserByID(ctx context.Context, userID string) (model.User, error) {
	row := store.db.QueryRow(ctx, `
SELECT id, COALESCE(email, ''), COALESCE(mobile, ''), name, display_name, avatar_url, is_admin, password_hash, email_verified_at, mobile_verified_at, created_at, updated_at
FROM users
WHERE id = $1
`, userID)
	var user model.User
	if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.PasswordHash, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.User{}, ErrNotFound
		}
		return model.User{}, fmt.Errorf("find user by id: %w", err)
	}
	return user, nil
}

func (store *Store) FindUserByName(ctx context.Context, name string) (model.User, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return model.User{}, badRequest("user name is required")
	}
	row := store.db.QueryRow(ctx, `
SELECT id, COALESCE(email, ''), COALESCE(mobile, ''), name, display_name, avatar_url, is_admin, password_hash, email_verified_at, mobile_verified_at, created_at, updated_at
FROM users
WHERE name = $1
`, name)
	var user model.User
	if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.PasswordHash, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.User{}, ErrNotFound
		}
		return model.User{}, fmt.Errorf("find user by name: %w", err)
	}
	return user, nil
}

func (store *Store) DeleteUserByID(ctx context.Context, userID string) error {
	tag, err := store.db.Exec(ctx, `DELETE FROM users WHERE id = $1`, userID)
	if err != nil {
		return fmt.Errorf("delete user: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (store *Store) FindUserByMobile(ctx context.Context, mobile string) (model.User, error) {
	resolvedMobile, err := authidentity.NormalizeMobile(mobile)
	if err != nil {
		return model.User{}, err
	}

	row := store.db.QueryRow(ctx, `
SELECT id, COALESCE(email, ''), COALESCE(mobile, ''), name, display_name, avatar_url, is_admin, password_hash, email_verified_at, mobile_verified_at, created_at, updated_at
FROM users
WHERE mobile = $1
`, resolvedMobile)
	var user model.User
	if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.PasswordHash, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.User{}, ErrNotFound
		}
		return model.User{}, fmt.Errorf("find user by mobile: %w", err)
	}
	return user, nil
}

func (store *Store) FindUserByLoginIdentifier(ctx context.Context, identifier string) (model.User, error) {
	resolvedIdentifier, err := authidentity.ResolveLoginIdentifier(identifier)
	if err != nil {
		return model.User{}, err
	}

	switch resolvedIdentifier.Kind {
	case authidentity.KindEmail:
		return store.FindUserByEmail(ctx, resolvedIdentifier.Value)
	case authidentity.KindMobile:
		user, err := store.FindUserByMobile(ctx, resolvedIdentifier.Value)
		if err != nil {
			return model.User{}, err
		}
		if !user.HasVerifiedMobile() {
			return model.User{}, ErrNotFound
		}
		return user, nil
	default:
		return model.User{}, badRequest("unsupported login identifier")
	}
}

func (store *Store) CreateMobileVerificationToken(ctx context.Context, userID string, mobile string, tokenHash string, expiresAt time.Time) error {
	resolvedMobile, err := authidentity.NormalizeMobile(mobile)
	if err != nil {
		return err
	}

	return coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if _, err := tx.Exec(ctx, `
DELETE FROM user_mobile_verification_tokens
WHERE user_id = $1
`, userID); err != nil {
			return fmt.Errorf("clear existing mobile verification tokens: %w", err)
		}

		var existingUserID string
		if err := tx.QueryRow(ctx, `
SELECT id
FROM users
WHERE mobile = $1 AND id <> $2
`, resolvedMobile, userID).Scan(&existingUserID); err != nil && !coredb.IsNotFound(err) {
			return fmt.Errorf("check mobile availability: %w", err)
		}
		if existingUserID != "" {
			return conflict("Mobile already registered. Try signing in instead.")
		}

		if _, err := tx.Exec(ctx, `
INSERT INTO user_mobile_verification_tokens (user_id, mobile, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4, NOW())
`, userID, resolvedMobile, tokenHash, expiresAt); err != nil {
			return fmt.Errorf("insert mobile verification token: %w", err)
		}

		return nil
	})
}

func (store *Store) DeleteMobileVerificationToken(ctx context.Context, userID string, tokenHash string) error {
	if _, err := store.db.Exec(ctx, `
DELETE FROM user_mobile_verification_tokens
WHERE user_id = $1 AND token_hash = $2
`, userID, tokenHash); err != nil {
		return fmt.Errorf("delete mobile verification token: %w", err)
	}
	return nil
}

func (store *Store) VerifyMobileToken(ctx context.Context, userID string, token string) (model.User, error) {
	var verificationErr error
	user, err := withTxValue(ctx, store.db, func(tx coredb.Tx) (model.User, error) {
		row := tx.QueryRow(ctx, `
SELECT u.id, COALESCE(u.email, ''), COALESCE(u.mobile, ''), u.name, u.display_name, u.avatar_url, u.is_admin, u.email_verified_at, u.mobile_verified_at, u.created_at, u.updated_at,
       t.mobile, t.token_hash, t.expires_at, t.consumed_at, t.failed_attempts, t.invalidated_at
FROM user_mobile_verification_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.user_id = $1
ORDER BY t.created_at DESC
LIMIT 1
FOR UPDATE
`, userID)

		var user model.User
		var tokenMobile string
		var tokenHash string
		var expiresAt time.Time
		var consumedAt *time.Time
		var failedAttempts int
		var invalidatedAt *time.Time
		if err := row.Scan(&user.ID, &user.Email, &user.Mobile, &user.Name, &user.DisplayName, &user.AvatarURL, &user.IsAdmin, &user.EmailVerifiedAt, &user.MobileVerifiedAt, &user.CreatedAt, &user.UpdatedAt, &tokenMobile, &tokenHash, &expiresAt, &consumedAt, &failedAttempts, &invalidatedAt); err != nil {
			if coredb.IsNotFound(err) {
				return model.User{}, badRequest("invalid mobile verification code")
			}
			return model.User{}, fmt.Errorf("load mobile verification token: %w", err)
		}

		if invalidatedAt != nil || failedAttempts >= mobileVerificationMaxFailedAttempts {
			verificationErr = badRequest("invalid mobile verification code")
			return model.User{}, nil
		}

		now := time.Now().UTC()
		matched, err := authidentity.MobileVerificationCodeMatchesHash(token, tokenHash)
		if err != nil {
			return model.User{}, err
		}
		if !matched {
			if consumedAt == nil && !now.After(expiresAt) {
				nextFailedAttempts := failedAttempts + 1
				if _, err := tx.Exec(ctx, `
UPDATE user_mobile_verification_tokens
SET failed_attempts = $3,
    invalidated_at = CASE
        WHEN $3 >= $4 AND invalidated_at IS NULL THEN $2
        ELSE invalidated_at
    END
WHERE user_id = $1 AND token_hash = $5
`, userID, now, nextFailedAttempts, mobileVerificationMaxFailedAttempts, tokenHash); err != nil {
					return model.User{}, fmt.Errorf("record mobile verification failure: %w", err)
				}
			}
			verificationErr = badRequest("invalid mobile verification code")
			return model.User{}, nil
		}
		if consumedAt != nil {
			return user, nil
		}
		if now.After(expiresAt) {
			verificationErr = badRequest("mobile verification code expired")
			return model.User{}, nil
		}

		commandTag, err := tx.Exec(ctx, `
UPDATE users
SET mobile = $2,
    mobile_verified_at = $3,
    updated_at = $3
WHERE id = $1
`, userID, tokenMobile, now)
		if err != nil {
			if constraint, ok := uniqueViolationConstraint(err); ok && constraint == "users_mobile_key" {
				return model.User{}, conflict("Mobile already registered. Try signing in instead.")
			}
			return model.User{}, fmt.Errorf("mark mobile verified: %w", err)
		}
		if commandTag.RowsAffected() == 0 {
			return model.User{}, fmt.Errorf("mark mobile verified: %w", ErrNotFound)
		}

		user, err = loadUserWithoutPasswordHash(ctx, tx, userID)
		if err != nil {
			return model.User{}, fmt.Errorf("load verified mobile user: %w", err)
		}

		if _, err := tx.Exec(ctx, `
UPDATE user_mobile_verification_tokens
SET consumed_at = $2
WHERE user_id = $1 AND token_hash = $3
`, userID, now, tokenHash); err != nil {
			return model.User{}, fmt.Errorf("consume mobile verification token: %w", err)
		}

		if _, err := tx.Exec(ctx, `
DELETE FROM user_mobile_verification_tokens
WHERE user_id = $1 AND token_hash <> $2
`, userID, tokenHash); err != nil {
			return model.User{}, fmt.Errorf("clear stale mobile verification tokens: %w", err)
		}

		return user, nil
	})
	if err != nil {
		return model.User{}, err
	}
	if verificationErr != nil {
		return model.User{}, verificationErr
	}
	return user, nil
}

func (store *Store) CreateSession(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) (model.UserSession, error) {
	sessionID, err := identifiers.NewEntityID("sess")
	if err != nil {
		return model.UserSession{}, fmt.Errorf("generate session id: %w", err)
	}
	now := time.Now().UTC()
	if _, err := store.db.Exec(ctx, `
INSERT INTO user_sessions (id, user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, $3, $4, $5)
	`, sessionID, userID, tokenHash, expiresAt, now); err != nil {
		return model.UserSession{}, fmt.Errorf("insert session: %w", err)
	}
	return model.UserSession{ID: sessionID, UserID: userID, ExpiresAt: expiresAt, CreatedAt: now}, nil
}

func (store *Store) AuthenticateSession(ctx context.Context, token string) (AuthenticatedUser, error) {
	tokenHash := secrets.HashToken(token)
	// Intentionally excludes `users.password_hash` to avoid loading sensitive fields on every request.
	row := store.db.QueryRow(ctx, `
SELECT s.id, s.user_id, s.session_family_id, s.expires_at, s.created_at, s.revoked_at,
       u.id, COALESCE(u.email, ''), COALESCE(u.mobile, ''), u.name, u.display_name, u.avatar_url, u.is_admin, u.email_verified_at, u.mobile_verified_at, u.created_at, u.updated_at
FROM user_sessions s
JOIN users u ON u.id = s.user_id
LEFT JOIN user_session_families f ON f.id = s.session_family_id
WHERE s.token_hash = $1
  AND s.revoked_at IS NULL
  AND s.expires_at > NOW()
  AND (s.session_family_id IS NULL OR (f.id IS NOT NULL AND f.revoked_at IS NULL AND f.expires_at > NOW()))
`, tokenHash)
	var authenticated AuthenticatedUser
	if err := row.Scan(
		&authenticated.Session.ID,
		&authenticated.Session.UserID,
		&authenticated.Session.SessionFamilyID,
		&authenticated.Session.ExpiresAt,
		&authenticated.Session.CreatedAt,
		&authenticated.Session.RevokedAt,
		&authenticated.User.ID,
		&authenticated.User.Email,
		&authenticated.User.Mobile,
		&authenticated.User.Name,
		&authenticated.User.DisplayName,
		&authenticated.User.AvatarURL,
		&authenticated.User.IsAdmin,
		&authenticated.User.EmailVerifiedAt,
		&authenticated.User.MobileVerifiedAt,
		&authenticated.User.CreatedAt,
		&authenticated.User.UpdatedAt,
	); err != nil {
		if coredb.IsNotFound(err) {
			return AuthenticatedUser{}, ErrUnauthorized
		}
		return AuthenticatedUser{}, fmt.Errorf("authenticate session: %w", err)
	}
	return authenticated, nil
}

func (store *Store) RevokeSession(ctx context.Context, sessionID string) error {
	return coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		now := time.Now().UTC()
		if _, err := tx.Exec(ctx, `
UPDATE user_session_families
SET revoked_at = $2
WHERE id = (SELECT session_family_id FROM user_sessions WHERE id = $1)
  AND revoked_at IS NULL
`, sessionID, now); err != nil {
			return fmt.Errorf("revoke session family: %w", err)
		}
		if _, err := tx.Exec(ctx, `
UPDATE user_sessions
SET revoked_at = $2
WHERE id = $1 AND revoked_at IS NULL
`, sessionID, now); err != nil {
			return fmt.Errorf("revoke session: %w", err)
		}
		return nil
	})
}

func modelUserFromRegistration(user registration.UserRecord) model.User {
	return model.User{
		ID:               user.ID,
		Email:            user.Email,
		Mobile:           user.Mobile,
		Name:             user.Name,
		DisplayName:      user.DisplayName,
		AvatarURL:        user.AvatarURL,
		IsAdmin:          user.IsAdmin,
		PasswordHash:     user.PasswordHash,
		EmailVerifiedAt:  user.EmailVerifiedAt,
		MobileVerifiedAt: user.MobileVerifiedAt,
		CreatedAt:        user.CreatedAt,
		UpdatedAt:        user.UpdatedAt,
	}
}

// IssueSessionTokenForUser mints a new user session token for the given user
// and persists its hash. Returns the plaintext token. Used by the daemon
// session-token exchange endpoint to let machines call browser-facing APIs.
func (store *Store) IssueSessionTokenForUser(ctx context.Context, userID string, ttl time.Duration) (string, error) {
	token, err := secrets.NewToken(32)
	if err != nil {
		return "", fmt.Errorf("generate session token: %w", err)
	}
	expiresAt := time.Now().UTC().Add(ttl)
	if _, err := store.CreateSession(ctx, userID, secrets.HashToken(token), expiresAt); err != nil {
		return "", fmt.Errorf("create session for machine: %w", err)
	}
	return token, nil
}
