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"
)

// InstanceFinding is the inventory classification of one EC2 instance.
type InstanceFinding struct {
	InstanceID        string                  `json:"instance_id"`
	ARN               string                  `json:"arn"`
	Region            string                  `json:"region"`
	InstanceType      string                  `json:"instance_type,omitempty"`
	State             string                  `json:"state"`
	Running           bool                    `json:"running"`
	LaunchTime        *time.Time              `json:"launch_time,omitempty"`
	AgeDays           int                     `json:"age_days,omitempty"`
	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"`
}

// InstanceInventorySummary aggregates instance findings.
type InstanceInventorySummary struct {
	Total            int                             `json:"total"`
	ByClassification map[InventoryClassification]int `json:"by_classification"`
	Running          int                             `json:"running"`
	MissingLabel     int                             `json:"missing_label"`
	CleanupEligible  int                             `json:"cleanup_eligible"`
}

// InstanceInventory is the account/region EC2-instance inventory report.
type InstanceInventory struct {
	Account     string                   `json:"account"`
	Region      string                   `json:"region"`
	GeneratedAt time.Time                `json:"generated_at"`
	Instances   []InstanceFinding        `json:"instances"`
	Summary     InstanceInventorySummary `json:"summary"`
	SummaryOnly bool                     `json:"-"`
}

// FormatText renders the instance inventory as Markdown (TextFormatter contract).
func (v *InstanceInventory) FormatText() string {
	var b strings.Builder
	fmt.Fprintf(&b, "# CP9 EC2 Instance 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, "- running(active): %d, missing-label: %d, cleanup-eligible: %d\n\n",
		v.Summary.Running, v.Summary.MissingLabel, v.Summary.CleanupEligible)
	if v.SummaryOnly {
		return b.String()
	}
	fmt.Fprintf(&b, "## Instances\n\n")
	fmt.Fprintf(&b, "| instance | state | running | class | conf | action | missing | reasons |\n")
	fmt.Fprintf(&b, "|---|---|---|---|---|---|---|---|\n")
	for i := range v.Instances {
		f := &v.Instances[i]
		fmt.Fprintf(&b, "| %s | %s | %t | %s | %s | %s | %s | %s |\n",
			f.InstanceID, f.State, f.Running, f.Classification, f.Confidence, f.RecommendedAction,
			strings.Join(f.MissingLabels, ","), strings.Join(f.EvidenceReasons, ","))
	}
	return b.String()
}

// InventoryInstances builds a read-only account-wide EC2 instance inventory for
// one region. Terminated instances are skipped (they are already gone). It never
// mutates or deletes anything.
func InventoryInstances(
	ctx context.Context,
	client InventoryEC2API,
	region, account string,
	now time.Time,
) (InstanceInventory, error) {
	if client == nil {
		return InstanceInventory{}, fmt.Errorf("ec2 client must not be nil")
	}
	inv := InstanceInventory{
		Account:     account,
		Region:      region,
		GeneratedAt: now.UTC(),
		Summary:     InstanceInventorySummary{ByClassification: map[InventoryClassification]int{}},
	}
	var token *string
	for {
		verbose.Logf("AWS ec2:DescribeInstances inventory region=%s page_token=%q", region, aws.ToString(token))
		out, err := client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{NextToken: token})
		if err != nil {
			return InstanceInventory{}, fmt.Errorf("ec2 DescribeInstances: %w", err)
		}
		for r := range out.Reservations {
			for i := range out.Reservations[r].Instances {
				inst := &out.Reservations[r].Instances[i]
				if inst.State != nil && inst.State.Name == ec2types.InstanceStateNameTerminated {
					continue
				}
				inv.Instances = append(inv.Instances, classifyInstance(inst, region, account, now))
			}
		}
		token = out.NextToken
		if token == nil || *token == "" {
			break
		}
	}
	slices.SortFunc(inv.Instances, func(a, b InstanceFinding) int { return strings.Compare(a.InstanceID, b.InstanceID) })
	for i := range inv.Instances {
		f := &inv.Instances[i]
		inv.Summary.Total++
		inv.Summary.ByClassification[f.Classification]++
		if f.Running {
			inv.Summary.Running++
		}
		if len(f.MissingLabels) > 0 {
			inv.Summary.MissingLabel++
		}
		if f.RecommendedAction == ActionSupportedByAWSNuke {
			inv.Summary.CleanupEligible++
		}
	}
	return inv, nil
}

func classifyInstance(inst *ec2types.Instance, region, account string, now time.Time) InstanceFinding {
	tags := ec2TagsToMap(inst.Tags)
	ttlRaw := strings.TrimSpace(tags[TagTTL])

	finding := InstanceFinding{
		InstanceID:   aws.ToString(inst.InstanceId),
		ARN:          fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, account, aws.ToString(inst.InstanceId)),
		Region:       region,
		InstanceType: string(inst.InstanceType),
		LaunchTime:   inst.LaunchTime,
		Tags:         tags,
	}
	if inst.State != nil {
		finding.State = string(inst.State.Name)
		finding.Running = inst.State.Name == ec2types.InstanceStateNameRunning ||
			inst.State.Name == ec2types.InstanceStateNamePending
	}
	if inst.LaunchTime != nil {
		finding.AgeDays = int(now.UTC().Sub(inst.LaunchTime.UTC()).Hours() / 24)
	}

	finding.Classification, finding.Confidence, finding.EvidenceReasons = classifyCP9Tags(tags)
	finding.MissingLabels = missingCP9Labels(tags, finding.Classification)
	finding.ResourceState = resourceState(finding.Running)
	finding.RecommendedAction = recommendCleanupAction(finding.Classification, finding.Running, ttlRaw, now)
	return finding
}
