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

// InventoryClassification is how strongly a resource is attributed to CP9.
type InventoryClassification string

const (
	InventoryConfirmedCP9 InventoryClassification = "confirmed_cp9"
	InventoryProbableCP9  InventoryClassification = "probable_cp9"
	InventoryUnknown      InventoryClassification = "unknown"
)

// InventoryConfidence grades the attribution evidence.
type InventoryConfidence string

const (
	ConfidenceHigh   InventoryConfidence = "high"
	ConfidenceMedium InventoryConfidence = "medium"
	ConfidenceLow    InventoryConfidence = "low"
	ConfidenceNone   InventoryConfidence = "none"
)

// InventoryResourceState is whether a resource is live or inactive.
type InventoryResourceState string

const (
	InventoryActive         InventoryResourceState = "active"
	InventoryStaleCandidate InventoryResourceState = "stale_candidate"
)

// InventoryAction answers "what can cleanup safely do with this resource".
type InventoryAction string

const (
	ActionSupportedByAWSNuke  InventoryAction = "supported_by_aws_nuke"
	ActionBlockedByCP9Filters InventoryAction = "blocked_by_cp9_filters"
	ActionRequiresQuarantine  InventoryAction = "requires_quarantine"
	ActionNotCleanupEligible  InventoryAction = "not_cleanup_eligible"
)

// VolumeFinding is the inventory classification of one EC2 volume.
type VolumeFinding struct {
	VolumeID           string                  `json:"volume_id"`
	ARN                string                  `json:"arn"`
	Region             string                  `json:"region"`
	SizeGiB            int32                   `json:"size_gib"`
	State              string                  `json:"state"`
	Attached           bool                    `json:"attached"`
	AttachedInstanceID string                  `json:"attached_instance_id,omitempty"`
	CreatedAt          *time.Time              `json:"created_at,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"`
}

// VolumeInventorySummary aggregates findings for quick review.
type VolumeInventorySummary struct {
	Total                       int                             `json:"total"`
	TotalGiB                    int64                           `json:"total_gib"`
	ByClassification            map[InventoryClassification]int `json:"by_classification"`
	Attached                    int                             `json:"attached"`
	Detached                    int                             `json:"detached"`
	DetachedGiB                 int64                           `json:"detached_gib"`
	MissingLabel                int                             `json:"missing_label"`
	UnresolvedScopePlaceholders int                             `json:"unresolved_scope_placeholders"`
	CleanupEligible             int                             `json:"cleanup_eligible"`
}

// VolumeInventory is the account/region EC2-volume inventory report.
type VolumeInventory struct {
	Account     string                 `json:"account"`
	Region      string                 `json:"region"`
	GeneratedAt time.Time              `json:"generated_at"`
	Volumes     []VolumeFinding        `json:"volumes"`
	Summary     VolumeInventorySummary `json:"summary"`
	SummaryOnly bool                   `json:"-"`
}

// FormatText renders the inventory as a human-readable Markdown report. It
// implements the ticat TextFormatter contract structurally.
func (v *VolumeInventory) FormatText() string {
	var b strings.Builder
	fmt.Fprintf(&b, "# CP9 EC2 Volume 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, "- total-size: %d GiB, detached: %d (%d GiB)\n", v.Summary.TotalGiB, v.Summary.Detached, v.Summary.DetachedGiB)
	fmt.Fprintf(&b, "- attached(active): %d, missing-label: %d, unresolved-scope-placeholders: %d, cleanup-eligible: %d\n\n",
		v.Summary.Attached, v.Summary.MissingLabel, v.Summary.UnresolvedScopePlaceholders, v.Summary.CleanupEligible)
	if v.SummaryOnly {
		return b.String()
	}
	fmt.Fprintf(&b, "## Volumes\n\n")
	fmt.Fprintf(&b, "| volume | state | attached | class | conf | action | missing | reasons |\n")
	fmt.Fprintf(&b, "|---|---|---|---|---|---|---|---|\n")
	for i := range v.Volumes {
		f := &v.Volumes[i]
		fmt.Fprintf(&b, "| %s | %s | %t | %s | %s | %s | %s | %s |\n",
			f.VolumeID, f.State, f.Attached, f.Classification, f.Confidence, f.RecommendedAction,
			strings.Join(f.MissingLabels, ","), strings.Join(f.EvidenceReasons, ","))
	}
	return b.String()
}

// InventoryEC2API is the read-only EC2 surface the inventory collectors need.
type InventoryEC2API interface {
	DescribeVolumes(ctx context.Context, params *ec2.DescribeVolumesInput,
		optFns ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error)
	DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput,
		optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
	DescribeSecurityGroups(ctx context.Context, params *ec2.DescribeSecurityGroupsInput,
		optFns ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error)
}

// InventoryVolumes builds a read-only account-wide EC2 volume inventory for one
// region. It never mutates or deletes anything.
func InventoryVolumes(
	ctx context.Context,
	client InventoryEC2API,
	region, account string,
	now time.Time,
) (VolumeInventory, error) {
	if client == nil {
		return VolumeInventory{}, fmt.Errorf("ec2 client must not be nil")
	}
	inv := VolumeInventory{
		Account:     account,
		Region:      region,
		GeneratedAt: now.UTC(),
		Summary:     VolumeInventorySummary{ByClassification: map[InventoryClassification]int{}},
	}
	var token *string
	for {
		verbose.Logf("AWS ec2:DescribeVolumes inventory region=%s page_token=%q", region, aws.ToString(token))
		out, err := client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{NextToken: token})
		if err != nil {
			return VolumeInventory{}, fmt.Errorf("ec2 DescribeVolumes: %w", err)
		}
		for i := range out.Volumes {
			inv.Volumes = append(inv.Volumes, classifyVolume(&out.Volumes[i], region, account, now))
		}
		token = out.NextToken
		if token == nil || *token == "" {
			break
		}
	}
	slices.SortFunc(inv.Volumes, func(a, b VolumeFinding) int { return strings.Compare(a.VolumeID, b.VolumeID) })
	for i := range inv.Volumes {
		f := &inv.Volumes[i]
		inv.Summary.Total++
		inv.Summary.TotalGiB += int64(f.SizeGiB)
		inv.Summary.ByClassification[f.Classification]++
		if f.Attached {
			inv.Summary.Attached++
		} else {
			inv.Summary.Detached++
			inv.Summary.DetachedGiB += int64(f.SizeGiB)
		}
		if len(f.MissingLabels) > 0 {
			inv.Summary.MissingLabel++
		}
		if f.RecommendedAction == ActionSupportedByAWSNuke {
			inv.Summary.CleanupEligible++
		}
		if slices.Contains(f.EvidenceReasons, "unresolved_cleanup_scope_tag") {
			inv.Summary.UnresolvedScopePlaceholders++
		}
	}
	return inv, nil
}

func classifyVolume(v *ec2types.Volume, region, account string, now time.Time) VolumeFinding {
	tags := ec2TagsToMap(v.Tags)
	ttlRaw := strings.TrimSpace(tags[TagTTL])

	finding := VolumeFinding{
		VolumeID:  aws.ToString(v.VolumeId),
		ARN:       fmt.Sprintf("arn:aws:ec2:%s:%s:volume/%s", region, account, aws.ToString(v.VolumeId)),
		Region:    region,
		SizeGiB:   aws.ToInt32(v.Size),
		State:     string(v.State),
		CreatedAt: v.CreateTime,
		Tags:      tags,
	}
	if v.CreateTime != nil {
		finding.AgeDays = int(now.UTC().Sub(v.CreateTime.UTC()).Hours() / 24)
	}

	finding.Attached = string(v.State) == string(ec2types.VolumeStateInUse)
	for _, att := range v.Attachments {
		if instance := strings.TrimSpace(aws.ToString(att.InstanceId)); instance != "" {
			finding.Attached = true
			finding.AttachedInstanceID = instance
		}
	}

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

// NOTE: ec2TagsToMap is defined in tracker.go and reused here.

// classifyCP9Tags is the type-agnostic CP9 attribution from a resource's tags:
// confirmed (owner+session), probable (cp9-managed / environment=cp9 / name
// token), or unknown. It returns the classification, a confidence grade, and the
// evidence reasons. Name-only is the weakest signal and is never enough on its
// own for cleanup.
func classifyCP9Tags(tags map[string]string) (InventoryClassification, InventoryConfidence, []string) {
	owner := strings.TrimSpace(tags[TagOwner])
	session := strings.TrimSpace(tags[TagSession])
	managed := strings.TrimSpace(tags[TagManaged])
	environment := strings.TrimSpace(tags["environment"])
	name := strings.TrimSpace(tags["Name"])

	switch {
	case resolvedCP9ScopeTagValue(owner) && resolvedCP9ScopeTagValue(session):
		reasons := []string{"tagged_owner_session"}
		if unresolvedCP9ScopeTagValue(tags[TagTTL]) {
			reasons = append(reasons, "unresolved_cleanup_scope_tag")
		}
		return InventoryConfirmedCP9, ConfidenceHigh, reasons
	case managed != "" || strings.EqualFold(environment, "cp9"):
		var reasons []string
		if unresolvedCP9ScopeTagValue(owner) || unresolvedCP9ScopeTagValue(session) || unresolvedCP9ScopeTagValue(tags[TagTTL]) {
			reasons = append(reasons, "unresolved_cleanup_scope_tag")
		}
		if managed != "" {
			reasons = append(reasons, "cp9_managed_tag")
		}
		if strings.EqualFold(environment, "cp9") {
			reasons = append(reasons, "environment_cp9")
		}
		return InventoryProbableCP9, ConfidenceMedium, reasons
	case strings.Contains(strings.ToLower(name), "cp9-"):
		return InventoryProbableCP9, ConfidenceLow, []string{"name_contains_cp9_token"}
	default:
		return InventoryUnknown, ConfidenceNone, []string{"no_cp9_evidence"}
	}
}

// missingCP9Labels reports which cp9 delete-scope labels are absent. It only
// matters once a resource has some cp9 evidence; unknown resources return nil.
func missingCP9Labels(tags map[string]string, classification InventoryClassification) []string {
	if classification == InventoryUnknown {
		return nil
	}
	var missing []string
	if !resolvedCP9ScopeTagValue(tags[TagOwner]) {
		missing = append(missing, TagOwner)
	}
	if !resolvedCP9ScopeTagValue(tags[TagSession]) {
		missing = append(missing, TagSession)
	}
	if !resolvedCP9ScopeTagValue(tags[TagTTL]) {
		missing = append(missing, TagTTL)
	}
	return missing
}

func resolvedCP9ScopeTagValue(value string) bool {
	value = strings.TrimSpace(value)
	if value == "" || strings.EqualFold(value, "required") {
		return false
	}
	return !unresolvedCP9ScopeTagValue(value)
}

func unresolvedCP9ScopeTagValue(value string) bool {
	value = strings.TrimSpace(value)
	return strings.HasPrefix(value, "__CP9_") && strings.HasSuffix(value, "__")
}

func resourceState(active bool) InventoryResourceState {
	if active {
		return InventoryActive
	}
	return InventoryStaleCandidate
}

// recommendCleanupAction maps a finding to the cleanup-capability answer, shared
// across resource types. Conservative by design: active resources and unknown
// resources are never quarantined or deleted; missing TTL is a label defect, not
// expiration; only confirmed (owner+session) resources with an expired TTL are in
// scope for the fail-closed aws-nuke path.
func recommendCleanupAction(
	classification InventoryClassification,
	active bool,
	ttlRaw string,
	now time.Time,
) InventoryAction {
	if active {
		return ActionNotCleanupEligible
	}
	switch classification {
	case InventoryConfirmedCP9:
		if ttlRaw == "" {
			return ActionNotCleanupEligible
		}
		ttl, err := parseCP9TTL(ttlRaw)
		if err != nil || ttl.After(now) {
			return ActionNotCleanupEligible
		}
		return ActionSupportedByAWSNuke
	case InventoryProbableCP9:
		return ActionRequiresQuarantine
	default:
		return ActionNotCleanupEligible
	}
}
