package aws

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"time"
)

const defaultMaxReapPasses = 4

var (
	defaultPostDeleteSettleWait     = 15 * time.Second
	defaultPostDeleteSettleInterval = 2 * time.Second
)

// ReapResult holds the outcome of an authoritative Stage-2 cleanup.
type ReapResult struct {
	Scope      Scope      `json:"scope"`
	DryRun     bool       `json:"dry_run"`
	UsedResume bool       `json:"used_resume"`
	Initial    int        `json:"initial"`
	Deleted    int        `json:"deleted"`
	Passes     int        `json:"passes"`
	Survivors  []Resource `json:"survivors,omitempty"`
}

// FormatText implements model.TextFormatter.
func (r *ReapResult) FormatText() string {
	var b strings.Builder
	scope := formatScope(r.Scope)
	if r.DryRun {
		fmt.Fprintf(&b, "[dry-run] reaper scope=%s initial=%d survivors=%d passes=%d",
			scope, r.Initial, len(r.Survivors), r.Passes)
	} else {
		fmt.Fprintf(&b, "reaper scope=%s initial=%d deleted=%d survivors=%d passes=%d",
			scope, r.Initial, r.Deleted, len(r.Survivors), r.Passes)
	}
	if r.UsedResume {
		b.WriteString(" resume=true")
	}
	for _, survivor := range r.Survivors {
		fmt.Fprintf(&b, "\n  survivor: %s", survivor.String())
	}
	return b.String()
}

func formatScope(scope Scope) string {
	var parts []string
	if scope.Owner != "" {
		parts = append(parts, "owner="+scope.Owner)
	}
	if scope.SessionID != "" {
		parts = append(parts, "session="+scope.SessionID)
	}
	if len(scope.Regions) > 0 {
		parts = append(parts, "regions="+strings.Join(scope.Regions, ","))
	}
	if scope.AllOwners {
		parts = append(parts, "all-owners=true")
	}
	return strings.Join(parts, " ")
}

// Reaper performs dependency-aware, owner/session scoped cleanup.
type Reaper struct {
	Scanner        ScopeScanner
	Cleaner        Cleaner
	MaxPasses      int
	SettleWait     time.Duration
	SettleInterval time.Duration
}

// NewReaper creates a scoped Stage-2 reaper.
func NewReaper(scanner ScopeScanner, cleaner Cleaner) *Reaper {
	return &Reaper{
		Scanner:        scanner,
		Cleaner:        cleaner,
		MaxPasses:      defaultMaxReapPasses,
		SettleWait:     defaultPostDeleteSettleWait,
		SettleInterval: defaultPostDeleteSettleInterval,
	}
}

// Reap performs a scope scan, deletes resources in dependency-safe order, and re-scans.
func (r *Reaper) Reap(ctx context.Context, scope Scope, dryRun bool, resume []Resource) (*ReapResult, error) {
	return r.reap(ctx, scope, dryRun, resume)
}

func (r *Reaper) reap(ctx context.Context, scope Scope, dryRun bool, resume []Resource) (*ReapResult, error) {
	scope = scope.Normalize()
	if err := scope.Validate(); err != nil {
		return nil, err
	}
	if r.Scanner == nil {
		return nil, fmt.Errorf("reaper scanner is not configured")
	}
	if r.Cleaner == nil && !dryRun {
		return nil, fmt.Errorf("reaper cleaner is not configured")
	}

	current, err := r.Scanner.Scan(ctx, scope)
	if err != nil {
		return nil, fmt.Errorf("initial scan: %w", err)
	}
	result := &ReapResult{
		Scope:   scope,
		DryRun:  dryRun,
		Initial: len(current),
	}
	if len(resume) > 0 {
		current = mergeResources(current, resume)
		result.UsedResume = true
	}
	sortResources(current)

	if dryRun {
		result.Passes = 1
		result.Survivors = current
		return result, nil
	}

	maxPasses := r.MaxPasses
	if maxPasses <= 0 {
		maxPasses = defaultMaxReapPasses
	}
	confirmedGone := map[string]struct{}{}
	for pass := 1; pass <= maxPasses; pass++ {
		result.Passes = pass
		current = filterConfirmedGone(current, confirmedGone)
		if len(current) == 0 {
			result.Survivors = nil
			return result, nil
		}
		for i := range current {
			resource := &current[i]
			deleted, deleteErr := r.Cleaner.Delete(ctx, resource)
			if deleteErr != nil {
				continue
			}
			if deleted {
				result.Deleted++
			} else {
				confirmedGone[resourceDedupeKey(resource)] = struct{}{}
			}
		}

		next, err := r.Scanner.Scan(ctx, scope)
		if err != nil {
			return nil, fmt.Errorf("post-delete scan: %w", err)
		}
		sortResources(next)
		next = filterConfirmedGone(next, confirmedGone)
		if len(next) == 0 {
			result.Survivors = nil
			return result, nil
		}
		current = next
	}

	if result.Deleted > 0 {
		settled, err := r.waitForTaggingSettle(ctx, scope, current, confirmedGone)
		if err != nil {
			return nil, err
		}
		if len(settled) == 0 {
			result.Survivors = nil
			return result, nil
		}
		current = settled
	}

	result.Survivors = current
	return result, nil
}

func (r *Reaper) waitForTaggingSettle(
	ctx context.Context,
	scope Scope,
	current []Resource,
	confirmedGone map[string]struct{},
) ([]Resource, error) {
	settleWait := r.SettleWait
	if settleWait <= 0 {
		settleWait = defaultPostDeleteSettleWait
	}
	settleInterval := r.SettleInterval
	if settleInterval <= 0 {
		settleInterval = defaultPostDeleteSettleInterval
	}

	deadline := time.Now().Add(settleWait)
	latest := filterConfirmedGone(append([]Resource(nil), current...), confirmedGone)

	for time.Now().Before(deadline) {
		if err := sleepUntilNextSettleScan(ctx, settleInterval); err != nil {
			return nil, err
		}
		next, err := r.Scanner.Scan(ctx, scope)
		if err != nil {
			return nil, fmt.Errorf("post-delete settle scan: %w", err)
		}
		sortResources(next)
		next = filterConfirmedGone(next, confirmedGone)
		if len(next) == 0 {
			return nil, nil
		}
		latest = next
	}

	return latest, nil
}

func sleepUntilNextSettleScan(ctx context.Context, interval time.Duration) error {
	timer := time.NewTimer(interval)
	defer timer.Stop()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func mergeResources(current, resume []Resource) []Resource {
	merged := append([]Resource(nil), current...)
	seen := make(map[string]struct{}, len(merged))
	for _, resource := range merged {
		seen[resource.Region+"\x00"+resource.ARN] = struct{}{}
	}
	for _, resource := range resume {
		key := resource.Region + "\x00" + resource.ARN
		if _, ok := seen[key]; ok {
			continue
		}
		if resumeResourceRequiresFreshScan(resource.Type) {
			continue
		}
		seen[key] = struct{}{}
		merged = append(merged, resource)
	}
	return merged
}

func resumeResourceRequiresFreshScan(resourceType ResourceType) bool {
	switch resourceType {
	case ResourceEC2Instance,
		ResourceEBSVolume,
		ResourceEBSSnapshot,
		ResourceEIP,
		ResourceENI,
		ResourceVPCEndpoint,
		ResourceVPCEndpointService:
		return true
	default:
		return false
	}
}

func filterConfirmedGone(resources []Resource, confirmedGone map[string]struct{}) []Resource {
	if len(resources) == 0 || len(confirmedGone) == 0 {
		return resources
	}
	filtered := resources[:0]
	for i := range resources {
		if _, ok := confirmedGone[resourceDedupeKey(&resources[i])]; ok {
			continue
		}
		filtered = append(filtered, resources[i])
	}
	return filtered
}

func sortResources(resources []Resource) {
	slices.SortFunc(resources, func(a, b Resource) int {
		if pa, pb := deletePriority(a.Type), deletePriority(b.Type); pa != pb {
			return pa - pb
		}
		if a.Region != b.Region {
			return strings.Compare(a.Region, b.Region)
		}
		return strings.Compare(a.ARN, b.ARN)
	})
}

func deletePriority(resourceType ResourceType) int {
	switch resourceType {
	case ResourceLoadBalancer:
		return 10
	case ResourceTargetGroup:
		return 20
	case ResourceVPCEndpointService:
		return 25
	case ResourceEKSNodegroup:
		return 30
	case ResourceEKSCluster:
		return 40
	case ResourceNATGateway:
		return 50
	case ResourceVPCEndpoint:
		return 60
	case ResourceEIP:
		return 70
	case ResourceInternetGateway:
		return 80
	case ResourceAutoScalingGroup:
		return 85
	case ResourceEC2Instance:
		return 90
	case ResourceENI:
		return 100
	case ResourceEBSVolume:
		return 110
	case ResourceEBSSnapshot:
		return 120
	case ResourceSubnet:
		return 130
	case ResourceRouteTable:
		return 140
	case ResourceSecurityGroup:
		return 150
	case ResourceVPC:
		return 160
	case ResourceLaunchTemplate:
		return 170
	case ResourceIAMInstanceProfile:
		return 180
	case ResourceIAMRole:
		return 190
	case ResourceIAMPolicy:
		return 200
	case ResourceSQS, ResourceSNS, ResourceLambda, ResourceDynamoDB, ResourceS3:
		return 210
	case ResourceRoute53HostedZone:
		return 220
	default:
		return 1000
	}
}

// SessionReapResult remains for older JSON consumers until the TTL sweep is rebuilt.
type SessionReapResult struct {
	SessionID string `json:"session_id"`
	Owner     string `json:"owner"`
	Total     int    `json:"total"`
	Deleted   int    `json:"deleted"`
	Errors    int    `json:"errors"`
}

// DeprecatedTTLSweepTimestamp is retained solely to avoid breaking older tests that
// still import this package after TTL cleanup moved behind destroy/operator flows.
var DeprecatedTTLSweepTimestamp = time.Time{}
