package aws

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

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"

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

// SecurityGroupFinding is the inventory classification of one EC2 security group.
type SecurityGroupFinding struct {
	GroupID           string                  `json:"group_id"`
	GroupName         string                  `json:"group_name,omitempty"`
	ARN               string                  `json:"arn"`
	Region            string                  `json:"region"`
	VpcID             string                  `json:"vpc_id,omitempty"`
	IsDefault         bool                    `json:"is_default"`
	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"`
}

// SecurityGroupInventorySummary aggregates security-group findings.
type SecurityGroupInventorySummary struct {
	Total            int                             `json:"total"`
	ByClassification map[InventoryClassification]int `json:"by_classification"`
	Protected        int                             `json:"protected"`
	MissingLabel     int                             `json:"missing_label"`
	CleanupEligible  int                             `json:"cleanup_eligible"`
}

// SecurityGroupInventory is the account/region security-group inventory report.
type SecurityGroupInventory struct {
	Account        string                        `json:"account"`
	Region         string                        `json:"region"`
	GeneratedAt    time.Time                     `json:"generated_at"`
	SecurityGroups []SecurityGroupFinding        `json:"security_groups"`
	Summary        SecurityGroupInventorySummary `json:"summary"`
	SummaryOnly    bool                          `json:"-"`
}

// FormatText renders the security-group inventory as Markdown (TextFormatter).
func (v *SecurityGroupInventory) FormatText() string {
	var b strings.Builder
	fmt.Fprintf(&b, "# CP9 EC2 Security Group 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\n", v.Summary.Total)
	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, "- protected(default): %d, missing-label: %d, cleanup-eligible: %d\n\n",
		v.Summary.Protected, v.Summary.MissingLabel, v.Summary.CleanupEligible)
	if v.SummaryOnly {
		return b.String()
	}
	fmt.Fprintf(&b, "## Security Groups\n\n")
	fmt.Fprintf(&b, "| group | name | default | class | conf | action | missing | reasons |\n")
	fmt.Fprintf(&b, "|---|---|---|---|---|---|---|---|\n")
	for i := range v.SecurityGroups {
		f := &v.SecurityGroups[i]
		fmt.Fprintf(&b, "| %s | %s | %t | %s | %s | %s | %s | %s |\n",
			f.GroupID, f.GroupName, f.IsDefault, f.Classification, f.Confidence, f.RecommendedAction,
			strings.Join(f.MissingLabels, ","), strings.Join(f.EvidenceReasons, ","))
	}
	return b.String()
}

// InventorySecurityGroups builds a read-only account-wide security-group
// inventory for one region. The default VPC security group is always treated as
// protected (never cleanup-eligible). It never mutates or deletes anything.
func InventorySecurityGroups(
	ctx context.Context,
	client InventoryEC2API,
	region, account string,
	now time.Time,
) (SecurityGroupInventory, error) {
	if client == nil {
		return SecurityGroupInventory{}, fmt.Errorf("ec2 client must not be nil")
	}
	inv := SecurityGroupInventory{
		Account:     account,
		Region:      region,
		GeneratedAt: now.UTC(),
		Summary:     SecurityGroupInventorySummary{ByClassification: map[InventoryClassification]int{}},
	}
	var token *string
	for {
		verbose.Logf("AWS ec2:DescribeSecurityGroups inventory region=%s page_token=%q", region, aws.ToString(token))
		out, err := client.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{NextToken: token})
		if err != nil {
			return SecurityGroupInventory{}, fmt.Errorf("ec2 DescribeSecurityGroups: %w", err)
		}
		for i := range out.SecurityGroups {
			inv.SecurityGroups = append(inv.SecurityGroups, classifySecurityGroup(&out.SecurityGroups[i], region, account, now))
		}
		token = out.NextToken
		if token == nil || *token == "" {
			break
		}
	}
	slices.SortFunc(inv.SecurityGroups, func(a, b SecurityGroupFinding) int { return strings.Compare(a.GroupID, b.GroupID) })
	for i := range inv.SecurityGroups {
		f := &inv.SecurityGroups[i]
		inv.Summary.Total++
		inv.Summary.ByClassification[f.Classification]++
		if f.IsDefault {
			inv.Summary.Protected++
		}
		if len(f.MissingLabels) > 0 {
			inv.Summary.MissingLabel++
		}
		if f.RecommendedAction == ActionSupportedByAWSNuke {
			inv.Summary.CleanupEligible++
		}
	}
	return inv, nil
}

func classifySecurityGroup(sg *ec2types.SecurityGroup, region, account string, now time.Time) SecurityGroupFinding {
	tags := ec2TagsToMap(sg.Tags)
	ttlRaw := strings.TrimSpace(tags[TagTTL])

	finding := SecurityGroupFinding{
		GroupID:   aws.ToString(sg.GroupId),
		GroupName: aws.ToString(sg.GroupName),
		ARN:       fmt.Sprintf("arn:aws:ec2:%s:%s:security-group/%s", region, account, aws.ToString(sg.GroupId)),
		Region:    region,
		VpcID:     aws.ToString(sg.VpcId),
		Tags:      tags,
	}
	finding.IsDefault = aws.ToString(sg.GroupName) == "default"

	finding.Classification, finding.Confidence, finding.EvidenceReasons = classifyCP9Tags(tags)
	finding.MissingLabels = missingCP9Labels(tags, finding.Classification)
	// The default SG is VPC-managed and protected; treat it as active so it is
	// never cleanup-eligible. Non-default SGs are judged by their tags; aws-nuke
	// still enforces dependency safety at delete time.
	finding.ResourceState = resourceState(finding.IsDefault)
	finding.RecommendedAction = recommendCleanupAction(finding.Classification, finding.IsDefault, ttlRaw, now)
	return finding
}
