package v1

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log/slog"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/ast"
	"github.com/pkg/errors"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/usememos/memos/internal/base"
	"github.com/usememos/memos/internal/util"
	"github.com/usememos/memos/internal/webhook"
	v1pb "github.com/usememos/memos/proto/gen/api/v1"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/server/auth"
	"github.com/usememos/memos/store"
)

const maxBatchGetUsers = 100

func (s *APIV1Service) ListUsers(ctx context.Context, request *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	userFind := &store.FindUser{}

	if request.Filter != "" {
		username, err := extractUsernameFromFilter(request.Filter)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err)
		}
		if username != "" {
			userFind.Username = &username
		}
	}

	users, err := s.Store.ListUsers(ctx, userFind)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
	}

	// TODO: Implement proper ordering, and pagination
	// For now, return all users with basic structure
	response := &v1pb.ListUsersResponse{
		Users:     []*v1pb.User{},
		TotalSize: int32(len(users)),
	}
	for _, user := range users {
		response.Users = append(response.Users, convertUserFromStore(user, currentUser))
	}
	return response, nil
}

func normalizeBatchUsernames(usernames []string) []string {
	uniqueUsernames := make([]string, 0, len(usernames))
	seen := make(map[string]struct{}, len(usernames))
	for _, username := range usernames {
		username = strings.TrimSpace(strings.ToLower(username))
		if username == "" || !base.UIDMatcher.MatchString(username) {
			continue
		}
		if _, ok := seen[username]; ok {
			continue
		}
		seen[username] = struct{}{}
		uniqueUsernames = append(uniqueUsernames, username)
	}
	return uniqueUsernames
}

func (s *APIV1Service) BatchGetUsers(ctx context.Context, request *v1pb.BatchGetUsersRequest) (*v1pb.BatchGetUsersResponse, error) {
	if len(request.Usernames) == 0 {
		return &v1pb.BatchGetUsersResponse{Users: []*v1pb.User{}}, nil
	}

	uniqueUsernames := normalizeBatchUsernames(request.Usernames)
	if len(uniqueUsernames) > maxBatchGetUsers {
		return nil, status.Errorf(codes.InvalidArgument, "too many usernames (max %d)", maxBatchGetUsers)
	}

	if len(uniqueUsernames) == 0 {
		return &v1pb.BatchGetUsersResponse{Users: []*v1pb.User{}}, nil
	}

	normal := store.Normal
	users, err := s.Store.ListUsers(ctx, &store.FindUser{
		UsernameList: uniqueUsernames,
		RowStatus:    &normal,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
	}

	currentUser, _ := s.fetchCurrentUser(ctx)
	response := &v1pb.BatchGetUsersResponse{
		Users: make([]*v1pb.User, 0, len(users)),
	}
	for _, user := range users {
		response.Users = append(response.Users, convertUserFromStore(user, currentUser))
	}
	return response, nil
}

func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest) (*v1pb.User, error) {
	user, err := ResolveUserByName(ctx, s.Store, request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %s", request.Name)
	}
	if user == nil {
		return nil, status.Errorf(codes.NotFound, "user not found")
	}
	currentUser, _ := s.fetchCurrentUser(ctx)
	return convertUserFromStore(user, currentUser), nil
}

func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
	// Get current user (might be nil for unauthenticated requests)
	currentUser, _ := s.fetchCurrentUser(ctx)

	// Check if there are any existing users (for first-time setup detection)
	limitOne := 1
	allUsers, err := s.Store.ListUsers(ctx, &store.FindUser{Limit: &limitOne})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
	}
	isFirstUser := len(allUsers) == 0

	// Check registration settings FIRST (unless it's the very first user)
	if !isFirstUser {
		// Only allow user registration if it is enabled in the settings, or if the user is a superuser
		if currentUser == nil || !isSuperUser(currentUser) {
			instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
			if err != nil {
				return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
			}
			if instanceGeneralSetting.DisallowUserRegistration {
				return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed")
			}
		}
	}

	// Determine the role to assign
	var roleToAssign store.Role
	if isFirstUser {
		// First-time setup: create the first user as ADMIN (no authentication required)
		roleToAssign = store.RoleAdmin
	} else if currentUser != nil && currentUser.Role == store.RoleAdmin {
		// Authenticated ADMIN user can create users with any role specified in request
		if request.User.Role != v1pb.User_ROLE_UNSPECIFIED {
			roleToAssign = convertUserRoleToStore(request.User.Role)
		} else {
			roleToAssign = store.RoleUser
		}
	} else {
		// Unauthenticated or non-ADMIN users can only create normal users
		roleToAssign = store.RoleUser
	}

	if !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {
		return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
	}

	// If validate_only is true, just validate without creating
	if request.ValidateOnly {
		// Perform validation checks without actually creating the user
		return &v1pb.User{
			Username:    request.User.Username,
			Email:       request.User.Email,
			DisplayName: request.User.DisplayName,
			Role:        convertUserRoleFromStore(roleToAssign),
		}, nil
	}

	passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err)
	}

	user, err := s.Store.CreateUser(ctx, &store.User{
		Username:     request.User.Username,
		Role:         roleToAssign,
		Email:        request.User.Email,
		Nickname:     request.User.DisplayName,
		PasswordHash: string(passwordHash),
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
	}

	return convertUserFromStore(user, user), nil
}

func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserRequest) (*v1pb.User, error) {
	if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
	}
	user, err := ResolveUserByName(ctx, s.Store, request.User.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	if user == nil {
		if request.AllowMissing {
			return nil, status.Errorf(codes.NotFound, "user not found")
		}
		return nil, status.Errorf(codes.NotFound, "user not found")
	}
	userID := user.ID
	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	// Check permission.
	// Only allow admin or self to update user.
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	currentTs := time.Now().Unix()
	update := &store.UpdateUser{
		ID:        user.ID,
		UpdatedTs: &currentTs,
	}
	instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get instance general setting: %v", err)
	}
	for _, field := range request.UpdateMask.Paths {
		switch field {
		case "username":
			if instanceGeneralSetting.DisallowChangeUsername {
				return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change username")
			}
			if !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) {
				return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
			}
			update.Username = &request.User.Username
		case "display_name":
			if instanceGeneralSetting.DisallowChangeNickname {
				return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change nickname")
			}
			update.Nickname = &request.User.DisplayName
		case "email":
			update.Email = &request.User.Email
		case "avatar_url":
			// Validate avatar MIME type to prevent XSS during upload
			if request.User.AvatarUrl != "" {
				imageType, _, err := extractImageInfo(request.User.AvatarUrl)
				if err != nil {
					return nil, status.Errorf(codes.InvalidArgument, "invalid avatar format: %v", err)
				}
				// Only allow safe image formats for avatars
				allowedAvatarTypes := map[string]bool{
					"image/png":  true,
					"image/jpeg": true,
					"image/jpg":  true,
					"image/gif":  true,
					"image/webp": true,
				}
				if !allowedAvatarTypes[imageType] {
					return nil, status.Errorf(codes.InvalidArgument, "invalid avatar image type: %s. Only PNG, JPEG, GIF, and WebP are allowed", imageType)
				}
			}
			update.AvatarURL = &request.User.AvatarUrl
		case "description":
			update.Description = &request.User.Description
		case "role":
			// Only allow admin to update role.
			if currentUser.Role != store.RoleAdmin {
				return nil, status.Errorf(codes.PermissionDenied, "permission denied")
			}
			role := convertUserRoleToStore(request.User.Role)
			update.Role = &role
		case "password":
			passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
			if err != nil {
				return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err)
			}
			passwordHashStr := string(passwordHash)
			update.PasswordHash = &passwordHashStr
		case "state":
			rowStatus := convertStateToStore(request.User.State)
			update.RowStatus = &rowStatus
		default:
			return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
		}
	}

	updatedUser, err := s.Store.UpdateUser(ctx, update)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
	}

	return convertUserFromStore(updatedUser, currentUser), nil
}

func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserRequest) (*emptypb.Empty, error) {
	user, err := ResolveUserByName(ctx, s.Store, request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	if user == nil {
		return nil, status.Errorf(codes.NotFound, "user not found")
	}
	userID := user.ID
	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
		ID: user.ID,
	}); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err)
	}

	return &emptypb.Empty{}, nil
}

func getDefaultUserGeneralSetting() *v1pb.UserSetting_GeneralSetting {
	return &v1pb.UserSetting_GeneralSetting{
		Locale:         "en",
		MemoVisibility: "PRIVATE",
		Theme:          "",
	}
}

func (s *APIV1Service) resolveUserFromName(ctx context.Context, name string) (*store.User, error) {
	user, err := ResolveUserByName(ctx, s.Store, name)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return nil, errors.Errorf("user not found: %s", name)
	}
	return user, nil
}

func (s *APIV1Service) resolveUserAndSettingKeyFromName(ctx context.Context, name string) (*store.User, string, error) {
	parts := strings.Split(name, "/")
	if len(parts) != 4 || parts[0] != "users" || parts[2] != "settings" {
		return nil, "", errors.Errorf("invalid resource name format: %s", name)
	}

	user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
	if err != nil {
		return nil, "", err
	}
	return user, parts[3], nil
}

func (s *APIV1Service) resolveUserAndWebhookIDFromName(ctx context.Context, name string) (*store.User, string, error) {
	parts := strings.Split(name, "/")
	if len(parts) != 4 || parts[0] != "users" || parts[2] != "webhooks" {
		return nil, "", errors.New("invalid webhook name format")
	}

	user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
	if err != nil {
		return nil, "", err
	}
	return user, parts[3], nil
}

func (s *APIV1Service) resolveUserAndNotificationIDFromName(ctx context.Context, name string) (*store.User, int32, error) {
	parts := strings.Split(name, "/")
	if len(parts) != 4 || parts[0] != "users" || parts[2] != "notifications" {
		return nil, 0, errors.Errorf("invalid notification name: %s", name)
	}

	user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
	if err != nil {
		return nil, 0, err
	}

	id, err := strconv.Atoi(parts[3])
	if err != nil {
		return nil, 0, errors.Errorf("invalid notification id: %s", parts[3])
	}

	return user, int32(id), nil
}

func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) {
	// Parse resource name: users/{user}/settings/{setting}
	user, settingKey, err := s.resolveUserAndSettingKeyFromName(ctx, request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}

	// Only allow user to get their own settings
	if currentUser.ID != userID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	// Convert setting key string to store enum
	storeKey, err := convertSettingKeyToStore(settingKey)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err)
	}

	userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
		UserID: &userID,
		Key:    storeKey,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err)
	}

	return convertUserSettingFromStore(userSetting, user, storeKey), nil
}

func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) {
	// Parse resource name: users/{user}/settings/{setting}
	user, settingKey, err := s.resolveUserAndSettingKeyFromName(ctx, request.Setting.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}

	// Only allow user to update their own settings
	if currentUser.ID != userID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
		return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
	}

	// Convert setting key string to store enum
	storeKey, err := convertSettingKeyToStore(settingKey)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid setting key: %v", err)
	}

	var updatedSetting *v1pb.UserSetting
	switch storeKey {
	case storepb.UserSetting_GENERAL:
		existingUserSetting, _ := s.Store.GetUserSetting(ctx, &store.FindUserSetting{
			UserID: &userID,
			Key:    storeKey,
		})

		generalSetting := &storepb.GeneralUserSetting{}
		if existingUserSetting != nil {
			// Start with existing general setting values.
			generalSetting = existingUserSetting.GetGeneral()
		}

		updatedGeneral := &v1pb.UserSetting_GeneralSetting{
			MemoVisibility: generalSetting.GetMemoVisibility(),
			Locale:         generalSetting.GetLocale(),
			Theme:          generalSetting.GetTheme(),
		}

		incomingGeneral := request.Setting.GetGeneralSetting()
		if incomingGeneral == nil {
			return nil, status.Errorf(codes.InvalidArgument, "general setting is required")
		}
		for _, field := range request.UpdateMask.Paths {
			switch field {
			case "memo_visibility":
				updatedGeneral.MemoVisibility = incomingGeneral.MemoVisibility
			case "theme":
				updatedGeneral.Theme = incomingGeneral.Theme
			case "locale":
				updatedGeneral.Locale = incomingGeneral.Locale
			default:
				// Ignore unsupported fields.
			}
		}

		updatedSetting = &v1pb.UserSetting{
			Name: request.Setting.Name,
			Value: &v1pb.UserSetting_GeneralSetting_{
				GeneralSetting: updatedGeneral,
			},
		}
	default:
		return nil, status.Errorf(codes.InvalidArgument, "setting type %s should not be updated via UpdateUserSetting", storeKey.String())
	}

	// Convert API setting to store setting
	storeSetting, err := convertUserSettingToStore(updatedSetting, userID, storeKey)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "failed to convert setting: %v", err)
	}

	// Upsert the setting
	if _, err := s.Store.UpsertUserSetting(ctx, storeSetting); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
	}

	return s.GetUserSetting(ctx, &v1pb.GetUserSettingRequest{Name: request.Setting.Name})
}

func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListUserSettingsRequest) (*v1pb.ListUserSettingsResponse, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid parent name: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}

	// Only allow user to list their own settings
	if currentUser.ID != userID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{
		UserID: &userID,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list user settings: %v", err)
	}

	settings := make([]*v1pb.UserSetting, 0, len(userSettings))
	for _, storeSetting := range userSettings {
		apiSetting := convertUserSettingFromStore(storeSetting, user, storeSetting.Key)
		if apiSetting != nil {
			settings = append(settings, apiSetting)
		}
	}

	hasGeneral := false
	for _, setting := range settings {
		if setting.GetGeneralSetting() != nil {
			hasGeneral = true
		}
	}
	if !hasGeneral {
		defaultGeneral := &v1pb.UserSetting{
			Name: fmt.Sprintf("%s/settings/%s", BuildUserName(user.Username), convertSettingKeyFromStore(storepb.UserSetting_GENERAL)),
			Value: &v1pb.UserSetting_GeneralSetting_{
				GeneralSetting: getDefaultUserGeneralSetting(),
			},
		}
		settings = append([]*v1pb.UserSetting{defaultGeneral}, settings...)
	}
	response := &v1pb.ListUserSettingsResponse{
		Settings:  settings,
		TotalSize: int32(len(settings)),
	}

	return response, nil
}

// ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
//
// Personal Access Tokens are used for:
// - Mobile app authentication
// - CLI tool authentication
// - API client authentication
// - Any programmatic access requiring Bearer token auth
//
// Security:
// - Only the token owner can list their tokens
// - Returns token metadata only (not the actual token value)
// - Invalid or expired tokens are filtered out
//
// Authentication: Required (session cookie or access token)
// Authorization: User can only list their own tokens.
func (s *APIV1Service) ListPersonalAccessTokens(ctx context.Context, request *v1pb.ListPersonalAccessTokensRequest) (*v1pb.ListPersonalAccessTokensResponse, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	userID := user.ID

	// Verify permission
	claims := auth.GetUserClaims(ctx)
	if claims == nil || claims.UserID != userID {
		currentUser, _ := s.fetchCurrentUser(ctx)
		if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
			return nil, status.Errorf(codes.PermissionDenied, "permission denied")
		}
	}

	tokens, err := s.Store.GetUserPersonalAccessTokens(ctx, userID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get access tokens: %v", err)
	}

	personalAccessTokens := make([]*v1pb.PersonalAccessToken, len(tokens))
	for i, token := range tokens {
		personalAccessTokens[i] = &v1pb.PersonalAccessToken{
			Name:        fmt.Sprintf("%s/personalAccessTokens/%s", BuildUserName(user.Username), token.TokenId),
			Description: token.Description,
			ExpiresAt:   token.ExpiresAt,
			CreatedAt:   token.CreatedAt,
			LastUsedAt:  token.LastUsedAt,
		}
	}

	return &v1pb.ListPersonalAccessTokensResponse{PersonalAccessTokens: personalAccessTokens}, nil
}

// CreatePersonalAccessToken creates a new Personal Access Token (PAT) for a user.
//
// Use cases:
// - User manually creates token in settings for mobile app
// - User creates token for CLI tool
// - User creates token for third-party integration
//
// Token properties:
// - Random string with memos_pat_ prefix
// - SHA-256 hash stored in database
// - Optional expiration time (can be never-expiring)
// - User-provided description for identification
//
// Security considerations:
// - Full token is only shown ONCE (in this response)
// - User should copy and store it securely
// - Token can be revoked by deleting it from settings
//
// Authentication: Required (session cookie or access token)
// Authorization: User can only create tokens for themselves.
func (s *APIV1Service) CreatePersonalAccessToken(ctx context.Context, request *v1pb.CreatePersonalAccessTokenRequest) (*v1pb.CreatePersonalAccessTokenResponse, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	userID := user.ID

	// Verify permission
	claims := auth.GetUserClaims(ctx)
	if claims == nil || claims.UserID != userID {
		currentUser, _ := s.fetchCurrentUser(ctx)
		if currentUser == nil || currentUser.ID != userID {
			return nil, status.Errorf(codes.PermissionDenied, "permission denied")
		}
	}

	// Generate PAT
	tokenID := util.GenUUID()
	token := auth.GeneratePersonalAccessToken()
	tokenHash := auth.HashPersonalAccessToken(token)

	var expiresAt *timestamppb.Timestamp
	if request.ExpiresInDays > 0 {
		expiresAt = timestamppb.New(time.Now().AddDate(0, 0, int(request.ExpiresInDays)))
	}

	patRecord := &storepb.PersonalAccessTokensUserSetting_PersonalAccessToken{
		TokenId:     tokenID,
		TokenHash:   tokenHash,
		Description: request.Description,
		ExpiresAt:   expiresAt,
		CreatedAt:   timestamppb.Now(),
	}

	if err := s.Store.AddUserPersonalAccessToken(ctx, userID, patRecord); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err)
	}

	return &v1pb.CreatePersonalAccessTokenResponse{
		PersonalAccessToken: &v1pb.PersonalAccessToken{
			Name:        fmt.Sprintf("%s/personalAccessTokens/%s", BuildUserName(user.Username), tokenID),
			Description: request.Description,
			ExpiresAt:   expiresAt,
			CreatedAt:   patRecord.CreatedAt,
		},
		Token: token, // Only returned on creation
	}, nil
}

// DeletePersonalAccessToken revokes a Personal Access Token.
//
// This endpoint:
// 1. Removes the token from the user's access tokens list
// 2. Immediately invalidates the token (subsequent API calls with it will fail)
//
// Use cases:
// - User revokes a compromised token
// - User removes token for unused app/device
// - User cleans up old tokens
//
// Authentication: Required (session cookie or access token)
// Authorization: User can only delete their own tokens.
func (s *APIV1Service) DeletePersonalAccessToken(ctx context.Context, request *v1pb.DeletePersonalAccessTokenRequest) (*emptypb.Empty, error) {
	parts := strings.Split(request.Name, "/")
	if len(parts) != 4 || parts[0] != "users" || parts[2] != "personalAccessTokens" {
		return nil, status.Errorf(codes.InvalidArgument, "invalid personal access token name")
	}

	user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	userID := user.ID
	tokenID := parts[3]

	// Verify permission
	claims := auth.GetUserClaims(ctx)
	if claims == nil || claims.UserID != userID {
		currentUser, _ := s.fetchCurrentUser(ctx)
		if currentUser == nil || currentUser.ID != userID {
			return nil, status.Errorf(codes.PermissionDenied, "permission denied")
		}
	}

	if err := s.Store.RemoveUserPersonalAccessToken(ctx, userID, tokenID); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to delete access token: %v", err)
	}

	return &emptypb.Empty{}, nil
}

func (s *APIV1Service) ListUserWebhooks(ctx context.Context, request *v1pb.ListUserWebhooksRequest) (*v1pb.ListUserWebhooksResponse, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	webhooks, err := s.Store.GetUserWebhooks(ctx, userID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err)
	}

	userWebhooks := make([]*v1pb.UserWebhook, 0, len(webhooks))
	for _, webhook := range webhooks {
		userWebhooks = append(userWebhooks, convertUserWebhookFromUserSetting(webhook, user))
	}

	return &v1pb.ListUserWebhooksResponse{
		Webhooks: userWebhooks,
	}, nil
}

func (s *APIV1Service) CreateUserWebhook(ctx context.Context, request *v1pb.CreateUserWebhookRequest) (*v1pb.UserWebhook, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	if request.Webhook.Url == "" {
		return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required")
	}
	if err := webhook.ValidateURL(strings.TrimSpace(request.Webhook.Url)); err != nil {
		return nil, err
	}

	webhookID := generateUserWebhookID()
	webhook := &storepb.WebhooksUserSetting_Webhook{
		Id:    webhookID,
		Title: request.Webhook.DisplayName,
		Url:   strings.TrimSpace(request.Webhook.Url),
	}

	err = s.Store.AddUserWebhook(ctx, userID, webhook)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to create webhook: %v", err)
	}

	return convertUserWebhookFromUserSetting(webhook, user), nil
}

func (s *APIV1Service) UpdateUserWebhook(ctx context.Context, request *v1pb.UpdateUserWebhookRequest) (*v1pb.UserWebhook, error) {
	if request.Webhook == nil {
		return nil, status.Errorf(codes.InvalidArgument, "webhook is required")
	}

	user, webhookID, err := s.resolveUserAndWebhookIDFromName(ctx, request.Webhook.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	// Get existing webhooks
	webhooks, err := s.Store.GetUserWebhooks(ctx, userID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err)
	}

	// Find the webhook to update
	var targetWebhook *storepb.WebhooksUserSetting_Webhook
	for _, webhook := range webhooks {
		if webhook.Id == webhookID {
			targetWebhook = webhook
			break
		}
	}

	if targetWebhook == nil {
		return nil, status.Errorf(codes.NotFound, "webhook not found")
	}

	// Update the webhook
	updatedWebhook := &storepb.WebhooksUserSetting_Webhook{
		Id:    webhookID,
		Title: targetWebhook.Title,
		Url:   targetWebhook.Url,
	}

	if request.UpdateMask != nil {
		for _, path := range request.UpdateMask.Paths {
			switch path {
			case "url":
				if request.Webhook.Url != "" {
					trimmed := strings.TrimSpace(request.Webhook.Url)
					if err := webhook.ValidateURL(trimmed); err != nil {
						return nil, err
					}
					updatedWebhook.Url = trimmed
				}
			case "display_name":
				updatedWebhook.Title = request.Webhook.DisplayName
			default:
				// Ignore unsupported fields
			}
		}
	} else {
		// If no update mask is provided, update all fields
		if request.Webhook.Url != "" {
			trimmed := strings.TrimSpace(request.Webhook.Url)
			if err := webhook.ValidateURL(trimmed); err != nil {
				return nil, err
			}
			updatedWebhook.Url = trimmed
		}
		updatedWebhook.Title = request.Webhook.DisplayName
	}

	err = s.Store.UpdateUserWebhook(ctx, userID, updatedWebhook)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to update webhook: %v", err)
	}

	return convertUserWebhookFromUserSetting(updatedWebhook, user), nil
}

func (s *APIV1Service) DeleteUserWebhook(ctx context.Context, request *v1pb.DeleteUserWebhookRequest) (*emptypb.Empty, error) {
	user, webhookID, err := s.resolveUserAndWebhookIDFromName(ctx, request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
	}
	userID := user.ID

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	// Get existing webhooks to verify the webhook exists
	webhooks, err := s.Store.GetUserWebhooks(ctx, userID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get user webhooks: %v", err)
	}

	// Check if webhook exists
	found := false
	for _, webhook := range webhooks {
		if webhook.Id == webhookID {
			found = true
			break
		}
	}

	if !found {
		return nil, status.Errorf(codes.NotFound, "webhook not found")
	}

	err = s.Store.RemoveUserWebhook(ctx, userID, webhookID)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to delete webhook: %v", err)
	}

	return &emptypb.Empty{}, nil
}

// Helper functions for webhook operations

// generateUserWebhookID generates a unique ID for user webhooks.
func generateUserWebhookID() string {
	b := make([]byte, 8)
	rand.Read(b)
	return hex.EncodeToString(b)
}

// convertUserWebhookFromUserSetting converts a storepb webhook to a v1pb UserWebhook.
func convertUserWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, user *store.User) *v1pb.UserWebhook {
	return &v1pb.UserWebhook{
		Name:        fmt.Sprintf("%s/webhooks/%s", BuildUserName(user.Username), webhook.Id),
		Url:         webhook.Url,
		DisplayName: webhook.Title,
		// Note: create_time and update_time are not available in the user setting webhook structure
		// This is a limitation of storing webhooks in user settings vs the dedicated webhook table
	}
}

func convertUserFromStore(user *store.User, viewer *store.User) *v1pb.User {
	userpb := &v1pb.User{
		Name:        BuildUserName(user.Username),
		State:       convertStateFromStore(user.RowStatus),
		CreateTime:  timestamppb.New(time.Unix(user.CreatedTs, 0)),
		UpdateTime:  timestamppb.New(time.Unix(user.UpdatedTs, 0)),
		Role:        convertUserRoleFromStore(user.Role),
		Username:    user.Username,
		DisplayName: user.Nickname,
		AvatarUrl:   user.AvatarURL,
		Description: user.Description,
	}
	if canViewerAccessUserEmail(viewer, user) {
		userpb.Email = user.Email
	}
	// Use the avatar URL instead of raw base64 image data to reduce the response size.
	if user.AvatarURL != "" {
		// Check if avatar url is base64 format.
		_, _, err := extractImageInfo(user.AvatarURL)
		if err == nil {
			userpb.AvatarUrl = fmt.Sprintf("/file/%s/avatar", userpb.Name)
		} else {
			userpb.AvatarUrl = user.AvatarURL
		}
	}
	return userpb
}

func canViewerAccessUserEmail(viewer, user *store.User) bool {
	if viewer == nil || user == nil {
		return false
	}
	return viewer.Role == store.RoleAdmin || viewer.ID == user.ID
}

func convertUserRoleFromStore(role store.Role) v1pb.User_Role {
	switch role {
	case store.RoleAdmin:
		return v1pb.User_ADMIN
	case store.RoleUser:
		return v1pb.User_USER
	default:
		return v1pb.User_ROLE_UNSPECIFIED
	}
}

func convertUserRoleToStore(role v1pb.User_Role) store.Role {
	switch role {
	case v1pb.User_ADMIN:
		return store.RoleAdmin
	default:
		return store.RoleUser
	}
}

// extractImageInfo extracts image type and base64 data from a data URI.
// Data URI format: data:image/png;base64,iVBORw0KGgo...
func extractImageInfo(dataURI string) (string, string, error) {
	dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
	matches := dataURIRegex.FindStringSubmatch(dataURI)
	if len(matches) != 3 {
		return "", "", errors.New("invalid data URI format")
	}
	imageType := matches[1]
	base64Data := matches[2]
	return imageType, base64Data, nil
}

// convertSettingKeyToStore converts API setting key to store enum.
func convertSettingKeyToStore(key string) (storepb.UserSetting_Key, error) {
	switch key {
	case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]:
		return storepb.UserSetting_GENERAL, nil
	case v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]:
		return storepb.UserSetting_WEBHOOKS, nil
	default:
		return storepb.UserSetting_KEY_UNSPECIFIED, errors.Errorf("unknown setting key: %s", key)
	}
}

// convertSettingKeyFromStore converts store enum to API setting key.
func convertSettingKeyFromStore(key storepb.UserSetting_Key) string {
	switch key {
	case storepb.UserSetting_GENERAL:
		return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_GENERAL)]
	case storepb.UserSetting_SHORTCUTS:
		return "SHORTCUTS" // Not defined in API proto
	case storepb.UserSetting_WEBHOOKS:
		return v1pb.UserSetting_Key_name[int32(v1pb.UserSetting_WEBHOOKS)]
	default:
		return "unknown"
	}
}

// convertUserSettingFromStore converts store UserSetting to API UserSetting.
func convertUserSettingFromStore(storeSetting *storepb.UserSetting, user *store.User, key storepb.UserSetting_Key) *v1pb.UserSetting {
	if storeSetting == nil {
		// Return default setting if none exists
		settingKey := convertSettingKeyFromStore(key)
		setting := &v1pb.UserSetting{
			Name: fmt.Sprintf("%s/settings/%s", BuildUserName(user.Username), settingKey),
		}

		switch key {
		case storepb.UserSetting_WEBHOOKS:
			setting.Value = &v1pb.UserSetting_WebhooksSetting_{
				WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{
					Webhooks: []*v1pb.UserWebhook{},
				},
			}
		default:
			// Default to general setting
			setting.Value = &v1pb.UserSetting_GeneralSetting_{
				GeneralSetting: getDefaultUserGeneralSetting(),
			}
		}
		return setting
	}

	settingKey := convertSettingKeyFromStore(storeSetting.Key)
	setting := &v1pb.UserSetting{
		Name: fmt.Sprintf("%s/settings/%s", BuildUserName(user.Username), settingKey),
	}

	switch storeSetting.Key {
	case storepb.UserSetting_GENERAL:
		if general := storeSetting.GetGeneral(); general != nil {
			setting.Value = &v1pb.UserSetting_GeneralSetting_{
				GeneralSetting: &v1pb.UserSetting_GeneralSetting{
					Locale:         general.Locale,
					MemoVisibility: general.MemoVisibility,
					Theme:          general.Theme,
				},
			}
		} else {
			setting.Value = &v1pb.UserSetting_GeneralSetting_{
				GeneralSetting: getDefaultUserGeneralSetting(),
			}
		}
	case storepb.UserSetting_WEBHOOKS:
		webhooks := storeSetting.GetWebhooks()
		apiWebhooks := make([]*v1pb.UserWebhook, 0)
		if webhooks != nil {
			apiWebhooks = make([]*v1pb.UserWebhook, 0, len(webhooks.Webhooks))
			for _, webhook := range webhooks.Webhooks {
				apiWebhook := &v1pb.UserWebhook{
					Name:        fmt.Sprintf("%s/webhooks/%s", BuildUserName(user.Username), webhook.Id),
					Url:         webhook.Url,
					DisplayName: webhook.Title,
				}
				apiWebhooks = append(apiWebhooks, apiWebhook)
			}
		}
		setting.Value = &v1pb.UserSetting_WebhooksSetting_{
			WebhooksSetting: &v1pb.UserSetting_WebhooksSetting{
				Webhooks: apiWebhooks,
			},
		}
	default:
		// Default to general setting if unknown key
		setting.Value = &v1pb.UserSetting_GeneralSetting_{
			GeneralSetting: getDefaultUserGeneralSetting(),
		}
	}

	return setting
}

// convertUserSettingToStore converts API UserSetting to store UserSetting.
func convertUserSettingToStore(apiSetting *v1pb.UserSetting, userID int32, key storepb.UserSetting_Key) (*storepb.UserSetting, error) {
	storeSetting := &storepb.UserSetting{
		UserId: userID,
		Key:    key,
	}

	switch key {
	case storepb.UserSetting_GENERAL:
		if general := apiSetting.GetGeneralSetting(); general != nil {
			storeSetting.Value = &storepb.UserSetting_General{
				General: &storepb.GeneralUserSetting{
					Locale:         general.Locale,
					MemoVisibility: general.MemoVisibility,
					Theme:          general.Theme,
				},
			}
		} else {
			return nil, errors.Errorf("general setting is required")
		}
	case storepb.UserSetting_WEBHOOKS:
		if webhooks := apiSetting.GetWebhooksSetting(); webhooks != nil {
			storeWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(webhooks.Webhooks))
			for _, webhook := range webhooks.Webhooks {
				storeWebhook := &storepb.WebhooksUserSetting_Webhook{
					Id:    extractWebhookIDFromName(webhook.Name),
					Title: webhook.DisplayName,
					Url:   webhook.Url,
				}
				storeWebhooks = append(storeWebhooks, storeWebhook)
			}
			storeSetting.Value = &storepb.UserSetting_Webhooks{
				Webhooks: &storepb.WebhooksUserSetting{
					Webhooks: storeWebhooks,
				},
			}
		} else {
			return nil, errors.Errorf("webhooks setting is required")
		}
	default:
		return nil, errors.Errorf("unsupported setting key: %v", key)
	}

	return storeSetting, nil
}

// extractWebhookIDFromName extracts webhook ID from resource name.
// e.g., "users/123/webhooks/webhook-id" -> "webhook-id".
func extractWebhookIDFromName(name string) string {
	parts := strings.Split(name, "/")
	if len(parts) >= 4 && parts[0] == "users" && parts[2] == "webhooks" {
		return parts[3]
	}
	return ""
}

// extractUsernameFromFilter extracts username from the filter string using CEL.
// Supported filter format: "username == 'steven'"
// Returns the username value and an error if the filter format is invalid.
func extractUsernameFromFilter(filterStr string) (string, error) {
	filterStr = strings.TrimSpace(filterStr)
	if filterStr == "" {
		return "", nil
	}

	// Create CEL environment with username variable
	env, err := cel.NewEnv(
		cel.Variable("username", cel.StringType),
	)
	if err != nil {
		return "", errors.Wrap(err, "failed to create CEL environment")
	}

	// Parse and check the filter expression
	celAST, issues := env.Compile(filterStr)
	if issues != nil && issues.Err() != nil {
		return "", errors.Wrapf(issues.Err(), "invalid filter expression: %s", filterStr)
	}

	// Extract username from the AST
	username, err := extractUsernameFromAST(celAST.NativeRep().Expr())
	if err != nil {
		return "", err
	}

	return username, nil
}

// extractUsernameFromAST extracts the username value from a CEL AST expression.
func extractUsernameFromAST(expr ast.Expr) (string, error) {
	if expr == nil {
		return "", errors.New("empty expression")
	}

	// Check if this is a call expression (for ==, !=, etc.)
	if expr.Kind() != ast.CallKind {
		return "", errors.New("filter must be a comparison expression (e.g., username == 'value')")
	}

	call := expr.AsCall()

	// We only support == operator
	if call.FunctionName() != "_==_" {
		return "", errors.Errorf("unsupported operator: %s (only '==' is supported)", call.FunctionName())
	}

	// The call should have exactly 2 arguments
	args := call.Args()
	if len(args) != 2 {
		return "", errors.New("invalid comparison expression")
	}

	// Try to extract username from either left or right side
	if username, ok := extractUsernameFromComparison(args[0], args[1]); ok {
		return username, nil
	}
	if username, ok := extractUsernameFromComparison(args[1], args[0]); ok {
		return username, nil
	}

	return "", errors.New("filter must compare 'username' field with a string constant")
}

// extractUsernameFromComparison tries to extract username value if left is 'username' ident and right is a string constant.
func extractUsernameFromComparison(left, right ast.Expr) (string, bool) {
	// Check if left side is 'username' identifier
	if left.Kind() != ast.IdentKind {
		return "", false
	}
	ident := left.AsIdent()
	if ident != "username" {
		return "", false
	}

	// Right side should be a constant string
	if right.Kind() != ast.LiteralKind {
		return "", false
	}
	literal := right.AsLiteral()

	// literal is a ref.Val, we need to get the Go value
	str, ok := literal.Value().(string)
	if !ok || str == "" {
		return "", false
	}

	return str, true
}

// ListUserNotifications lists all notifications for a user.
// Notifications are backed by the inbox storage layer and represent activities
// that require user attention (e.g., memo comments).
func (s *APIV1Service) ListUserNotifications(ctx context.Context, request *v1pb.ListUserNotificationsRequest) (*v1pb.ListUserNotificationsResponse, error) {
	user, err := s.resolveUserFromName(ctx, request.Parent)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
	}
	userID := user.ID

	// Verify the requesting user has permission to view these notifications
	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}
	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != userID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	// Fetch inbox items from storage.
	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
		ReceiverID: &userID,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err)
	}

	// Convert storage layer inboxes to API notifications.
	userIDs := make([]int32, 0, len(inboxes)*2)
	for _, inbox := range inboxes {
		userIDs = append(userIDs, inbox.ReceiverID, inbox.SenderID)
	}
	usersByID, err := s.listUsersByID(ctx, userIDs)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
	}
	memosByID, err := s.listMemosByID(ctx, collectInboxMemoIDs(inboxes))
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list notification memos: %v", err)
	}

	notifications := []*v1pb.UserNotification{}
	for _, inbox := range inboxes {
		notification, err := s.convertInboxToUserNotificationWithUsersAndMemos(inbox, currentUser, usersByID, memosByID)
		if err != nil {
			if status.Code(err) == codes.NotFound {
				slog.Warn("Skipping notification with missing user",
					slog.Int64("notification_id", int64(inbox.ID)),
					slog.Int64("receiver_id", int64(inbox.ReceiverID)),
					slog.Int64("sender_id", int64(inbox.SenderID)),
				)
				continue
			}
			return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err)
		}
		if notification.Type == v1pb.UserNotification_TYPE_UNSPECIFIED {
			continue
		}
		notifications = append(notifications, notification)
	}

	return &v1pb.ListUserNotificationsResponse{
		Notifications: notifications,
	}, nil
}

// UpdateUserNotification updates a notification's status (e.g., marking as read/archived).
// Only the notification owner can update their notifications.
func (s *APIV1Service) UpdateUserNotification(ctx context.Context, request *v1pb.UpdateUserNotificationRequest) (*v1pb.UserNotification, error) {
	if request.Notification == nil {
		return nil, status.Errorf(codes.InvalidArgument, "notification is required")
	}

	user, notificationID, err := s.resolveUserAndNotificationIDFromName(ctx, request.Notification.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err)
	}

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}

	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != user.ID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}
	// Verify ownership before updating
	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
		ID: &notificationID,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
	}
	if len(inboxes) == 0 {
		return nil, status.Errorf(codes.NotFound, "notification not found")
	}
	inbox := inboxes[0]
	if inbox.ReceiverID != currentUser.ID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	// Build update request based on field mask
	update := &store.UpdateInbox{
		ID: notificationID,
	}

	for _, path := range request.UpdateMask.Paths {
		switch path {
		case "status":
			// Convert API status enum to storage enum
			var inboxStatus store.InboxStatus
			switch request.Notification.Status {
			case v1pb.UserNotification_UNREAD:
				inboxStatus = store.UNREAD
			case v1pb.UserNotification_ARCHIVED:
				inboxStatus = store.ARCHIVED
			default:
				return nil, status.Errorf(codes.InvalidArgument, "invalid status")
			}
			update.Status = inboxStatus
		default:
			return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", path)
		}
	}

	updatedInbox, err := s.Store.UpdateInbox(ctx, update)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err)
	}

	notification, err := s.convertInboxToUserNotification(ctx, updatedInbox, currentUser)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to convert inbox: %v", err)
	}

	return notification, nil
}

// DeleteUserNotification permanently deletes a notification.
// Only the notification owner can delete their notifications.
func (s *APIV1Service) DeleteUserNotification(ctx context.Context, request *v1pb.DeleteUserNotificationRequest) (*emptypb.Empty, error) {
	user, notificationID, err := s.resolveUserAndNotificationIDFromName(ctx, request.Name)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid notification name: %v", err)
	}

	currentUser, err := s.fetchCurrentUser(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
	}

	if currentUser == nil {
		return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
	}
	if currentUser.ID != user.ID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}
	// Verify ownership before deletion
	inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
		ID: &notificationID,
	})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
	}
	if len(inboxes) == 0 {
		return nil, status.Errorf(codes.NotFound, "notification not found")
	}
	inbox := inboxes[0]
	if inbox.ReceiverID != currentUser.ID {
		return nil, status.Errorf(codes.PermissionDenied, "permission denied")
	}

	if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{
		ID: notificationID,
	}); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err)
	}

	return &emptypb.Empty{}, nil
}

// convertInboxToUserNotification converts a storage-layer inbox to an API notification.
// This handles the mapping between the internal inbox representation and the public API.
func (s *APIV1Service) convertInboxToUserNotification(ctx context.Context, inbox *store.Inbox, viewer *store.User) (*v1pb.UserNotification, error) {
	usersByID, err := s.listUsersByID(ctx, []int32{inbox.ReceiverID, inbox.SenderID})
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list notification users: %v", err)
	}
	memosByID, err := s.listMemosByID(ctx, collectInboxMemoIDs([]*store.Inbox{inbox}))
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to list notification memos: %v", err)
	}
	return s.convertInboxToUserNotificationWithUsersAndMemos(inbox, viewer, usersByID, memosByID)
}

func collectInboxMemoIDs(inboxes []*store.Inbox) []int32 {
	memoIDs := make([]int32, 0, len(inboxes)*2)
	for _, inbox := range inboxes {
		if inbox == nil || inbox.Message == nil {
			continue
		}
		switch inbox.Message.Type {
		case storepb.InboxMessage_MEMO_COMMENT:
			payload := inbox.Message.GetMemoComment()
			if payload != nil {
				memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
			}
		case storepb.InboxMessage_MEMO_MENTION:
			payload := inbox.Message.GetMemoMention()
			if payload != nil {
				memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
			}
		default:
			// Ignore notification types without memo references.
		}
	}
	return memoIDs
}

func (s *APIV1Service) convertInboxToUserNotificationWithUsersAndMemos(inbox *store.Inbox, viewer *store.User, usersByID map[int32]*store.User, memosByID map[int32]*store.Memo) (*v1pb.UserNotification, error) {
	receiver := usersByID[inbox.ReceiverID]
	if receiver == nil {
		return nil, status.Errorf(codes.NotFound, "notification receiver not found")
	}
	sender := usersByID[inbox.SenderID]
	if sender == nil {
		return nil, status.Errorf(codes.NotFound, "notification sender not found")
	}

	notification := &v1pb.UserNotification{
		Name:       fmt.Sprintf("%s/notifications/%d", BuildUserName(receiver.Username), inbox.ID),
		Sender:     BuildUserName(sender.Username),
		SenderUser: convertUserFromStore(sender, viewer),
		CreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)),
	}

	// Convert status from storage enum to API enum
	switch inbox.Status {
	case store.UNREAD:
		notification.Status = v1pb.UserNotification_UNREAD
	case store.ARCHIVED:
		notification.Status = v1pb.UserNotification_ARCHIVED
	default:
		notification.Status = v1pb.UserNotification_STATUS_UNSPECIFIED
	}

	// Extract notification type and payload from the inbox message.
	if inbox.Message != nil {
		switch inbox.Message.Type {
		case storepb.InboxMessage_MEMO_COMMENT:
			notification.Type = v1pb.UserNotification_MEMO_COMMENT
			payload, err := s.convertMemoCommentNotificationPayload(viewer, inbox.Message, memosByID)
			if err != nil {
				return nil, err
			}
			if payload != nil {
				notification.Payload = &v1pb.UserNotification_MemoComment{
					MemoComment: payload,
				}
			}
		case storepb.InboxMessage_MEMO_MENTION:
			notification.Type = v1pb.UserNotification_MEMO_MENTION
			payload, err := s.convertMemoMentionNotificationPayload(viewer, inbox.Message, memosByID)
			if err != nil {
				return nil, err
			}
			if payload != nil {
				notification.Payload = &v1pb.UserNotification_MemoMention{
					MemoMention: payload,
				}
			}
		default:
			notification.Type = v1pb.UserNotification_TYPE_UNSPECIFIED
		}
	}

	return notification, nil
}

func canViewerAccessMemo(viewer *store.User, memo *store.Memo) bool {
	if memo == nil {
		return false
	}
	if viewer != nil && isSuperUser(viewer) {
		return true
	}
	if memo.Visibility == store.Private {
		return viewer != nil && viewer.ID == memo.CreatorID
	}
	if memo.Visibility == store.Protected {
		return viewer != nil
	}
	return true
}

func (s *APIV1Service) memoNotificationSnippet(memo *store.Memo) (string, error) {
	if memo == nil || memo.Content == "" {
		return "", nil
	}

	snippet, err := s.getMemoContentSnippet(memo.Content)
	if err != nil {
		return "", err
	}
	return snippet, nil
}

func (s *APIV1Service) convertMemoCommentNotificationPayload(viewer *store.User, message *storepb.InboxMessage, memosByID map[int32]*store.Memo) (*v1pb.UserNotification_MemoCommentPayload, error) {
	memoComment := message.GetMemoComment()
	if message == nil || message.Type != storepb.InboxMessage_MEMO_COMMENT || memoComment == nil {
		return nil, nil
	}

	commentMemo := memosByID[memoComment.MemoId]
	if !canViewerAccessMemo(viewer, commentMemo) {
		return nil, nil
	}

	relatedMemo := memosByID[memoComment.RelatedMemoId]
	if !canViewerAccessMemo(viewer, relatedMemo) {
		return nil, nil
	}

	memoSnippet, err := s.memoNotificationSnippet(commentMemo)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get comment memo snippet")
	}
	relatedMemoSnippet, err := s.memoNotificationSnippet(relatedMemo)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get related memo snippet")
	}

	return &v1pb.UserNotification_MemoCommentPayload{
		Memo:               fmt.Sprintf("%s%s", MemoNamePrefix, commentMemo.UID),
		RelatedMemo:        fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID),
		MemoSnippet:        memoSnippet,
		RelatedMemoSnippet: relatedMemoSnippet,
	}, nil
}

func (s *APIV1Service) convertMemoMentionNotificationPayload(viewer *store.User, message *storepb.InboxMessage, memosByID map[int32]*store.Memo) (*v1pb.UserNotification_MemoMentionPayload, error) {
	memoMention := message.GetMemoMention()
	if message == nil || message.Type != storepb.InboxMessage_MEMO_MENTION || memoMention == nil {
		return nil, nil
	}

	memo := memosByID[memoMention.MemoId]
	if !canViewerAccessMemo(viewer, memo) {
		return nil, nil
	}

	memoSnippet, err := s.memoNotificationSnippet(memo)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get mention memo snippet")
	}

	payload := &v1pb.UserNotification_MemoMentionPayload{
		Memo:        fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
		MemoSnippet: memoSnippet,
	}
	if memoMention.RelatedMemoId != 0 {
		relatedMemo := memosByID[memoMention.RelatedMemoId]
		if canViewerAccessMemo(viewer, relatedMemo) {
			payload.RelatedMemo = fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID)
			relatedMemoSnippet, err := s.memoNotificationSnippet(relatedMemo)
			if err != nil {
				return nil, errors.Wrap(err, "failed to get related memo snippet")
			}
			payload.RelatedMemoSnippet = relatedMemoSnippet
		}
	}

	return payload, nil
}
