package content

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	store "github.com/tidbcloud/AgentTeam/internal/store/content"
)

type GCStore interface {
	LeaseDueStagedPayloadGCForBinding(context.Context, store.GCBindingScope, time.Time, string, time.Time, int) ([]store.StagedPayloadGC, error)
	PrepareStagedPayloadGC(context.Context, store.StagedPayloadGC, string, time.Time) (string, error)
	MarkStagedPayloadGCDeleted(context.Context, store.StagedPayloadGC, string, time.Time) (bool, error)
	MarkStagedPayloadGCAttemptFailed(context.Context, store.StagedPayloadGC, string, error, time.Time) (bool, error)
}

type GCBindingScope = store.GCBindingScope

// GCWorker reconciles staged payload reachability and deletes only payloads
// protected by the store's deletion fence.
type GCWorker struct {
	store         GCStore
	tree          contentpayload.TreePayloadBackend
	document      contentpayload.DocumentPayloadBackend
	leaseOwner    string
	leaseDuration time.Duration
	scope         GCBindingScope
	now           func() time.Time
}

// NewScopedTreeGCWorker constructs the active tree-only reconciler for one
// exact registered binding. A legacy document GC row fails closed instead of
// selecting another backend.
func NewScopedTreeGCWorker(store GCStore, tree contentpayload.TreePayloadBackend, scope GCBindingScope, leaseOwner string, leaseDuration time.Duration) (*GCWorker, error) {
	leaseOwner = strings.TrimSpace(leaseOwner)
	scope = storeScope(scope)
	if store == nil || tree == nil || leaseOwner == "" || leaseDuration <= 0 || scope.WorkspaceID == "" || scope.Domain == "" || scope.BindingKey == "" {
		return nil, fmt.Errorf("tree content gc worker dependencies are required")
	}
	return &GCWorker{store: store, tree: tree, scope: scope, leaseOwner: leaseOwner, leaseDuration: leaseDuration, now: func() time.Time { return time.Now().UTC() }}, nil
}

func storeScope(scope GCBindingScope) GCBindingScope {
	scope.WorkspaceID = strings.TrimSpace(scope.WorkspaceID)
	scope.Domain = strings.TrimSpace(scope.Domain)
	scope.BindingKey = strings.TrimSpace(scope.BindingKey)
	return scope
}

type GCWorkerResult struct{ Leased, Retained, Deleted, Retried, Failed, Stale int }

func (worker *GCWorker) RunOnce(ctx context.Context, limit int) (GCWorkerResult, error) {
	if worker == nil {
		return GCWorkerResult{}, fmt.Errorf("content gc worker is nil")
	}
	now := worker.now()
	rows, err := worker.store.LeaseDueStagedPayloadGCForBinding(ctx, worker.scope, now, worker.leaseOwner, now.Add(worker.leaseDuration), limit)
	if err != nil {
		return GCWorkerResult{}, err
	}
	result := GCWorkerResult{Leased: len(rows)}
	for _, row := range rows {
		action, err := worker.store.PrepareStagedPayloadGC(ctx, row, worker.leaseOwner, worker.now())
		if err != nil {
			return result, err
		}
		switch action {
		case store.GCActionRetained:
			result.Retained++
			continue
		case store.GCActionStale:
			result.Stale++
			continue
		case store.GCActionDelete:
		default:
			return result, fmt.Errorf("unsupported content gc action %q", action)
		}
		deleteErr := worker.deletePayload(ctx, row)
		if deleteErr != nil {
			updated, markErr := worker.store.MarkStagedPayloadGCAttemptFailed(ctx, row, worker.leaseOwner, deleteErr, worker.now())
			if markErr != nil {
				return result, markErr
			}
			if !updated {
				result.Stale++
			} else if row.Attempts+1 >= store.MaxGCAttempts {
				result.Failed++
			} else {
				result.Retried++
			}
			continue
		}
		updated, err := worker.store.MarkStagedPayloadGCDeleted(ctx, row, worker.leaseOwner, worker.now())
		if err != nil {
			return result, err
		}
		if updated {
			result.Deleted++
		} else {
			result.Stale++
		}
	}
	return result, nil
}

func (worker *GCWorker) deletePayload(ctx context.Context, row store.StagedPayloadGC) error {
	ref := contentpayload.ManifestRef(row.PayloadRef)
	switch row.Model {
	case store.ModelTree:
		return worker.tree.DeletePayload(ctx, ref)
	case store.ModelDocument:
		if worker.document == nil {
			return store.ErrModelConflict
		}
		return worker.document.DeletePayload(ctx, ref)
	default:
		return fmt.Errorf("unsupported staged content payload model %q", row.Model)
	}
}
