package server

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/tidbcloud/observability/vm-migration/internal/jsonio"
	"github.com/tidbcloud/observability/vm-migration/internal/migration"
)

// Deploy/undeploy waits, fixed per target.
const (
	deployTimeout   = time.Hour
	undeployTimeout = 2 * time.Minute
)

// errNoProvisioner marks deploy/undeploy starts while the serve process has
// no in-cluster Kubernetes config; the HTTP layer maps it to 503.
var errNoProvisioner = errors.New("kubernetes in-cluster config unavailable; deploy/undeploy disabled")

// vmselectJob deploys or removes the migration's temporary vmselects.
//
// Targets run sequentially in canonical order (migration first); the first
// failure aborts the job naming the vmselect. Both operations are
// idempotent, so a failed job is simply re-POSTed.
type vmselectJob struct {
	m           *Migration
	provisioner migration.VmselectProvisioner
	undeploy    bool
	artifact    *vmselectArtifact
}

func (j *vmselectJob) Kind() jobKind {
	if j.undeploy {
		return jobVmselectUndeploy
	}
	return jobVmselectDeploy
}

// Run resolves the effective specs — the compiled plan with the tune
// doc's vmselect section merged, read once per job — and applies them.
func (j *vmselectJob) Run(ctx context.Context) error {
	compiled, err := j.m.compiledPlan()
	if err != nil {
		return err
	}
	doc, err := migration.LoadTuneDoc(j.m.tunePath())
	if err != nil {
		return err
	}
	pair, err := migration.EffectiveVmselectSpecs(compiled, doc.Vmselect)
	if err != nil {
		return err
	}
	timeout := deployTimeout
	if j.undeploy {
		timeout = undeployTimeout
	}
	specs := pair.All()
	j.artifact = &vmselectArtifact{Kind: string(j.Kind()), Vmselects: []vmselectResult{}}
	for _, spec := range specs {
		started := time.Now()
		var action string
		if j.undeploy {
			action, err = j.provisioner.Remove(ctx, spec.Namespace, spec.Name, timeout, j.m.logs)
		} else {
			action, err = j.provisioner.Ensure(ctx, spec, timeout, j.m.logs)
		}
		if err != nil {
			return fmt.Errorf("vmselect %s: %w", spec.Name, err)
		}
		j.artifact.Vmselects = append(j.artifact.Vmselects, vmselectResult{
			Name:              spec.Name,
			Image:             spec.Image,
			Action:            action,
			StorageIDs:        spec.StorageIDs,
			ReadyAfterSeconds: time.Since(started).Seconds(),
		})
	}
	return nil
}

func (j *vmselectJob) WriteArtifact(path string, rec *jobRecord) error {
	j.artifact.ID = rec.ID
	j.artifact.StartedAt = rec.StartedAt
	j.artifact.FinishedAt = rec.FinishedAt
	return jsonio.WriteFile(path, j.artifact)
}

// vmselectArtifact is the JSON artifact written after a vmselect job
// completes.
type vmselectArtifact struct {
	ID         string           `json:"id"`
	Kind       string           `json:"kind"`
	StartedAt  string           `json:"startedAt"`
	FinishedAt string           `json:"finishedAt"`
	Vmselects  []vmselectResult `json:"vmselects"`
}

// vmselectResult reports one vmselect's outcome. Image is the effective
// spec's image (plan with the tune override); on undeploy nothing is
// applied.
type vmselectResult struct {
	Name              string  `json:"name"`
	Image             string  `json:"image"`
	Action            string  `json:"action"`
	StorageIDs        []int   `json:"storageIds"`
	ReadyAfterSeconds float64 `json:"readyAfterSeconds"`
}

// StartVmselectDeploy starts a vmselect-deploy job for both temporary
// vmselects, waiting up to deployTimeout for each rollout. Returns the job
// ID, or errNoProvisioner outside a cluster.
func (m *Migration) StartVmselectDeploy() (string, error) {
	return m.startVmselectJob(false)
}

// StartVmselectUndeploy starts a vmselect-undeploy job for both temporary
// vmselects. Returns the job ID, or errNoProvisioner outside a cluster.
func (m *Migration) StartVmselectUndeploy() (string, error) {
	return m.startVmselectJob(true)
}

func (m *Migration) startVmselectJob(undeploy bool) (string, error) {
	if m.provisioner == nil {
		return "", errNoProvisioner
	}
	// Fail the POST synchronously on a pre-restructure snapshot instead of
	// letting the job die asynchronously with empty specs.
	if _, err := m.compiledPlan(); err != nil {
		return "", err
	}
	return m.start(&vmselectJob{
		m:           m,
		provisioner: m.provisioner,
		undeploy:    undeploy,
	})
}
