package cron

import (
	"context"
	"log/slog"
	"math"
	"strconv"
	"strings"
	"time"

	"github.com/ez-api/ez-api/internal/model"
	"github.com/ez-api/ez-api/internal/service"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"
)

const (
	logRetentionDaysKey = "meta:log:retention_days"
	logMaxRecordsKey    = "meta:log:max_records"
)

// LogCleaner deletes old log records using retention days and max records limits.
type LogCleaner struct {
	db            *gorm.DB
	rdb           *redis.Client
	retentionDays int
	maxRecords    int64
	partitioner   *service.LogPartitioner
}

func NewLogCleaner(db *gorm.DB, rdb *redis.Client, retentionDays int, maxRecords int64, partitioner *service.LogPartitioner) *LogCleaner {
	return &LogCleaner{
		db:            db,
		rdb:           rdb,
		retentionDays: retentionDays,
		maxRecords:    maxRecords,
		partitioner:   partitioner,
	}
}

// RunOnce executes a single log cleanup. Called by scheduler.
func (c *LogCleaner) RunOnce(ctx context.Context) {
	if c == nil || c.db == nil {
		return
	}
	if err := c.cleanOnce(ctx); err != nil {
		slog.Default().Warn("log cleaner run failed", "err", err)
	}
}

func (c *LogCleaner) cleanOnce(ctx context.Context) error {
	retentionDays := c.resolveRetentionDays(ctx)
	maxRecords := c.resolveMaxRecords(ctx)

	if retentionDays <= 0 && maxRecords <= 0 {
		return nil
	}

	if c.partitioner != nil && c.partitioner.Enabled() {
		return c.cleanPartitioned(retentionDays, maxRecords)
	}

	var deleted int64
	if retentionDays > 0 {
		cutoff := time.Now().UTC().AddDate(0, 0, -retentionDays)
		res := c.db.Unscoped().Where("created_at < ?", cutoff).Delete(&model.LogRecord{})
		if res.Error != nil {
			return res.Error
		}
		deleted += res.RowsAffected
	}

	if maxRecords > 0 {
		if maxRecords > int64(math.MaxInt) {
			maxRecords = int64(math.MaxInt)
		}
		var cutoff struct {
			ID uint
		}
		if err := c.db.Unscoped().
			Model(&model.LogRecord{}).
			Select("id").
			Order("id desc").
			Offset(int(maxRecords - 1)).
			Limit(1).
			Scan(&cutoff).Error; err != nil {
			return err
		}
		if cutoff.ID > 0 {
			res := c.db.Unscoped().Where("id < ?", cutoff.ID).Delete(&model.LogRecord{})
			if res.Error != nil {
				return res.Error
			}
			deleted += res.RowsAffected
		}
	}

	if deleted > 0 {
		slog.Default().Info("log cleanup completed", "deleted", deleted, "retention_days", retentionDays, "max_records", maxRecords)
	}

	return nil
}

func (c *LogCleaner) cleanPartitioned(retentionDays int, maxRecords int64) error {
	var deleted int64

	if retentionDays > 0 {
		cutoff := time.Now().UTC().AddDate(0, 0, -retentionDays)
		dropped, err := c.partitioner.DropPartitionsBefore(cutoff)
		if err != nil {
			return err
		}
		if dropped > 0 {
			slog.Default().Info("log partition cleanup completed", "dropped_tables", dropped, "retention_days", retentionDays)
		}
		table, err := c.partitioner.EnsurePartitionFor(time.Now().UTC())
		if err != nil {
			return err
		}
		res := c.db.Table(table).Unscoped().Where("created_at < ?", cutoff).Delete(&model.LogRecord{})
		if res.Error != nil {
			return res.Error
		}
		deleted += res.RowsAffected
	}

	if maxRecords > 0 {
		if maxRecords > int64(math.MaxInt) {
			maxRecords = int64(math.MaxInt)
		}
		table, err := c.partitioner.EnsurePartitionFor(time.Now().UTC())
		if err != nil {
			return err
		}
		var cutoff struct {
			ID uint
		}
		if err := c.db.Unscoped().
			Table(table).
			Select("id").
			Order("id desc").
			Offset(int(maxRecords - 1)).
			Limit(1).
			Scan(&cutoff).Error; err != nil {
			return err
		}
		if cutoff.ID > 0 {
			res := c.db.Table(table).Unscoped().Where("id < ?", cutoff.ID).Delete(&model.LogRecord{})
			if res.Error != nil {
				return res.Error
			}
			deleted += res.RowsAffected
		}
	}

	if deleted > 0 {
		slog.Default().Info("log cleanup completed", "deleted", deleted, "retention_days", retentionDays, "max_records", maxRecords)
	}
	return nil
}

func (c *LogCleaner) resolveRetentionDays(ctx context.Context) int {
	days := c.retentionDays
	if days < 0 {
		days = 0
	}
	if c.rdb == nil {
		return days
	}
	raw, err := c.rdb.Get(ctx, logRetentionDaysKey).Result()
	if err == redis.Nil {
		return days
	}
	if err != nil {
		slog.Default().Warn("log cleaner failed to read retention_days", "err", err)
		return days
	}
	if v, ok := parsePositiveInt(raw); ok {
		return v
	}
	slog.Default().Warn("log cleaner invalid retention_days", "value", raw)
	return days
}

func (c *LogCleaner) resolveMaxRecords(ctx context.Context) int64 {
	max := c.maxRecords
	if max < 0 {
		max = 0
	}
	if c.rdb == nil {
		return max
	}
	raw, err := c.rdb.Get(ctx, logMaxRecordsKey).Result()
	if err == redis.Nil {
		return max
	}
	if err != nil {
		slog.Default().Warn("log cleaner failed to read max_records", "err", err)
		return max
	}
	if v, ok := parsePositiveInt64(raw); ok {
		return v
	}
	slog.Default().Warn("log cleaner invalid max_records", "value", raw)
	return max
}

func parsePositiveInt(raw string) (int, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, false
	}
	v, err := strconv.Atoi(raw)
	if err != nil || v <= 0 {
		return 0, false
	}
	return v, true
}

func parsePositiveInt64(raw string) (int64, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, false
	}
	v, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || v <= 0 {
		return 0, false
	}
	return v, true
}
