package alerter

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities"
	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/provider"
	"github.com/shopspring/decimal"
)

type Notifier interface {
	NotifyAlert(ctx context.Context, userID entities.UserID, alert *entities.Alert, currentPrice decimal.Decimal) error
}

type Storage interface {
	AllActiveAlerts(ctx context.Context) ([]entities.Alert, error)
	DisableAlert(ctx context.Context, id entities.AlertID) error
	GetLastAlertCheck(ctx context.Context) (time.Time, error)
	SetLastAlertCheck(ctx context.Context, t time.Time) error
}

type Alerter struct {
	log           *slog.Logger
	cache         *alertsCache
	priceProvider provider.Provider
	notifier      Notifier
	storage       Storage
	lastCheckedAt time.Time
}

const interval = time.Minute

func New(log *slog.Logger, priceProvider provider.Provider, notifier Notifier, storage Storage) *Alerter {
	return &Alerter{
		log:           log,
		cache:         newCache(),
		priceProvider: priceProvider,
		notifier:      notifier,
		storage:       storage,
		lastCheckedAt: time.Now(), // safe default; overwritten by LoadAlerts if DB has a stored value
	}
}

func (a *Alerter) LoadAlerts(ctx context.Context) error {
	alerts, err := a.storage.AllActiveAlerts(ctx)
	if err != nil {
		return fmt.Errorf("failed to load alerts: %w", err)
	}

	for i := range alerts {
		a.cache.Add(&alerts[i])
	}

	a.log.Info("alerts loaded", "count", len(alerts))

	// Restore last check time so missed candles are checked on next tick.
	t, err := a.storage.GetLastAlertCheck(ctx)
	if err != nil {
		return fmt.Errorf("failed to load last alert check time: %w", err)
	}
	if !t.IsZero() {
		a.lastCheckedAt = t
		a.log.Info("resuming alert checks from", "since", t)
	}

	return nil
}

func (a *Alerter) AddAlert(alert *entities.Alert) {
	a.cache.Add(alert)
}

func (a *Alerter) RemoveAlert(id entities.AlertID) {
	a.cache.Remove(id)
}

func (a *Alerter) Run(ctx context.Context) {
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()

		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
			}

			a.log.Info("start checking alerts")

			if err := a.checkAlerts(ctx); err != nil {
				a.log.Error("failed to check alerts", "err", err)
				continue
			}

			a.log.Info("alerts checked")
		}
	}()
}

// klineMaxCandles matches the per-request limit of the Bybit kline API.
// Used to select the coarsest candle interval that still fits in one request.
const klineMaxCandles = 1000

// candleIntervalTable lists Bybit kline intervals in ascending duration order.
// selectCandleInterval picks the smallest entry whose window covers the gap.
var candleIntervalTable = []provider.KlineInterval{
	provider.Kline1m,
	provider.Kline3m,
	provider.Kline5m,
	provider.Kline15m,
	provider.Kline30m,
	provider.Kline1H,
	provider.Kline4H,
	provider.Kline6H,
	provider.Kline12H,
	provider.Kline1D,
	provider.Kline1W,
}

// selectCandleInterval returns the smallest interval whose single-request window
// (klineMaxCandles * intervalDuration) covers the entire gap.
func selectCandleInterval(gap time.Duration) provider.KlineInterval {
	for _, iv := range candleIntervalTable {
		if gap <= time.Duration(klineMaxCandles)*iv.ToDuration() {
			return iv
		}
	}
	return candleIntervalTable[len(candleIntervalTable)-1]
}

// TODO: parallel checking for different instruments.
// TODO: get one candle before interval

func (a *Alerter) checkAlerts(ctx context.Context) error {
	now := time.Now()
	gap := now.Sub(a.lastCheckedAt)
	candleInterval := selectCandleInterval(gap)

	// Truncate to the selected interval boundary so we always re-check the candle
	// that was still forming at the previous tick — its High/Low may have changed
	// since.
	from := a.lastCheckedAt.Truncate(candleInterval.ToDuration())

	a.log.Debug("checking alerts", "from", from, "to", now, "interval", candleInterval, "gap", gap.Round(time.Second))

	instruments := a.cache.Instruments()

	for _, instrument := range instruments {
		candles, err := a.priceProvider.Candles(ctx, instrument, from, now, candleInterval)
		if err != nil {
			a.log.Error("failed to get candles", "instrument", instrument.ID, "err", err)
			return fmt.Errorf("failed to get candles: %w", err)
		}

		for _, alert := range a.cache.AlertsByInstrument(instrument.ID) {
			if triggered, price := alertTriggeredByCandles(alert, candles); triggered {
				a.triggerAlert(ctx, alert, price)
			}
		}
	}

	a.lastCheckedAt = now

	if err := a.storage.SetLastAlertCheck(ctx, now); err != nil {
		a.log.Error("failed to persist last alert check time", "err", err)
	}

	return nil
}

// alertTriggeredByCandles returns true and the triggering price if any candle
// caused the alert condition to be met.
func alertTriggeredByCandles(alert *entities.Alert, candles []entities.Candle) (bool, decimal.Decimal) {
	for _, candle := range candles {
		switch alert.Condition {
		case entities.AlertConditionAbove:
			if candle.High.GreaterThanOrEqual(alert.Price) {
				return true, candle.High
			}
		case entities.AlertConditionBelow:
			if candle.Low.LessThanOrEqual(alert.Price) {
				return true, candle.Low
			}
		}
	}
	return false, decimal.Zero
}

func (a *Alerter) triggerAlert(ctx context.Context, alert *entities.Alert, currentPrice decimal.Decimal) {
	if err := a.notifier.NotifyAlert(ctx, alert.UserID, alert, currentPrice); err != nil {
		a.log.Error("failed to notify alert", "alert_id", alert.ID, "err", err)
		return
	}

	a.cache.Remove(alert.ID)

	if err := a.storage.DisableAlert(ctx, alert.ID); err != nil {
		a.log.Error("failed to disable alert in db", "alert_id", alert.ID, "err", err)
	}
}
