package aws

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

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/autoscaling"
	"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
	tagtypes "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi/types"

	"github.com/tidbcloud/controlplane9/internal/verbose"
)

// PurgeMode is the cleanup mode inferred from cp9 purge inputs.
type PurgeMode string

const (
	// PurgeModeBroad discovers expired cp9-managed owner/session candidates.
	PurgeModeBroad PurgeMode = "broad"
	// PurgeModeExact targets a single explicit owner/session pair and ignores TTL.
	PurgeModeExact PurgeMode = "exact"
)

// ResolvePurgeMode infers the purge mode from owner/session inputs.
//
// Neither owner nor session => broad expired mode.
// Both owner and session    => exact mode.
// Exactly one of the two     => error (fail fast); purge never operates on a
// half-specified scope.
func ResolvePurgeMode(owner, session string) (PurgeMode, error) {
	owner = strings.TrimSpace(owner)
	session = strings.TrimSpace(session)
	switch {
	case owner == "" && session == "":
		return PurgeModeBroad, nil
	case owner != "" && session != "":
		return PurgeModeExact, nil
	default:
		return "", fmt.Errorf("owner and session must be supplied together (got owner=%q session=%q)", owner, session)
	}
}

// ExpiredManagedScope is a broad-mode purge candidate: a cp9-managed
// owner/session pair whose cp9-ttl has already expired.
type ExpiredManagedScope struct {
	Owner         string     `json:"owner"`
	Session       string     `json:"session"`
	TTL           time.Time  `json:"ttl"`
	Resources     []Resource `json:"-"`
	VerifiedStale bool       `json:"-"`
}

// managedPurgeFilterValues are the cp9-managed tag values that mark a resource
// as in scope for broad expired discovery. Mirrors the session-cleanup shell
// reaper: cp9-managed in {true, terraform-substrate}. Crucially this does NOT
// require environment=cp9, so starter and terraform-substrate sessions remain
// discoverable.
func managedPurgeFilterValues() []string {
	return []string{"true", TagManagedTerraformSubstrate}
}

// FindExpiredManagedScopes discovers broad-mode purge candidates from the
// regional tag index. Callers that also merge account-global evidence such as
// IAM must use FindManagedScopes and filter only after that merge.
func FindExpiredManagedScopes(ctx context.Context, client TaggingAPI, now time.Time) ([]ExpiredManagedScope, error) {
	scopes, err := FindManagedScopes(ctx, client)
	if err != nil {
		return nil, err
	}
	return FilterExpiredManagedScopes(scopes, now), nil
}

// FindManagedScopes returns every valid owner/session TTL observed in the
// regional tag index, keeping the latest TTL for each scope. It is read-only.
func FindManagedScopes(ctx context.Context, client TaggingAPI) ([]ExpiredManagedScope, error) {
	if client == nil {
		return nil, fmt.Errorf("tagging api client must not be nil")
	}
	filters := []tagtypes.TagFilter{{Key: aws.String(TagManaged), Values: managedPurgeFilterValues()}}
	var scopes []ExpiredManagedScope
	var token *string
	for {
		verbose.Logf("AWS tagging:GetResources purge-discovery filters=%v page_token=%q", filters, aws.ToString(token))
		out, err := client.GetResources(ctx, &resourcegroupstaggingapi.GetResourcesInput{
			TagFilters:       filters,
			PaginationToken:  token,
			ResourcesPerPage: aws.Int32(100),
		})
		if err != nil {
			return nil, fmt.Errorf("tagging api GetResources: %w", err)
		}
		for _, mapping := range out.ResourceTagMappingList {
			tags := taggingTagsToMap(mapping.Tags)
			arn := strings.TrimSpace(aws.ToString(mapping.ResourceARN))
			owner := strings.TrimSpace(tags[TagOwner])
			session := strings.TrimSpace(tags[TagSession])
			ttlRaw := strings.TrimSpace(tags[TagTTL])
			if owner == "" || session == "" || ttlRaw == "" {
				continue
			}
			ttl, err := parseCP9TTL(ttlRaw)
			if err != nil {
				continue
			}
			candidate := ExpiredManagedScope{
				Owner:   owner,
				Session: session,
				TTL:     ttl,
			}
			if arn != "" {
				candidate.Resources = []Resource{{
					ARN:       arn,
					Type:      ParseResourceType(arn),
					Region:    ParseResourceRegion(arn),
					Owner:     owner,
					SessionID: session,
				}}
			}
			scopes = appendManagedScope(scopes, &candidate)
		}
		token = out.PaginationToken
		if token == nil || *token == "" {
			break
		}
	}
	slices.SortFunc(scopes, func(a, b ExpiredManagedScope) int {
		if a.Owner != b.Owner {
			return strings.Compare(a.Owner, b.Owner)
		}
		return strings.Compare(a.Session, b.Session)
	})
	return scopes, nil
}

// FindManagedASGScopes returns owner/session TTLs observed on Auto Scaling
// groups. Resource Groups Tagging API omits these groups, so broad purge must
// merge this evidence before expiration is evaluated.
func FindManagedASGScopes(ctx context.Context, client AutoScalingGraphAPI) ([]ExpiredManagedScope, error) {
	if client == nil {
		return nil, fmt.Errorf("autoscaling client must not be nil")
	}
	var scopes []ExpiredManagedScope
	var token *string
	for {
		verbose.Logf("AWS autoscaling:DescribeAutoScalingGroups purge-discovery page_token=%q", aws.ToString(token))
		out, err := client.DescribeAutoScalingGroups(ctx, &autoscaling.DescribeAutoScalingGroupsInput{
			NextToken: token,
		})
		if err != nil {
			return nil, fmt.Errorf("list Auto Scaling groups for managed-scope discovery: %w", err)
		}
		for i := range out.AutoScalingGroups {
			tags := AutoScalingTagMap(out.AutoScalingGroups[i].Tags)
			owner := strings.TrimSpace(tags[TagOwner])
			session := strings.TrimSpace(tags[TagSession])
			ttlRaw := strings.TrimSpace(tags[TagTTL])
			managed := strings.TrimSpace(tags[TagManaged])
			if owner == "" || session == "" || ttlRaw == "" {
				continue
			}
			if !slices.Contains(managedPurgeFilterValues(), managed) {
				continue
			}
			ttl, err := parseCP9TTL(ttlRaw)
			if err != nil {
				continue
			}
			arn := strings.TrimSpace(aws.ToString(out.AutoScalingGroups[i].AutoScalingGroupARN))
			candidate := ExpiredManagedScope{
				Owner:   owner,
				Session: session,
				TTL:     ttl,
			}
			if arn != "" {
				candidate.Resources = []Resource{{
					ARN:       arn,
					Type:      ResourceAutoScalingGroup,
					Region:    ParseResourceRegion(arn),
					Owner:     owner,
					SessionID: session,
				}}
			}
			scopes = appendManagedScope(scopes, &candidate)
		}
		token = out.NextToken
		if token == nil || aws.ToString(token) == "" {
			break
		}
	}
	slices.SortFunc(scopes, func(a, b ExpiredManagedScope) int {
		if a.Owner != b.Owner {
			return strings.Compare(a.Owner, b.Owner)
		}
		return strings.Compare(a.Session, b.Session)
	})
	return scopes, nil
}

// FilterExpiredManagedScopes returns only scopes whose latest observed TTL is
// at or before now. Filtering after all regional and account-global evidence is
// merged prevents expired residue from selecting a live same-name successor.
func FilterExpiredManagedScopes(scopes []ExpiredManagedScope, now time.Time) []ExpiredManagedScope {
	expired := make([]ExpiredManagedScope, 0, len(scopes))
	for _, scope := range scopes {
		if !scope.TTL.IsZero() && !scope.TTL.After(now) {
			expired = append(expired, scope)
		}
	}
	return expired
}

func taggingTagsToMap(tags []tagtypes.Tag) map[string]string {
	m := make(map[string]string, len(tags))
	for _, tag := range tags {
		m[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
	}
	return m
}

// parseCP9TTL parses a cp9-ttl tag value. The shell reaper uses jq
// fromdateiso8601 (UTC RFC3339); accept that plus a few common ISO8601 variants.
func parseCP9TTL(raw string) (time.Time, error) {
	layouts := []string{
		time.RFC3339,
		time.RFC3339Nano,
		"2006-01-02T15-04-05Z",
		"2006-01-02T15:04:05Z0700",
		"2006-01-02T15:04:05",
	}
	for _, layout := range layouts {
		if ts, err := time.Parse(layout, raw); err == nil {
			return ts.UTC(), nil
		}
	}
	return time.Time{}, fmt.Errorf("unparseable cp9-ttl %q", raw)
}

// ParseCP9TTL exposes the cleanup tag parser to the command's live-dependency
// classifier so discovery and execution use one TTL contract.
func ParseCP9TTL(raw string) (time.Time, error) {
	return parseCP9TTL(raw)
}
