package aws

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

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/eks"
	ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"

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

// InventoryEKSAPI is the strictly read-only EKS surface used by inventory.
type InventoryEKSAPI 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)
	ListTagsForResource(
		ctx context.Context,
		params *eks.ListTagsForResourceInput,
		optFns ...func(*eks.Options),
	) (*eks.ListTagsForResourceOutput, error)
	ListNodegroups(
		ctx context.Context,
		params *eks.ListNodegroupsInput,
		optFns ...func(*eks.Options),
	) (*eks.ListNodegroupsOutput, error)
}

// EKSFinding is the read-only attribution of one live EKS cluster.
type EKSFinding struct {
	ClusterName       string                  `json:"cluster_name"`
	ARN               string                  `json:"arn"`
	Region            string                  `json:"region"`
	Status            string                  `json:"status"`
	Active            bool                    `json:"active"`
	RoleARN           string                  `json:"role_arn,omitempty"`
	VpcID             string                  `json:"vpc_id,omitempty"`
	CreatedAt         *time.Time              `json:"created_at,omitempty"`
	Nodegroups        []string                `json:"nodegroups,omitempty"`
	NodegroupCount    int                     `json:"nodegroup_count"`
	Tags              map[string]string       `json:"tags,omitempty"`
	Classification    InventoryClassification `json:"classification"`
	Confidence        InventoryConfidence     `json:"confidence"`
	ResourceState     InventoryResourceState  `json:"resource_state"`
	EvidenceReasons   []string                `json:"evidence_reasons"`
	MissingLabels     []string                `json:"missing_labels,omitempty"`
	RecommendedAction InventoryAction         `json:"recommended_action"`
}

// EKSInventorySummary aggregates EKS findings for scheduled cleanup output.
type EKSInventorySummary struct {
	Total            int                             `json:"total"`
	ByClassification map[InventoryClassification]int `json:"by_classification"`
	Active           int                             `json:"active"`
	Nodegroups       int                             `json:"nodegroups"`
	MissingLabel     int                             `json:"missing_label"`
	CleanupEligible  int                             `json:"cleanup_eligible"`
}

// EKSInventory is the account/region EKS inventory report.
type EKSInventory struct {
	Account     string              `json:"account"`
	Region      string              `json:"region"`
	GeneratedAt time.Time           `json:"generated_at"`
	Clusters    []EKSFinding        `json:"clusters"`
	Summary     EKSInventorySummary `json:"summary"`
	SummaryOnly bool                `json:"-"`
}

// FormatText renders the EKS inventory as a Markdown report.
func (v *EKSInventory) FormatText() string {
	var b strings.Builder
	fmt.Fprintf(&b, "# CP9 EKS Inventory\n\n")
	fmt.Fprintf(&b, "- account: %s\n- region: %s\n- generated_at: %s\n\n",
		v.Account, v.Region, v.GeneratedAt.Format(time.RFC3339))
	fmt.Fprintf(&b, "## Summary\n\n")
	fmt.Fprintf(&b, "- total: %d, active: %d, nodegroups: %d\n", v.Summary.Total, v.Summary.Active, v.Summary.Nodegroups)
	fmt.Fprintf(&b, "- confirmed_cp9: %d, probable_cp9: %d, unknown: %d\n",
		v.Summary.ByClassification[InventoryConfirmedCP9],
		v.Summary.ByClassification[InventoryProbableCP9],
		v.Summary.ByClassification[InventoryUnknown])
	fmt.Fprintf(&b, "- missing-label: %d, cleanup-eligible: %d\n\n", v.Summary.MissingLabel, v.Summary.CleanupEligible)
	if v.SummaryOnly {
		return b.String()
	}
	fmt.Fprintf(&b, "## Clusters\n\n")
	fmt.Fprintf(&b, "| cluster | status | nodegroups | vpc | class | conf | action | missing | reasons |\n")
	fmt.Fprintf(&b, "|---|---|---:|---|---|---|---|---|---|\n")
	for i := range v.Clusters {
		finding := &v.Clusters[i]
		fmt.Fprintf(&b, "| %s | %s | %d | %s | %s | %s | %s | %s | %s |\n",
			finding.ClusterName,
			finding.Status,
			finding.NodegroupCount,
			finding.VpcID,
			finding.Classification,
			finding.Confidence,
			finding.RecommendedAction,
			strings.Join(finding.MissingLabels, ","),
			strings.Join(finding.EvidenceReasons, ","),
		)
	}
	return b.String()
}

// InventoryEKS builds a strictly read-only EKS inventory for one region.
func InventoryEKS(
	ctx context.Context,
	client InventoryEKSAPI,
	region, account string,
	now time.Time,
) (EKSInventory, error) {
	if client == nil {
		return EKSInventory{}, fmt.Errorf("EKS client must not be nil")
	}
	inv := EKSInventory{
		Account:     account,
		Region:      region,
		GeneratedAt: now.UTC(),
		Summary:     EKSInventorySummary{ByClassification: map[InventoryClassification]int{}},
	}
	clusterNames, err := inventoryEKSClusterNames(ctx, client, region)
	if err != nil {
		return EKSInventory{}, err
	}
	for _, clusterName := range clusterNames {
		finding, err := inventoryEKSCluster(ctx, client, clusterName, region, now)
		if err != nil {
			// Inventory is observational: a cluster can disappear after
			// ListClusters while a cleanup run is deleting it. Treat that
			// eventual-consistency race as an absent cluster, but keep every
			// other read failure fail-closed so the report cannot claim a false
			// account-wide result.
			if isNotFoundErr(err) {
				continue
			}
			return EKSInventory{}, err
		}
		inv.Clusters = append(inv.Clusters, finding)
	}
	slices.SortFunc(inv.Clusters, func(a, b EKSFinding) int { return strings.Compare(a.ClusterName, b.ClusterName) })
	for i := range inv.Clusters {
		finding := &inv.Clusters[i]
		inv.Summary.Total++
		inv.Summary.ByClassification[finding.Classification]++
		inv.Summary.Nodegroups += finding.NodegroupCount
		if finding.Active {
			inv.Summary.Active++
		}
		if len(finding.MissingLabels) > 0 {
			inv.Summary.MissingLabel++
		}
		if finding.RecommendedAction == ActionSupportedByAWSNuke {
			inv.Summary.CleanupEligible++
		}
	}
	return inv, nil
}

func inventoryEKSCluster(
	ctx context.Context,
	client InventoryEKSAPI,
	clusterName, region string,
	now time.Time,
) (EKSFinding, error) {
	verbose.Logf("AWS eks:DescribeCluster inventory region=%s cluster=%s", region, clusterName)
	out, err := client.DescribeCluster(ctx, &eks.DescribeClusterInput{Name: awssdk.String(clusterName)})
	if err != nil {
		return EKSFinding{}, fmt.Errorf("EKS DescribeCluster %s: %w", clusterName, err)
	}
	if out.Cluster == nil {
		return EKSFinding{}, fmt.Errorf("EKS DescribeCluster %s returned no cluster", clusterName)
	}
	cluster := out.Cluster
	arn := strings.TrimSpace(awssdk.ToString(cluster.Arn))
	verbose.Logf("AWS eks:ListTagsForResource inventory region=%s arn=%s", region, arn)
	tagOut, err := client.ListTagsForResource(ctx, &eks.ListTagsForResourceInput{ResourceArn: awssdk.String(arn)})
	if err != nil {
		return EKSFinding{}, fmt.Errorf("EKS ListTagsForResource %s: %w", arn, err)
	}
	tags := map[string]string{}
	maps.Copy(tags, tagOut.Tags)
	nodegroups, err := inventoryEKSNodegroupNames(ctx, client, clusterName, region)
	if err != nil {
		return EKSFinding{}, err
	}
	status := cluster.Status
	active := status == ekstypes.ClusterStatusActive ||
		status == ekstypes.ClusterStatusCreating ||
		status == ekstypes.ClusterStatusUpdating ||
		status == ekstypes.ClusterStatusPending
	classification, confidence, reasons := classifyCP9Tags(tags)
	finding := EKSFinding{
		ClusterName:     firstNonEmpty(awssdk.ToString(cluster.Name), clusterName),
		ARN:             arn,
		Region:          region,
		Status:          string(status),
		Active:          active,
		RoleARN:         awssdk.ToString(cluster.RoleArn),
		CreatedAt:       cluster.CreatedAt,
		Nodegroups:      nodegroups,
		NodegroupCount:  len(nodegroups),
		Tags:            tags,
		Classification:  classification,
		Confidence:      confidence,
		ResourceState:   resourceState(active),
		EvidenceReasons: reasons,
	}
	if cluster.ResourcesVpcConfig != nil {
		finding.VpcID = awssdk.ToString(cluster.ResourcesVpcConfig.VpcId)
	}
	finding.MissingLabels = missingCP9Labels(tags, classification)
	finding.RecommendedAction = recommendCleanupAction(classification, active, strings.TrimSpace(tags[TagTTL]), now)
	return finding, nil
}

func inventoryEKSClusterNames(ctx context.Context, client InventoryEKSAPI, region string) ([]string, error) {
	var names []string
	var token *string
	for {
		verbose.Logf("AWS eks:ListClusters inventory region=%s page_token=%q", region, awssdk.ToString(token))
		out, err := client.ListClusters(ctx, &eks.ListClustersInput{NextToken: token})
		if err != nil {
			return nil, fmt.Errorf("EKS ListClusters: %w", err)
		}
		names = append(names, out.Clusters...)
		token = out.NextToken
		if token == nil || awssdk.ToString(token) == "" {
			break
		}
	}
	slices.Sort(names)
	return names, nil
}

func inventoryEKSNodegroupNames(
	ctx context.Context,
	client InventoryEKSAPI,
	clusterName, region string,
) ([]string, error) {
	var names []string
	var token *string
	for {
		verbose.Logf("AWS eks:ListNodegroups inventory region=%s cluster=%s page_token=%q", region, clusterName, awssdk.ToString(token))
		out, err := client.ListNodegroups(ctx, &eks.ListNodegroupsInput{
			ClusterName: awssdk.String(clusterName),
			NextToken:   token,
		})
		if err != nil {
			return nil, fmt.Errorf("EKS ListNodegroups %s: %w", clusterName, err)
		}
		names = append(names, out.Nodegroups...)
		token = out.NextToken
		if token == nil || awssdk.ToString(token) == "" {
			break
		}
	}
	slices.Sort(names)
	return names, nil
}
