package cmd

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/tidbcloud/clinic-cli/internal/timeexpr"
)

func diagnoseCommandHandler(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	command := newDiagnoseCommand(staticRuntime(ctx), stdout)
	if err := executeCobraCommand(command, args); err != nil {
		return normalizeFamilyCommandError(err, "diagnose")
	}
	return nil
}

func newDiagnoseCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts diagnoseOptions
	command := newCommand(commandMetadata{
		Use:   "diagnose <seed>",
		Short: "Correlate evidence from a free-text seed.",
		Long: `Correlate evidence from a free-text seed.

Behavior:
  This MVP correlates evidence only. It does not infer root cause.
  Output includes the profile timezone plus local and UTC query windows.
  json emits the full diagnose.v2 evidence bundle.
  Expansion controls: --no-expand, --max-seed-expand, --max-related-logs.

Time formats:
  now, now-<duration>, datetime with optional timezone, or epoch in s/ms/ns.`,
		Example: `  clinic diagnose "Request range exceeds bound" --since 24h --output json
  clinic diagnose "Request range exceeds bound" --cluster-id cluster-42 --start 1712123000 --end 1712123600`,
	}, stdout)
	command.Args = func(_ *cobra.Command, args []string) error {
		switch len(args) {
		case 0:
			return errors.New("missing required seed")
		case 1:
			if strings.HasPrefix(args[0], "-") || strings.TrimSpace(args[0]) == "" {
				return errors.New("missing required seed")
			}
			return nil
		default:
			return rejectUnexpectedArgs(args[1:])
		}
	}
	command.Flags().StringVar(&opts.ClusterID, "cluster-id", "", "Target cluster scope.")
	command.Flags().StringVar(&opts.Start, "start", "", "Explicit time range start.")
	command.Flags().StringVar(&opts.End, "end", "", "Explicit time range end.")
	command.Flags().StringVar(&opts.Since, "since", "", "Relative time window such as 1h or 24h.")
	command.Flags().StringVar(&opts.Around, "around", "", "Center time for a window search.")
	command.Flags().StringVar(&opts.Window, "window", "", "Window width for around-based searches.")
	command.Flags().IntVar(&opts.Limit, "limit", 100, "Maximum entries to fetch.")
	command.Flags().BoolVar(&opts.NoExpand, "no-expand", false, "Skip related-log expansion.")
	command.Flags().IntVar(&opts.MaxSeedExpand, "max-seed-expand", diagnoseDefaultMaxSeedExpand, "Maximum seed matches eligible for related-log expansion.")
	command.Flags().IntVar(&opts.MaxRelatedLogs, "max-related-logs", diagnoseDefaultMaxRelatedLogs, "Maximum related logs to keep. Zero keeps none.")
	command.Flags().StringVarP(&opts.Output, "output", "o", "table", "Output format. Supported: table, json. Default: table.")
	command.RunE = func(command *cobra.Command, args []string) error {
		opts.Seed = args[0]
		opts.MaxRelatedLogsSet = command.Flags().Changed("max-related-logs")
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runDiagnoseSearchWithOutput(ctx, opts, time.Now(), stdout)
	}
	return command
}

func runDiagnoseSearch(ctx *RuntimeContext, opts diagnoseOptions, now time.Time) error {
	return runDiagnoseSearchWithOutput(ctx, opts, now, io.Discard)
}

func runDiagnoseSearchWithOutput(ctx *RuntimeContext, opts diagnoseOptions, now time.Time, stdout io.Writer) error {
	opts = normalizeDiagnoseExpansionOptions(opts)
	if opts.Output != "table" && opts.Output != "json" {
		return errors.New("output must be one of: table, json")
	}
	if opts.Limit <= 0 {
		return errors.New("limit must be greater than 0")
	}
	if opts.Limit > lokiMaxLimit {
		return fmt.Errorf("limit must be less than or equal to %d", lokiMaxLimit)
	}
	if opts.MaxSeedExpand <= 0 {
		return errors.New("max-seed-expand must be greater than 0")
	}
	if opts.MaxRelatedLogs < 0 {
		return errors.New("max-related-logs must be greater than or equal to 0")
	}
	bundle, err := runDiagnoseBundle(ctx, opts, now)
	if err != nil {
		if bundle.SchemaVersion != "" {
			if writeErr := writeDiagnoseSearchOutput(stdout, bundle, opts.Output); writeErr != nil {
				return errors.Join(err, writeErr)
			}
		}
		return err
	}
	return writeDiagnoseSearchOutput(stdout, bundle, opts.Output)
}

func resolveDiagnoseTimeScope(ctx *RuntimeContext, opts diagnoseOptions, now time.Time) (start, end time.Time, err error) {
	switch {
	case opts.Since != "":
		if opts.Start != "" || opts.End != "" || opts.Around != "" || opts.Window != "" {
			return time.Time{}, time.Time{}, errors.New("--since cannot be combined with --start, --end, --around, or --window")
		}

		duration, err := timeexpr.ParseDuration(opts.Since)
		if err != nil {
			return time.Time{}, time.Time{}, fmt.Errorf("invalid --since: %w", err)
		}
		return now.Add(-duration), now, nil

	case opts.Around != "" || opts.Window != "":
		if opts.Around == "" || opts.Window == "" {
			return time.Time{}, time.Time{}, errors.New("--around and --window must be used together")
		}
		if opts.Start != "" || opts.End != "" {
			return time.Time{}, time.Time{}, errors.New("--around/--window cannot be combined with --start or --end")
		}

		aroundNS, err := parseFlexibleTimeToUnixNanoWithNow(opts.Around, time.Local, now)
		if err != nil {
			return time.Time{}, time.Time{}, fmt.Errorf("invalid --around: %w", err)
		}
		window, err := timeexpr.ParseDuration(opts.Window)
		if err != nil {
			return time.Time{}, time.Time{}, fmt.Errorf("invalid --window: %w", err)
		}

		around := time.Unix(0, aroundNS)
		half := window / 2
		start = around.Add(-half)
		end = around.Add(window - half)
		return start, end, nil
	}

	resolvedStart := optionalFlagOrEnvWithContext(ctx, opts.Start, envClinicRangeStart)
	resolvedEnd := optionalFlagOrEnvWithContext(ctx, opts.End, envClinicRangeEnd)
	if resolvedStart == "" && resolvedEnd == "" {
		duration, err := timeexpr.ParseDuration("1h")
		if err != nil {
			return time.Time{}, time.Time{}, err
		}
		return now.Add(-duration), now, nil
	}
	if resolvedStart == "" {
		return time.Time{}, time.Time{}, errors.New("missing required --start")
	}
	if resolvedEnd == "" {
		return time.Time{}, time.Time{}, errors.New("missing required --end")
	}

	startNS, err := parseFlexibleTimeToUnixNanoWithNow(resolvedStart, time.Local, now)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("invalid --start: %w", err)
	}
	endNS, err := parseFlexibleTimeToUnixNanoWithNow(resolvedEnd, time.Local, now)
	if err != nil {
		return time.Time{}, time.Time{}, fmt.Errorf("invalid --end: %w", err)
	}
	if startNS >= endNS {
		return time.Time{}, time.Time{}, errors.New("start must be before end")
	}

	return time.Unix(0, startNS), time.Unix(0, endNS), nil
}

type diagnoseOptions struct {
	Seed              string
	ClusterID         string
	Start             string
	End               string
	Since             string
	Around            string
	Window            string
	Limit             int
	NoExpand          bool
	MaxSeedExpand     int
	MaxRelatedLogs    int
	MaxRelatedLogsSet bool
	Output            string
}

func normalizeDiagnoseExpansionOptions(opts diagnoseOptions) diagnoseOptions {
	if opts.MaxSeedExpand == 0 {
		opts.MaxSeedExpand = diagnoseDefaultMaxSeedExpand
	}
	if opts.MaxRelatedLogs == 0 && !opts.MaxRelatedLogsSet {
		opts.MaxRelatedLogs = diagnoseDefaultMaxRelatedLogs
	}
	return opts
}
