package migration

import (
	"errors"
	"fmt"
	"io/fs"
	"os"

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

// TuneFile is the per-migration runtime knob override document, stored next
// to the plan. One section per knob family; each is read at its own
// consumption point — vmctl at every shard boundary, vmselect at deploy. An
// absent file means "no overrides".
const TuneFile = "tune.json"

// EffectiveTune returns the vmctl knobs the next shard runs with: the plan
// pair with the tune doc's vmctl section overriding.
//
// A present file must decode strictly and pass validation; anything else is
// an error the caller fails the shard on — a bad tune must stop the
// migration, not silently degrade it.
func EffectiveTune(path string, rateLimit, concurrency *int) (schema.VmctlTune, error) {
	doc, err := LoadTuneDoc(path)
	if err != nil {
		return schema.VmctlTune{}, err
	}
	return doc.Vmctl.MergedOver(rateLimit, concurrency), nil
}

// LoadTuneDoc reads the tune document at path. A missing file is an empty
// doc. A present file decodes strictly and must validate.
func LoadTuneDoc(path string) (schema.TuneDoc, error) {
	if path == "" {
		return schema.TuneDoc{}, nil
	}
	var doc schema.TuneDoc
	if err := jsonio.ReadFileStrict(path, &doc); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return schema.TuneDoc{}, nil
		}
		return schema.TuneDoc{}, fmt.Errorf("%s: %w", path, err)
	}
	if err := validateTuneDoc(&doc); err != nil {
		return schema.TuneDoc{}, fmt.Errorf("%s: %w", path, err)
	}
	return doc, nil
}

func validateTuneDoc(doc *schema.TuneDoc) error {
	if err := doc.Vmctl.Validate(); err != nil {
		return fmt.Errorf("vmctl: %w", err)
	}
	if err := doc.Vmselect.Validate(); err != nil {
		return fmt.Errorf("vmselect: %w", err)
	}
	return nil
}

// UpdateTuneDoc loads the tune document, applies mutate, and writes the
// result back atomically. An all-empty result removes the document. serve
// is single-instance (instance lock + ReadWriteOnce volume), so the
// read-modify-write has no concurrent writer.
func UpdateTuneDoc(path string, mutate func(doc *schema.TuneDoc)) error {
	doc, err := LoadTuneDoc(path)
	if err != nil {
		return err
	}
	mutate(&doc)
	if doc.IsEmpty() {
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			return err
		}
		return nil
	}
	return status.DumpJSONAtomic(path, &doc)
}

// EffectiveVmselectSpecs merges the tune doc's vmselect section over the
// compiled plan in schema space, then converts: per-target non-nil fields
// override, the rest inherit. Consumed by the deploy job at its start.
func EffectiveVmselectSpecs(doc *schema.CompiledDoc, tune schema.VmselectTune) (VmselectSpecPair, error) {
	effective := *doc
	effective.Vmselects = schema.CompiledVmselects{
		Image:     doc.Vmselects.Image,
		Migration: applyTuneTarget(doc.Vmselects.Migration, tune.Migration),
		Candidate: applyTuneTarget(doc.Vmselects.Candidate, tune.Candidate),
	}
	pair, err := VmselectSpecs(&effective)
	if err != nil {
		return VmselectSpecPair{}, err
	}
	// The image is shared in the plan but per-target in a tune override.
	if tune.Migration != nil && tune.Migration.Image != nil {
		pair.Migration.Image = *tune.Migration.Image
	}
	if tune.Candidate != nil && tune.Candidate.Image != nil {
		pair.Candidate.Image = *tune.Candidate.Image
	}
	return pair, nil
}

// applyTuneTarget merges one target's tune overrides in schema space; nil
// fields inherit the compiled plan's value.
func applyTuneTarget(t schema.CompiledVmselectTarget, o *schema.TuneTarget) schema.CompiledVmselectTarget {
	if o == nil {
		return t
	}
	if o.Replicas != nil {
		t.Replicas = o.Replicas
	}
	if o.Resources != nil {
		if o.Resources.Requests.CPU != nil {
			t.Resources.Requests.CPU = o.Resources.Requests.CPU
		}
		if o.Resources.Requests.Memory != nil {
			t.Resources.Requests.Memory = o.Resources.Requests.Memory
		}
		if o.Resources.Limits.CPU != nil {
			t.Resources.Limits.CPU = o.Resources.Limits.CPU
		}
		if o.Resources.Limits.Memory != nil {
			t.Resources.Limits.Memory = o.Resources.Limits.Memory
		}
	}
	return t
}

// PlanKnobs returns the migrate action's knobs from the plan.
//
// Prepared plans render every migrate action from one config, so the first
// one speaks for all.
func PlanKnobs(p *schema.MigrationLoopPlan) (rateLimit, concurrency *int) {
	for i := range p.Tenants {
		for _, c := range p.Tenants[i].Cmds {
			if c.Action != nil && c.Action.Kind == schema.ActionMigrate {
				return c.Action.RateLimitBytes, c.Action.Concurrency
			}
		}
	}
	return nil, nil
}
