package aws

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

// Scope defines the owner/session/region boundary for authoritative C4 scans.
type Scope struct {
	Owner      string   `json:"owner,omitempty"`
	SessionID  string   `json:"session_id,omitempty"`
	Regions    []string `json:"regions"`
	AllOwners  bool     `json:"all_owners,omitempty"`
	AdminSweep bool     `json:"admin_sweep,omitempty"`
}

// Normalize trims fields and sorts/deduplicates regions.
func (s Scope) Normalize() Scope {
	s.Owner = strings.ToLower(strings.TrimSpace(s.Owner))
	s.SessionID = strings.TrimSpace(s.SessionID)
	s.Regions = normalizeRegions(s.Regions)
	return s
}

// Validate rejects empty or unsafe scopes.
func (s Scope) Validate() error {
	normalized := s.Normalize()
	if len(normalized.Regions) == 0 {
		return fmt.Errorf("scope regions must not be empty")
	}
	if normalized.Owner == "" && normalized.SessionID == "" {
		return fmt.Errorf("scope must include owner and/or session")
	}
	if normalized.Owner == "" && normalized.SessionID != "" && !normalized.AllOwners {
		return fmt.Errorf("ownerless session scope requires validated admin sweep")
	}
	if normalized.AllOwners && normalized.Owner != "" {
		return fmt.Errorf("all-owners scope must not also set owner")
	}
	if normalized.AllOwners && !normalized.AdminSweep {
		return fmt.Errorf("all-owners scope requires validated admin sweep")
	}
	return nil
}

func normalizeRegions(regions []string) []string {
	set := make(map[string]struct{}, len(regions))
	var normalized []string
	for _, region := range regions {
		region = strings.TrimSpace(region)
		if region == "" {
			continue
		}
		if _, ok := set[region]; ok {
			continue
		}
		set[region] = struct{}{}
		normalized = append(normalized, region)
	}
	slices.Sort(normalized)
	return normalized
}

// ScopeScanner discovers resources across every region in scope.
type ScopeScanner interface {
	Scan(ctx context.Context, scope Scope) ([]Resource, error)
}

// KnownResourceVerifier proves one persisted handoff identity directly through
// its owning AWS service. The bool is false only when the service proves the
// resource is already absent; an existing resource outside scope returns an
// error and must never be adopted for cleanup.
type KnownResourceVerifier interface {
	VerifyKnownResource(ctx context.Context, scope Scope, resource *Resource) (bool, error)
}

// TrackerFactory creates a region-specific tracker.
type TrackerFactory func(region string) (Tracker, error)

// RegionalScanner fans out scope scans across region-scoped trackers.
type RegionalScanner struct {
	Factory TrackerFactory
	mu      sync.Mutex
	cache   map[string]Tracker
}

// VerifyKnownResource routes an exact read-only ownership proof to the same
// region-specific tracker used by Scan.
func (s *RegionalScanner) VerifyKnownResource(
	ctx context.Context,
	scope Scope,
	resource *Resource,
) (bool, error) {
	if resource == nil {
		return false, fmt.Errorf("known resource is nil")
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	scope = scope.Normalize()
	if err := scope.Validate(); err != nil {
		return false, err
	}
	region := strings.TrimSpace(resource.Region)
	if region == "" {
		region = scope.Regions[0]
	}
	if !slices.Contains(scope.Regions, region) {
		return false, fmt.Errorf("known resource %q region %q is outside cleanup scope", resource.ARN, region)
	}
	if s.Factory == nil {
		return false, fmt.Errorf("regional scanner factory is not configured")
	}
	if s.cache == nil {
		s.cache = make(map[string]Tracker)
	}
	tracker, ok := s.cache[region]
	if !ok {
		var err error
		tracker, err = s.Factory(region)
		if err != nil {
			return false, fmt.Errorf("build tracker for region %s: %w", region, err)
		}
		s.cache[region] = tracker
	}
	verifier, ok := tracker.(KnownResourceVerifier)
	if !ok {
		return false, fmt.Errorf("tracker for region %s cannot prove persisted resource ownership", region)
	}
	return verifier.VerifyKnownResource(ctx, scope, resource)
}

// Scan discovers resources in every configured region and returns a de-duplicated set.
func (s *RegionalScanner) Scan(ctx context.Context, scope Scope) ([]Resource, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	scope = scope.Normalize()
	if err := scope.Validate(); err != nil {
		return nil, err
	}
	if s.Factory == nil {
		return nil, fmt.Errorf("regional scanner factory is not configured")
	}

	var resources []Resource
	seen := make(map[string]struct{})

	for _, region := range scope.Regions {
		if s.cache == nil {
			s.cache = make(map[string]Tracker)
		}
		tracker, ok := s.cache[region]
		if !ok {
			var err error
			tracker, err = s.Factory(region)
			if err != nil {
				return nil, fmt.Errorf("build tracker for region %s: %w", region, err)
			}
			s.cache[region] = tracker
		}

		found, err := scanRegion(ctx, tracker, scope, region)
		if err != nil {
			return nil, fmt.Errorf("scan region %s: %w", region, err)
		}
		for i := range found {
			resource := found[i]
			key := resourceDedupeKey(&resource)
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			resources = append(resources, resource)
		}
	}

	sortResources(resources)
	return resources, nil
}

type ownerAndSessionTracker interface {
	FindByOwnerAndSession(ctx context.Context, owner, sessionID string) ([]Resource, error)
}

func scanRegion(ctx context.Context, tracker Tracker, scope Scope, region string) ([]Resource, error) {
	switch {
	case scope.Owner != "" && scope.SessionID != "":
		if combined, ok := tracker.(ownerAndSessionTracker); ok {
			return combined.FindByOwnerAndSession(ctx, scope.Owner, scope.SessionID)
		}
		owned, err := tracker.FindByOwner(ctx, scope.Owner)
		if err != nil {
			return nil, err
		}
		return filterBySession(owned, scope.SessionID, region), nil
	case scope.Owner != "":
		owned, err := tracker.FindByOwner(ctx, scope.Owner)
		if err != nil {
			return nil, err
		}
		return withRegionFallback(owned, region), nil
	case scope.SessionID != "":
		sessionResources, err := tracker.FindBySession(ctx, scope.SessionID)
		if err != nil {
			return nil, err
		}
		if scope.AllOwners {
			return withRegionFallback(sessionResources, region), nil
		}
		return withRegionFallback(sessionResources, region), nil
	default:
		return nil, fmt.Errorf("unsupported empty scope")
	}
}

func filterBySession(resources []Resource, sessionID, region string) []Resource {
	var filtered []Resource
	for _, resource := range resources {
		if resource.SessionID != sessionID {
			continue
		}
		if resource.Region == "" {
			resource.Region = region
		}
		filtered = append(filtered, resource)
	}
	return filtered
}

func withRegionFallback(resources []Resource, region string) []Resource {
	for i := range resources {
		if resources[i].Region == "" {
			resources[i].Region = region
		}
	}
	return resources
}

// CleanerFactory creates a region-specific resource cleaner.
type CleanerFactory func(region string) (Cleaner, error)

// RegionalCleaner dispatches deletes through region-appropriate cleaners.
type RegionalCleaner struct {
	Factory CleanerFactory
	cache   map[string]Cleaner
}

// Delete routes a delete call to the cleaner for the resource's region.
func (c *RegionalCleaner) Delete(ctx context.Context, resource *Resource) (bool, error) {
	if c.Factory == nil {
		return false, fmt.Errorf("regional cleaner factory is not configured")
	}
	if resource == nil {
		return false, fmt.Errorf("resource must not be nil")
	}
	if strings.TrimSpace(resource.Region) == "" {
		return false, fmt.Errorf("resource %s has no region", resource.ARN)
	}
	if c.cache == nil {
		c.cache = make(map[string]Cleaner)
	}
	cleaner, ok := c.cache[resource.Region]
	if !ok {
		var err error
		cleaner, err = c.Factory(resource.Region)
		if err != nil {
			return false, fmt.Errorf("build cleaner for region %s: %w", resource.Region, err)
		}
		c.cache[resource.Region] = cleaner
	}
	return cleaner.Delete(ctx, resource)
}
