package admin

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/config"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	"github.com/ultisuite/ulti-backend/internal/permission"
	"github.com/ultisuite/ulti-backend/internal/securityaudit"
	platformusers "github.com/ultisuite/ulti-backend/internal/users"
)

var ErrNotFound = errors.New("not found")

type Service struct {
	db     *pgxpool.Pool
	audit  *securityaudit.Logger
	cfg    *config.Config
	nc     *nextcloud.Client
	logger *slog.Logger
}

func NewService(db *pgxpool.Pool, audit *securityaudit.Logger, cfg *config.Config, nc *nextcloud.Client) *Service {
	return &Service{
		db:     db,
		audit:  audit,
		cfg:    cfg,
		nc:     nc,
		logger: slog.Default().With("component", "admin-service"),
	}
}

type UsersList struct {
	Users      []map[string]any     `json:"users"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

type UserFilter struct {
	Status string
	Role   string
	Q      string
}

func (s *Service) ListUsers(ctx context.Context, params query.ListParams, filter UserFilter) (UsersList, error) {
	whereSQL, args := buildUserFilter(filter)

	var total int64
	totalSQL := "SELECT COUNT(*) FROM users" + whereSQL
	if err := s.db.QueryRow(ctx, totalSQL, args...).Scan(&total); err != nil {
		return UsersList{}, err
	}

	listSQL := `
		SELECT id, external_id, email, name, status, platform_admin, invited_at, disabled_at, created_at, updated_at
		FROM users` + whereSQL + `
		ORDER BY created_at DESC
		LIMIT $` + strconv.Itoa(len(args)+1) + ` OFFSET $` + strconv.Itoa(len(args)+2)
	args = append(args, params.Limit(), params.Offset())

	rows, err := s.db.Query(ctx, listSQL, args...)
	if err != nil {
		return UsersList{}, err
	}
	defer rows.Close()

	users := make([]map[string]any, 0)
	for rows.Next() {
		user, err := scanUserRow(rows)
		if err != nil {
			return UsersList{}, err
		}
		users = append(users, user)
	}
	if err := rows.Err(); err != nil {
		return UsersList{}, err
	}
	if err := s.attachUsersStorage(ctx, users); err != nil {
		return UsersList{}, err
	}

	return UsersList{
		Users:      users,
		Pagination: params.Meta(&total),
	}, nil
}

func buildUserFilter(filter UserFilter) (string, []any) {
	clauses := make([]string, 0, 3)
	args := make([]any, 0, 3)
	if role := strings.TrimSpace(filter.Role); role != "" {
		switch permission.AccountRole(role) {
		case permission.AccountRoleAdmin:
			clauses = append(clauses, "platform_admin = true AND status = 'active'")
		case permission.AccountRoleUser:
			clauses = append(clauses, "platform_admin = false AND status = 'active'")
		case permission.AccountRoleGuest:
			clauses = append(clauses, "status = 'invited'")
		case permission.AccountRoleSuspended:
			clauses = append(clauses, "status = 'disabled'")
		}
	}
	if strings.TrimSpace(filter.Status) != "" {
		args = append(args, strings.TrimSpace(filter.Status))
		clauses = append(clauses, "status = $"+strconv.Itoa(len(args)))
	}
	if strings.TrimSpace(filter.Q) != "" {
		q := "%" + strings.ToLower(strings.TrimSpace(filter.Q)) + "%"
		args = append(args, q)
		idx := strconv.Itoa(len(args))
		clauses = append(clauses, "(LOWER(email) LIKE $"+idx+" OR LOWER(name) LIKE $"+idx+" OR LOWER(external_id) LIKE $"+idx+")")
	}
	if len(clauses) == 0 {
		return "", args
	}
	return " WHERE " + strings.Join(clauses, " AND "), args
}

func (s *Service) GetUser(ctx context.Context, userID string) (map[string]any, error) {
	row := s.db.QueryRow(ctx, `
		SELECT id, external_id, email, name, status, platform_admin, invited_at, disabled_at, created_at, updated_at
		FROM users WHERE id = $1
	`, userID)
	user, err := scanUserRow(row)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	mailCount, mailUsedStorage, err := s.mailUsageForUser(ctx, userID)
	if err != nil {
		return nil, err
	}

	var mailMax, driveMax, photosMax int64
	if err := s.db.QueryRow(ctx, `
		SELECT
			COALESCE((preferences->>'mail_max_storage_bytes')::bigint, 5368709120),
			COALESCE((preferences->>'drive_max_storage_bytes')::bigint, 5368709120),
			COALESCE((preferences->>'photos_max_storage_bytes')::bigint, 5368709120)
		FROM settings WHERE user_id = $1
	`, userID).Scan(&mailMax, &driveMax, &photosMax); err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return nil, err
	}

	email, _ := user["email"].(string)
	extID, _ := user["external_id"].(string)
	driveUsedStorage := s.driveUsageForUser(ctx, email, extID)

	user["quota"] = map[string]any{
			"mail": map[string]any{
				"count":              mailCount,
				"used_storage_bytes": mailUsedStorage,
				"max_storage_bytes":  mailMax,
			},
			"drive": map[string]any{
				"used_storage_bytes": driveUsedStorage,
				"max_storage_bytes":  driveMax,
			},
			"photos": map[string]any{
				"used_storage_bytes": int64(0),
				"max_storage_bytes":  photosMax,
			},
		}
	return user, nil
}

type userRowScanner interface {
	Scan(dest ...any) error
}

func scanUserRow(row userRowScanner) (map[string]any, error) {
	var id, extID, email, name, status string
	var platformAdmin bool
	var invitedAt, disabledAt *time.Time
	var createdAt, updatedAt time.Time
	if err := row.Scan(&id, &extID, &email, &name, &status, &platformAdmin, &invitedAt, &disabledAt, &createdAt, &updatedAt); err != nil {
		return nil, err
	}
	return map[string]any{
		"id":              id,
		"external_id":     extID,
		"email":           email,
		"name":            name,
		"status":          status,
		"platform_admin":  platformAdmin,
		"role":            string(permission.DeriveAccountRole(platformAdmin, status)),
		"invited_at":      invitedAt,
		"disabled_at":     disabledAt,
		"created_at":      createdAt,
		"updated_at":      updatedAt,
	}, nil
}

func (s *Service) SetUserRole(ctx context.Context, actorSub, userID, role string) (map[string]any, error) {
	accountRole, ok := permission.ParseAccountRole(role)
	if !ok {
		return nil, fmt.Errorf("invalid role")
	}

	var extID string
	var currentAdmin bool
	var currentStatus string
	err := s.db.QueryRow(ctx, `
		SELECT external_id, platform_admin, status FROM users WHERE id = $1
	`, userID).Scan(&extID, &currentAdmin, &currentStatus)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}

	if currentAdmin && accountRole != permission.AccountRoleAdmin {
		count, err := platformusers.CountPlatformAdmins(ctx, s.db)
		if err != nil {
			return nil, err
		}
		if count <= 1 {
			return nil, platformusers.ErrLastPlatformAdmin
		}
	}

	switch accountRole {
	case permission.AccountRoleAdmin:
		_, err = s.db.Exec(ctx, `
			UPDATE users
			SET platform_admin = true,
			    status = 'active',
			    disabled_at = NULL,
			    updated_at = NOW()
			WHERE id = $1
		`, userID)
	case permission.AccountRoleUser:
		_, err = s.db.Exec(ctx, `
			UPDATE users
			SET platform_admin = false,
			    status = 'active',
			    disabled_at = NULL,
			    updated_at = NOW()
			WHERE id = $1
		`, userID)
	case permission.AccountRoleGuest:
		_, err = s.db.Exec(ctx, `
			UPDATE users
			SET platform_admin = false,
			    status = 'invited',
			    invited_at = COALESCE(invited_at, NOW()),
			    disabled_at = NULL,
			    updated_at = NOW()
			WHERE id = $1
		`, userID)
	case permission.AccountRoleSuspended:
		_, err = s.db.Exec(ctx, `
			UPDATE users
			SET platform_admin = false,
			    status = 'disabled',
			    disabled_at = NOW(),
			    updated_at = NOW()
			WHERE id = $1
		`, userID)
	}
	if err != nil {
		return nil, err
	}

	s.logAudit(ctx, actorSub, "set_user_role", map[string]any{
		"target_user": userID,
		"role":        accountRole,
		"from_status": currentStatus,
		"from_admin":  currentAdmin,
	})
	return s.GetUser(ctx, userID)
}

func (s *Service) CreateUser(ctx context.Context, actorSub string, req createUserRequest) (map[string]any, error) {
	var id string
	if err := s.db.QueryRow(ctx, `
		INSERT INTO users (external_id, email, name, status)
		VALUES ($1, $2, $3, 'active')
		RETURNING id
	`, strings.TrimSpace(req.ExternalID), strings.ToLower(strings.TrimSpace(req.Email)), strings.TrimSpace(req.Name)).Scan(&id); err != nil {
		return nil, err
	}
	s.logAudit(ctx, actorSub, "create_user", map[string]any{
		"target_user":  id,
		"external_id":  strings.TrimSpace(req.ExternalID),
		"email":        strings.ToLower(strings.TrimSpace(req.Email)),
		"initial_name": strings.TrimSpace(req.Name),
	})
	if err := s.applyOrgDefaultQuotas(ctx, id); err != nil {
		return nil, err
	}
	return s.GetUser(ctx, id)
}

func (s *Service) InviteUser(ctx context.Context, actorSub string, req inviteUserRequest) (map[string]any, error) {
	var id string
	inviteExternalID := "invite:" + uuid.NewString()
	if err := s.db.QueryRow(ctx, `
		INSERT INTO users (external_id, email, name, status, invited_at)
		VALUES ($1, $2, $3, 'invited', NOW())
		RETURNING id
	`, inviteExternalID, strings.ToLower(strings.TrimSpace(req.Email)), strings.TrimSpace(req.Name)).Scan(&id); err != nil {
		return nil, err
	}
	s.logAudit(ctx, actorSub, "invite_user", map[string]any{
		"target_user": id,
		"email":       strings.ToLower(strings.TrimSpace(req.Email)),
	})
	if err := s.applyOrgDefaultQuotas(ctx, id); err != nil {
		return nil, err
	}
	return s.GetUser(ctx, id)
}

func (s *Service) UpdateUser(ctx context.Context, actorSub, userID string, req updateUserRequest) (map[string]any, error) {
	result, err := s.db.Exec(ctx, `
		UPDATE users
		SET email = COALESCE($2, email),
		    name = COALESCE($3, name),
		    updated_at = NOW()
		WHERE id = $1
	`, userID, trimStringPtr(req.Email), trimStringPtr(req.Name))
	if err != nil {
		return nil, err
	}
	if result.RowsAffected() == 0 {
		return nil, ErrNotFound
	}
	s.logAudit(ctx, actorSub, "update_user", map[string]any{
		"target_user": userID,
	})
	return s.GetUser(ctx, userID)
}

func trimStringPtr(v *string) *string {
	if v == nil {
		return nil
	}
	s := strings.TrimSpace(*v)
	return &s
}

func (s *Service) SetQuota(ctx context.Context, actorSub, userID string, req setQuotaRequest) error {
	exists, err := s.userExists(ctx, userID)
	if err != nil {
		return err
	}
	if !exists {
		return ErrNotFound
	}

	_, err = s.db.Exec(ctx, `
		INSERT INTO settings (user_id, preferences)
		VALUES (
			$1,
			jsonb_strip_nulls(jsonb_build_object(
				'mail_max_storage_bytes', $2::text,
				'drive_max_storage_bytes', $3::text,
				'photos_max_storage_bytes', $4::text
			))
		)
		ON CONFLICT (user_id) DO UPDATE SET
			preferences = settings.preferences || jsonb_strip_nulls(jsonb_build_object(
				'mail_max_storage_bytes', $2::text,
				'drive_max_storage_bytes', $3::text,
				'photos_max_storage_bytes', $4::text
			)),
			updated_at = NOW()
	`, userID, req.MailMaxStorageBytes, req.DriveMaxStorageBytes, req.PhotosMaxStorageBytes)
	if err != nil {
		return err
	}
	s.logAudit(ctx, actorSub, "set_quota", map[string]any{
		"target_user":               userID,
		"mail_max_storage_bytes":    req.MailMaxStorageBytes,
		"drive_max_storage_bytes":   req.DriveMaxStorageBytes,
		"photos_max_storage_bytes":  req.PhotosMaxStorageBytes,
		"multi_service_quota_patch": true,
	})
	return nil
}

func (s *Service) DisableUser(ctx context.Context, actorSub, userID string) error {
	result, err := s.db.Exec(ctx, `
		UPDATE users
		SET status = 'disabled', disabled_at = NOW(), updated_at = NOW()
		WHERE id = $1
	`, userID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	s.logAudit(ctx, actorSub, "disable_user", map[string]any{"target_user": userID})
	return nil
}

func (s *Service) ReactivateUser(ctx context.Context, actorSub, userID string) error {
	result, err := s.db.Exec(ctx, `
		UPDATE users
		SET status = 'active', disabled_at = NULL, updated_at = NOW()
		WHERE id = $1
	`, userID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	s.logAudit(ctx, actorSub, "reactivate_user", map[string]any{"target_user": userID})
	return nil
}

func (s *Service) DeleteUser(ctx context.Context, actorSub, userID string) error {
	result, err := s.db.Exec(ctx, `DELETE FROM users WHERE id = $1`, userID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	s.logAudit(ctx, actorSub, "delete_user", map[string]any{"target_user": userID})
	return nil
}

func (s *Service) userExists(ctx context.Context, userID string) (bool, error) {
	var exists bool
	if err := s.db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`, userID).Scan(&exists); err != nil {
		return false, err
	}
	return exists, nil
}

type AuditLogsList struct {
	Logs       []map[string]any     `json:"logs"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListAuditLogs(ctx context.Context, params query.ListParams) (AuditLogsList, error) {
	var total int64
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM audit_logs`).Scan(&total); err != nil {
		return AuditLogsList{}, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, actor, action, details, created_at
		FROM audit_logs ORDER BY created_at DESC
		LIMIT $1 OFFSET $2
	`, params.Limit(), params.Offset())
	if err != nil {
		return AuditLogsList{}, err
	}
	defer rows.Close()

	logs := make([]map[string]any, 0)
	for rows.Next() {
		var id, actor, action string
		var details []byte
		var createdAt time.Time
		if err := rows.Scan(&id, &actor, &action, &details, &createdAt); err != nil {
			return AuditLogsList{}, err
		}
		logs = append(logs, map[string]any{
			"id":         id,
			"actor":      actor,
			"action":     action,
			"details":    json.RawMessage(details),
			"created_at": createdAt,
		})
	}
	if err := rows.Err(); err != nil {
		return AuditLogsList{}, err
	}

	return AuditLogsList{
		Logs:       logs,
		Pagination: params.Meta(&total),
	}, nil
}

type AuditExport struct {
	Content     []byte
	ContentType string
	FileName    string
}

func (s *Service) ExportAuditLogs(ctx context.Context, format string, limit int) (AuditExport, error) {
	rows, err := s.db.Query(ctx, `
		SELECT id, actor, action, details, created_at
		FROM audit_logs
		ORDER BY created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return AuditExport{}, err
	}
	defer rows.Close()

	type rowItem struct {
		ID        string
		Actor     string
		Action    string
		Details   []byte
		CreatedAt time.Time
	}
	items := make([]rowItem, 0, limit)
	for rows.Next() {
		var item rowItem
		if err := rows.Scan(&item.ID, &item.Actor, &item.Action, &item.Details, &item.CreatedAt); err != nil {
			return AuditExport{}, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return AuditExport{}, err
	}

	now := time.Now().UTC().Format("20060102T150405Z")
	switch format {
	case "csv":
		buf := &bytes.Buffer{}
		w := csv.NewWriter(buf)
		if err := w.Write([]string{"id", "actor", "action", "details_json", "created_at"}); err != nil {
			return AuditExport{}, err
		}
		for _, item := range items {
			if err := w.Write([]string{
				item.ID, item.Actor, item.Action, string(item.Details), item.CreatedAt.UTC().Format(time.RFC3339),
			}); err != nil {
				return AuditExport{}, err
			}
		}
		w.Flush()
		if err := w.Error(); err != nil {
			return AuditExport{}, err
		}
		return AuditExport{
			Content:     buf.Bytes(),
			ContentType: "text/csv; charset=utf-8",
			FileName:    fmt.Sprintf("audit-%s.csv", now),
		}, nil
	default:
		buf := &bytes.Buffer{}
		enc := json.NewEncoder(buf)
		for _, item := range items {
			entry := map[string]any{
				"id":         item.ID,
				"actor":      item.Actor,
				"action":     item.Action,
				"details":    json.RawMessage(item.Details),
				"created_at": item.CreatedAt.UTC().Format(time.RFC3339),
			}
			if err := enc.Encode(entry); err != nil {
				return AuditExport{}, err
			}
		}
		return AuditExport{
			Content:     buf.Bytes(),
			ContentType: "application/x-ndjson; charset=utf-8",
			FileName:    fmt.Sprintf("audit-%s.ndjson", now),
		}, nil
	}
}

func (s *Service) GetStats(ctx context.Context) (map[string]any, error) {
	stats := map[string]any{}

	var totalUsers, activeUsers, disabledUsers, invitedUsers int64
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users`).Scan(&totalUsers); err != nil {
		return nil, err
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE status = 'active'`).Scan(&activeUsers); err != nil {
		return nil, err
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE status = 'disabled'`).Scan(&disabledUsers); err != nil {
		return nil, err
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE status = 'invited'`).Scan(&invitedUsers); err != nil {
		return nil, err
	}

	var mailCount, accountCount, audit24h int64
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM messages`).Scan(&mailCount); err != nil {
		return nil, err
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM mail_accounts`).Scan(&accountCount); err != nil {
		return nil, err
	}
	if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM audit_logs WHERE created_at >= NOW() - INTERVAL '24 hour'`).Scan(&audit24h); err != nil {
		return nil, err
	}

	type topActor struct {
		Actor string `json:"actor"`
		Count int64  `json:"count"`
	}
	topActors := make([]topActor, 0, 5)
	rows, err := s.db.Query(ctx, `
		SELECT actor, COUNT(*) as c
		FROM audit_logs
		WHERE created_at >= NOW() - INTERVAL '7 day'
		GROUP BY actor
		ORDER BY c DESC
		LIMIT 5
	`)
	if err != nil {
		return nil, err
	}
	for rows.Next() {
		var item topActor
		if err := rows.Scan(&item.Actor, &item.Count); err != nil {
			rows.Close()
			return nil, err
		}
		topActors = append(topActors, item)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, err
	}
	rows.Close()

	var usersNearMailQuota int64
	if err := s.db.QueryRow(ctx, `
		SELECT COUNT(*) FROM (
			SELECT
				u.id,
				COALESCE(SUM(
					octet_length(COALESCE(m.body_text, '')) +
					octet_length(COALESCE(m.body_html, '')) +
					COALESCE(att.attachment_bytes, 0)
				), 0) AS used_storage,
				COALESCE((s.preferences->>'mail_max_storage_bytes')::bigint, 5368709120) AS max_storage
			FROM users u
			LEFT JOIN mail_accounts ma ON ma.user_id = u.id
			LEFT JOIN messages m ON m.account_id = ma.id
			LEFT JOIN (
				SELECT message_id, SUM(size) AS attachment_bytes
				FROM attachments
				GROUP BY message_id
			) att ON att.message_id = m.id
			LEFT JOIN settings s ON s.user_id = u.id
			GROUP BY u.id, s.preferences
		) q
		WHERE q.max_storage > 0 AND q.used_storage::numeric / q.max_storage::numeric >= 0.9
	`).Scan(&usersNearMailQuota); err != nil {
		return nil, err
	}

	stats["users"] = map[string]any{
		"total":    totalUsers,
		"active":   activeUsers,
		"disabled": disabledUsers,
		"invited":  invitedUsers,
	}
	stats["services"] = map[string]any{
		"mail_accounts_total": accountCount,
		"messages_total":      mailCount,
		"audit_events_24h":    audit24h,
	}
	stats["quotas"] = map[string]any{
		"users_near_mail_quota_90pct": usersNearMailQuota,
	}

	storage, err := s.globalStorageStats(ctx)
	if err != nil {
		return nil, err
	}
	stats["storage"] = storage

	stats["audit"] = map[string]any{
		"top_actors_7d": topActors,
	}
	return stats, nil
}

const defaultQuotaBytes int64 = 5368709120

func (s *Service) globalStorageStats(ctx context.Context) (map[string]any, error) {
	var mailUsed int64
	if err := s.db.QueryRow(ctx, `
		SELECT COALESCE(SUM(
			octet_length(COALESCE(m.body_text, '')) +
			octet_length(COALESCE(m.body_html, '')) +
			COALESCE(att.attachment_bytes, 0)
		), 0)
		FROM messages m
		LEFT JOIN (
			SELECT message_id, SUM(size) AS attachment_bytes
			FROM attachments
			GROUP BY message_id
		) att ON att.message_id = m.id
	`).Scan(&mailUsed); err != nil {
		return nil, err
	}

	var mailAllocated, driveAllocated, photosAllocated int64
	if err := s.db.QueryRow(ctx, `
		SELECT
			COALESCE(SUM(COALESCE((s.preferences->>'mail_max_storage_bytes')::bigint, $1)), 0),
			COALESCE(SUM(COALESCE((s.preferences->>'drive_max_storage_bytes')::bigint, $1)), 0),
			COALESCE(SUM(COALESCE((s.preferences->>'photos_max_storage_bytes')::bigint, $1)), 0)
		FROM users u
		LEFT JOIN settings s ON s.user_id = u.id
	`, defaultQuotaBytes).Scan(&mailAllocated, &driveAllocated, &photosAllocated); err != nil {
		return nil, err
	}

	driveUsed := s.sumDriveUsageAllUsers(ctx)

	return map[string]any{
		"mail": map[string]any{
			"used_bytes":      mailUsed,
			"allocated_bytes": mailAllocated,
		},
		"drive": map[string]any{
			"used_bytes":      driveUsed,
			"allocated_bytes": driveAllocated,
			"tracked":         s.nc != nil,
		},
		"photos": map[string]any{
			"used_bytes":      int64(0),
			"allocated_bytes": photosAllocated,
			"tracked":         false,
		},
	}, nil
}

func (s *Service) sumDriveUsageAllUsers(ctx context.Context) int64 {
	if s.nc == nil {
		return 0
	}
	rows, err := s.db.Query(ctx, `
		SELECT email, external_id FROM users WHERE status != 'disabled'
	`)
	if err != nil {
		s.logger.Debug("drive usage aggregate failed", "error", err)
		return 0
	}
	defer rows.Close()

	var total int64
	for rows.Next() {
		var email, externalID string
		if err := rows.Scan(&email, &externalID); err != nil {
			continue
		}
		total += s.driveUsageForUser(ctx, email, externalID)
	}
	return total
}

func (s *Service) attachUsersStorage(ctx context.Context, users []map[string]any) error {
	if len(users) == 0 {
		return nil
	}
	ids := make([]string, 0, len(users))
	byID := make(map[string]map[string]any, len(users))
	for _, user := range users {
		id, _ := user["id"].(string)
		if id == "" {
			continue
		}
		ids = append(ids, id)
		byID[id] = user
	}
	mailUsed, err := s.mailUsageByUserIDs(ctx, ids)
	if err != nil {
		return err
	}
	for id, user := range byID {
		email, _ := user["email"].(string)
		extID, _ := user["external_id"].(string)
		user["storage"] = map[string]any{
			"mail_used_bytes":  mailUsed[id],
			"drive_used_bytes": s.driveUsageForUser(ctx, email, extID),
		}
	}
	return nil
}

func (s *Service) mailUsageByUserIDs(ctx context.Context, userIDs []string) (map[string]int64, error) {
	out := make(map[string]int64, len(userIDs))
	if len(userIDs) == 0 {
		return out, nil
	}
	rows, err := s.db.Query(ctx, `
		SELECT
			ma.user_id::text,
			COALESCE(SUM(
				octet_length(COALESCE(m.body_text, '')) +
				octet_length(COALESCE(m.body_html, '')) +
				COALESCE(att.attachment_bytes, 0)
			), 0)
		FROM mail_accounts ma
		LEFT JOIN messages m ON m.account_id = ma.id
		LEFT JOIN (
			SELECT message_id, SUM(size) AS attachment_bytes
			FROM attachments
			GROUP BY message_id
		) att ON att.message_id = m.id
		WHERE ma.user_id = ANY($1::uuid[])
		GROUP BY ma.user_id
	`, userIDs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var userID string
		var used int64
		if err := rows.Scan(&userID, &used); err != nil {
			return nil, err
		}
		out[userID] = used
	}
	return out, rows.Err()
}

func (s *Service) mailUsageForUser(ctx context.Context, userID string) (count, bytes int64, err error) {
	err = s.db.QueryRow(ctx, `
		SELECT
			COALESCE(COUNT(m.id), 0),
			COALESCE(SUM(
				octet_length(COALESCE(m.body_text, '')) +
				octet_length(COALESCE(m.body_html, '')) +
				COALESCE(att.attachment_bytes, 0)
			), 0)
		FROM messages m
		JOIN mail_accounts ma ON m.account_id = ma.id
		LEFT JOIN (
			SELECT message_id, SUM(size) AS attachment_bytes
			FROM attachments
			GROUP BY message_id
		) att ON att.message_id = m.id
		WHERE ma.user_id = $1
	`, userID).Scan(&count, &bytes)
	return count, bytes, err
}

func (s *Service) driveUsageForUser(ctx context.Context, email, externalID string) int64 {
	if s.nc == nil {
		return 0
	}
	ncUserID := nextcloud.UserIDFromClaims(email, externalID)
	if ncUserID == "" {
		return 0
	}
	quota, err := s.nc.GetQuota(ctx, ncUserID)
	if err != nil {
		s.logger.Debug("drive quota lookup failed", "nc_user", ncUserID, "error", err)
		return 0
	}
	if quota.Used < 0 {
		return 0
	}
	return quota.Used
}

func (s *Service) logAudit(ctx context.Context, actor, action string, details map[string]any) {
	if s.audit == nil {
		return
	}
	if action == "delete_user" {
		s.audit.Log(ctx, actor, securityaudit.ActionCriticalDeletion, map[string]any{
			"target":       "user",
			"target_user":  details["target_user"],
			"admin_action": true,
		})
		return
	}
	s.audit.Log(ctx, actor, securityaudit.ActionAdminAction, map[string]any{
		"action":  action,
		"details": details,
	})
}
