package cmd

import (
	"context"
	"fmt"
	"net/url"
	"slices"
	"strings"
	"time"

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/eks"
	"github.com/aws/aws-sdk-go-v2/service/iam"
)

const orphanOIDCMinimumAge = time.Hour

type orphanOIDCIAMAPI interface {
	ListOpenIDConnectProviders(
		ctx context.Context,
		params *iam.ListOpenIDConnectProvidersInput,
		optFns ...func(*iam.Options),
	) (*iam.ListOpenIDConnectProvidersOutput, error)
	GetOpenIDConnectProvider(
		ctx context.Context,
		params *iam.GetOpenIDConnectProviderInput,
		optFns ...func(*iam.Options),
	) (*iam.GetOpenIDConnectProviderOutput, error)
	ListRoles(
		ctx context.Context,
		params *iam.ListRolesInput,
		optFns ...func(*iam.Options),
	) (*iam.ListRolesOutput, error)
	DeleteOpenIDConnectProvider(
		ctx context.Context,
		params *iam.DeleteOpenIDConnectProviderInput,
		optFns ...func(*iam.Options),
	) (*iam.DeleteOpenIDConnectProviderOutput, error)
}

type orphanOIDCEKSAPI interface {
	ListClusters(
		ctx context.Context,
		params *eks.ListClustersInput,
		optFns ...func(*eks.Options),
	) (*eks.ListClustersOutput, error)
	DescribeCluster(
		ctx context.Context,
		params *eks.DescribeClusterInput,
		optFns ...func(*eks.Options),
	) (*eks.DescribeClusterOutput, error)
}

type orphanOIDCProvider struct {
	arn    string
	url    string
	region string
}

type orphanOIDCSweepResult struct {
	Scanned  int `json:"scanned"`
	Eligible int `json:"eligible"`
	Deleted  int `json:"deleted"`
}

var purgeOrphanOIDCSweep = realPurgeOrphanOIDCSweep

func realPurgeOrphanOIDCSweep(
	ctx context.Context,
	opCfg purgeOperatorConfig,
	shouldDelete bool,
) (orphanOIDCSweepResult, error) {
	cfg, err := cmdLoadAWSConfig(ctx, opCfg.Profile, opCfg.Region)
	if err != nil {
		return orphanOIDCSweepResult{}, fmt.Errorf("load AWS config for orphan OIDC cleanup: %w", err)
	}
	return cleanupOrphanEKSOpenIDConnectProviders(
		ctx,
		iam.NewFromConfig(cfg),
		func(region string) orphanOIDCEKSAPI {
			regionCfg := cfg
			regionCfg.Region = region
			return eks.NewFromConfig(regionCfg)
		},
		opCfg.Region,
		time.Now().UTC(),
		shouldDelete,
	)
}

func cleanupOrphanEKSOpenIDConnectProviders(
	ctx context.Context,
	iamClient orphanOIDCIAMAPI,
	eksClient func(string) orphanOIDCEKSAPI,
	cleanupRegion string,
	now time.Time,
	shouldDelete bool,
) (orphanOIDCSweepResult, error) {
	if iamClient == nil || eksClient == nil || strings.TrimSpace(cleanupRegion) == "" {
		return orphanOIDCSweepResult{}, fmt.Errorf("orphan OIDC cleanup requires IAM and EKS clients plus a region")
	}
	providers, scanned, err := listUntaggedEKSOpenIDConnectProviders(ctx, iamClient, cleanupRegion, now)
	result := orphanOIDCSweepResult{Scanned: scanned}
	if err != nil || len(providers) == 0 {
		return result, err
	}

	activeIssuers, rolePolicies, err := readOrphanOIDCReferences(ctx, iamClient, eksClient, providers)
	if err != nil {
		return result, err
	}
	if shouldDelete {
		// Re-read every external reference before the first mutation. The account
		// cleanup lease serializes cleaners; this second proof also narrows the
		// remaining external create/delete race to the deletion window.
		activeIssuers, rolePolicies, err = readOrphanOIDCReferences(ctx, iamClient, eksClient, providers)
		if err != nil {
			return result, err
		}
	}

	eligible := make([]orphanOIDCProvider, 0, len(providers))
	for _, provider := range providers {
		if _, active := activeIssuers[provider.url]; active || oidcProviderReferenced(provider.arn, rolePolicies) {
			continue
		}
		eligible = append(eligible, provider)
	}
	result.Eligible = len(eligible)
	if !shouldDelete {
		return result, nil
	}

	for _, provider := range eligible {
		latest, err := iamClient.GetOpenIDConnectProvider(ctx, &iam.GetOpenIDConnectProviderInput{
			OpenIDConnectProviderArn: awssdk.String(provider.arn),
		})
		if err != nil {
			if destroyIAMNotFound(err) {
				continue
			}
			return result, fmt.Errorf("re-read orphan OIDC provider %s: %w", provider.arn, err)
		}
		if latest == nil || len(latest.Tags) != 0 || normalizeOIDCURL(awssdk.ToString(latest.Url)) != provider.url ||
			latest.CreateDate == nil || now.Sub(latest.CreateDate.UTC()) < orphanOIDCMinimumAge {
			continue
		}
		if _, err := iamClient.DeleteOpenIDConnectProvider(ctx, &iam.DeleteOpenIDConnectProviderInput{
			OpenIDConnectProviderArn: awssdk.String(provider.arn),
		}); err != nil {
			if destroyIAMNotFound(err) {
				continue
			}
			return result, fmt.Errorf("delete orphan OIDC provider %s: %w", provider.arn, err)
		}
		result.Deleted++
	}
	return result, nil
}

func listUntaggedEKSOpenIDConnectProviders(
	ctx context.Context,
	client orphanOIDCIAMAPI,
	cleanupRegion string,
	now time.Time,
) ([]orphanOIDCProvider, int, error) {
	var providers []orphanOIDCProvider
	scanned := 0
	out, err := client.ListOpenIDConnectProviders(ctx, &iam.ListOpenIDConnectProvidersInput{})
	if err != nil {
		return nil, scanned, fmt.Errorf("list OIDC providers: %w", err)
	}
	if out == nil {
		return nil, scanned, fmt.Errorf("list OIDC providers returned no output")
	}
	for _, entry := range out.OpenIDConnectProviderList {
		arn := strings.TrimSpace(awssdk.ToString(entry.Arn))
		if arn == "" {
			continue
		}
		scanned++
		_, providerURL, found := strings.Cut(arn, ":oidc-provider/")
		if !found {
			continue
		}
		region, ok := eksOIDCRegion(providerURL)
		if !ok || region != cleanupRegion {
			continue
		}
		detail, err := client.GetOpenIDConnectProvider(ctx, &iam.GetOpenIDConnectProviderInput{
			OpenIDConnectProviderArn: awssdk.String(arn),
		})
		if err != nil {
			if destroyIAMNotFound(err) {
				continue
			}
			return nil, scanned, fmt.Errorf("get OIDC provider %s: %w", arn, err)
		}
		if detail == nil || len(detail.Tags) != 0 || detail.CreateDate == nil ||
			now.Sub(detail.CreateDate.UTC()) < orphanOIDCMinimumAge {
			continue
		}
		providerURL = normalizeOIDCURL(awssdk.ToString(detail.Url))
		detailRegion, ok := eksOIDCRegion(providerURL)
		if !ok || detailRegion != cleanupRegion {
			continue
		}
		providers = append(providers, orphanOIDCProvider{
			arn: arn, url: providerURL, region: region,
		})
	}
	slices.SortFunc(providers, func(a, b orphanOIDCProvider) int { return strings.Compare(a.arn, b.arn) })
	return providers, scanned, nil
}

func readOrphanOIDCReferences(
	ctx context.Context,
	iamClient orphanOIDCIAMAPI,
	eksClient func(string) orphanOIDCEKSAPI,
	providers []orphanOIDCProvider,
) (map[string]struct{}, []string, error) {
	regions := make([]string, 0, len(providers))
	for _, provider := range providers {
		if !slices.Contains(regions, provider.region) {
			regions = append(regions, provider.region)
		}
	}
	slices.Sort(regions)
	activeIssuers := map[string]struct{}{}
	for _, region := range regions {
		client := eksClient(region)
		if client == nil {
			return nil, nil, fmt.Errorf("EKS client for region %s is nil", region)
		}
		input := &eks.ListClustersInput{}
		for {
			out, err := client.ListClusters(ctx, input)
			if err != nil {
				return nil, nil, fmt.Errorf("list EKS clusters in %s: %w", region, err)
			}
			if out == nil {
				return nil, nil, fmt.Errorf("list EKS clusters in %s returned no output", region)
			}
			for _, name := range out.Clusters {
				cluster, err := client.DescribeCluster(ctx, &eks.DescribeClusterInput{Name: awssdk.String(name)})
				if err != nil {
					return nil, nil, fmt.Errorf("describe EKS cluster %s in %s: %w", name, region, err)
				}
				if cluster == nil || cluster.Cluster == nil || cluster.Cluster.Identity == nil ||
					cluster.Cluster.Identity.Oidc == nil || strings.TrimSpace(awssdk.ToString(cluster.Cluster.Identity.Oidc.Issuer)) == "" {
					return nil, nil, fmt.Errorf("EKS cluster %s in %s has no OIDC issuer", name, region)
				}
				activeIssuers[normalizeOIDCURL(awssdk.ToString(cluster.Cluster.Identity.Oidc.Issuer))] = struct{}{}
			}
			if out.NextToken == nil || awssdk.ToString(out.NextToken) == "" {
				break
			}
			input.NextToken = out.NextToken
		}
	}

	var policies []string
	roleInput := &iam.ListRolesInput{}
	for {
		out, err := iamClient.ListRoles(ctx, roleInput)
		if err != nil {
			return nil, nil, fmt.Errorf("list IAM role trust policies: %w", err)
		}
		if out == nil {
			return nil, nil, fmt.Errorf("list IAM roles returned no output")
		}
		for _, role := range out.Roles {
			raw := awssdk.ToString(role.AssumeRolePolicyDocument)
			if raw == "" {
				continue
			}
			decoded, err := url.QueryUnescape(raw)
			if err != nil {
				return nil, nil, fmt.Errorf("decode IAM role trust policy: %w", err)
			}
			policies = append(policies, decoded)
		}
		if !out.IsTruncated || out.Marker == nil {
			break
		}
		roleInput.Marker = out.Marker
	}
	return activeIssuers, policies, nil
}

func oidcProviderReferenced(arn string, policies []string) bool {
	for _, policy := range policies {
		if strings.Contains(policy, arn) {
			return true
		}
	}
	return false
}

func normalizeOIDCURL(raw string) string {
	return strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(raw), "https://"), "/")
}

func eksOIDCRegion(providerURL string) (string, bool) {
	const prefix = "oidc.eks."
	if !strings.HasPrefix(providerURL, prefix) {
		return "", false
	}
	rest := strings.TrimPrefix(providerURL, prefix)
	region, _, ok := strings.Cut(rest, ".")
	if !ok || region == "" || !strings.Contains(rest, "/id/") {
		return "", false
	}
	return region, true
}
