package aws

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"

	awssdk "github.com/aws/aws-sdk-go-v2/aws"

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

const (
	defaultCredentialsSecret = "aws-creds"
	defaultProviderConfig    = "default"
)

// InjectStep implements the cluster.Step interface for the cred_wire lifecycle phase.
// It validates AWS credentials, records the session for resource tracking, and
// projects an initial AWS secret/config marker into the selected cluster.
type InjectStep struct {
	Profile          string
	SourceProfile    string
	Region           string
	SessionID        string
	StateDir         string
	Selected         *host.Session
	AccountAllowlist []string
	OwnerAllowlist   map[string][]string
	STSClient        STSAPI // nil = load from config at runtime
	CommandRunner    func(cmd *exec.Cmd) ([]byte, error)
}

// Name returns the step identifier.
func (s *InjectStep) Name() cluster.StepName {
	return cluster.StepCredWire
}

// Run validates AWS access, writes the initial AWS state file, and applies the
// initial AWS credential secret needed by downstream C2 consumers.
func (s *InjectStep) Run(ctx context.Context) error {
	stsClient := s.STSClient
	if stsClient == nil {
		cfg, err := LoadAWSConfig(ctx, s.Profile, s.Region)
		if err != nil {
			return fmt.Errorf("aws inject: %w", err)
		}
		stsClient = newSTSClient(&cfg)
	}

	result, err := ValidateAccess(ctx, stsClient, s.Profile, s.Region)
	if err != nil {
		return fmt.Errorf("cred wire: %w", err)
	}
	if !result.Valid {
		return fmt.Errorf("cred wire: credentials invalid for profile=%s region=%s", s.Profile, s.Region)
	}
	if len(s.AccountAllowlist) > 0 && !slices.Contains(s.AccountAllowlist, result.Account) {
		return fmt.Errorf("cred wire: AWS account %q is not in clouds.aws.account_allowlist", result.Account)
	}
	owner, err := ResolveOwnerIdentity(result, s.OwnerAllowlist)
	if err != nil {
		return fmt.Errorf("cred wire: %w", err)
	}

	creds, err := s.loadCredentialsMaterial(ctx)
	if err != nil {
		return fmt.Errorf("cred wire: %w", err)
	}

	state := &SessionState{
		SessionID:          s.SessionID,
		Owner:              owner,
		Profile:            s.Profile,
		SourceProfile:      effectiveSourceProfile(s.Profile, s.SourceProfile),
		Region:             s.Region,
		Account:            result.Account,
		CredentialsSecret:  defaultCredentialsSecret,
		ProviderConfigName: defaultProviderConfig,
		CredentialExpiry:   creds.Expiration,
	}
	if err := SaveSessionState(s.StateDir, state); err != nil {
		return fmt.Errorf("cred wire: save state: %w", err)
	}

	if s.Selected != nil {
		if err := s.applyCredentialSecret(ctx, creds.Text); err != nil {
			return fmt.Errorf("cred wire: apply secret: %w", err)
		}
		if err := s.applyProviderConfigMarker(ctx); err != nil {
			return fmt.Errorf("cred wire: apply provider config marker: %w", err)
		}
	}

	return nil
}

type exportedCredentialProcess struct {
	AccessKeyID     string `json:"AccessKeyId"`
	SecretAccessKey string `json:"SecretAccessKey"`
	SessionToken    string `json:"SessionToken"`
	Expiration      string `json:"Expiration,omitempty"`
}

type assumedRoleCredentialOutput struct {
	Credentials exportedCredentialProcess `json:"Credentials"`
}

type SelectedCredentialMaterial struct {
	Text       string
	Expiration string
}

func (s *InjectStep) loadCredentialsMaterial(ctx context.Context) (*SelectedCredentialMaterial, error) {
	return LoadSelectedCredentialMaterialWithMetadata(ctx, s.Profile, s.SourceProfile, s.Region, s.runCommand)
}

func LoadSelectedCredentialMaterial(
	ctx context.Context,
	profile, sourceProfile, region string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (string, error) {
	material, err := LoadSelectedCredentialMaterialWithMetadata(ctx, profile, sourceProfile, region, commandRunner)
	if err != nil {
		return "", err
	}
	return material.Text, nil
}

func LoadSelectedCredentialMaterialWithMetadata(
	ctx context.Context,
	profile, sourceProfile, region string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (*SelectedCredentialMaterial, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return nil, fmt.Errorf("cannot determine home directory: %w", err)
	}
	selectedProfileName := strings.TrimSpace(profile)
	if selectedProfileName == "" {
		selectedProfileName = "default"
	}
	sourceProfileName := effectiveSourceProfile(profile, sourceProfile)
	if sourceProfileName == "" {
		sourceProfileName = "default"
	}

	credFile := filepath.Join(home, ".aws", "credentials")
	if data, err := os.ReadFile(credFile); err == nil && len(data) > 0 {
		if selected, ok := selectSharedCredentialsProfile(string(data), sourceProfileName, selectedProfileName, region); ok {
			return &SelectedCredentialMaterial{Text: selected}, nil
		}
	}

	if exported, err := exportProfileCredentials(ctx, sourceProfileName, commandRunner); err == nil {
		return &SelectedCredentialMaterial{
			Text: formatSelectedCredentialSecret(
				selectedProfileName,
				region,
				exported.AccessKeyID,
				exported.SecretAccessKey,
				exported.SessionToken,
			),
			Expiration: strings.TrimSpace(exported.Expiration),
		}, nil
	}

	accessKey := strings.TrimSpace(os.Getenv("AWS_ACCESS_KEY_ID"))
	secretKey := strings.TrimSpace(os.Getenv("AWS_SECRET_ACCESS_KEY"))
	if accessKey == "" || secretKey == "" {
		return nil, fmt.Errorf(
			"no AWS credential material available for profile %q; expected ~/.aws/credentials, "+
				"`aws configure export-credentials --profile %s --format process`, or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY",
			sourceProfileName, sourceProfileName,
		)
	}

	return &SelectedCredentialMaterial{
		Text: formatSelectedCredentialSecret(selectedProfileName, region, accessKey, secretKey, strings.TrimSpace(os.Getenv("AWS_SESSION_TOKEN"))),
	}, nil
}

func ExportSelectedCredentialMaterialWithMetadata(
	ctx context.Context,
	profile, region string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (*SelectedCredentialMaterial, error) {
	selectedProfileName := strings.TrimSpace(profile)
	if selectedProfileName == "" {
		selectedProfileName = "default"
	}
	exported, err := exportProfileCredentials(ctx, selectedProfileName, commandRunner)
	if err != nil {
		return nil, err
	}
	return &SelectedCredentialMaterial{
		Text: formatSelectedCredentialSecret(
			selectedProfileName,
			region,
			exported.AccessKeyID,
			exported.SecretAccessKey,
			exported.SessionToken,
		),
		Expiration: strings.TrimSpace(exported.Expiration),
	}, nil
}

func ExportAssumedRoleCredentialMaterialWithMetadata(
	ctx context.Context,
	sourceProfile, selectedProfileName, region, roleARN, owner string,
	sessionTags map[string]string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (*SelectedCredentialMaterial, error) {
	selectedProfileName = strings.TrimSpace(selectedProfileName)
	if selectedProfileName == "" {
		selectedProfileName = "default"
	}
	sourceProfile = strings.TrimSpace(sourceProfile)
	if sourceProfile == "" {
		sourceProfile = selectedProfileName
	}
	roleARN = strings.TrimSpace(roleARN)
	if roleARN == "" {
		return nil, fmt.Errorf("assumed-role credential material requires role ARN")
	}

	exported, err := exportProfileCredentials(ctx, sourceProfile, commandRunner)
	if err != nil {
		return nil, err
	}
	assumed, err := exportTaggedAssumedRoleCredentials(
		ctx,
		exported,
		region,
		roleARN,
		owner,
		sessionTags,
		commandRunner,
	)
	if err != nil {
		return nil, err
	}
	creds := assumed.Credentials
	if strings.TrimSpace(creds.AccessKeyID) == "" || strings.TrimSpace(creds.SecretAccessKey) == "" {
		return nil, fmt.Errorf("assumed-role credentials for %s are incomplete", roleARN)
	}
	return &SelectedCredentialMaterial{
		Text: formatSelectedCredentialSecret(
			selectedProfileName,
			region,
			creds.AccessKeyID,
			creds.SecretAccessKey,
			creds.SessionToken,
		),
		Expiration: strings.TrimSpace(creds.Expiration),
	}, nil
}

func exportTaggedAssumedRoleCredentials(
	ctx context.Context,
	source *exportedCredentialProcess,
	region string,
	roleARN string,
	owner string,
	sessionTags map[string]string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (*assumedRoleCredentialOutput, error) {
	if source == nil {
		return nil, errors.New("source credential material is nil")
	}
	args := []string{
		"sts", "assume-role",
		"--role-arn", roleARN,
		"--role-session-name", cleanupProbeSessionName(owner),
		"--duration-seconds", "3600",
		"--output", "json",
	}
	tags := cleanupProbeTags(sessionTags)
	if len(tags) > 0 {
		args = append(args, "--tags")
		for _, tag := range tags {
			args = append(args, fmt.Sprintf("Key=%s,Value=%s", awssdk.ToString(tag.Key), awssdk.ToString(tag.Value)))
		}
		args = append(args, "--transitive-tag-keys", TagOwner, TagSession)
	}
	cmd := exec.CommandContext(ctx, "aws", args...)
	cmd.Env = append(os.Environ(),
		"AWS_ACCESS_KEY_ID="+source.AccessKeyID,
		"AWS_SECRET_ACCESS_KEY="+source.SecretAccessKey,
		"AWS_SESSION_TOKEN="+source.SessionToken,
		"AWS_REGION="+region,
		"AWS_DEFAULT_REGION="+region,
	)
	var output []byte
	var err error
	if commandRunner != nil {
		output, err = commandRunner(cmd)
	} else {
		output, err = verbose.RunCommand(cmd)
	}
	if err != nil {
		return nil, fmt.Errorf("assume role %s for selected credentials: %w", roleARN, err)
	}
	var assumed assumedRoleCredentialOutput
	if err := json.Unmarshal(output, &assumed); err != nil {
		return nil, fmt.Errorf("parse assumed-role credentials: %w", err)
	}
	return &assumed, nil
}

func formatSharedCredentials(profileName, region, accessKey, secretKey, sessionToken string) string {
	var b strings.Builder
	fmt.Fprintf(&b, "[%s]\n", profileName)
	fmt.Fprintf(&b, "aws_access_key_id = %s\n", accessKey)
	fmt.Fprintf(&b, "aws_secret_access_key = %s\n", secretKey)
	if sessionToken != "" {
		fmt.Fprintf(&b, "aws_session_token = %s\n", sessionToken)
	}
	if strings.TrimSpace(region) != "" {
		fmt.Fprintf(&b, "region = %s\n", region)
	}
	return b.String()
}

func formatSelectedCredentialSecret(profileName, region, accessKey, secretKey, sessionToken string) string {
	selectedProfile := strings.TrimSpace(profileName)
	if selectedProfile == "" {
		selectedProfile = "default"
	}
	secretRegion := strings.TrimSpace(region)

	var b strings.Builder
	b.WriteString(formatSharedCredentials("default", secretRegion, accessKey, secretKey, sessionToken))
	if selectedProfile != "default" {
		b.WriteString("\n")
		b.WriteString(formatSharedCredentials(selectedProfile, secretRegion, accessKey, secretKey, sessionToken))
	}
	return b.String()
}

func selectSharedCredentialsProfile(credentialsText, sourceProfileName, selectedProfileName, region string) (string, bool) {
	sourceProfileName = strings.TrimSpace(sourceProfileName)
	if sourceProfileName == "" {
		sourceProfileName = "default"
	}
	selectedProfileName = strings.TrimSpace(selectedProfileName)
	if selectedProfileName == "" {
		selectedProfileName = "default"
	}
	targetHeader := "[" + sourceProfileName + "]"
	var (
		inProfile       bool
		accessKeyID     string
		secretAccessKey string
		sessionToken    string
		profileRegion   string
	)
	scanner := bufio.NewScanner(strings.NewReader(credentialsText))
	for scanner.Scan() {
		line := scanner.Text()
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
			if inProfile {
				break
			}
			if trimmed == targetHeader {
				inProfile = true
			}
			continue
		}
		if !inProfile {
			continue
		}
		parts := strings.SplitN(trimmed, "=", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(parts[0]))
		value := strings.TrimSpace(parts[1])
		switch key {
		case "aws_access_key_id":
			accessKeyID = value
		case "aws_secret_access_key":
			secretAccessKey = value
		case "aws_session_token":
			sessionToken = value
		case "region":
			profileRegion = value
		}
	}
	if !inProfile || accessKeyID == "" || secretAccessKey == "" {
		return "", false
	}
	if strings.TrimSpace(profileRegion) == "" {
		profileRegion = strings.TrimSpace(region)
	}
	return formatSelectedCredentialSecret(selectedProfileName, profileRegion, accessKeyID, secretAccessKey, sessionToken), true
}

func exportProfileCredentials(
	ctx context.Context,
	profile string,
	commandRunner func(cmd *exec.Cmd) ([]byte, error),
) (*exportedCredentialProcess, error) {
	cmd := exec.CommandContext(ctx, "aws", "configure", "export-credentials", "--profile", profile, "--format", "process")
	var output []byte
	var err error
	if commandRunner != nil {
		output, err = commandRunner(cmd)
	} else {
		output, err = verbose.RunCommand(cmd)
	}
	if err != nil {
		return nil, err
	}
	var exported exportedCredentialProcess
	if err := json.Unmarshal(output, &exported); err != nil {
		return nil, fmt.Errorf("parse exported credentials: %w", err)
	}
	if exported.AccessKeyID == "" || exported.SecretAccessKey == "" {
		return nil, fmt.Errorf("exported credentials for profile %q are incomplete", profile)
	}
	return &exported, nil
}

func (s *InjectStep) applyCredentialSecret(ctx context.Context, credentials string) error {
	if s.Selected == nil {
		return nil
	}
	manifest := fmt.Sprintf(`apiVersion: v1
kind: Namespace
metadata:
  name: crossplane-system
---
apiVersion: v1
kind: Secret
metadata:
  name: %s
  namespace: crossplane-system
type: Opaque
stringData:
  creds: |
%s
`, defaultCredentialsSecret, indentBlock(credentials, 4))
	return s.applyManifest(ctx, manifest)
}

func (s *InjectStep) applyProviderConfigMarker(ctx context.Context) error {
	if s.Selected == nil {
		return nil
	}
	manifest := fmt.Sprintf(`apiVersion: v1
kind: ConfigMap
metadata:
  name: cp9-providerconfig
  namespace: crossplane-system
data:
  providerConfigName: %s
  apiVersionHint: aws.upbound.io/v1beta1
  kindHint: ProviderConfig
`, defaultProviderConfig)
	return s.applyManifest(ctx, manifest)
}

func (s *InjectStep) applyManifest(ctx context.Context, manifest string) error {
	output, err := host.RunKubectl(ctx, s.Selected, []byte(manifest), "apply", "-f", "-")
	if err != nil {
		return fmt.Errorf("%w\n%s", err, string(output))
	}
	return nil
}

func (s *InjectStep) runCommand(cmd *exec.Cmd) ([]byte, error) {
	if s.CommandRunner != nil {
		return s.CommandRunner(cmd)
	}
	return verbose.RunCommand(cmd)
}

func indentBlock(text string, spaces int) string {
	pad := strings.Repeat(" ", spaces)
	lines := strings.Split(strings.TrimRight(text, "\n"), "\n")
	for i, line := range lines {
		lines[i] = pad + line
	}
	return strings.Join(lines, "\n")
}
