package migration

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

	"github.com/prometheus/common/model"

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

// defaultStepInterval is the shard size applied when the plan window carries
// no step interval.
const defaultStepInterval = "6h"

// ManifestOptions carries manifest generation inputs.
type ManifestOptions struct {
	Region         string
	Vmcluster      string
	TenantsFile    string
	SrcVmselectURL string
	DstVminsertURL string
	Start          string
	End            string
	StepInterval   string
	Output         string
}

// ManifestOutput separates the manifest payload from operator progress.
type ManifestOutput struct {
	Result   io.Writer
	Progress io.Writer
}

func (o ManifestOutput) normalized() ManifestOutput {
	if o.Result == nil {
		o.Result = io.Discard
	}
	if o.Progress == nil {
		o.Progress = io.Discard
	}
	return o
}

// Validate parses and cross-checks the args, returning the parsed step interval
// and coverage window.
//
// This method validates parseable-string invariants (RFC3339 timestamps,
// Prometheus duration) and the end-after-start cross-check.
func (o ManifestOptions) Validate() (step time.Duration, start, end time.Time, err error) {
	step, err = parseDuration(o.effectiveStepInterval())
	if err != nil {
		return 0, time.Time{}, time.Time{}, err
	}
	start, err = status.ParseTime(o.Start)
	if err != nil {
		return 0, time.Time{}, time.Time{}, fmt.Errorf("--start: %w", err)
	}
	end, err = status.ParseTime(o.End)
	if err != nil {
		return 0, time.Time{}, time.Time{}, fmt.Errorf("--end: %w", err)
	}
	if !end.After(start) {
		return 0, time.Time{}, time.Time{}, errors.New("--end must be later than --start")
	}
	return step, start, end, nil
}

// effectiveStepInterval returns StepInterval with defaultStepInterval
// substituted when empty.
func (o ManifestOptions) effectiveStepInterval() string {
	if o.StepInterval == "" {
		return defaultStepInterval
	}
	return o.StepInterval
}

// ComputeManifest validates opts and returns the manifest document.
//
// Nothing is written.
func ComputeManifest(opts ManifestOptions) (*schema.Manifest, error) {
	step, start, end, err := opts.Validate()
	if err != nil {
		return nil, err
	}
	tenantList, err := tenants.ReadFile(opts.TenantsFile)
	if err != nil {
		return nil, err
	}
	manifest := &schema.Manifest{
		Region:       opts.Region,
		Vmcluster:    opts.Vmcluster,
		Tenants:      []schema.ManifestTenant{},
		StepInterval: opts.effectiveStepInterval(),
	}
	for _, tenant := range tenantList {
		manifest.Tenants = append(manifest.Tenants, buildTimedTenant(tenant, opts.SrcVmselectURL, opts.DstVminsertURL, start, end, step))
	}
	return manifest, nil
}

// BuildManifest writes the computed manifest to opts.Output, or to Result
// when Output is empty. File output also initializes the matching
// <stem>-status.json next to it. Progress receives human-readable status
// messages.
func BuildManifest(ctx context.Context, opts ManifestOptions, output ManifestOutput) error {
	output = output.normalized()
	manifest, err := ComputeManifest(opts)
	if err != nil {
		return err
	}

	if opts.Output == "" {
		payload, err := jsonio.MarshalIndent(manifest)
		if err != nil {
			return err
		}
		_, err = output.Result.Write(payload)
		return err
	}
	if err := jsonio.WriteFile(opts.Output, manifest); err != nil {
		return err
	}
	printer.Printf(output.Progress, "wrote %s", opts.Output)

	statusPath := status.PathForManifest(opts.Output)
	if _, err := os.Stat(statusPath); err == nil {
		return nil
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}
	doc := status.BuildInitialStatus(manifest, opts.Output)
	if err := status.WriteJSONAtomic(statusPath, doc); err != nil {
		return err
	}
	printer.Printf(output.Progress, "wrote %s (total=%d pending=%d)",
		statusPath, doc.Summary.Total, doc.Summary.Pending)
	return nil
}

func buildTimedTenant(tenant, sourceURL, destinationURL string, start, end time.Time, step time.Duration) schema.ManifestTenant {
	shards := []schema.ManifestShard{}
	cur := end
	shardIndex := 1
	for cur.After(start) {
		prev := cur.Add(-step)
		if prev.Before(start) {
			prev = start
		}
		shards = append(shards, schema.ManifestShard{
			ShardID:   string(schema.NewShardID(schema.TenantID(tenant), shardIndex)),
			TimeStart: status.FormatTime(prev),
			TimeEnd:   status.FormatTime(cur),
		})
		cur = prev
		shardIndex++
	}
	return schema.ManifestTenant{
		Tenant:          tenant,
		SourceAddr:      vmquery.RenderURL(sourceURL, tenant),
		DestinationAddr: vmquery.RenderURL(destinationURL, tenant),
		Shards:          shards,
	}
}

// parseDuration wraps model.ParseDuration to enforce a positive interval.
//
// Prometheus's parser supports y/w/d/h/m/s/ms and combinations.
// Negative durations are rejected — a step interval must advance forward.
func parseDuration(value string) (time.Duration, error) {
	if value == "" {
		return 0, errors.New("invalid step interval \"\"; expected a duration like 1d, 6h, or 30m")
	}
	d, err := model.ParseDuration(value)
	if err != nil {
		return 0, fmt.Errorf("invalid step interval %q: %w", value, err)
	}
	if d <= 0 {
		return 0, fmt.Errorf("invalid step interval %q; must be positive", value)
	}
	return time.Duration(d), nil
}
