package cmd

import (
	"context"
	"fmt"
	"strings"

	"github.com/innerr/ticat/pkg/core/model"

	cpaws "github.com/tidbcloud/controlplane9/internal/aws"
	"github.com/tidbcloud/controlplane9/internal/config"
	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
	"github.com/tidbcloud/controlplane9/internal/host"
)

// refreshCredsNamespaces is the set of namespaces where the aws-creds secret is
// consumed across the dev-full / dedicated personas. cp9 mints credentials
// once and applies the same secret to each so every in-cluster AWS controller and
// SDK consumer reads the same fresh credentials.
var refreshCredsNamespaces = []string{
	"crossplane-system",
	"cp9-global",
	"cp9-premium",
	"cp9-dedicated",
}

type refreshCredsResult struct {
	Secret     string   `json:"secret"`
	RoleARN    string   `json:"role_arn"`
	Expiry     string   `json:"expiry,omitempty"`
	Namespaces []string `json:"namespaces"`
	Restarted  []string `json:"restarted,omitempty"`
}

type refreshCredsNativeRuntime struct {
	Required        bool   `json:"required"`
	Identity        string `json:"identity"`
	InstanceProfile string `json:"instance_profile,omitempty"`
	RoleARN         string `json:"role_arn,omitempty"`
}

func (r *refreshCredsNativeRuntime) String() string {
	return fmt.Sprintf(
		"AWS credential refresh is not required for remote EC2 runtime identity %q (instance profile %q, role %q)",
		r.Identity,
		r.InstanceProfile,
		r.RoleARN,
	)
}

func refreshCredsNativeRuntimeResult(selected *host.Session) (*refreshCredsNativeRuntime, bool, error) {
	if selected == nil || selected.Mode != config.HostModeRemoteEC2K3d {
		return nil, false, nil
	}
	if selected.Remote == nil ||
		strings.TrimSpace(selected.Remote.InstanceProfileName) == "" ||
		strings.TrimSpace(selected.Remote.RuntimeRoleARN) == "" {
		return nil, true, fmt.Errorf(
			"legacy or incomplete remote session %q has no persisted runtime instance profile identity; "+
				"it cannot be migrated safely in place: destroy it and run cp9 up again",
			selected.ClusterName,
		)
	}
	result := &refreshCredsNativeRuntime{
		Required:        false,
		Identity:        "ec2-instance-profile",
		InstanceProfile: strings.TrimSpace(selected.Remote.InstanceProfileName),
		RoleARN:         strings.TrimSpace(selected.Remote.RuntimeRoleARN),
	}
	return result, true, nil
}

func (r *refreshCredsResult) String() string {
	b := &strings.Builder{}
	fmt.Fprintf(b, "refreshed aws-creds secret %q in %d namespace(s) (assumed %s",
		r.Secret, len(r.Namespaces), r.RoleARN)
	if r.Expiry != "" {
		fmt.Fprintf(b, ", expires %s", r.Expiry)
	}
	fmt.Fprintf(b, ")\n  namespaces: %s", strings.Join(r.Namespaces, ", "))
	if len(r.Restarted) > 0 {
		fmt.Fprintf(b, "\n  restarted: %s", strings.Join(r.Restarted, ", "))
	}
	return b.String()
}

// refreshCredsSelectedHost resolves the session host and then loads the
// persisted host state so remote-ec2-k3d sessions carry their Remote metadata.
// Without the load, host.RunKubectl cannot reach the remote host and
// refresh-creds fails with "remote selected host metadata is not available".
func refreshCredsSelectedHost(session *config.SessionConfig) *host.Session {
	selected := host.ResolveSessionConfig(session)
	if loaded, err := host.Load(selected.StateDir); err == nil {
		selected = loaded
	}
	return selected
}

// refreshCredsCmd implements `cp9 refresh-creds`. Local sessions re-mint the
// env's AWS session credentials from the configured source profile + assume-role
// and patch the aws-creds secret in every consuming namespace. Remote EC2 k3d
// sessions use the node's rotating Instance Profile credentials, so the command
// reports that refresh is not required and performs no Kubernetes writes.
//
// The env mints these creds once at `cp9 up` and refreshes them via a Tilt
// local_resource that stops when Tilt stops; this makes the refresh a first-class,
// on-demand operation an agent loop (or human) can call periodically. With
// restart=true it also rolls the deployments that mount the secret so SDK
// consumers that cache the mounted credentials file pick up the new creds
// immediately (Crossplane providers re-read the secret on reconcile and do not
// need a restart).
func refreshCredsCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		ctx := context.Background()

		cfgPath := args.GetRaw("config")
		restart := strings.EqualFold(strings.TrimSpace(args.GetRaw("restart")), "true")

		session, err := config.LoadSession(cfgPath)
		if err != nil {
			return err
		}
		selected := refreshCredsSelectedHost(session)
		nativeResult, remoteRuntime, err := refreshCredsNativeRuntimeResult(selected)
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryConfig, "cp9 refresh-creds: %v", err)
		}
		if remoteRuntime {
			return model.Output(cc, env, nativeResult)
		}
		state, err := cpaws.LoadSessionState(selected.StateDir)
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryConfig,
				"cp9 refresh-creds: load session state (run cp9 up first?): %v", err)
		}
		roleARN := strings.TrimSpace(state.RoleARN)
		if roleARN == "" {
			return cp9errors.New(cp9errors.CategoryConfig,
				"cp9 refresh-creds: session has no assume-role ARN to refresh from")
		}

		material, err := cpaws.ExportAssumedRoleCredentialMaterialWithMetadata(
			ctx, state.SourceProfile, state.Profile, state.Region, roleARN, state.Owner, state.SessionTags, nil,
		)
		if err != nil {
			return cp9errors.Newf(cp9errors.CategoryResource, "cp9 refresh-creds: mint credentials: %v", err)
		}

		secretName := strings.TrimSpace(state.CredentialsSecret)
		if secretName == "" {
			secretName = "aws-creds"
		}

		result := &refreshCredsResult{Secret: secretName, RoleARN: roleARN, Expiry: material.Expiration}
		for _, ns := range refreshCredsNamespaces {
			manifest := refreshCredsSecretManifest(ns, secretName, material.Text)
			if out, err := host.RunKubectl(ctx, selected, []byte(manifest), "apply", "-f", "-"); err != nil {
				return cp9errors.Newf(cp9errors.CategoryResource,
					"cp9 refresh-creds: apply %s in namespace %s: %v\n%s", secretName, ns, err, string(out))
			}
			result.Namespaces = append(result.Namespaces, ns)
		}

		if restart {
			result.Restarted = restartAWSCredsConsumers(ctx, selected, refreshCredsNamespaces, secretName)
		}

		return model.Output(cc, env, result)
	}
}

func refreshCredsSecretManifest(namespace, secretName, credsINI string) string {
	return fmt.Sprintf(`apiVersion: v1
kind: Secret
metadata:
  name: %s
  namespace: %s
type: Opaque
stringData:
  creds: |
%s
`, secretName, namespace, refreshCredsIndent(credsINI, 4))
}

func refreshCredsIndent(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")
}

// restartAWSCredsConsumers best-effort rolls every Deployment in the given
// namespaces that mounts the aws-creds secret as a volume. Errors are ignored so
// a partial cluster (missing namespace, RBAC) does not fail the refresh.
func restartAWSCredsConsumers(
	ctx context.Context, selected *host.Session, namespaces []string, secretName string,
) []string {
	// Emit the name of every Deployment that mounts secretName as a secret volume.
	tmpl := `{{range .items}}{{$n := .metadata.name}}` +
		`{{range .spec.template.spec.volumes}}{{if .secret}}` +
		`{{if eq .secret.secretName "` + secretName + `"}}{{$n}}{{"\n"}}{{end}}` +
		`{{end}}{{end}}{{end}}`
	var restarted []string
	for _, ns := range namespaces {
		out, err := host.RunKubectl(ctx, selected, nil, "-n", ns, "get", "deploy", "-o", "go-template="+tmpl)
		if err != nil {
			continue
		}
		for name := range strings.FieldsSeq(string(out)) {
			if _, err := host.RunKubectl(
				ctx, selected, nil, "-n", ns, "rollout", "restart", "deployment/"+name,
			); err == nil {
				restarted = append(restarted, ns+"/"+name)
			}
		}
	}
	return restarted
}
