package telegram

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"sync"

	"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities"
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
	"github.com/shopspring/decimal"
)

// Usecase defines the business logic operations required by the bot.
type Usecase interface {
	RegisterNewUser(ctx context.Context, user *entities.User) error
	UserByID(ctx context.Context, userID entities.UserID) (*entities.User, error)
	UserByTgID(ctx context.Context, telegramID entities.TelegramID) (*entities.User, error)
	InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error)
	CreateAlert(ctx context.Context, alert *entities.Alert) (entities.AlertID, error)
	Alert(ctx context.Context, alertID entities.AlertID) (*entities.Alert, error)
	Alerts(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Alert, error)
	RemoveAlert(ctx context.Context, alertID entities.AlertID) error
	UpdateAlertPrice(ctx context.Context, alertID entities.AlertID, price decimal.Decimal) error
}

// Alerter keeps the in-memory alert cache in sync with created/removed alerts.
type Alerter interface {
	AddAlert(alert *entities.Alert)
	RemoveAlert(id entities.AlertID)
}

// PriceProvider fetches the current market price for an instrument.
type PriceProvider interface {
	Price(ctx context.Context, instrument entities.Instrument) (*entities.Price, error)
}

// Reply keyboard button labels.
const (
	btnAddAlert    = "Add Alert"
	btnMyAlerts    = "My Alerts"
	btnInstruments = "Instruments"
)

type flowStep string

const (
	stepAddAlertPrice  flowStep = "add_alert_price"
	stepEditAlertPrice flowStep = "edit_alert_price"
)

type userState struct {
	step         flowStep
	instrument   entities.Instrument // set during add_alert flow
	currentPrice *entities.Price     // fetched when instrument is selected
	alertID      entities.AlertID    // set during edit flow
}

// Bot is the Telegram bot handling all user interactions.
type Bot struct {
	api      *tgbotapi.BotAPI
	log      *slog.Logger
	usecase  Usecase
	alerter  Alerter
	provider PriceProvider
	states   map[entities.TelegramID]*userState
	mu       sync.Mutex
}

// New creates and initialises a new Bot. Call SetAlerter before Run to enable cache sync.
func New(token string, log *slog.Logger, uc Usecase, pp PriceProvider) (*Bot, error) {
	api, err := tgbotapi.NewBotAPI(token)
	if err != nil {
		return nil, fmt.Errorf("failed to create bot: %w", err)
	}

	return &Bot{
		api:      api,
		log:      log,
		usecase:  uc,
		provider: pp,
		states:   make(map[entities.TelegramID]*userState),
	}, nil
}

// SetAlerter injects the alerter after construction, resolving the circular dependency.
func (b *Bot) SetAlerter(al Alerter) {
	b.alerter = al
}

// Run starts the update loop and blocks until ctx is cancelled.
func (b *Bot) Run(ctx context.Context) {
	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates := b.api.GetUpdatesChan(u)
	b.log.Info("bot started", "username", b.api.Self.UserName)

	for {
		select {
		case <-ctx.Done():
			b.api.StopReceivingUpdates()
			return
		case update, ok := <-updates:
			if !ok {
				return
			}
			go b.handleUpdate(ctx, update)
		}
	}
}

// NotifyAlert implements alerter.Notifier. Called by the alerter service when a price threshold is hit.
func (b *Bot) NotifyAlert(ctx context.Context, userID entities.UserID, alert *entities.Alert, currentPrice decimal.Decimal) error {
	user, err := b.usecase.UserByID(ctx, userID)
	if err != nil {
		return fmt.Errorf("failed to get user: %w", err)
	}

	text := fmt.Sprintf(
		"Alert triggered!\n\n%s/%s %s %s\nCurrent price: %s",
		alert.Instrument.BaseCurrency,
		alert.Instrument.QuoteCurrency,
		alert.Condition,
		alert.Price.String(),
		currentPrice.String(),
	)
	msg := tgbotapi.NewMessage(int64(user.TelegramID), text)
	kb := menuKeyboard()
	kb.ResizeKeyboard = true
	msg.ReplyMarkup = kb
	_, err = b.api.Send(msg)
	return err
}

// --- Routing ---

func (b *Bot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
	switch {
	case update.Message != nil:
		b.handleMessage(ctx, update.Message)
	case update.CallbackQuery != nil:
		b.handleCallback(ctx, update.CallbackQuery)
	}
}

func (b *Bot) handleMessage(ctx context.Context, msg *tgbotapi.Message) {
	tgID := entities.TelegramID(msg.From.ID)
	chatID := msg.Chat.ID

	if msg.IsCommand() {
		b.handleCommand(ctx, tgID, chatID, msg.Command())
		return
	}

	// Map persistent reply-keyboard buttons to actions.
	switch msg.Text {
	case btnAddAlert:
		b.cmdAddAlert(ctx, tgID, chatID)
		return
	case btnMyAlerts:
		b.cmdMyAlerts(ctx, tgID, chatID)
		return
	case btnInstruments:
		b.cmdInstruments(ctx, tgID, chatID)
		return
	}

	// Route plain-text input to the active multi-step flow.
	state := b.getState(tgID)
	switch state.step {
	case stepAddAlertPrice:
		b.handleAddAlertPrice(ctx, tgID, chatID, msg.Text, state)
	case stepEditAlertPrice:
		b.handleEditAlertPrice(ctx, tgID, chatID, msg.Text, state)
	default:
		b.sendMenu(chatID, "Use the menu below or /add_alert to set a price alert.")
	}
}

func (b *Bot) handleCommand(ctx context.Context, tgID entities.TelegramID, chatID int64, cmd string) {
	switch cmd {
	case "start":
		b.cmdStart(ctx, tgID, chatID)
	case "instruments":
		b.cmdInstruments(ctx, tgID, chatID)
	case "add_alert":
		b.cmdAddAlert(ctx, tgID, chatID)
	case "my_alerts":
		b.cmdMyAlerts(ctx, tgID, chatID)
	case "cancel":
		b.cmdCancel(tgID, chatID)
	default:
		b.sendMenu(chatID, "Unknown command.\n\nAvailable commands:\n/start — register\n/instruments — list trading pairs\n/add_alert — create a price alert\n/my_alerts — view your alerts\n/cancel — cancel current operation")
	}
}

func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
	tgID := entities.TelegramID(cb.From.ID)
	chatID := cb.Message.Chat.ID

	// Acknowledge the callback immediately so the button stops spinning.
	b.api.Request(tgbotapi.NewCallback(cb.ID, "")) //nolint:errcheck

	data := cb.Data
	switch {
	case strings.HasPrefix(data, "instrument:"):
		instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
		b.handleInstrumentSelected(ctx, tgID, chatID, instrID)
	case strings.HasPrefix(data, "edit_alert:"):
		alertID := entities.AlertID(strings.TrimPrefix(data, "edit_alert:"))
		b.handleEditAlertStart(tgID, chatID, alertID)
	case strings.HasPrefix(data, "remove_alert:"):
		alertID := entities.AlertID(strings.TrimPrefix(data, "remove_alert:"))
		b.handleRemoveAlertConfirm(chatID, alertID)
	case strings.HasPrefix(data, "confirm_remove:"):
		alertID := entities.AlertID(strings.TrimPrefix(data, "confirm_remove:"))
		b.handleRemoveAlertDo(ctx, chatID, alertID)
	case data == "cancel_remove":
		b.sendMenu(chatID, "Removal cancelled.")
	}
}

// --- Command handlers ---

func (b *Bot) cmdStart(ctx context.Context, tgID entities.TelegramID, chatID int64) {
	// If already registered, just greet them.
	if _, err := b.usecase.UserByTgID(ctx, tgID); err == nil {
		b.sendMenu(chatID, "Welcome back! Use the menu below to manage your alerts.")
		return
	}

	user := &entities.User{TelegramID: tgID}
	if err := b.usecase.RegisterNewUser(ctx, user); err != nil {
		b.log.Error("failed to register user", "tg_id", tgID, "err", err)
		b.sendMenu(chatID, "Registration failed. Please try again.")
		return
	}
	b.sendMenu(chatID, "Welcome! You are now registered.\n\nUse the menu below to get started.")
}

func (b *Bot) cmdInstruments(ctx context.Context, _ entities.TelegramID, chatID int64) {
	instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
	if err != nil {
		b.log.Error("failed to list instruments", "err", err)
		b.sendMenu(chatID, "Failed to load instruments.")
		return
	}
	if len(instruments) == 0 {
		b.sendMenu(chatID, "No instruments available.")
		return
	}

	var sb strings.Builder
	sb.WriteString("Available trading pairs:\n\n")
	for _, instr := range instruments {
		fmt.Fprintf(&sb, "- %s/%s\n", instr.BaseCurrency, instr.QuoteCurrency)
	}
	b.sendMenu(chatID, sb.String())
}

func (b *Bot) cmdAddAlert(ctx context.Context, tgID entities.TelegramID, chatID int64) {
	if _, err := b.requireUser(ctx, tgID, chatID); err != nil {
		return
	}

	instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
	if err != nil {
		b.log.Error("failed to list instruments", "err", err)
		b.sendMenu(chatID, "Failed to load instruments.")
		return
	}
	if len(instruments) == 0 {
		b.sendMenu(chatID, "No instruments available.")
		return
	}

	var rows [][]tgbotapi.InlineKeyboardButton
	for _, instr := range instruments {
		btn := tgbotapi.NewInlineKeyboardButtonData(
			fmt.Sprintf("%s/%s", instr.BaseCurrency, instr.QuoteCurrency),
			fmt.Sprintf("instrument:%s", instr.ID),
		)
		rows = append(rows, tgbotapi.NewInlineKeyboardRow(btn))
	}

	msg := tgbotapi.NewMessage(chatID, "Select a trading pair:")
	msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(rows...)
	b.sendMsg(msg)
}

func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID int64) {
	user, err := b.requireUser(ctx, tgID, chatID)
	if err != nil {
		return
	}

	alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 20)
	if err != nil {
		b.log.Error("failed to get alerts", "err", err)
		b.sendMenu(chatID, "Failed to load alerts.")
		return
	}
	if len(alerts) == 0 {
		b.sendMenu(chatID, "You have no active alerts. Use \"Add Alert\" to create one.")
		return
	}

	b.sendMenu(chatID, fmt.Sprintf("Your active alerts (%d):", len(alerts)))

	for _, alert := range alerts {
		text := fmt.Sprintf("%s/%s — %s %s",
			alert.Instrument.BaseCurrency,
			alert.Instrument.QuoteCurrency,
			alert.Condition,
			alert.Price.String(),
		)
		row := tgbotapi.NewInlineKeyboardRow(
			tgbotapi.NewInlineKeyboardButtonData("Edit price", fmt.Sprintf("edit_alert:%s", alert.ID)),
			tgbotapi.NewInlineKeyboardButtonData("Remove", fmt.Sprintf("remove_alert:%s", alert.ID)),
		)
		msg := tgbotapi.NewMessage(chatID, text)
		msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
		b.sendMsg(msg)
	}
}

func (b *Bot) cmdCancel(tgID entities.TelegramID, chatID int64) {
	b.setState(tgID, &userState{})
	b.sendMenu(chatID, "Operation cancelled.")
}

// --- Multi-step flow handlers ---

// handleInstrumentSelected fetches the current price, stores it in state, and prompts for a target price.
func (b *Bot) handleInstrumentSelected(ctx context.Context, tgID entities.TelegramID, chatID int64, instrID entities.InstrumentID) {
	instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
	if err != nil {
		b.log.Error("failed to list instruments", "err", err)
		b.sendMenu(chatID, "Failed to load instruments.")
		return
	}

	var selected entities.Instrument
	for _, instr := range instruments {
		if instr.ID == instrID {
			selected = instr
			break
		}
	}
	if selected.ID == "" {
		b.sendMenu(chatID, "Unknown instrument. Please try again.")
		return
	}

	state := &userState{
		step:       stepAddAlertPrice,
		instrument: selected,
	}

	price, err := b.provider.Price(ctx, selected)
	if err != nil {
		b.log.Error("failed to fetch current price", "instrument", selected.ID, "err", err)
		b.setState(tgID, state)
		b.send(chatID, fmt.Sprintf("Selected: %s/%s\n\nCould not fetch current price. Enter your target price:", selected.BaseCurrency, selected.QuoteCurrency))
		return
	}

	state.currentPrice = price
	b.setState(tgID, state)
	b.send(chatID, fmt.Sprintf(
		"Selected: %s/%s\n\nCurrent price:\n  Ask: %s\n  Bid: %s\n\nEnter your target price:",
		selected.BaseCurrency, selected.QuoteCurrency,
		price.Ask.String(), price.Bid.String(),
	))
}

// handleAddAlertPrice parses the target price, auto-determines the condition from the current
// market price (target >= ask → above, target < ask → below), and creates the alert.
func (b *Bot) handleAddAlertPrice(ctx context.Context, tgID entities.TelegramID, chatID int64, text string, state *userState) {
	target, err := decimal.NewFromString(strings.TrimSpace(text))
	if err != nil || !target.IsPositive() {
		b.send(chatID, "Invalid price. Please enter a positive number (e.g. 50000.5):")
		return
	}

	// Use the price already fetched at instrument-selection time; re-fetch only if missing.
	currentPrice := state.currentPrice
	if currentPrice == nil {
		currentPrice, err = b.provider.Price(ctx, state.instrument)
		if err != nil {
			b.log.Error("failed to fetch current price", "instrument", state.instrument.ID, "err", err)
			b.send(chatID, "Could not fetch current price. Please try again:")
			return
		}
		state.currentPrice = currentPrice
		b.setState(tgID, state)
	}

	// Determine condition automatically: above ask or below ask.
	var condition entities.AlertCondition
	if target.GreaterThanOrEqual(currentPrice.Ask) {
		condition = entities.AlertConditionAbove
	} else {
		condition = entities.AlertConditionBelow
	}

	user, err := b.requireUser(ctx, tgID, chatID)
	if err != nil {
		return
	}

	alert := &entities.Alert{
		UserID:     user.ID,
		Price:      target,
		Condition:  condition,
		Instrument: state.instrument,
	}

	id, err := b.usecase.CreateAlert(ctx, alert)
	if err != nil {
		b.log.Error("failed to create alert", "err", err)
		b.sendMenu(chatID, "Failed to create alert. Please try again.")
		b.setState(tgID, &userState{})
		return
	}

	alert.ID = id
	b.alerter.AddAlert(alert)
	b.setState(tgID, &userState{})
	b.sendMenu(chatID, fmt.Sprintf(
		"Alert created!\n\n%s/%s %s %s\n\nYou will be notified when the price reaches your target.",
		state.instrument.BaseCurrency, state.instrument.QuoteCurrency,
		condition, target.String(),
	))
}

// handleEditAlertStart begins the edit flow for a specific alert.
func (b *Bot) handleEditAlertStart(tgID entities.TelegramID, chatID int64, alertID entities.AlertID) {
	b.setState(tgID, &userState{
		step:    stepEditAlertPrice,
		alertID: alertID,
	})
	b.send(chatID, "Enter the new target price:")
}

// handleEditAlertPrice applies the new price entered by the user.
func (b *Bot) handleEditAlertPrice(ctx context.Context, tgID entities.TelegramID, chatID int64, text string, state *userState) {
	price, err := decimal.NewFromString(strings.TrimSpace(text))
	if err != nil || !price.IsPositive() {
		b.send(chatID, "Invalid price. Please enter a positive number:")
		return
	}

	if err := b.usecase.UpdateAlertPrice(ctx, state.alertID, price); err != nil {
		b.log.Error("failed to update alert price", "alert_id", state.alertID, "err", err)
		b.sendMenu(chatID, "Failed to update alert.")
		b.setState(tgID, &userState{})
		return
	}

	// Refresh the alerter cache: remove stale entry and re-add with updated price.
	b.alerter.RemoveAlert(state.alertID)
	if updated, err := b.usecase.Alert(ctx, state.alertID); err == nil {
		b.alerter.AddAlert(updated)
	}

	b.setState(tgID, &userState{})
	b.sendMenu(chatID, fmt.Sprintf("Alert price updated to %s.", price.String()))
}

// handleRemoveAlertConfirm asks the user to confirm deletion.
func (b *Bot) handleRemoveAlertConfirm(chatID int64, alertID entities.AlertID) {
	row := tgbotapi.NewInlineKeyboardRow(
		tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove:%s", alertID)),
		tgbotapi.NewInlineKeyboardButtonData("Cancel", "cancel_remove"),
	)
	msg := tgbotapi.NewMessage(chatID, "Are you sure you want to remove this alert?")
	msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
	b.sendMsg(msg)
}

// handleRemoveAlertDo deletes the alert after confirmation.
func (b *Bot) handleRemoveAlertDo(ctx context.Context, chatID int64, alertID entities.AlertID) {
	if err := b.usecase.RemoveAlert(ctx, alertID); err != nil {
		b.log.Error("failed to remove alert", "alert_id", alertID, "err", err)
		b.sendMenu(chatID, "Failed to remove alert.")
		return
	}
	b.alerter.RemoveAlert(alertID)
	b.sendMenu(chatID, "Alert removed.")
}

// --- Helpers ---

func (b *Bot) requireUser(ctx context.Context, tgID entities.TelegramID, chatID int64) (*entities.User, error) {
	user, err := b.usecase.UserByTgID(ctx, tgID)
	if err != nil {
		b.sendMenu(chatID, "You are not registered. Please send /start first.")
		return nil, err
	}
	return user, nil
}

func (b *Bot) getState(tgID entities.TelegramID) *userState {
	b.mu.Lock()
	defer b.mu.Unlock()
	if s, ok := b.states[tgID]; ok {
		return s
	}
	s := &userState{}
	b.states[tgID] = s
	return s
}

func (b *Bot) setState(tgID entities.TelegramID, state *userState) {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.states[tgID] = state
}

// menuKeyboard returns the persistent reply keyboard shown at the bottom of the chat.
func menuKeyboard() tgbotapi.ReplyKeyboardMarkup {
	return tgbotapi.NewReplyKeyboard(
		tgbotapi.NewKeyboardButtonRow(
			tgbotapi.NewKeyboardButton(btnAddAlert),
			tgbotapi.NewKeyboardButton(btnMyAlerts),
		),
		tgbotapi.NewKeyboardButtonRow(
			tgbotapi.NewKeyboardButton(btnInstruments),
		),
	)
}

// sendMenu sends a text message and (re-)attaches the persistent menu keyboard.
func (b *Bot) sendMenu(chatID int64, text string) {
	msg := tgbotapi.NewMessage(chatID, text)
	kb := menuKeyboard()
	kb.ResizeKeyboard = true
	msg.ReplyMarkup = kb
	if _, err := b.api.Send(msg); err != nil {
		b.log.Error("failed to send message", "chat_id", chatID, "err", err)
	}
}

// send sends a plain text message without altering the keyboard.
func (b *Bot) send(chatID int64, text string) {
	if _, err := b.api.Send(tgbotapi.NewMessage(chatID, text)); err != nil {
		b.log.Error("failed to send message", "chat_id", chatID, "err", err)
	}
}

// sendMsg sends a pre-built MessageConfig.
func (b *Bot) sendMsg(msg tgbotapi.MessageConfig) {
	if _, err := b.api.Send(msg); err != nil {
		b.log.Error("failed to send message", "chat_id", msg.ChatID, "err", err)
	}
}
