package aws

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"slices"
	"strings"

	"gopkg.in/yaml.v3"
)

var defaultAWSNukeResourceTypes = []string{
	"ACMCertificate",
	"ACMPCACertificateAuthority",
	"ACMPCACertificateAuthorityState",
	"AMPScraper",
	"AMPWorkspace",
	"APIGatewayAPIKey",
	"APIGatewayDomainName",
	"APIGatewayRestAPI",
	"APIGatewayUsagePlan",
	"APIGatewayV2API",
	"APIGatewayV2VpcLink",
	"APIGatewayVpcLink",
	"AccessAnalyzer",
	"AmplifyApp",
	"AppRegistryApplication",
	"AppSyncAPI",
	"AppSyncGraphqlAPI",
	"ApplicationAutoScalingScalableTarget",
	"AutoScalingGroup",
	"BedrockAgentCoreAgentRuntime",
	"BedrockAgentCoreBrowser",
	"BedrockAgentCoreCodeInterpreter",
	"BedrockAgentCoreGateway",
	"BedrockAgentCoreMemory",
	"BedrockAgentCoreOauth2CredentialProvider",
	"BedrockAgentCoreWorkloadIdentity",
	"BedrockCustomModel",
	"BedrockEvaluationJob",
	"BedrockGuardrail",
	"BedrockModelCustomizationJob",
	"BedrockProvisionedModelThroughput",
	"BudgetsBudget",
	"CloudFormationStack",
	"CloudFrontDistribution",
	"CloudTrailTrail",
	"CloudWatchAlarm",
	"CloudWatchLogsLogGroup",
	"CodeArtifactDomain",
	"CodeArtifactRepository",
	"CodeBuildProject",
	"CodeStarNotificationRule",
	"CognitoUserPool",
	"DSQLCluster",
	"DocDBCluster",
	"DocDBEventSubscription",
	"DocDBInstance",
	"DocDBParameterGroup",
	"DocDBSnapshot",
	"DocDBSubnetGroup",
	"DynamoDBTable",
	"EC2Address",
	"EC2DHCPOption",
	"EC2EgressOnlyInternetGateway",
	"EC2Host",
	"EC2Image",
	"EC2Instance",
	"EC2InstanceConnectEndpoint",
	"EC2InternetGateway",
	"EC2KeyPair",
	"EC2LaunchTemplate",
	"EC2NATGateway",
	"EC2NetworkACL",
	"EC2NetworkInterface",
	"EC2RouteTable",
	"EC2SecurityGroup",
	"EC2Snapshot",
	"EC2Subnet",
	"EC2TGW",
	"EC2TGWAttachment",
	"EC2TGWConnectPeer",
	"EC2VPC",
	"EC2VPCEndpoint",
	"EC2VPCEndpointConnection",
	"EC2VPCEndpointServiceConfiguration",
	"EC2VPNConnection",
	"EC2VerifiedAccessEndpoint",
	"EC2VerifiedAccessGroup",
	"EC2VerifiedAccessInstance",
	"EC2VerifiedAccessTrustProvider",
	"EC2Volume",
	"ECRPublicRepository",
	"ECRRepository",
	"ECSCapacityProvider",
	"ECSCluster",
	"ECSService",
	"ECSTask",
	"EFSFileSystem",
	"EKSCluster",
	"EKSFargateProfile",
	"EKSNodegroup",
	"ELB",
	"ELBv2",
	"ELBv2ListenerRule",
	"ELBv2TargetGroup",
	"ESDomain",
	"ElasticacheCacheCluster",
	"ElasticacheSubnetGroup",
	"FSxBackup",
	"FSxFileSystem",
	"FirehoseDeliveryStream",
	"IAMInstanceProfile",
	"IAMInstanceProfileRole",
	"IAMOpenIDConnectProvider",
	"IAMPolicy",
	"IAMRole",
	"IAMRolePolicy",
	"IAMRolePolicyAttachment",
	"IAMUser",
	"IAMUserHTTPSGitCredential",
	"IoTSiteWiseAsset",
	"IoTSiteWiseAssetModel",
	"IoTTwinMakerComponentType",
	"IoTTwinMakerWorkspace",
	"KMSAlias",
	"KMSKey",
	"LambdaEventSourceMapping",
	"LambdaFunction",
	"LightsailInstance",
	"MSKCluster",
	"MemoryDBACL",
	"MemoryDBCluster",
	"MemoryDBParameterGroup",
	"MemoryDBSubnetGroup",
	"MemoryDBUser",
	"NeptuneCluster",
	"NeptuneGraph",
	"NeptuneInstance",
	"NetworkManagerConnectPeer",
	"NetworkManagerCoreNetwork",
	"NetworkManagerGlobalNetwork",
	"NetworkManagerNetworkAttachment",
	"OSCollection",
	"OSDomain",
	"OSPipeline",
	"PinpointApp",
	"PipesPipe",
	"RDSClusterSnapshot",
	"RDSDBCluster",
	"RDSDBClusterParameterGroup",
	"RDSDBParameterGroup",
	"RDSDBSubnetGroup",
	"RDSEventSubscription",
	"RDSInstance",
	"RDSOptionGroup",
	"RDSProxy",
	"RDSSnapshot",
	"RedshiftCluster",
	"RedshiftSnapshot",
	"RedshiftSnapshotSchedule",
	"ResourceExplorer2Index",
	"ResourceExplorer2View",
	"ResourceGroupGroup",
	"Route53HealthCheck",
	"Route53HostedZone",
	"Route53Profile",
	"S3Bucket",
	"SFNStateMachine",
	"SNSTopic",
	"SQSQueue",
	"SSMDocument",
	"SSMParameter",
	"SageMakerDomain",
	"SageMakerUserProfiles",
	"SecretsManagerSecret",
	"ServiceDiscoveryNamespace",
	"ShieldProtection",
	"ShieldProtectionGroup",
	"TextractAdapter",
	"TransferServer",
	"TransferServerUser",
}

// awsNukeTagFilterProperties is derived from aws-nuke v3.65.0 native resource
// implementations that expose generic tag:<key> properties. libnuke treats
// filters as non-matching when a property cannot be read, so cp9 must not apply
// tag filters to unproven resource types.
var awsNukeTagFilterProperties = func() map[string][2]string {
	filters := make(map[string][2]string, len(defaultAWSNukeResourceTypes))
	for _, resourceType := range defaultAWSNukeResourceTypes {
		if resourceType == "LambdaEventSourceMapping" {
			continue
		}
		filters[resourceType] = [2]string{"tag:" + TagOwner, "tag:" + TagSession}
	}
	filters["IAMRolePolicy"] = [2]string{"tag:role:" + TagOwner, "tag:role:" + TagSession}
	filters["IAMRolePolicyAttachment"] = [2]string{"tag:role:" + TagOwner, "tag:role:" + TagSession}
	return filters
}()

// NukeConfigRequest is the fail-closed cp9 boundary used to generate an
// aws-nuke config. Filters in aws-nuke exclude resources, so cp9 writes
// inverted owner/session filters: any resource that does not match both tags is
// filtered out before deletion.
type NukeConfigRequest struct {
	AccountID     string
	Scope         Scope
	ResourceTypes []string
}

type awsNukeConfig struct {
	Blocklist                []string                    `yaml:"blocklist"`
	BypassAliasCheckAccounts []string                    `yaml:"bypass-alias-check-accounts"`
	NoBlocklistTermsDefault  bool                        `yaml:"no-blocklist-terms-default"`
	Regions                  []string                    `yaml:"regions"`
	Accounts                 map[string]awsNukeAccount   `yaml:"accounts"`
	ResourceTypes            *awsNukeResourceTypeSection `yaml:"resource-types,omitempty"`
}

type awsNukeAccount struct {
	Filters       map[string][]awsNukeFilter `yaml:"filters"`
	ResourceTypes awsNukeResourceTypeSection `yaml:"resource-types"`
	Settings      map[string]map[string]any  `yaml:"settings,omitempty"`
	Presets       []string                   `yaml:"presets,omitempty"`
}

type awsNukeResourceTypeSection struct {
	Includes []string `yaml:"includes,omitempty"`
	Excludes []string `yaml:"excludes,omitempty"`
}

type awsNukeFilter struct {
	Property string `yaml:"property,omitempty"`
	Type     string `yaml:"type,omitempty"`
	Value    string `yaml:"value"`
	Invert   bool   `yaml:"invert,omitempty"`
}

// GenerateNukeConfig renders the deterministic aws-nuke YAML config for one
// cp9 owner/session/account scope.
func GenerateNukeConfig(req *NukeConfigRequest) ([]byte, error) {
	cfg, err := buildAWSNukeConfig(req)
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	enc := yaml.NewEncoder(&buf)
	enc.SetIndent(2)
	if err := enc.Encode(cfg); err != nil {
		return nil, fmt.Errorf("marshal aws-nuke config: %w", err)
	}
	if err := enc.Close(); err != nil {
		return nil, fmt.Errorf("close aws-nuke config encoder: %w", err)
	}
	return buf.Bytes(), nil
}

// WriteNukeConfig writes a generated aws-nuke config under the session
// destroy directory.
func WriteNukeConfig(path string, req *NukeConfigRequest) error {
	data, err := GenerateNukeConfig(req)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create aws-nuke config dir: %w", err)
	}
	if err := os.WriteFile(path, data, 0o600); err != nil {
		return fmt.Errorf("write aws-nuke config: %w", err)
	}
	return nil
}

func buildAWSNukeConfig(req *NukeConfigRequest) (*awsNukeConfig, error) {
	if req == nil {
		return nil, fmt.Errorf("aws-nuke config request is required")
	}
	accountID := strings.TrimSpace(req.AccountID)
	if accountID == "" {
		return nil, fmt.Errorf("aws-nuke account id is required")
	}
	scope := req.Scope.Normalize()
	if scope.Owner == "" || scope.SessionID == "" {
		return nil, fmt.Errorf("aws-nuke scope requires both owner and session")
	}
	if err := scope.Validate(); err != nil {
		return nil, fmt.Errorf("aws-nuke scope: %w", err)
	}

	regions := append([]string{"global"}, scope.Regions...)
	regions = normalizeRegions(regions)
	resourceTypes := append([]string(nil), req.ResourceTypes...)
	if len(resourceTypes) == 0 {
		resourceTypes = append([]string(nil), defaultAWSNukeResourceTypes...)
	}
	resourceTypes = normalizeAWSNukeResourceTypes(resourceTypes)
	if len(resourceTypes) == 0 {
		return nil, fmt.Errorf("aws-nuke resource types must not be empty")
	}
	filters, err := awsNukeFailClosedFilters(resourceTypes, scope)
	if err != nil {
		return nil, err
	}

	return &awsNukeConfig{
		Blocklist:                []string{safeAWSNukeBlocklistAccount(accountID)},
		BypassAliasCheckAccounts: []string{accountID},
		NoBlocklistTermsDefault:  true,
		Regions:                  regions,
		Accounts: map[string]awsNukeAccount{
			accountID: {
				Filters:       filters,
				ResourceTypes: awsNukeResourceTypeSection{Includes: resourceTypes},
			},
		},
	}, nil
}

func awsNukeFailClosedFilters(
	resourceTypes []string,
	scope Scope,
) (map[string][]awsNukeFilter, error) {
	filters := make(map[string][]awsNukeFilter, len(resourceTypes))
	for _, resourceType := range resourceTypes {
		if resourceType == "LambdaEventSourceMapping" {
			filters[resourceType] = []awsNukeFilter{awsNukeLambdaEventSourceMappingFilter(scope.SessionID)}
			continue
		}
		properties, ok := awsNukeTagFilterProperties[resourceType]
		if !ok {
			return nil, fmt.Errorf("unsupported aws-nuke resource type %q: no verified fail-closed tag filters", resourceType)
		}
		filters[resourceType] = []awsNukeFilter{
			awsNukeOwnerTagFilter(properties[0], scope.Owner),
			{Property: properties[1], Value: scope.SessionID, Invert: true},
		}
	}
	return filters, nil
}

func awsNukeLambdaEventSourceMappingFilter(session string) awsNukeFilter {
	return awsNukeFilter{
		Property: "FunctionArn",
		Type:     "regex",
		Value:    `^arn:[^:]+:lambda:[^:]+:[0-9]{12}:function:cp9-` + regexp.QuoteMeta(session) + `-starter-cluster-events$`,
		Invert:   true,
	}
}

func awsNukeOwnerTagFilter(property, owner string) awsNukeFilter {
	values := ownerTagFilterValues(owner)
	escaped := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		escaped = append(escaped, regexp.QuoteMeta(value))
	}
	if len(escaped) == 0 {
		return awsNukeFilter{Property: property, Value: owner, Invert: true}
	}
	return awsNukeFilter{
		Property: property,
		Type:     "regex",
		Value:    "^(?:" + strings.Join(escaped, "|") + ")$",
		Invert:   true,
	}
}

func safeAWSNukeBlocklistAccount(accountID string) string {
	if accountID == "000000000000" {
		return "999999999999"
	}
	return "000000000000"
}

func normalizeAWSNukeResourceTypes(resourceTypes []string) []string {
	set := make(map[string]struct{}, len(resourceTypes))
	var normalized []string
	for _, resourceType := range resourceTypes {
		resourceType = strings.TrimSpace(resourceType)
		if resourceType == "" {
			continue
		}
		if _, ok := set[resourceType]; ok {
			continue
		}
		set[resourceType] = struct{}{}
		normalized = append(normalized, resourceType)
	}
	slices.Sort(normalized)
	return normalized
}

// NukeRunRequest describes one aws-nuke invocation.
type NukeRunRequest struct {
	ConfigPath       string
	Profile          string
	Region           string
	AccountID        string
	AssumeRoleARN    string
	SessionName      string
	AssumeRoleNoTags bool
	AccessKeyID      string
	SecretAccessKey  string
	SessionToken     string
	Delete           bool
	MaxWaitRetries   int
}

// NukeRunner runs aws-nuke.
type NukeRunner struct {
	Binary string
}

// Run executes aws-nuke.
func (r NukeRunner) Run(ctx context.Context, req *NukeRunRequest) ([]byte, error) {
	name, args, err := NukeCommand(req, r.Binary)
	if err != nil {
		return nil, err
	}
	cmd := exec.CommandContext(ctx, name, args...)
	cmd.Env = awsNukeCommandEnv(os.Environ(), req)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return out, fmt.Errorf("aws-nuke %s failed: %w\n%s", awsNukeRunMode(req.Delete), err, strings.TrimSpace(string(out)))
	}
	return out, nil
}

// NukeListingDegradedTypes reports the resource types whose *listing* failed in
// an aws-nuke run, and whether that was the run's only category of error.
//
// aws-nuke exits non-zero when it cannot list a resource type even if every
// delete it attempted succeeded. The common cause is an account-wide state cp9
// does not control: an ACM-PCA certificate authority sitting in DELETED for its
// restoration window makes that type unlistable for days, for every session in
// the account. Callers already treat the survivor count as the verdict, so they
// use this to report a degraded listing in one line instead of echoing the whole
// aws-nuke output on every pass.
//
// Returns listingOnly=false when any error line is something other than a listing
// failure, when there are no error lines at all, and when no resource type could be
// named: in each case cp9 cannot confirm the known-benign condition, and an
// unexplained non-zero exit must stay loud rather than be filed as a degradation.
func NukeListingDegradedTypes(out []byte) (types []string, listingOnly bool) {
	seen := map[string]struct{}{}
	errorLines := 0
	for line := range strings.SplitSeq(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") {
		if !strings.Contains(line, "level=error") {
			continue
		}
		errorLines++
		if !strings.Contains(line, `msg="listing failed`) {
			return nil, false
		}
		if resourceType := awsNukeLogResourceType(line); resourceType != "" {
			seen[resourceType] = struct{}{}
		}
	}
	// A listing failure we cannot attribute to a type is not a report, so treat it
	// as unexplained and let the caller stay loud.
	if errorLines == 0 || len(seen) == 0 {
		return nil, false
	}
	types = make([]string, 0, len(seen))
	for resourceType := range seen {
		types = append(types, resourceType)
	}
	slices.Sort(types)
	return types, true
}

// awsNukeLogResourceType reads the trailing unquoted logrus
// "resource_type=ACMPCACertificateAuthority" field out of one aws-nuke log line.
func awsNukeLogResourceType(line string) string {
	const key = " resource_type="
	// Leading space required: without it this also matches the tail of a longer
	// field name such as parent_resource_type=.
	idx := strings.LastIndex(line, key)
	if idx < 0 {
		return ""
	}
	value := line[idx+len(key):]
	if end := strings.IndexAny(value, " \t"); end >= 0 {
		value = value[:end]
	}
	return strings.TrimSpace(strings.Trim(value, `"`))
}

// NukeCommand returns the executable and args for one aws-nuke run.
func NukeCommand(req *NukeRunRequest, binary string) (string, []string, error) {
	if req == nil {
		return "", nil, fmt.Errorf("aws-nuke run request is required")
	}
	if strings.TrimSpace(req.ConfigPath) == "" {
		return "", nil, fmt.Errorf("aws-nuke config path is required")
	}
	if strings.TrimSpace(req.Profile) == "" && strings.TrimSpace(req.AccessKeyID) == "" {
		return "", nil, fmt.Errorf("aws-nuke profile or static credentials are required")
	}
	if strings.TrimSpace(req.Profile) == "" && strings.TrimSpace(req.AccessKeyID) != "" &&
		strings.TrimSpace(req.SecretAccessKey) == "" {
		return "", nil, fmt.Errorf("aws-nuke static credentials require a secret access key")
	}
	if strings.TrimSpace(req.Region) == "" {
		return "", nil, fmt.Errorf("aws-nuke default region is required")
	}
	if strings.TrimSpace(req.AccountID) == "" {
		return "", nil, fmt.Errorf("aws-nuke account id is required")
	}
	binary = strings.TrimSpace(firstNonEmptyString(binary, os.Getenv("CP9_AWS_NUKE_BIN")))
	name := binary
	if name == "" {
		found, err := exec.LookPath("aws-nuke")
		if err != nil {
			return "", nil, fmt.Errorf("aws-nuke binary not found; install aws-nuke or set CP9_AWS_NUKE_BIN")
		}
		name = found
	}

	baseArgs := []string{
		"run",
		"--config", req.ConfigPath,
		"--default-region", req.Region,
		"--no-alias-check",
		"--no-prompt",
		"--prompt-delay", "3",
		"--quiet",
		"--max-wait-retries", fmt.Sprintf("%d", maxAWSNukeWaitRetries(req.MaxWaitRetries)),
	}
	args := make([]string, 0, len(baseArgs)+8)
	args = append(args, baseArgs...)
	if profile := strings.TrimSpace(req.Profile); profile != "" {
		args = append(args, "--profile", profile)
	}
	if roleARN := strings.TrimSpace(req.AssumeRoleARN); roleARN != "" {
		args = append(args, "--assume-role-arn", roleARN)
	}
	if sessionName := strings.TrimSpace(req.SessionName); sessionName != "" {
		args = append(args, "--assume-role-session-name", sessionName)
	}
	if req.Delete {
		args = append(args, "--no-dry-run")
	}
	return name, args, nil
}

func awsNukeCommandEnv(base []string, req *NukeRunRequest) []string {
	if req == nil || strings.TrimSpace(req.AccessKeyID) == "" {
		return append([]string(nil), base...)
	}
	env := make([]string, 0, len(base)+3)
	for _, entry := range base {
		key, _, ok := strings.Cut(entry, "=")
		if ok && (key == "AWS_PROFILE" || key == "AWS_DEFAULT_PROFILE") {
			continue
		}
		env = append(env, entry)
	}
	return append(env, awsNukeCredentialEnv(req)...)
}

func awsNukeCredentialEnv(req *NukeRunRequest) []string {
	if req == nil || strings.TrimSpace(req.AccessKeyID) == "" {
		return nil
	}
	env := []string{
		"AWS_ACCESS_KEY_ID=" + strings.TrimSpace(req.AccessKeyID),
		"AWS_SECRET_ACCESS_KEY=" + strings.TrimSpace(req.SecretAccessKey),
	}
	if token := strings.TrimSpace(req.SessionToken); token != "" {
		env = append(env, "AWS_SESSION_TOKEN="+token)
	}
	return env
}

func maxAWSNukeWaitRetries(value int) int {
	if value > 0 {
		return value
	}
	return 10
}

func awsNukeRunMode(deleteMode bool) string {
	if deleteMode {
		return "delete"
	}
	return "dry-run"
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}
