package cmd

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

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

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

type byocLayerStatus struct {
	State   string `json:"state"`
	Message string `json:"message,omitempty"`
}

type byocStatusLayers struct {
	Baseline         byocLayerStatus `json:"baseline"`
	Lease            byocLayerStatus `json:"lease"`
	AccountSetup     byocLayerStatus `json:"account_setup"`
	ImageMirror      byocLayerStatus `json:"image_mirror"`
	RoleChain        byocLayerStatus `json:"role_chain"`
	NetworkPreflight byocLayerStatus `json:"network_preflight"`
	RegionalRuntime  byocLayerStatus `json:"regional_runtime"`
	Catalog          byocLayerStatus `json:"catalog"`
}

type byocStatusResult struct {
	State                 string           `json:"state"`
	Session               string           `json:"session"`
	Persona               string           `json:"persona"`
	StateDir              string           `json:"state_dir"`
	Region                string           `json:"region"`
	ControlPlaneAccountID string           `json:"control_plane_account_id,omitempty"`
	CustomerAccountID     string           `json:"customer_account_id"`
	SetupBundleCommit     string           `json:"setup_bundle_commit,omitempty"`
	SetupBundleDigest     string           `json:"setup_bundle_digest,omitempty"`
	ImageClosureDigest    string           `json:"image_closure_digest,omitempty"`
	Message               string           `json:"message"`
	Activation            *activationRef   `json:"activation,omitempty"`
	Layers                byocStatusLayers `json:"layers"`
	Dataplane             byocLayerStatus  `json:"dataplane"`
	O11YRuntime           byocLayerStatus  `json:"o11y_runtime"`
}

type byocActivateResult struct {
	State              string `json:"state"`
	Session            string `json:"session"`
	CustomerAccountID  string `json:"customer_account_id"`
	SetupBundleCommit  string `json:"setup_bundle_commit"`
	SetupBundleDigest  string `json:"setup_bundle_digest"`
	ImageClosureDigest string `json:"image_closure_digest"`
	JournalPath        string `json:"journal_path"`
	ResumeCommand      string `json:"resume_command"`
	Dataplane          string `json:"dataplane"`
	O11YRuntime        string `json:"o11y_runtime"`
}

func (r *byocActivateResult) String() string {
	return fmt.Sprintf(
		"byoc %s: session %s, customer account %s; dataplane %s; o11y runtime %s",
		r.State,
		r.Session,
		r.CustomerAccountID,
		r.Dataplane,
		r.O11YRuntime,
	)
}

func (r *byocStatusResult) String() string {
	if r == nil {
		return "byoc status: <nil>"
	}
	var b strings.Builder
	fmt.Fprintf(&b, "byoc %s: %s", r.State, r.Message)
	fmt.Fprintf(&b, "\ncustomer account: %s", r.CustomerAccountID)
	fmt.Fprintf(&b, "\ndataplane: %s", r.Dataplane.State)
	fmt.Fprintf(&b, "\no11y runtime: %s", r.O11YRuntime.State)
	return b.String()
}

func byocActivateCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		result, err := runBYOCActivate(context.Background(), args.GetRaw("config"))
		if err != nil {
			return err
		}
		return model.Output(cc, env, result)
	}
}

func runBYOCActivate(ctx context.Context, cfgPath string) (*byocActivateResult, error) {
	resolved, selected, err := resolveDevFullSession(cfgPath, "cp9 byoc")
	if err != nil {
		return nil, err
	}
	input, bundle, closure, err := buildBYOCActivationInput(resolved, selected)
	if err != nil {
		return nil, err
	}
	driver, err := newBYOCAWSDriver(selected, input.RepoRoot, bundle, closure)
	if err != nil {
		return nil, err
	}
	journal, err := byoc.ActivatePrepared(ctx, input, driver, nil)
	if err != nil {
		return nil, err
	}
	return &byocActivateResult{
		State: string(journal.State), Session: input.Session, CustomerAccountID: input.CustomerAccountID,
		SetupBundleCommit: input.BundleCommit, SetupBundleDigest: input.BundleDigest, ImageClosureDigest: input.ImageClosureDigest,
		JournalPath: byoc.JournalPath(input.StateDir), ResumeCommand: byocActivationCommand(cfgPath),
		Dataplane: "not_created", O11YRuntime: "not_deployed",
	}, nil
}

func buildBYOCActivationInput(resolved *config.SessionConfig, selected *host.Session) (byoc.Input, *accountsetup.Bundle, *byoc.ImageClosure, error) {
	if resolved == nil || !config.BYOCConfigured(resolved) {
		return byoc.Input{}, nil, nil, cp9errors.New(cp9errors.CategoryConfig, "cp9 byoc activate requires a BYOC customer handoff")
	}
	if selected == nil || !selected.IsRemote() || selected.Remote == nil || strings.TrimSpace(selected.KubeconfigPath) == "" ||
		strings.TrimSpace(selected.Context) == "" {
		return byoc.Input{}, nil, nil, cp9errors.New(
			cp9errors.CategoryResource,
			"cp9 byoc activate requires a ready remote Dev full baseline; run cp9 up first",
		)
	}
	if strings.TrimSpace(selected.Remote.InstanceID) == "" || strings.TrimSpace(selected.Remote.PublicIP) == "" ||
		strings.TrimSpace(selected.Remote.RuntimeRoleARN) == "" ||
		strings.TrimSpace(selected.Remote.RemoteSecurityGroup) == "" {
		return byoc.Input{}, nil, nil, cp9errors.New(
			cp9errors.CategoryResource,
			"remote baseline is missing instance, public-IP, runtime-role, or security-group identity",
		)
	}
	awsState, err := cpaws.LoadSessionState(selected.StateDir)
	if err != nil {
		return byoc.Input{}, nil, nil, fmt.Errorf("load control-plane AWS identity: %w", err)
	}
	repoRoot, err := sessionRepoRoot(resolved)
	if err != nil {
		return byoc.Input{}, nil, nil, err
	}
	bundle, err := accountsetup.PinnedBundle()
	if err != nil {
		return byoc.Input{}, nil, nil, err
	}
	closure, err := byoc.DiscoverCoreImageClosure(repoRoot)
	if err != nil {
		return byoc.Input{}, nil, nil, err
	}
	machine, err := os.Hostname()
	if err != nil {
		return byoc.Input{}, nil, nil, fmt.Errorf("resolve activation machine identity: %w", err)
	}
	owner := strings.TrimSpace(awsState.SessionTags[cpaws.TagOwner])
	if owner == "" {
		owner = strings.TrimSpace(awsState.Owner)
	}
	ttl := resolved.Host.TTL.Duration
	if ttl <= 0 {
		ttl = 8 * time.Hour
	}
	zones := make([]byoc.AvailabilityZone, 0, len(resolved.BYOC.Customer.AvailabilityZones))
	for _, zone := range resolved.BYOC.Customer.AvailabilityZones {
		zones = append(zones, byoc.AvailabilityZone{Name: zone.Name, ID: zone.ID})
	}
	input := byoc.Input{
		Session: resolved.Name, Owner: owner, Machine: machine,
		LeaseExpiresAt: time.Now().UTC().Add(ttl).Format(time.RFC3339), StateDir: selected.StateDir,
		Region: resolved.Target.Region, ControlPlaneAccountID: awsState.Account,
		CustomerAccountID: resolved.BYOC.Customer.AccountID, TenantID: resolved.BYOC.Customer.TenantID,
		AvailabilityZones: zones, TiDBCIDR: resolved.BYOC.Customer.TiDB.CIDR,
		TiDBHostedZoneName: resolved.BYOC.Customer.TiDB.HostedZoneName, TiDBHostedZoneID: resolved.BYOC.Customer.TiDB.HostedZoneID,
		O11YHostedZoneName: resolved.BYOC.Customer.O11Y.HostedZoneName, O11YHostedZoneID: resolved.BYOC.Customer.O11Y.HostedZoneID,
		PCAARN: resolved.BYOC.Customer.TiDB.PCAARN, ImageSyncRegion: resolved.BYOC.Customer.ImageSyncRegion,
		ClinicAccountID:     resolved.BYOC.AccountSetup.ClinicAccountID,
		ReuseAccountSetup:   resolved.BYOC.AccountSetup.ReuseExistingStacks,
		ControlPlaneProfile: awsState.Profile, CustomerProfile: resolved.BYOC.AccountSetup.Profile,
		RemoteInstanceID: selected.Remote.InstanceID, RemotePublicIP: selected.Remote.PublicIP,
		RemoteRuntimeRoleARN:  selected.Remote.RuntimeRoleARN,
		RemoteSecurityGroupID: selected.Remote.RemoteSecurityGroup, RemoteSubnetID: resolved.Host.EC2.SubnetID,
		KubeconfigPath: selected.KubeconfigPath, KubeContext: selected.Context, RepoRoot: repoRoot,
		BundleCommit: bundle.UpstreamCommit, BundleDigest: bundle.Digest, ImageClosureDigest: closure.Digest,
	}
	return input, bundle, closure, nil
}

//nolint:nilnil // An unconfigured BYOC lifecycle has no BYOC sub-result.
func runBYOCDestroy(
	ctx context.Context,
	resolved *config.SessionConfig,
	selected *host.Session,
	stateDir string,
) (*byoc.DestroyResult, error) {
	if resolved == nil || !config.BYOCConfigured(resolved) {
		return nil, nil
	}
	if strings.TrimSpace(stateDir) == "" {
		return nil, cp9errors.New(cp9errors.CategoryResource, "BYOC destroy requires the session state directory")
	}
	build := func(journal *byoc.Journal) (byoc.Input, *byoc.AWSDriver, error) {
		if selected == nil {
			return byoc.Input{}, nil, cp9errors.New(
				cp9errors.CategoryResource,
				"nonterminal BYOC destroy requires the persisted remote host",
			)
		}
		input, bundle, closure, err := buildBYOCActivationInput(resolved, selected)
		if err != nil {
			return byoc.Input{}, nil, err
		}
		bindBYOCDestroyProvenance(&input, journal)
		driver, err := newBYOCAWSDriver(selected, input.RepoRoot, bundle, closure)
		if err != nil {
			return byoc.Input{}, nil, err
		}
		return input, driver, nil
	}
	request := byoc.DestroySessionRequest{
		StateDir: stateDir, Session: resolved.Name,
		CustomerAccountID: resolved.BYOC.Customer.AccountID,
		ImageSyncRegion:   resolved.BYOC.Customer.ImageSyncRegion,
		Build:             build,
	}
	request.ObserveTerminalRemoteRuntimeHost = func(
		ctx context.Context,
		journal *byoc.Journal,
	) (bool, error) {
		input, err := buildBYOCTerminalHostObservationInput(resolved, selected, stateDir, journal)
		if err != nil {
			return false, err
		}
		return byoc.ObserveRemoteRuntimeHostGoneForDestroy(ctx, input)
	}
	return byoc.DestroySession(ctx, request)
}

func buildBYOCTerminalHostObservationInput(
	resolved *config.SessionConfig,
	selected *host.Session,
	stateDir string,
	journal *byoc.Journal,
) (byoc.Input, error) {
	if resolved == nil || selected == nil || selected.Remote == nil || journal == nil {
		return byoc.Input{}, errors.New("terminal BYOC remote-host observation requires persisted lifecycle identity")
	}
	if selected.StateDir != stateDir {
		return byoc.Input{}, errors.New("terminal BYOC remote-host observation state directory does not match the selected session")
	}
	awsState, err := cpaws.LoadSessionState(stateDir)
	if err != nil {
		return byoc.Input{}, fmt.Errorf("load control-plane AWS identity for terminal BYOC host observation: %w", err)
	}
	owner := strings.TrimSpace(awsState.SessionTags[cpaws.TagOwner])
	if owner == "" {
		owner = strings.TrimSpace(awsState.Owner)
	}
	zones := make([]byoc.AvailabilityZone, 0, len(resolved.BYOC.Customer.AvailabilityZones))
	for _, zone := range resolved.BYOC.Customer.AvailabilityZones {
		zones = append(zones, byoc.AvailabilityZone{Name: zone.Name, ID: zone.ID})
	}
	input := byoc.Input{
		Session: resolved.Name, Owner: owner, StateDir: stateDir,
		Region: resolved.Target.Region, ControlPlaneAccountID: awsState.Account,
		CustomerAccountID: resolved.BYOC.Customer.AccountID, TenantID: resolved.BYOC.Customer.TenantID,
		AvailabilityZones: zones, TiDBCIDR: resolved.BYOC.Customer.TiDB.CIDR,
		TiDBHostedZoneName: resolved.BYOC.Customer.TiDB.HostedZoneName, TiDBHostedZoneID: resolved.BYOC.Customer.TiDB.HostedZoneID,
		O11YHostedZoneName: resolved.BYOC.Customer.O11Y.HostedZoneName, O11YHostedZoneID: resolved.BYOC.Customer.O11Y.HostedZoneID,
		PCAARN: resolved.BYOC.Customer.TiDB.PCAARN, ImageSyncRegion: resolved.BYOC.Customer.ImageSyncRegion,
		ClinicAccountID:     resolved.BYOC.AccountSetup.ClinicAccountID,
		ReuseAccountSetup:   resolved.BYOC.AccountSetup.ReuseExistingStacks,
		ControlPlaneProfile: awsState.Profile,
		RemoteInstanceID:    selected.Remote.InstanceID, RemoteRuntimeRoleARN: selected.Remote.RuntimeRoleARN,
		RemoteSecurityGroupID: selected.Remote.RemoteSecurityGroup, RemoteSubnetID: resolved.Host.EC2.SubnetID,
	}
	bindBYOCDestroyProvenance(&input, journal)
	fingerprint, err := byoc.Fingerprint(input)
	if err != nil {
		return byoc.Input{}, fmt.Errorf("reconstruct terminal BYOC host observation identity: %w", err)
	}
	if fingerprint != journal.Fingerprint {
		return byoc.Input{}, errors.New("terminal BYOC remote-host observation identity does not match the activation journal")
	}
	return input, nil
}

func bindBYOCDestroyProvenance(input *byoc.Input, journal *byoc.Journal) {
	if input == nil || journal == nil {
		return
	}
	if journal.Provenance != nil {
		if value := strings.TrimSpace(journal.Provenance.BundleCommit); value != "" {
			input.BundleCommit = value
		}
		if value := strings.TrimSpace(journal.Provenance.BundleDigest); value != "" {
			input.BundleDigest = value
		}
		if value := strings.TrimSpace(journal.Provenance.ImageClosureDigest); value != "" {
			input.ImageClosureDigest = value
		}
		return
	}
	if record := journal.Stages[byoc.StageAccountSetup]; record != nil {
		if value := strings.TrimSpace(record.Artifacts["bundle_commit"]); value != "" {
			input.BundleCommit = value
		}
		if value := strings.TrimSpace(record.Artifacts["bundle_digest"]); value != "" {
			input.BundleDigest = value
		}
	}
	if record := journal.Stages[byoc.StageImageMirror]; record != nil {
		if value := strings.TrimSpace(record.Artifacts["closure_digest"]); value != "" {
			input.ImageClosureDigest = value
		}
	}
}

func byocStatusCmd() model.NormalCmd {
	return func(args model.ArgVals, cc *model.Cli, env *model.Env, _ []model.ParsedCmd) error {
		syncVerbose(env)
		result, err := runBYOCStatus(context.Background(), args.GetRaw("config"))
		if err != nil {
			return err
		}
		return model.Output(cc, env, result)
	}
}

func runBYOCStatus(ctx context.Context, cfgPath string) (*byocStatusResult, error) {
	resolved, selected, err := resolveDevFullSession(cfgPath, "cp9 byoc")
	if err != nil {
		return nil, err
	}
	if !config.BYOCConfigured(resolved) {
		return nil, cp9errors.New(cp9errors.CategoryConfig,
			"cp9 byoc commands require a byoc customer handoff")
	}

	unobserved := byocLayerStatus{State: "not_observed"}
	result := &byocStatusResult{
		State:             "not_ready",
		Session:           resolved.Name,
		Persona:           config.SessionPersona(resolved),
		StateDir:          selected.StateDir,
		Region:            resolved.Target.Region,
		CustomerAccountID: resolved.BYOC.Customer.AccountID,
		Message:           "Dev full baseline is not ready; run cp9 up first",
		Activation:        &activationRef{Command: byocActivationCommand(cfgPath)},
		Layers: byocStatusLayers{
			Baseline:         byocLayerStatus{State: "not_ready", Message: "run cp9 up first"},
			Lease:            unobserved,
			AccountSetup:     unobserved,
			ImageMirror:      unobserved,
			RoleChain:        unobserved,
			NetworkPreflight: unobserved,
			RegionalRuntime:  unobserved,
			Catalog:          unobserved,
		},
		Dataplane:   byocLayerStatus{State: "not_created", Message: "created by the BYOC-A product train"},
		O11YRuntime: byocLayerStatus{State: "not_deployed", Message: "out of scope for the first BYOC slice"},
	}
	if selected.Mode == "" || strings.TrimSpace(selected.KubeconfigPath) == "" || strings.TrimSpace(selected.Context) == "" {
		return result, nil
	}

	result.Layers.Baseline = byocLayerStatus{State: "ready"}
	journal, err := byoc.LoadJournal(selected.StateDir)
	if errors.Is(err, os.ErrNotExist) {
		result.Message = "BYOC Region has not been activated"
		return result, nil
	}
	if err != nil {
		return nil, err
	}
	result.State = string(journal.State)
	result.Message = "BYOC activation journal is " + string(journal.State)
	result.Activation.Command = byocActivationCommand(cfgPath)
	if journal.State == byoc.StateDestroyed {
		destroyed := byocLayerStatus{State: string(byoc.StateDestroyed)}
		result.Message = "BYOC session-owned closure is terminally destroyed"
		result.Activation = nil
		result.Layers.Lease = destroyed
		result.Layers.AccountSetup = destroyed
		result.Layers.ImageMirror = destroyed
		result.Layers.RoleChain = destroyed
		result.Layers.NetworkPreflight = destroyed
		result.Layers.RegionalRuntime = destroyed
		result.Layers.Catalog = destroyed
		return result, nil
	}
	input, bundle, closure, err := buildBYOCActivationInput(resolved, selected)
	if err != nil {
		return nil, err
	}
	driver, err := newBYOCAWSDriver(selected, input.RepoRoot, bundle, closure)
	if err != nil {
		return nil, err
	}
	observation, err := byoc.ObserveActivation(ctx, input, driver, journal)
	if err != nil {
		return nil, err
	}
	result.ControlPlaneAccountID = input.ControlPlaneAccountID
	result.SetupBundleCommit = input.BundleCommit
	result.SetupBundleDigest = input.BundleDigest
	result.ImageClosureDigest = input.ImageClosureDigest
	setObservedBYOCLayer(&result.Layers.Lease, observation, byoc.StageLease)
	setObservedBYOCLayer(&result.Layers.AccountSetup, observation, byoc.StageAccountSetup)
	setObservedBYOCLayer(&result.Layers.ImageMirror, observation, byoc.StageImageMirror)
	setObservedBYOCLayer(&result.Layers.NetworkPreflight, observation, byoc.StageNetworkAssets)
	setObservedBYOCLayer(&result.Layers.RegionalRuntime, observation, byoc.StageRegionalRuntime)
	setObservedBYOCLayer(&result.Layers.Catalog, observation, byoc.StageCatalog)
	result.Layers.RoleChain = combineObservedBYOCLayers(
		observation.Stages[byoc.StageControlPlaneRole],
		observation.Stages[byoc.StageRoleChainProof],
	)
	if observation.Ready && journal.State == byoc.StateReady {
		result.State = "ready"
		result.Message = "BYOC activation journal and all live stages are ready"
		return result, nil
	}
	for _, stage := range byoc.ActivationStages {
		observed := observation.Stages[stage]
		if observed.State != byoc.ObservedReady {
			result.State = "needs_reconcile"
			if observed.State == byoc.ObservedFailed {
				result.State = "failed"
			}
			result.Message = fmt.Sprintf("BYOC stage %s is %s", stage, observed.State)
			if observed.Message != "" {
				result.Message += ": " + observed.Message
			}
			break
		}
	}
	return result, nil
}

func setObservedBYOCLayer(target *byocLayerStatus, observation *byoc.ActivationObservation, stage byoc.Stage) {
	observed := observation.Stages[stage]
	*target = byocLayerStatus{State: string(observed.State), Message: observed.Message}
}

func combineObservedBYOCLayers(values ...byoc.StageObservation) byocLayerStatus {
	for _, value := range values {
		if value.State != byoc.ObservedReady {
			return byocLayerStatus{State: string(value.State), Message: value.Message}
		}
	}
	return byocLayerStatus{State: string(byoc.ObservedReady)}
}

func byocActivationCommand(cfgPath string) string {
	command := "cp9 byoc activate"
	if strings.TrimSpace(cfgPath) != "" {
		command += " config=" + shellQuote(cfgPath)
	}
	return command
}
