package memorysearch

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

const (
	rollbackRetention         = 24 * time.Hour
	querySuccessAcceleration  = time.Hour
	failedGenerationRetention = time.Hour
	fencedBindingRetention    = 24 * time.Hour
	unboundBindingRetention   = 7 * 24 * time.Hour
	unknownOwnerRetention     = 7 * 24 * time.Hour
)

// RunRetentionGC applies lifecycle TTLs without ever deleting an active
// current generation. Deletion is pool-aware and uses close-before-rename.
func (r *Reconciler) RunRetentionGC(ctx context.Context) error {
	r.retentionMu.Lock()
	defer r.retentionMu.Unlock()
	if err := ctx.Err(); err != nil {
		return err
	}
	now := r.clock()
	current, _ := loadCurrent(r.derivativeRoot)
	state, stateErr := r.lifecycle.load()
	entries, err := os.ReadDir(r.generationsDir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil
		}
		return err
	}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		name := entry.Name()
		if err := validateBasename(name); err != nil {
			continue
		}
		if current != nil &&
			current.BindingFingerprint == r.opts.Binding.Fingerprint() &&
			current.GenerationBasename == name {
			// Ordinary retention never deletes the serviceable current.
			// Binding-wide expiry is handled by Manager maintenance only
			// after Fence/Unbound/UnknownOwner closed the reconciler.
			continue
		}
		info, err := entry.Info()
		if err != nil {
			continue
		}
		expired := false
		if _, err := os.Stat(filepath.Join(r.generationsDir, name, buildCompleteMarker)); err != nil {
			expired = now.Sub(info.ModTime()) >= failedGenerationRetention
		} else if stateErr == nil {
			expired = generationExpired(state, name, now)
		}
		if !expired {
			continue
		}
		if err := r.retireAndTrashGeneration(ctx, name); err != nil {
			return err
		}
	}
	return nil
}

func generationExpired(state *LifecycleState, basename string, now time.Time) bool {
	switch state.BindingState {
	case BindingLifecycleFenced:
		return !state.BindingChangedAt.IsZero() &&
			now.Sub(state.BindingChangedAt) >= fencedBindingRetention
	case BindingLifecycleUnbound:
		return !state.BindingChangedAt.IsZero() &&
			now.Sub(state.BindingChangedAt) >= unboundBindingRetention
	case BindingLifecycleUnknownOwner:
		return !state.BindingChangedAt.IsZero() &&
			now.Sub(state.BindingChangedAt) >= unknownOwnerRetention
	}
	generation, ok := state.Generations[basename]
	if !ok || generation.Kind != GenerationLifecycleRollback ||
		generation.SupersededAt == nil {
		return false
	}
	deadline := generation.SupersededAt.Add(rollbackRetention)
	for _, current := range state.Generations {
		if current.Kind != GenerationLifecycleCurrent ||
			current.FirstSuccessfulQueryAt == nil {
			continue
		}
		accelerated := current.FirstSuccessfulQueryAt.Add(querySuccessAcceleration)
		if accelerated.Before(deadline) {
			deadline = accelerated
		}
	}
	return !now.Before(deadline)
}

func (r *Reconciler) retireAndTrashGeneration(ctx context.Context, basename string) error {
	if err := validateBasename(basename); err != nil || basename == "" {
		return ErrInvalidBasename
	}
	// Linearize the final current check and retirement/rename against publish's
	// current pointer swap. We deliberately do not hold this mutex while
	// discovering candidates or building indexes.
	r.namespaceMu.Lock()
	defer r.namespaceMu.Unlock()
	if current, err := loadCurrent(r.derivativeRoot); err == nil &&
		current.BindingFingerprint == r.opts.Binding.Fingerprint() &&
		current.GenerationBasename == basename {
		return nil
	}
	pooled, state, opening := r.pools.RetireForGC(basename)
	if opening {
		return nil
	}
	if pooled && state != poolClosed {
		// Retirement is non-blocking for reconcile/maintenance. An in-flight
		// query closes the reader on last release; the next GC round observes
		// poolClosed and performs the rename. A generation with no registered
		// pool has no open Bleve handle and is safe to rename immediately.
		return nil
	}
	source := filepath.Join(r.generationsDir, basename)
	trashed, err := moveGenerationToTrash(r.derivativeRoot, source, r.clock())
	if err != nil {
		return err
	}
	if trashed != "" {
		select {
		case r.trashWake <- struct{}{}:
		default:
		}
	}
	if err := r.pools.Forget(basename); err != nil {
		return fmt.Errorf("forget retired pool %s: %w", basename, err)
	}
	return nil
}
