// Package scheduler validates scheduled market data refresh configuration and
// renders dry-run evidence. It does not own the long-running worker loop; the
// runner can consume the validated shapes in a later orchestration layer.
package scheduler

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
	"gopkg.in/yaml.v3"
)

const (
	exitOK       = 0
	exitRejected = 1
	exitUsage    = 2
)

// Config is the top-level YAML shape for scheduled market data refresh.
type Config struct {
	Schedules []ScheduleConfig `yaml:"schedules"`
}

// ScheduleConfig declares one named refresh schedule.
type ScheduleConfig struct {
	Name             string         `yaml:"name"`
	Provider         string         `yaml:"provider"`
	Selector         SelectorConfig `yaml:"selector"`
	Timeframe        string         `yaml:"timeframe"`
	Cadence          string         `yaml:"cadence"`
	Timezone         string         `yaml:"timezone"`
	BackfillWindow   string         `yaml:"backfill_window"`
	ParallelismLimit int            `yaml:"parallelism_limit"`
}

// SelectorConfig is the named universe selector for a schedule.
type SelectorConfig struct {
	Kind    string   `yaml:"kind"`
	Market  string   `yaml:"market"`
	Venue   string   `yaml:"venue"`
	Name    string   `yaml:"name"`
	Symbols []string `yaml:"symbols"`
}

// ValidationOptions carries deterministic inputs for dry-run validation.
type ValidationOptions struct {
	Now          time.Time
	Capabilities map[market.Provider]market.ProviderCapability
}

// Validation reports every schedule's dry-run status.
type Validation struct {
	Items []ValidationItem
}

// HasRejected reports whether any item was rejected.
func (v Validation) HasRejected() bool {
	for _, item := range v.Items {
		if item.Status == "rejected" {
			return true
		}
	}
	return false
}

// ValidationItem is a stable text/JSONL evidence row.
type ValidationItem struct {
	Schedule         string `json:"schedule"`
	Status           string `json:"status"`
	Provider         string `json:"provider,omitempty"`
	Selector         string `json:"selector,omitempty"`
	Timeframe        string `json:"timeframe,omitempty"`
	Cadence          string `json:"cadence,omitempty"`
	Timezone         string `json:"timezone,omitempty"`
	BackfillWindow   string `json:"backfill_window,omitempty"`
	ParallelismLimit int    `json:"parallelism_limit,omitempty"`
	NextRun          string `json:"next_run,omitempty"`
	Reason           string `json:"reason,omitempty"`
}

// LoadConfig reads a YAML schedule config.
func LoadConfig(r io.Reader) (Config, error) {
	var cfg Config
	dec := yaml.NewDecoder(r)
	dec.KnownFields(true)
	if err := dec.Decode(&cfg); err != nil {
		return Config{}, fmt.Errorf("decode schedule config: %w", err)
	}
	return cfg, nil
}

// ValidateConfig validates config and computes the next dry-run window.
func ValidateConfig(cfg Config, opts ValidationOptions) Validation {
	now := opts.Now
	if now.IsZero() {
		now = time.Now()
	}
	if len(cfg.Schedules) == 0 {
		return Validation{Items: []ValidationItem{{
			Schedule: "unknown",
			Status:   "rejected",
			Reason:   "no schedules declared",
		}}}
	}

	seen := make(map[string]bool)
	items := make([]ValidationItem, 0, len(cfg.Schedules))
	for _, sched := range cfg.Schedules {
		items = append(items, validateSchedule(sched, opts.Capabilities, now, seen))
	}
	return Validation{Items: items}
}

func validateSchedule(s ScheduleConfig, caps map[market.Provider]market.ProviderCapability, now time.Time, seen map[string]bool) ValidationItem {
	name := strings.TrimSpace(s.Name)
	if name == "" {
		name = "unknown"
	}
	item := ValidationItem{
		Schedule:         name,
		Provider:         strings.TrimSpace(s.Provider),
		Selector:         strings.TrimSpace(s.Selector.Name),
		Timeframe:        strings.TrimSpace(s.Timeframe),
		Timezone:         strings.TrimSpace(s.Timezone),
		ParallelismLimit: s.ParallelismLimit,
	}

	var reasons []string
	if s.Name == "" {
		reasons = append(reasons, "name is required")
	} else if seen[s.Name] {
		reasons = append(reasons, fmt.Sprintf("duplicate schedule name %q", s.Name))
	}
	seen[s.Name] = true
	if item.Provider == "" {
		reasons = append(reasons, "provider is required")
	}
	if item.Selector == "" {
		reasons = append(reasons, "selector.name is required")
	}
	if s.Selector.Kind == "" {
		reasons = append(reasons, "selector.kind is required")
	}
	if s.Selector.Venue == "" {
		reasons = append(reasons, "selector.venue is required")
	}
	if len(s.Selector.Symbols) == 0 {
		reasons = append(reasons, "selector.symbols is required")
	}
	if s.Timeframe == "" {
		reasons = append(reasons, "timeframe is required")
	}
	if s.Cadence == "" {
		reasons = append(reasons, "cadence is required")
	}
	if s.Timezone == "" {
		reasons = append(reasons, "timezone is required")
	}
	if s.BackfillWindow == "" {
		reasons = append(reasons, "backfill_window is required")
	}
	if s.ParallelismLimit <= 0 {
		reasons = append(reasons, "parallelism_limit must be greater than zero")
	}

	cadence, err := parseWindow(s.Cadence)
	if err != nil {
		reasons = append(reasons, fmt.Sprintf("invalid cadence %q: %v", s.Cadence, err))
	} else {
		item.Cadence = cadence.String()
	}
	backfill, err := parseWindow(s.BackfillWindow)
	if err != nil {
		reasons = append(reasons, fmt.Sprintf("invalid backfill_window %q: %v", s.BackfillWindow, err))
	} else {
		item.BackfillWindow = backfill.String()
	}
	loc, err := time.LoadLocation(s.Timezone)
	if err != nil && s.Timezone != "" {
		reasons = append(reasons, fmt.Sprintf("invalid timezone %q: %v", s.Timezone, err))
	}
	tf, tfLabel, err := parseTimeframe(s.Timeframe)
	if err != nil {
		reasons = append(reasons, err.Error())
	} else {
		item.Timeframe = tfLabel
	}

	provider := market.Provider(s.Provider)
	capability, ok := caps[provider]
	if s.Provider != "" && !ok {
		reasons = append(reasons, fmt.Sprintf("unsupported provider %q", s.Provider))
	}
	if ok && err == nil {
		decision := capability.CheckBars(market.ProviderCapabilityRequest{
			Provider:  provider,
			Market:    market.Market(s.Selector.Market),
			Venue:     market.Venue(s.Selector.Venue),
			Timeframe: tf,
		})
		if !decision.Accepted {
			reasons = append(reasons, "unsupported provider/timeframe combination: "+decision.Reason)
		}
	}

	if len(reasons) > 0 {
		item.Status = "rejected"
		item.Reason = strings.Join(reasons, "; ")
		return item
	}
	item.Status = "valid"
	item.NextRun = nextWindow(now, loc, cadence).Format(time.RFC3339)
	return item
}

func parseTimeframe(value string) (market.Timeframe, string, error) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "daily", "1d":
		return market.TimeframeDaily, "daily", nil
	case "monthly", "1mo":
		return market.TimeframeMonthly, "monthly", fmt.Errorf("unsupported timeframe %q (only daily is supported)", value)
	case "minute_1", "1m":
		return market.TimeframeMin1, "minute_1", fmt.Errorf("unsupported timeframe %q (only daily is supported)", value)
	case "minute_5", "5m":
		return market.TimeframeMin5, "minute_5", fmt.Errorf("unsupported timeframe %q (only daily is supported)", value)
	default:
		return "", "", fmt.Errorf("unsupported timeframe %q (only daily is supported)", value)
	}
}

func parseWindow(value string) (time.Duration, error) {
	trimmed := strings.TrimSpace(strings.ToLower(value))
	if trimmed == "daily" {
		return 24 * time.Hour, nil
	}
	if strings.HasSuffix(trimmed, "d") && len(trimmed) > 1 {
		days, err := strconv.Atoi(strings.TrimSuffix(trimmed, "d"))
		if err != nil || days <= 0 {
			return 0, fmt.Errorf("expected positive day count")
		}
		return time.Duration(days) * 24 * time.Hour, nil
	}
	d, err := time.ParseDuration(trimmed)
	if err != nil {
		return 0, err
	}
	if d <= 0 {
		return 0, fmt.Errorf("expected positive duration")
	}
	return d, nil
}

func nextWindow(now time.Time, loc *time.Location, cadence time.Duration) time.Time {
	localNow := now.In(loc)
	dayStart := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), 0, 0, 0, 0, loc)
	if !localNow.After(dayStart) {
		return dayStart
	}
	elapsed := localNow.Sub(dayStart)
	steps := elapsed/cadence + 1
	return dayStart.Add(steps * cadence)
}

// WriteText renders stable operator-facing dry-run lines.
func WriteText(w io.Writer, validation Validation) error {
	for _, item := range validation.Items {
		if item.Status == "rejected" {
			if _, err := fmt.Fprintf(w, "schedule=%s status=rejected reason=%q\n", item.Schedule, item.Reason); err != nil {
				return err
			}
			continue
		}
		if _, err := fmt.Fprintf(w,
			"schedule=%s status=valid provider=%s selector=%s timeframe=%s cadence=%s timezone=%s backfill_window=%s parallelism_limit=%d next_run=%s\n",
			item.Schedule, item.Provider, item.Selector, item.Timeframe, item.Cadence, item.Timezone, item.BackfillWindow, item.ParallelismLimit, item.NextRun,
		); err != nil {
			return err
		}
	}
	return nil
}

// WriteJSONL renders one JSON object per validation item.
func WriteJSONL(w io.Writer, validation Validation) error {
	enc := json.NewEncoder(w)
	for _, item := range validation.Items {
		if err := enc.Encode(item); err != nil {
			return err
		}
	}
	return nil
}

// RunCheck implements the schedule config validation command.
func RunCheck(args []string, stdout, stderr io.Writer, now func() time.Time, caps map[market.Provider]market.ProviderCapability) int {
	fs := flag.NewFlagSet("alt-worker-schedule-check", flag.ContinueOnError)
	fs.SetOutput(stderr)
	file := fs.String("file", "", "path to schedule YAML file")
	output := fs.String("output", "text", "output format: text or jsonl")
	at := fs.String("at", "", "dry-run reference time in RFC3339")
	if err := fs.Parse(args); err != nil {
		return exitUsage
	}
	if *file == "" {
		fmt.Fprintln(stderr, "schedule config file is required: --file <path>")
		return exitUsage
	}
	f, err := os.Open(*file)
	if err != nil {
		fmt.Fprintf(stderr, "open schedule config: %v\n", err)
		return exitUsage
	}
	defer f.Close()
	cfg, err := LoadConfig(f)
	if err != nil {
		fmt.Fprintf(stderr, "validate schedule config: %v\n", err)
		return exitUsage
	}
	nowValue := now()
	if *at != "" {
		parsed, err := time.Parse(time.RFC3339, *at)
		if err != nil {
			fmt.Fprintf(stderr, "invalid --at time %q: %v\n", *at, err)
			return exitUsage
		}
		nowValue = parsed
	}
	validation := ValidateConfig(cfg, ValidationOptions{Now: nowValue, Capabilities: caps})
	switch *output {
	case "text":
		err = WriteText(stdout, validation)
	case "jsonl":
		err = WriteJSONL(stdout, validation)
	default:
		fmt.Fprintf(stderr, "unknown output format %q (want text or jsonl)\n", *output)
		return exitUsage
	}
	if err != nil {
		fmt.Fprintf(stderr, "write schedule validation: %v\n", err)
		return exitUsage
	}
	if validation.HasRejected() {
		return exitRejected
	}
	return exitOK
}

// CapabilityMap builds a deterministic capability map for callers.
func CapabilityMap(capabilities ...market.ProviderCapability) map[market.Provider]market.ProviderCapability {
	out := make(map[market.Provider]market.ProviderCapability, len(capabilities))
	for _, capability := range capabilities {
		out[capability.Provider] = capability
	}
	return out
}

// SortedScheduleNames returns names in deterministic order for future status
// surfaces without exposing internal map iteration order.
func SortedScheduleNames(cfg Config) []string {
	names := make([]string, 0, len(cfg.Schedules))
	for _, sched := range cfg.Schedules {
		names = append(names, sched.Name)
	}
	sort.Strings(names)
	return names
}
