package notificationimp

import (
	"context"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/tech/sendico/notification/internal/server/notificationimp/telegram"
	"github.com/tech/sendico/pkg/merrors"
	msg "github.com/tech/sendico/pkg/messaging"
	confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.uber.org/zap"
)

const (
	defaultConfirmationTimeout = 120 * time.Second
)

type confirmationManager struct {
	logger mlogger.Logger
	tg     telegram.Client
	sender string
	outbox msg.Producer

	mu               sync.Mutex
	pendingByMessage map[string]*confirmationState
	pendingByRequest map[string]*confirmationState
}

type confirmationState struct {
	request          model.ConfirmationRequest
	requestMessageID string
	targetChatID     string
	callbackSubject  string
	clarified        bool
	timer            *time.Timer
}

func newConfirmationManager(logger mlogger.Logger, tg telegram.Client, outbox msg.Producer) *confirmationManager {
	if logger != nil {
		logger = logger.Named("confirmations")
	}
	return &confirmationManager{
		logger:           logger,
		tg:               tg,
		outbox:           outbox,
		sender:           string(mservice.Notifications),
		pendingByMessage: map[string]*confirmationState{},
		pendingByRequest: map[string]*confirmationState{},
	}
}

func (m *confirmationManager) Stop() {
	if m == nil {
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	for _, state := range m.pendingByMessage {
		if state.timer != nil {
			state.timer.Stop()
		}
	}
	m.pendingByMessage = map[string]*confirmationState{}
	m.pendingByRequest = map[string]*confirmationState{}
}

func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error {
	if m == nil {
		return merrors.Internal("confirmation manager is nil")
	}
	if request == nil {
		return merrors.InvalidArgument("confirmation request is nil", "request")
	}
	if m.tg == nil {
		return merrors.InvalidArgument("telegram client is not configured", "telegram")
	}

	req := normalizeConfirmationRequest(*request)
	if req.RequestID == "" {
		return merrors.InvalidArgument("confirmation request_id is required", "request_id")
	}
	if req.TargetChatID == "" {
		return merrors.InvalidArgument("confirmation target_chat_id is required", "target_chat_id")
	}
	if req.RequestedMoney == nil || strings.TrimSpace(req.RequestedMoney.Amount) == "" || strings.TrimSpace(req.RequestedMoney.Currency) == "" {
		return merrors.InvalidArgument("confirmation requested_money is required", "requested_money")
	}
	if req.SourceService == "" {
		return merrors.InvalidArgument("confirmation source_service is required", "source_service")
	}

	m.mu.Lock()
	if _, ok := m.pendingByRequest[req.RequestID]; ok {
		m.mu.Unlock()
		m.logger.Info("Confirmation request already pending", zap.String("request_id", req.RequestID))
		return nil
	}
	m.mu.Unlock()

	message := confirmationPrompt(&req)
	sent, err := m.tg.SendText(ctx, req.TargetChatID, message, "")
	if err != nil {
		m.logger.Warn("Failed to send confirmation request to Telegram", zap.Error(err), zap.String("request_id", req.RequestID))
		return err
	}
	if sent == nil || strings.TrimSpace(sent.MessageID) == "" {
		return merrors.Internal("telegram confirmation message id is missing")
	}

	state := &confirmationState{
		request:          req,
		requestMessageID: strings.TrimSpace(sent.MessageID),
		targetChatID:     strings.TrimSpace(req.TargetChatID),
		callbackSubject:  confirmationCallbackSubject(req.SourceService, req.Rail),
	}
	timeout := time.Duration(req.TimeoutSeconds) * time.Second
	if timeout <= 0 {
		timeout = defaultConfirmationTimeout
	}
	state.timer = time.AfterFunc(timeout, func() {
		m.handleTimeout(state.requestMessageID)
	})

	m.mu.Lock()
	m.pendingByMessage[state.requestMessageID] = state
	m.pendingByRequest[req.RequestID] = state
	m.mu.Unlock()

	m.logger.Info("Confirmation request sent", zap.String("request_id", req.RequestID), zap.String("message_id", state.requestMessageID), zap.String("callback_subject", state.callbackSubject))
	return nil
}

func (m *confirmationManager) HandleUpdate(ctx context.Context, update *telegram.Update) {
	if m == nil || update == nil || update.Message == nil {
		return
	}
	message := update.Message
	if message.ReplyToMessage == nil {
		return
	}

	replyToID := strconv.FormatInt(message.ReplyToMessage.MessageID, 10)
	state := m.lookupByMessageID(replyToID)
	if state == nil {
		return
	}

	chatID := strconv.FormatInt(message.Chat.ID, 10)
	if chatID != state.targetChatID {
		m.logger.Debug("Telegram reply ignored: chat mismatch", zap.String("expected_chat_id", state.targetChatID), zap.String("chat_id", chatID))
		return
	}

	rawReply := message.ToModel()
	if !state.isUserAllowed(message.From) {
		m.publishResult(state, &model.ConfirmationResult{
			RequestID:  state.request.RequestID,
			Status:     model.ConfirmationStatusRejected,
			ParseError: "unauthorized_user",
			RawReply:   rawReply,
		})
		m.sendNotice(ctx, state, rawReply, "Only approved users can confirm this payment.")
		m.removeState(state.requestMessageID)
		return
	}

	money, reason, err := parseConfirmationReply(message.Text)
	if err != nil {
		m.mu.Lock()
		state.clarified = true
		m.mu.Unlock()
		m.sendNotice(ctx, state, rawReply, clarificationMessage(reason))
		return
	}

	m.mu.Lock()
	clarified := state.clarified
	m.mu.Unlock()
	status := model.ConfirmationStatusConfirmed
	if clarified {
		status = model.ConfirmationStatusClarified
	}
	m.publishResult(state, &model.ConfirmationResult{
		RequestID: state.request.RequestID,
		Money:     money,
		RawReply:  rawReply,
		Status:    status,
	})
	m.removeState(state.requestMessageID)
}

func (m *confirmationManager) lookupByMessageID(messageID string) *confirmationState {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.pendingByMessage[strings.TrimSpace(messageID)]
}

func (m *confirmationManager) handleTimeout(messageID string) {
	state := m.lookupByMessageID(messageID)
	if state == nil {
		return
	}
	m.publishResult(state, &model.ConfirmationResult{
		RequestID: state.request.RequestID,
		Status:    model.ConfirmationStatusTimeout,
	})
	m.removeState(messageID)
}

func (m *confirmationManager) removeState(messageID string) {
	messageID = strings.TrimSpace(messageID)
	if messageID == "" {
		return
	}
	m.mu.Lock()
	state := m.pendingByMessage[messageID]
	if state != nil && state.timer != nil {
		state.timer.Stop()
	}
	delete(m.pendingByMessage, messageID)
	if state != nil {
		delete(m.pendingByRequest, state.request.RequestID)
	}
	m.mu.Unlock()
}

func (m *confirmationManager) publishResult(state *confirmationState, result *model.ConfirmationResult) {
	if m == nil || state == nil || result == nil {
		return
	}
	if m.outbox == nil {
		m.logger.Warn("Confirmation result skipped: producer not configured", zap.String("request_id", state.request.RequestID))
		return
	}
	env := confirmations.ConfirmationResult(m.sender, result, state.request.SourceService, state.request.Rail)
	if err := m.outbox.SendMessage(env); err != nil {
		m.logger.Warn("Failed to publish confirmation result", zap.Error(err), zap.String("request_id", state.request.RequestID))
		return
	}
	m.logger.Info("Confirmation result published", zap.String("request_id", state.request.RequestID), zap.String("status", string(result.Status)))
}

func (m *confirmationManager) sendNotice(ctx context.Context, state *confirmationState, reply *model.TelegramMessage, text string) {
	if m == nil || m.tg == nil || state == nil {
		return
	}
	replyID := ""
	if reply != nil {
		replyID = reply.MessageID
	}
	if _, err := m.tg.SendText(ctx, state.targetChatID, text, replyID); err != nil {
		m.logger.Warn("Failed to send clarification notice", zap.Error(err), zap.String("request_id", state.request.RequestID))
	}
}

func (s *confirmationState) isUserAllowed(user *telegram.User) bool {
	if s == nil {
		return false
	}
	allowed := s.request.AcceptedUserIDs
	if len(allowed) == 0 {
		return true
	}
	if user == nil {
		return false
	}
	userID := strconv.FormatInt(user.ID, 10)
	for _, id := range allowed {
		if id == userID {
			return true
		}
	}
	return false
}

func confirmationCallbackSubject(sourceService, rail string) string {
	sourceService = strings.ToLower(strings.TrimSpace(sourceService))
	if sourceService == "" {
		sourceService = "unknown"
	}
	rail = strings.ToLower(strings.TrimSpace(rail))
	if rail == "" {
		rail = "default"
	}
	return "confirmations." + sourceService + "." + rail
}

func normalizeConfirmationRequest(request model.ConfirmationRequest) model.ConfirmationRequest {
	request.RequestID = strings.TrimSpace(request.RequestID)
	request.TargetChatID = strings.TrimSpace(request.TargetChatID)
	request.PaymentIntentID = strings.TrimSpace(request.PaymentIntentID)
	request.QuoteRef = strings.TrimSpace(request.QuoteRef)
	request.SourceService = strings.TrimSpace(request.SourceService)
	request.Rail = strings.TrimSpace(request.Rail)
	request.AcceptedUserIDs = normalizeStringList(request.AcceptedUserIDs)
	if request.RequestedMoney != nil {
		request.RequestedMoney.Amount = strings.TrimSpace(request.RequestedMoney.Amount)
		request.RequestedMoney.Currency = strings.TrimSpace(request.RequestedMoney.Currency)
	}
	return request
}

func normalizeStringList(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	result := make([]string, 0, len(values))
	seen := map[string]struct{}{}
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	if len(result) == 0 {
		return nil
	}
	return result
}

var amountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`)
var currencyPattern = regexp.MustCompile(`^[A-Za-z]{3,10}$`)

func parseConfirmationReply(text string) (*paymenttypes.Money, string, error) {
	text = strings.TrimSpace(text)
	if text == "" {
		return nil, "empty", merrors.InvalidArgument("empty reply")
	}
	parts := strings.Fields(text)
	if len(parts) < 2 {
		if len(parts) == 1 && amountPattern.MatchString(parts[0]) {
			return nil, "missing_currency", merrors.InvalidArgument("currency is required")
		}
		return nil, "missing_amount", merrors.InvalidArgument("amount is required")
	}
	if len(parts) > 2 {
		return nil, "format", merrors.InvalidArgument("reply format is invalid")
	}
	amount := parts[0]
	currency := parts[1]
	if !amountPattern.MatchString(amount) {
		return nil, "invalid_amount", merrors.InvalidArgument("amount format is invalid")
	}
	if !currencyPattern.MatchString(currency) {
		return nil, "invalid_currency", merrors.InvalidArgument("currency format is invalid")
	}
	return &paymenttypes.Money{
		Amount:   amount,
		Currency: strings.ToUpper(currency),
	}, "", nil
}

func confirmationPrompt(req *model.ConfirmationRequest) string {
	var builder strings.Builder
	builder.WriteString("Payment confirmation required\n")
	if req.PaymentIntentID != "" {
		builder.WriteString("Payment intent: ")
		builder.WriteString(req.PaymentIntentID)
		builder.WriteString("\n")
	}
	if req.QuoteRef != "" {
		builder.WriteString("Quote ref: ")
		builder.WriteString(req.QuoteRef)
		builder.WriteString("\n")
	}
	if req.RequestedMoney != nil {
		builder.WriteString("Requested: ")
		builder.WriteString(req.RequestedMoney.Amount)
		builder.WriteString(" ")
		builder.WriteString(req.RequestedMoney.Currency)
		builder.WriteString("\n")
	}
	builder.WriteString("Reply with \"<amount> <currency>\" (e.g., 12.34 USD).")
	return builder.String()
}

func clarificationMessage(reason string) string {
	switch reason {
	case "missing_currency":
		return "Currency code is required. Reply with \"<amount> <currency>\" (e.g., 12.34 USD)."
	case "missing_amount":
		return "Amount is required. Reply with \"<amount> <currency>\" (e.g., 12.34 USD)."
	case "invalid_amount":
		return "Amount must be a decimal number. Reply with \"<amount> <currency>\" (e.g., 12.34 USD)."
	case "invalid_currency":
		return "Currency must be a code like USD or EUR. Reply with \"<amount> <currency>\"."
	default:
		return "Reply with \"<amount> <currency>\" (e.g., 12.34 USD)."
	}
}
