package jobs

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
)

// importDateLayout matches the KIS YYYYMMDD date fields the payload date range
// uses (see the KIS daily chart request fixture).
const importDateLayout = "20060102"

// DailyBarImporter is the import surface a daily bar job dispatches to. The
// jobs package depends only on this interface so the concrete importer (and its
// provider/storage wiring) stays out of the job boundary.
type DailyBarImporter interface {
	ImportDailyBars(ctx context.Context, request importer.DailyBarRequest) (importer.Result, error)
}

// DailyBarImportPayload is the decoded KindImportDailyBars job payload. It
// carries the provider and the universe selector (kind, venue/market/name, and
// symbols) plus an optional date range the provider applies when fetching.
// Timeframe is optional: empty defaults to "daily"; non-daily values are
// rejected as a typed error before reaching the importer.
type DailyBarImportPayload struct {
	Provider     string   `json:"provider"`
	SelectorKind string   `json:"selector_kind"`
	Market       string   `json:"market"`
	Venue        string   `json:"venue"`
	Name         string   `json:"name"`
	Symbols      []string `json:"symbols"`
	From         string   `json:"from,omitempty"`
	To           string   `json:"to,omitempty"`
	Timeframe    string   `json:"timeframe,omitempty"`
}

// selector builds the domain universe selector this payload describes.
func (p DailyBarImportPayload) selector() market.UniverseSelector {
	return market.UniverseSelector{
		Kind:    market.UniverseSelectorKind(p.SelectorKind),
		Market:  market.Market(p.Market),
		Venue:   market.Venue(p.Venue),
		Symbols: p.Symbols,
		Name:    p.Name,
	}
}

// request resolves the payload into an importer request, parsing the optional
// YYYYMMDD date range so the provider/date-range contract reaches the importer.
func (p DailyBarImportPayload) request() (importer.DailyBarRequest, error) {
	req := importer.DailyBarRequest{
		Provider: market.Provider(p.Provider),
		Selector: p.selector(),
	}
	from, err := parseImportDate("from", p.From)
	if err != nil {
		return importer.DailyBarRequest{}, err
	}
	to, err := parseImportDate("to", p.To)
	if err != nil {
		return importer.DailyBarRequest{}, err
	}
	req.From = from
	req.To = to
	return req, nil
}

// parseImportDate parses an optional YYYYMMDD date. An empty value yields the
// zero time; a malformed value is an error.
func parseImportDate(field, value string) (time.Time, error) {
	if value == "" {
		return time.Time{}, nil
	}
	t, err := time.Parse(importDateLayout, value)
	if err != nil {
		return time.Time{}, fmt.Errorf("daily bar import payload: invalid %s date %q: %w", field, value, err)
	}
	return t, nil
}

// importTimeframes is the closed set of valid import timeframe values.
// Only empty (defaults to daily) and "daily" are accepted; explicit minute
// values (e.g. "minute", "1m", "5m", "minute_1", "minute_5") are rejected as
// a typed error before reaching the importer.
var importTimeframes = map[string]bool{
	"":      true,
	"daily": true,
}

// DecodeDailyBarImportPayload decodes and validates a KindImportDailyBars
// payload.
func DecodeDailyBarImportPayload(raw json.RawMessage) (DailyBarImportPayload, error) {
	var p DailyBarImportPayload
	if err := json.Unmarshal(raw, &p); err != nil {
		return DailyBarImportPayload{}, fmt.Errorf("decode daily bar import payload: %w", err)
	}
	if p.Provider == "" {
		return DailyBarImportPayload{}, fmt.Errorf("daily bar import payload: provider is required")
	}
	if p.SelectorKind == "" {
		return DailyBarImportPayload{}, fmt.Errorf("daily bar import payload: selector_kind is required")
	}
	if len(p.Symbols) == 0 {
		return DailyBarImportPayload{}, fmt.Errorf("daily bar import payload: symbols is required")
	}
	if p.Timeframe != "" && !importTimeframes[p.Timeframe] {
		return DailyBarImportPayload{}, fmt.Errorf("daily bar import payload: unsupported timeframe %q (only empty or \"daily\" is supported)", p.Timeframe)
	}
	return p, nil
}

// importTimeframe resolves the payload's optional timeframe to a domain
// Timeframe value.  Empty defaults to daily; non-daily values are returned as a
// typed error so that the caller can reject them before any fetch.
func importTimeframe(tf string) (market.Timeframe, error) {
	if tf == "" || tf == "daily" {
		return market.TimeframeDaily, nil
	}
	return market.TimeframeDaily, fmt.Errorf("import_daily_bars job: unsupported timeframe %q (only daily is supported)", tf)
}

// RegisterDailyBarImportHandler registers the real KindImportDailyBars handler,
// replacing the built-in placeholder. It decodes the payload into a daily bar
// request, gates it through the provider capability matrix (rejecting provider
// mismatches and unsupported market/venue/daily-bar combinations before any
// fetch), and dispatches to the importer. RegisterBuiltins still registers all
// placeholders first; callers invoke this afterwards to wire the live importer.
//
// Both Register (for Execute) and RegisterWithResult (for ExecuteWithResult)
// are called so that the scheduler's executor can obtain actual import bar
// counts from the real runtime path instead of silently reporting
// "success with no evidence".
func RegisterDailyBarImportHandler(runner *Runner, capability market.ProviderCapability, imp DailyBarImporter) {
	exec := func(ctx context.Context, payload json.RawMessage) (Result, error) {
		p, err := DecodeDailyBarImportPayload(payload)
		if err != nil {
			return Result{}, err
		}
		tf, err := importTimeframe(p.Timeframe)
		if err != nil {
			return Result{}, err
		}
		decision := capability.CheckBars(market.ProviderCapabilityRequest{
			Provider:  market.Provider(p.Provider),
			Market:    market.Market(p.Market),
			Venue:     market.Venue(p.Venue),
			Timeframe: tf,
		})
		if !decision.Accepted {
			return Result{}, fmt.Errorf("daily bar import payload: %s", decision.Reason)
		}
		req, err := p.request()
		if err != nil {
			return Result{}, err
		}
		res, err := imp.ImportDailyBars(ctx, req)
		if err != nil {
			return Result{}, fmt.Errorf("import daily bars job: %w", err)
		}
		return Result{
			Bars:        res.Bars,
			Instruments: res.Instruments,
		}, nil
	}

	runner.Register(KindImportDailyBars, func(ctx context.Context, payload json.RawMessage) error {
		_, err := exec(ctx, payload)
		return err
	})
	runner.RegisterWithResult(KindImportDailyBars, exec)
}
