package server

import (
	"context"
	"log/slog"
	"time"

	"gorm.io/gorm"

	"security-center-backend/internal/model"
)

const (
	defaultAuditRetentionDays  = 90
	auditRetentionInterval     = 24 * time.Hour
	auditRetentionInitialDelay = time.Minute
	auditCleanupBatchSize      = 1000
)

// resolveRetentionDays applies the 90-day fallback when the configured value is unset or invalid.
func resolveRetentionDays(days int) int {
	if days <= 0 {
		return defaultAuditRetentionDays
	}
	return days
}

// StartAuditRetention runs the audit_logs retention cleanup loop in-process.
// The first run is delayed by one minute to avoid the startup peak, then runs daily.
// It exits when ctx is cancelled. Errors are logged and retried on the next tick.
func StartAuditRetention(ctx context.Context, db *gorm.DB, retentionDays int, logger *slog.Logger) {
	if db == nil {
		return
	}
	if logger == nil {
		logger = slog.Default()
	}
	days := resolveRetentionDays(retentionDays)
	logger.Info("audit retention cleanup scheduler started",
		"retention_days", days,
		"interval", auditRetentionInterval.String(),
		"initial_delay", auditRetentionInitialDelay.String(),
	)

	run := func() {
		cutoff := time.Now().AddDate(0, 0, -days)
		deleted, err := cleanupAuditLogs(ctx, db, cutoff, auditCleanupBatchSize)
		if err != nil {
			if ctx.Err() != nil {
				return
			}
			logger.Error("audit retention cleanup failed", "error", err)
			return
		}
		logger.Info("audit retention cleanup finished", "deleted", deleted, "retention_days", days)
		if err := writeRetentionCleanupAudit(ctx, db, deleted, days); err != nil && ctx.Err() == nil {
			logger.Warn("failed to write audit retention cleanup entry", "error", err)
		}
	}

	timer := time.NewTimer(auditRetentionInitialDelay)
	defer timer.Stop()
	ticker := time.NewTicker(auditRetentionInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			logger.Info("audit retention cleanup scheduler stopped")
			return
		case <-timer.C:
			run()
		case <-ticker.C:
			run()
		}
	}
}

// cleanupAuditLogs hard-deletes audit_logs rows older than olderThan in batches of
// batchSize to avoid long transactions. It loops until no expired rows remain and
// returns the total number of deleted rows.
func cleanupAuditLogs(ctx context.Context, db *gorm.DB, olderThan time.Time, batchSize int) (deleted int64, err error) {
	if db == nil {
		return 0, nil
	}
	if batchSize <= 0 {
		batchSize = auditCleanupBatchSize
	}
	for {
		if err := ctx.Err(); err != nil {
			return deleted, err
		}
		var ids []uint64
		if err := db.WithContext(ctx).
			Model(&model.AuditLog{}).
			Where("created_at < ?", olderThan).
			Order("id ASC").
			Limit(batchSize).
			Pluck("id", &ids).Error; err != nil {
			return deleted, err
		}
		if len(ids) == 0 {
			return deleted, nil
		}
		result := db.WithContext(ctx).
			Where("id IN ?", ids).
			Delete(&model.AuditLog{})
		if result.Error != nil {
			return deleted, result.Error
		}
		deleted += result.RowsAffected
		if result.RowsAffected == 0 {
			// Defensive: no progress (e.g. rows vanished concurrently); avoid spinning.
			return deleted, nil
		}
	}
}

// writeRetentionCleanupAudit records one audit_logs entry per cleanup round so the
// retention job itself is auditable. Written directly via GORM (the async audit
// service is heavier than needed here); actor_type is 'system'.
func writeRetentionCleanupAudit(ctx context.Context, db *gorm.DB, deleted int64, retentionDays int) error {
	entry := &model.AuditLog{
		ActorType:    "system",
		Action:       "retention_cleanup",
		ResourceType: "audit",
		Metadata: model.JSONB{
			"deleted":        deleted,
			"retention_days": retentionDays,
			"ran_at":         time.Now().UTC().Format(time.RFC3339),
		},
	}
	return db.WithContext(ctx).Create(entry).Error
}
