package socket

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	protoSocket "git.toki-labs.com/toki/proto-socket/go"

	altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
	"git.toki-labs.com/toki/alt/packages/domain/market"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
	"git.toki-labs.com/toki/alt/services/worker/internal/scheduler"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage"
)

// importDateLayout matches the YYYYMMDD date fields the import command carries,
// mirroring the job payload date contract so the same dates reach the provider.
const importDateLayout = "20060102"

// unixMilliMs converts a time.Time to Unix milliseconds. Zero times return 0
// to avoid negative epoch values that would mislead CLI output or client UI.
func unixMilliMs(t time.Time) int64 {
	if t.IsZero() {
		return 0
	}
	return t.UnixMilli()
}

// validImportTimeframes defines the set of timeframes accepted for import_daily_bars.
// Currently only daily is supported; minute values are intentionally rejected.
var validImportTimeframes = map[string]bool{
	"daily":    true,
	"minute_1": true,
	"minute_5": true,
	"monthly":  true,
}

const (
	marketErrorInvalidRequest = "invalid_request"
	marketErrorUnavailable    = "unavailable"
	marketErrorNotFound       = "not_found"
	marketErrorTimeout        = "timeout"
	marketErrorInternal       = "internal"
)

func marketHandlers(deps Deps) []sessionHandler {
	handlers := []sessionHandler{
		{
			requestType: protoSocket.TypeNameOf(&altv1.ListInstrumentsRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.ListInstrumentsRequest, *altv1.ListInstrumentsResponse](
					&client.Communicator,
					func(req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
						return handleListInstruments(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.ListBarsRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.ListBarsRequest, *altv1.ListBarsResponse](
					&client.Communicator,
					func(req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error) {
						return handleListBars(deps, req)
					},
				)
			},
		},
		{
			requestType: protoSocket.TypeNameOf(&altv1.ImportDailyBarsRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.ImportDailyBarsRequest, *altv1.ImportDailyBarsResponse](
					&client.Communicator,
					func(req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
						return handleImportDailyBars(deps, req)
					},
				)
			},
		},
	}
	if deps.MonthlyAggregator != nil {
		handlers = append(handlers, sessionHandler{
			requestType: protoSocket.TypeNameOf(&altv1.AggregateMonthlyBarsRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.AggregateMonthlyBarsRequest, *altv1.AggregateMonthlyBarsResponse](
					&client.Communicator,
					func(req *altv1.AggregateMonthlyBarsRequest) (*altv1.AggregateMonthlyBarsResponse, error) {
						return handleAggregateMonthlyBars(deps, req)
					},
				)
			},
		})
	}
	if deps.RefreshStatus != nil {
		handlers = append(handlers, sessionHandler{
			requestType: protoSocket.TypeNameOf(&altv1.SchedulerRefreshStatusRequest{}),
			register: func(client *protoSocket.WsClient) {
				protoSocket.AddRequestListenerTyped[*altv1.SchedulerRefreshStatusRequest, *altv1.SchedulerRefreshStatusResponse](
					&client.Communicator,
					func(req *altv1.SchedulerRefreshStatusRequest) (*altv1.SchedulerRefreshStatusResponse, error) {
						return handleSchedulerRefreshStatus(deps, req)
					},
				)
			},
		})
	}
	return handlers
}

// importTimeframeNormalise resolves the proto Timeframe into a string accepted
// by the provider layer. Unspecified defaults to "daily"; known enum names are
// mapped explicitly.  Unknown enum values return ("", unknownTimeframeErr) so
// that the caller can reject them as a typed invalid_request error instead of
// panicking on slice operations.
func importTimeframeNormalise(tf altv1.Timeframe) (string, error) {
	if tf == altv1.Timeframe_TIMEFRAME_UNSPECIFIED {
		return "daily", nil
	}
	// Map known proto enum values to their lowercase name (without TIMEFRAME_ prefix).
	switch tf {
	case altv1.Timeframe_TIMEFRAME_DAILY:
		return "daily", nil
	case altv1.Timeframe_TIMEFRAME_MINUTE_1:
		return "minute_1", nil
	case altv1.Timeframe_TIMEFRAME_MINUTE_5:
		return "minute_5", nil
	case altv1.Timeframe_TIMEFRAME_MONTHLY:
		return "monthly", nil
	default:
		// Unknown enum value: return the raw string name (if available) so the
		// caller can produce a stable error message.  Do NOT panic on slice.
		name := tf.String()
		if strings.HasPrefix(name, "TIMEFRAME_") {
			return strings.ToLower(name[len("TIMEFRAME_"):]), fmt.Errorf("unsupported timeframe %q", strings.ToLower(name[len("TIMEFRAME_"):]))
		}
		return strings.ToLower(name), fmt.Errorf("unsupported timeframe %q", strings.ToLower(name))
	}
}

// handleImportDailyBars validates the import command, converts it into the
// importer request, and runs the import. The worker owns provider fetch and
// persistence; counts come back so the operator surface can confirm what landed.
func handleImportDailyBars(deps Deps, req *altv1.ImportDailyBarsRequest) (*altv1.ImportDailyBarsResponse, error) {
	if req.GetProvider() == "" {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("provider is required")}, nil
	}
	if req.GetSelectorKind() == "" {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("selector_kind is required")}, nil
	}
	if len(req.GetSymbols()) == 0 {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest("symbols is required")}, nil
	}

	// Normalise/validate the import timeframe.  Daily (unspecified) is the only
	// accepted value for import_daily_bars at this stage; minute values are
	// rejected as a typed invalid_request so the operator sees a stable error.
	tfStr, tfErr := importTimeframeNormalise(req.GetTimeframe())
	if tfErr != nil {
		return &altv1.ImportDailyBarsResponse{
			Error: marketInvalidRequest(tfErr.Error()),
		}, nil
	}
	if !validImportTimeframes[tfStr] {
		return &altv1.ImportDailyBarsResponse{
			Error: marketInvalidRequest(fmt.Sprintf("import_daily_bars supports timeframe daily; got %s", tfStr)),
		}, nil
	}
	if tfStr != "daily" {
		return &altv1.ImportDailyBarsResponse{
			Error: marketInvalidRequest(fmt.Sprintf("import_daily_bars supports timeframe daily; got %s", tfStr)),
		}, nil
	}

	mkt, _, err := marketFilterFromProto(req.GetMarket())
	if err != nil {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	venue, err := venueFromProto(req.GetVenue())
	if err != nil {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	from, to, err := parseImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd())
	if err != nil {
		return &altv1.ImportDailyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	if deps.DailyBarImporter == nil {
		return &altv1.ImportDailyBarsResponse{Error: marketUnavailableError("daily bar import is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	result, err := deps.DailyBarImporter.ImportDailyBars(ctx, importer.DailyBarRequest{
		Provider: market.Provider(req.GetProvider()),
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorKind(req.GetSelectorKind()),
			Market:  mkt,
			Venue:   venue,
			Symbols: req.GetSymbols(),
			Name:    req.GetName(),
		},
		From: from,
		To:   to,
	})
	if err != nil {
		return &altv1.ImportDailyBarsResponse{Error: marketBackendErrorInfo(err)}, nil
	}
	return &altv1.ImportDailyBarsResponse{
		Provider:        req.GetProvider(),
		InstrumentCount: int32(result.Instruments),
		BarCount:        int32(result.Bars),
	}, nil
}

// parseImportDateRange requires both endpoints of the import date range and
// returns them parsed for the importer request. The live KIS importer requires
// a bounded from/to range and rejects an inverted range, so the worker closes
// those operator-input errors here as invalid_request before reaching the
// importer instead of letting them surface as an internal backend failure.
func parseImportDateRange(fromValue, toValue string) (time.Time, time.Time, error) {
	if fromValue == "" {
		return time.Time{}, time.Time{}, errors.New("from_yyyymmdd is required")
	}
	if toValue == "" {
		return time.Time{}, time.Time{}, errors.New("to_yyyymmdd is required")
	}
	from, err := time.Parse(importDateLayout, fromValue)
	if err != nil {
		return time.Time{}, time.Time{}, errors.New("from_yyyymmdd must be YYYYMMDD")
	}
	to, err := time.Parse(importDateLayout, toValue)
	if err != nil {
		return time.Time{}, time.Time{}, errors.New("to_yyyymmdd must be YYYYMMDD")
	}
	if from.After(to) {
		return time.Time{}, time.Time{}, errors.New("from_yyyymmdd cannot be after to_yyyymmdd")
	}
	return from.UTC(), to.UTC(), nil
}

func handleListInstruments(deps Deps, req *altv1.ListInstrumentsRequest) (*altv1.ListInstrumentsResponse, error) {
	marketFilter, hasMarketFilter, err := marketFilterFromProto(req.GetMarket())
	if err != nil {
		return &altv1.ListInstrumentsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	if deps.Instruments == nil {
		return &altv1.ListInstrumentsResponse{Error: marketUnavailableError("instrument store is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	insts, err := deps.Instruments.ListInstruments(ctx)
	if err != nil {
		return &altv1.ListInstrumentsResponse{Error: marketBackendErrorInfo(err)}, nil
	}
	filtered := filterInstruments(insts, marketFilter, hasMarketFilter, req.GetProvider())
	return &altv1.ListInstrumentsResponse{Instruments: instrumentsToProto(filtered)}, nil
}

func handleListBars(deps Deps, req *altv1.ListBarsRequest) (*altv1.ListBarsResponse, error) {
	if req.GetInstrumentId() == "" {
		return &altv1.ListBarsResponse{Error: marketInvalidRequest("instrument_id is required")}, nil
	}
	timeframe, err := timeframeFromProto(req.GetTimeframe())
	if err != nil {
		return &altv1.ListBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	if req.GetFromUnixMs() == 0 {
		return &altv1.ListBarsResponse{Error: marketInvalidRequest("from_unix_ms is required")}, nil
	}
	if req.GetToUnixMs() == 0 {
		return &altv1.ListBarsResponse{Error: marketInvalidRequest("to_unix_ms is required")}, nil
	}
	if req.GetFromUnixMs() > req.GetToUnixMs() {
		return &altv1.ListBarsResponse{Error: marketInvalidRequest("from_unix_ms cannot be after to_unix_ms")}, nil
	}
	if deps.Bars == nil {
		return &altv1.ListBarsResponse{Error: marketUnavailableError("bar store is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	from := time.UnixMilli(req.GetFromUnixMs()).UTC()
	to := time.UnixMilli(req.GetToUnixMs()).UTC()
	bars, err := deps.Bars.GetBars(ctx, market.InstrumentID(req.GetInstrumentId()), timeframe, from, to)
	if err != nil {
		return &altv1.ListBarsResponse{Error: marketBackendErrorInfo(err)}, nil
	}
	return &altv1.ListBarsResponse{Bars: barsToProto(bars)}, nil
}

func marketFilterFromProto(m altv1.Market) (market.Market, bool, error) {
	switch m {
	case altv1.Market_MARKET_UNSPECIFIED:
		return "", false, nil
	case altv1.Market_MARKET_KR, altv1.Market_MARKET_US:
		converted, err := marketFromProto(m)
		return converted, true, err
	default:
		return "", false, errors.New("market filter is unsupported")
	}
}

func filterInstruments(insts []market.Instrument, marketFilter market.Market, hasMarketFilter bool, provider string) []market.Instrument {
	if !hasMarketFilter && provider == "" {
		return insts
	}
	out := make([]market.Instrument, 0, len(insts))
	for _, inst := range insts {
		if hasMarketFilter && inst.Market != marketFilter {
			continue
		}
		if provider != "" && inst.ProviderSymbols[provider] == "" {
			continue
		}
		out = append(out, inst)
	}
	return out
}

func marketInvalidRequest(reason string) *altv1.ErrorInfo {
	return errorInfo(marketErrorInvalidRequest, "invalid market request: "+reason)
}

func marketUnavailableError(message string) *altv1.ErrorInfo {
	return errorInfo(marketErrorUnavailable, message)
}

func handleSchedulerRefreshStatus(deps Deps, req *altv1.SchedulerRefreshStatusRequest) (*altv1.SchedulerRefreshStatusResponse, error) {
	var statuses []scheduler.RefreshStatus
	if name := req.GetScheduleName(); name != "" {
		s, ok := deps.RefreshStatus.Get(name)
		if ok {
			s = enrichWithNextRun(s, deps.RefreshStatus)
			statuses = []scheduler.RefreshStatus{s}
		}
	} else {
		all := deps.RefreshStatus.All()
		for _, s := range all {
			s = enrichWithNextRun(s, deps.RefreshStatus)
			statuses = append(statuses, s)
		}
	}

	entries := make([]*altv1.SchedulerRefreshStatusEntry, 0, len(statuses))
	for _, s := range statuses {
		entries = append(entries, &altv1.SchedulerRefreshStatusEntry{
			Schedule:          s.Schedule,
			Status:            s.Status,
			LastSuccessUnixMs: unixMilliMs(s.LastSuccess),
			LastError:         s.LastError,
			NextRunUnixMs:     unixMilliMs(s.NextRun),
			ImportedBarCount:  int32(s.ImportedBarCount),
			MissingCount:      int32(s.MissingCount),
			GapCount:          int32(s.GapCount),
			DuplicateCount:    int32(s.DuplicateCount),
			ProviderDelayDays: int32(s.ProviderDelay),
		})
	}
	return &altv1.SchedulerRefreshStatusResponse{Entries: entries}, nil
}

// enrichWithNextRun computes the next_run time from the stored schedule config
// for the given status. If the config is not set or the schedule is not found,
// NextRun is left as zero (rendered as 0 / "NONE").
func enrichWithNextRun(s scheduler.RefreshStatus, qr RefreshStatusQuerier) scheduler.RefreshStatus {
	// The store implements ScheduleConfig() via the underlying *RefreshStatusStore.
	type configStore interface {
		ScheduleConfig() scheduler.Config
	}
	if cs, ok := qr.(configStore); ok {
		cfg := cs.ScheduleConfig()
		for _, sc := range cfg.Schedules {
			if sc.Name != s.Schedule {
				continue
			}
			loc, err := time.LoadLocation(sc.Timezone)
			if err != nil {
				return s
			}
			cadence, err := parseWindowDuration(sc.Cadence)
			if err != nil {
				return s
			}
			next := nextWindowForStatus(s, loc, cadence)
			if !next.IsZero() && next.After(s.NextRun) {
				s.NextRun = next
			}
			return s
		}
	}
	return s
}

// parseWindowDuration converts a cadence string like "daily", "7d", or a Go
// duration string into time.Duration. Mirrors scheduler.parseWindow.
func parseWindowDuration(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
}

// nextWindowForStatus computes the next run window based on the last success
// time. If LastSuccess is zero, it uses the current now (passed via opts).
func nextWindowForStatus(s scheduler.RefreshStatus, loc *time.Location, cadence time.Duration) time.Time {
	if s.LastSuccess.IsZero() {
		// First run: next window from now
		return computeNextWindow(time.Now(), loc, cadence)
	}
	// Subsequent runs: next window after last success
	return computeNextWindow(s.LastSuccess, loc, cadence)
}

func computeNextWindow(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)
}

func marketBackendErrorInfo(err error) *altv1.ErrorInfo {
	if err == nil {
		return nil
	}
	switch {
	case errors.Is(err, storage.ErrInstrumentNotFound):
		return errorInfo(marketErrorNotFound, err.Error())
	case errors.Is(err, ErrMonthlySourceBarsNotFound):
		return errorInfo(marketErrorNotFound, err.Error())
	case errors.Is(err, context.DeadlineExceeded):
		return errorInfo(marketErrorTimeout, err.Error())
	default:
		return errorInfo(marketErrorInternal, err.Error())
	}
}

// handleAggregateMonthlyBars validates the aggregation request, converts it
// into an AggregateMonthlyRequest and delegates to the MonthlyBarAggregator.
// The response carries instrument counts, source daily bar counts, monthly bar
// counts, and per-instrument provenance entries.
func handleAggregateMonthlyBars(deps Deps, req *altv1.AggregateMonthlyBarsRequest) (*altv1.AggregateMonthlyBarsResponse, error) {
	if req.GetProvider() == "" {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest("provider is required")}, nil
	}
	if req.GetSelectorKind() == "" {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest("selector_kind is required")}, nil
	}
	if len(req.GetSymbols()) == 0 {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest("symbols is required")}, nil
	}

	mkt, _, err := marketFilterFromProto(req.GetMarket())
	if err != nil {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	venue, err := venueFromProto(req.GetVenue())
	if err != nil {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	from, to, err := parseImportDateRange(req.GetFromYyyymmdd(), req.GetToYyyymmdd())
	if err != nil {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketInvalidRequest(err.Error())}, nil
	}
	if deps.MonthlyAggregator == nil {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketUnavailableError("monthly aggregation is not available")}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
	defer cancel()

	result, err := deps.MonthlyAggregator.AggregateMonthlyBars(ctx, AggregateMonthlyRequest{
		Provider: market.Provider(req.GetProvider()),
		Selector: market.UniverseSelector{
			Kind:    market.UniverseSelectorKind(req.GetSelectorKind()),
			Market:  mkt,
			Venue:   venue,
			Symbols: req.GetSymbols(),
			Name:    req.GetName(),
		},
		From: from,
		To:   to,
	})
	if err != nil {
		return &altv1.AggregateMonthlyBarsResponse{Error: marketBackendErrorInfo(err)}, nil
	}

	provenance := make([]*altv1.MonthlyProvenance, 0, len(result.Provenance))
	for _, p := range result.Provenance {
		provenance = append(provenance, &altv1.MonthlyProvenance{
			InstrumentId:        string(p.InstrumentID),
			SourceDailyBarCount: int32(p.SourceDailyCount),
			MonthlyBarCount:     int32(p.MonthlyBarCount),
			SourceStartYyyymmdd: p.SourceStart.Format(importDateLayout),
			SourceEndYyyymmdd:   p.SourceEnd.Format(importDateLayout),
			AggregationRuleId:   p.AggregationRuleID,
			SourceTimeframe:     altv1.Timeframe_TIMEFRAME_DAILY,
			TargetTimeframe:     altv1.Timeframe_TIMEFRAME_MONTHLY,
		})
	}

	return &altv1.AggregateMonthlyBarsResponse{
		Provider:            req.GetProvider(),
		InstrumentCount:     int32(result.Instruments),
		SourceDailyBarCount: int32(result.SourceDailyCount),
		MonthlyBarCount:     int32(result.MonthlyBarCount),
		Provenance:          provenance,
	}, nil
}
