package bot

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

	storagemodel "github.com/tech/sendico/gateway/chsettle/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"go.uber.org/zap"
)

const unauthorizedMessage = "*Unauthorized*\nYour Telegram account is not allowed to perform treasury operations."
const unauthorizedChatMessage = "*Unauthorized Chat*\nThis Telegram chat is not allowed to perform treasury operations."

const amountInputHint = "*Amount format*\nEnter amount as a decimal number using a dot separator and without currency.\nExample: `1250.75`"

type SendTextFunc func(ctx context.Context, chatID string, text string) error

type ScheduleTracker interface {
	TrackScheduled(record *storagemodel.TreasuryRequest)
	Untrack(requestID string)
}

type AccountProfile struct {
	AccountID   string
	AccountCode string
	Currency    string
}

type CreateRequestInput struct {
	OperationType   storagemodel.TreasuryOperationType
	TelegramUserID  string
	LedgerAccountID string
	ChatID          string
	Amount          string
}

type TreasuryService interface {
	ExecutionDelay() time.Duration
	MaxPerOperationLimit() string

	GetActiveRequestForAccount(ctx context.Context, ledgerAccountID string) (*storagemodel.TreasuryRequest, error)
	GetAccountProfile(ctx context.Context, ledgerAccountID string) (*AccountProfile, error)
	CreateRequest(ctx context.Context, input CreateRequestInput) (*storagemodel.TreasuryRequest, error)
	ConfirmRequest(ctx context.Context, requestID string, telegramUserID string) (*storagemodel.TreasuryRequest, error)
	CancelRequest(ctx context.Context, requestID string, telegramUserID string) (*storagemodel.TreasuryRequest, error)
}

type UserBinding struct {
	TelegramUserID  string
	LedgerAccountID string
	AllowedChatIDs  []string
}

type UserBindingResolver interface {
	ResolveUserBinding(ctx context.Context, telegramUserID string) (*UserBinding, error)
}

type limitError interface {
	error
	LimitKind() string
	LimitMax() string
}

type Router struct {
	logger mlogger.Logger

	service TreasuryService
	dialogs *Dialogs
	send    SendTextFunc
	tracker ScheduleTracker

	users UserBindingResolver
}

func NewRouter(
	logger mlogger.Logger,
	service TreasuryService,
	send SendTextFunc,
	tracker ScheduleTracker,
	users UserBindingResolver,
) *Router {
	if logger != nil {
		logger = logger.Named("treasury_router")
	}
	return &Router{
		logger:  logger,
		service: service,
		dialogs: NewDialogs(),
		send:    send,
		tracker: tracker,
		users:   users,
	}
}

func (r *Router) Enabled() bool {
	return r != nil && r.service != nil && r.users != nil
}

func (r *Router) HandleUpdate(ctx context.Context, update *model.TelegramWebhookUpdate) bool {
	if !r.Enabled() || update == nil || update.Message == nil {
		return false
	}
	message := update.Message
	chatID := strings.TrimSpace(message.ChatID)
	userID := strings.TrimSpace(message.FromUserID)
	text := strings.TrimSpace(message.Text)

	if chatID == "" || userID == "" {
		return false
	}
	command := parseCommand(text)
	if r.logger != nil {
		r.logger.Debug("Telegram treasury update received",
			zap.Int64("update_id", update.UpdateID),
			zap.String("chat_id", chatID),
			zap.String("telegram_user_id", userID),
			zap.String("command", strings.TrimSpace(string(command))),
			zap.String("message_text", text),
			zap.String("reply_to_message_id", strings.TrimSpace(message.ReplyToMessageID)),
		)
	}

	binding, err := r.users.ResolveUserBinding(ctx, userID)
	if err != nil {
		if r.logger != nil {
			r.logger.Warn("Failed to resolve treasury user binding",
				zap.Error(err),
				zap.String("telegram_user_id", userID),
				zap.String("chat_id", chatID))
		}
		_ = r.sendText(ctx, chatID, "*Temporary issue*\nUnable to check treasury authorization right now. Please try again.")
		return true
	}
	if binding == nil || strings.TrimSpace(binding.LedgerAccountID) == "" {
		r.logUnauthorized(update)
		_ = r.sendText(ctx, chatID, unauthorizedMessage)
		return true
	}
	if !isChatAllowed(chatID, binding.AllowedChatIDs) {
		r.logUnauthorized(update)
		_ = r.sendText(ctx, chatID, unauthorizedChatMessage)
		return true
	}
	accountID := strings.TrimSpace(binding.LedgerAccountID)

	switch command {
	case CommandStart:
		profile := r.resolveAccountProfile(ctx, accountID)
		_ = r.sendText(ctx, chatID, welcomeMessage(profile))
		return true
	case CommandHelp:
		profile := r.resolveAccountProfile(ctx, accountID)
		_ = r.sendText(ctx, chatID, helpMessage(displayAccountCode(profile, accountID), profile.Currency))
		return true
	case CommandFund:
		if r.logger != nil {
			r.logger.Info("Treasury funding dialog requested",
				zap.String("chat_id", chatID),
				zap.String("telegram_user_id", userID),
				zap.String("ledger_account_id", accountID))
		}
		r.startAmountDialog(ctx, userID, accountID, chatID, storagemodel.TreasuryOperationFund)
		return true
	case CommandWithdraw:
		if r.logger != nil {
			r.logger.Info("Treasury withdrawal dialog requested",
				zap.String("chat_id", chatID),
				zap.String("telegram_user_id", userID),
				zap.String("ledger_account_id", accountID))
		}
		r.startAmountDialog(ctx, userID, accountID, chatID, storagemodel.TreasuryOperationWithdraw)
		return true
	case CommandConfirm:
		if r.logger != nil {
			r.logger.Info("Treasury confirmation requested",
				zap.String("chat_id", chatID),
				zap.String("telegram_user_id", userID),
				zap.String("ledger_account_id", accountID))
		}
		r.confirm(ctx, userID, accountID, chatID)
		return true
	case CommandCancel:
		if r.logger != nil {
			r.logger.Info("Treasury cancellation requested",
				zap.String("chat_id", chatID),
				zap.String("telegram_user_id", userID),
				zap.String("ledger_account_id", accountID))
		}
		r.cancel(ctx, userID, accountID, chatID)
		return true
	}

	session, hasSession := r.dialogs.Get(userID)
	if hasSession {
		switch session.State {
		case DialogStateWaitingAmount:
			r.captureAmount(ctx, userID, accountID, chatID, session.OperationType, text)
			return true
		case DialogStateWaitingConfirmation:
			_ = r.sendText(ctx, chatID, confirmationCommandsMessage())
			return true
		}
	}

	if strings.HasPrefix(text, "/") {
		_ = r.sendText(ctx, chatID, supportedCommandsMessage())
		return true
	}
	if strings.TrimSpace(message.ReplyToMessageID) != "" {
		return false
	}
	if text != "" {
		_ = r.sendText(ctx, chatID, supportedCommandsMessage())
		return true
	}
	return false
}

func (r *Router) startAmountDialog(ctx context.Context, userID, accountID, chatID string, operation storagemodel.TreasuryOperationType) {
	active, err := r.service.GetActiveRequestForAccount(ctx, accountID)
	if err != nil {
		if r.logger != nil {
			r.logger.Warn("Failed to check active treasury request", zap.Error(err), zap.String("telegram_user_id", userID), zap.String("ledger_account_id", accountID))
		}
		_ = r.sendText(ctx, chatID, "*Temporary issue*\nUnable to check pending treasury operations right now. Please try again.")
		return
	}
	if active != nil {
		_ = r.sendText(ctx, chatID, pendingRequestMessage(active))
		r.dialogs.Set(userID, DialogSession{
			State:           DialogStateWaitingConfirmation,
			LedgerAccountID: accountID,
			RequestID:       active.RequestID,
		})
		return
	}
	r.dialogs.Set(userID, DialogSession{
		State:           DialogStateWaitingAmount,
		OperationType:   operation,
		LedgerAccountID: accountID,
	})
	profile := r.resolveAccountProfile(ctx, accountID)
	_ = r.sendText(ctx, chatID, amountPromptMessage(operation, profile, accountID))
}

func (r *Router) captureAmount(ctx context.Context, userID, accountID, chatID string, operation storagemodel.TreasuryOperationType, amount string) {
	record, err := r.service.CreateRequest(ctx, CreateRequestInput{
		OperationType:   operation,
		TelegramUserID:  userID,
		LedgerAccountID: accountID,
		ChatID:          chatID,
		Amount:          amount,
	})
	if err != nil {
		if record != nil {
			_ = r.sendText(ctx, chatID, pendingRequestMessage(record))
			r.dialogs.Set(userID, DialogSession{
				State:           DialogStateWaitingConfirmation,
				LedgerAccountID: accountID,
				RequestID:       record.RequestID,
			})
			return
		}
		var typed limitError
		if errors.As(err, &typed) {
			switch typed.LimitKind() {
			case "per_operation":
				_ = r.sendText(ctx, chatID, "*Amount exceeds allowed limit*\n\n*Max per operation:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
				return
			case "daily":
				_ = r.sendText(ctx, chatID, "*Daily amount limit exceeded*\n\n*Max per day:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
				return
			}
		}
		if errors.Is(err, merrors.ErrInvalidArg) {
			_ = r.sendText(ctx, chatID, "*Invalid amount*\n\n"+amountInputHint+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
			return
		}
		_ = r.sendText(ctx, chatID, "*Failed to create treasury request*\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
		return
	}
	if record == nil {
		_ = r.sendText(ctx, chatID, "*Failed to create treasury request*\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
		return
	}
	r.dialogs.Set(userID, DialogSession{
		State:           DialogStateWaitingConfirmation,
		LedgerAccountID: accountID,
		RequestID:       record.RequestID,
	})
	_ = r.sendText(ctx, chatID, confirmationPrompt(record))
}

func (r *Router) confirm(ctx context.Context, userID string, accountID string, chatID string) {
	requestID := ""
	if session, ok := r.dialogs.Get(userID); ok && strings.TrimSpace(session.RequestID) != "" {
		requestID = strings.TrimSpace(session.RequestID)
	} else {
		active, err := r.service.GetActiveRequestForAccount(ctx, accountID)
		if err == nil && active != nil {
			requestID = strings.TrimSpace(active.RequestID)
		}
	}
	if requestID == "" {
		_ = r.sendText(ctx, chatID, "*No pending treasury operation.*")
		return
	}
	record, err := r.service.ConfirmRequest(ctx, requestID, userID)
	if err != nil {
		_ = r.sendText(ctx, chatID, "*Unable to confirm treasury request.*\n\nUse "+markdownCommand(CommandCancel)+" or create a new request with "+markdownCommand(CommandFund)+" or "+markdownCommand(CommandWithdraw)+".")
		return
	}
	if r.tracker != nil {
		r.tracker.TrackScheduled(record)
	}
	r.dialogs.Clear(userID)
	delay := int64(r.service.ExecutionDelay().Seconds())
	if delay < 0 {
		delay = 0
	}
	_ = r.sendText(ctx, chatID,
		"*Operation confirmed*\n\n"+
			"*Execution:* scheduled in "+markdownCode(formatSeconds(delay))+".\n"+
			"You can cancel during this cooldown with "+markdownCommand(CommandCancel)+".\n\n"+
			"You will receive a follow-up message with execution success or failure.\n\n"+
			"*Request ID:* "+markdownCode(strings.TrimSpace(record.RequestID)))
}

func (r *Router) cancel(ctx context.Context, userID string, accountID string, chatID string) {
	requestID := ""
	if session, ok := r.dialogs.Get(userID); ok && strings.TrimSpace(session.RequestID) != "" {
		requestID = strings.TrimSpace(session.RequestID)
	} else {
		active, err := r.service.GetActiveRequestForAccount(ctx, accountID)
		if err == nil && active != nil {
			requestID = strings.TrimSpace(active.RequestID)
		}
	}
	if requestID == "" {
		r.dialogs.Clear(userID)
		_ = r.sendText(ctx, chatID, "*No pending treasury operation.*")
		return
	}
	record, err := r.service.CancelRequest(ctx, requestID, userID)
	if err != nil {
		_ = r.sendText(ctx, chatID, "*Unable to cancel treasury request.*")
		return
	}
	if r.tracker != nil {
		r.tracker.Untrack(record.RequestID)
	}
	r.dialogs.Clear(userID)
	_ = r.sendText(ctx, chatID, "*Operation cancelled*\n\n*Request ID:* "+markdownCode(strings.TrimSpace(record.RequestID)))
}

func (r *Router) sendText(ctx context.Context, chatID string, text string) error {
	if r == nil || r.send == nil {
		return nil
	}
	chatID = strings.TrimSpace(chatID)
	text = strings.TrimSpace(text)
	if chatID == "" || text == "" {
		return nil
	}
	if err := r.send(ctx, chatID, text); err != nil {
		if r.logger != nil {
			r.logger.Warn("Failed to send treasury bot response",
				zap.Error(err),
				zap.String("chat_id", chatID),
				zap.String("message_text", text))
		}
		return err
	}
	return nil
}

func (r *Router) logUnauthorized(update *model.TelegramWebhookUpdate) {
	if r == nil || r.logger == nil || update == nil || update.Message == nil {
		return
	}
	message := update.Message
	r.logger.Warn("unauthorized_access",
		zap.String("event", "unauthorized_access"),
		zap.String("telegram_user_id", strings.TrimSpace(message.FromUserID)),
		zap.String("chat_id", strings.TrimSpace(message.ChatID)),
		zap.String("message_text", strings.TrimSpace(message.Text)),
		zap.Time("timestamp", time.Now()),
	)
}

func pendingRequestMessage(record *storagemodel.TreasuryRequest) string {
	if record == nil {
		return "*Pending treasury operation already exists.*\n\nUse " + markdownCommand(CommandCancel) + "."
	}
	return "*Pending Treasury Operation*\n\n" +
		"*Account:* " + markdownCode(requestAccountDisplay(record)) + "\n" +
		"*Request ID:* " + markdownCode(strings.TrimSpace(record.RequestID)) + "\n" +
		"*Status:* " + markdownCode(strings.TrimSpace(string(record.Status))) + "\n" +
		"*Amount:* " + markdownCode(strings.TrimSpace(record.Amount)+" "+strings.TrimSpace(record.Currency)) + "\n\n" +
		"Wait for execution or cancel with " + markdownCommand(CommandCancel) + "."
}

func confirmationPrompt(record *storagemodel.TreasuryRequest) string {
	if record == nil {
		return "*Request created.*\n\nUse " + markdownCommand(CommandConfirm) + " or " + markdownCommand(CommandCancel) + "."
	}
	title := "*Funding request created.*"
	if record.OperationType == storagemodel.TreasuryOperationWithdraw {
		title = "*Withdrawal request created.*"
	}
	return title + "\n\n" +
		"*Account:* " + markdownCode(requestAccountDisplay(record)) + "\n" +
		"*Amount:* " + markdownCode(strings.TrimSpace(record.Amount)+" "+strings.TrimSpace(record.Currency)) + "\n\n" +
		confirmationCommandsMessage()
}

func welcomeMessage(profile *AccountProfile) string {
	accountCode := displayAccountCode(profile, "")
	currency := ""
	if profile != nil {
		currency = strings.ToUpper(strings.TrimSpace(profile.Currency))
	}
	if accountCode == "" {
		accountCode = "N/A"
	}
	if currency == "" {
		currency = "N/A"
	}
	return "*Sendico Treasury Bot*\n\n" +
		"*Attached account:* " + markdownCode(accountCode) + " (" + markdownCode(currency) + ")\n" +
		"Use " + markdownCommand(CommandFund) + " to credit your account and " + markdownCommand(CommandWithdraw) + " to debit it.\n" +
		"After entering an amount, use " + markdownCommand(CommandConfirm) + " or " + markdownCommand(CommandCancel) + ".\n" +
		"Use " + markdownCommand(CommandHelp) + " for detailed usage."
}

func amountPromptMessage(operation storagemodel.TreasuryOperationType, profile *AccountProfile, fallbackAccountID string) string {
	title := "*Funding request*"
	if operation == storagemodel.TreasuryOperationWithdraw {
		title = "*Withdrawal request*"
	}
	accountCode := displayAccountCode(profile, fallbackAccountID)
	currency := ""
	if profile != nil {
		currency = strings.ToUpper(strings.TrimSpace(profile.Currency))
	}
	if accountCode == "" {
		accountCode = "N/A"
	}
	if currency == "" {
		currency = "N/A"
	}
	return title + "\n\n" +
		"*Account:* " + markdownCode(accountCode) + " (" + markdownCode(currency) + ")\n\n" +
		amountInputHint
}

func requestAccountDisplay(record *storagemodel.TreasuryRequest) string {
	if record == nil {
		return ""
	}
	if code := strings.TrimSpace(record.LedgerAccountCode); code != "" {
		return code
	}
	return strings.TrimSpace(record.LedgerAccountID)
}

func displayAccountCode(profile *AccountProfile, fallbackAccountID string) string {
	if profile != nil {
		if code := strings.TrimSpace(profile.AccountCode); code != "" {
			return code
		}
		if id := strings.TrimSpace(profile.AccountID); id != "" {
			return id
		}
	}
	return strings.TrimSpace(fallbackAccountID)
}

func (r *Router) resolveAccountProfile(ctx context.Context, ledgerAccountID string) *AccountProfile {
	if r == nil || r.service == nil {
		return &AccountProfile{AccountID: strings.TrimSpace(ledgerAccountID)}
	}
	profile, err := r.service.GetAccountProfile(ctx, ledgerAccountID)
	if err != nil {
		if r.logger != nil {
			r.logger.Warn("Failed to resolve treasury account profile",
				zap.Error(err),
				zap.String("ledger_account_id", strings.TrimSpace(ledgerAccountID)))
		}
		return &AccountProfile{AccountID: strings.TrimSpace(ledgerAccountID)}
	}
	if profile == nil {
		return &AccountProfile{AccountID: strings.TrimSpace(ledgerAccountID)}
	}
	if strings.TrimSpace(profile.AccountID) == "" {
		profile.AccountID = strings.TrimSpace(ledgerAccountID)
	}
	return profile
}

func isChatAllowed(chatID string, allowedChatIDs []string) bool {
	chatID = strings.TrimSpace(chatID)
	if chatID == "" {
		return false
	}
	if len(allowedChatIDs) == 0 {
		return true
	}
	for _, allowed := range allowedChatIDs {
		if strings.TrimSpace(allowed) == chatID {
			return true
		}
	}
	return false
}

func formatSeconds(value int64) string {
	if value == 1 {
		return "1 second"
	}
	return strconv.FormatInt(value, 10) + " seconds"
}
