package mynalogsync

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"go.uber.org/zap"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/mynalog"
	"scrabble/backend/internal/payments"
)

// MailInterval is how often the letter queues are drained. Nothing here talks to the tax service,
// so a short tick costs only a local query.
const MailInterval = time.Minute

// mailBatch caps one drain, so a backlog is delivered over several ticks rather than in one burst
// through the relay.
const mailBatch = 50

// EmailLookup resolves an account's confirmed address. It is the seam onto the account domain,
// which the payments side must not import wholesale.
type EmailLookup interface {
	ConfirmedEmail(ctx context.Context, accountID uuid.UUID) (string, bool, error)
}

// Mailer tells buyers what happened to their purchase: that it went through, that a tax receipt
// was issued for it, and — if the money went back — that the receipt has been annulled.
//
// It is driven entirely by durable queues rather than by the payment path. The purchase letter
// rides the existing payment-event outbox on its own cursor, and the two receipt letters ride
// columns on the export row, so nothing in the crediting or refunding code had to change and no
// letter is lost to a relay hiccup.
type Mailer struct {
	payments *payments.Service
	emails   EmailLookup
	mail     account.Mailer
	from     string
	baseURL  string
	loc      *time.Location
	log      *zap.Logger
}

// NewMailer builds the buyer mailer. It returns nil when there is no relay or no sender address, so
// an installation without email simply does not write to buyers.
func NewMailer(svc *payments.Service, emails EmailLookup, mail account.Mailer,
	from, baseURL string, loc *time.Location, log *zap.Logger) *Mailer {

	if svc == nil || emails == nil || mail == nil || from == "" {
		return nil
	}
	if loc == nil {
		loc = time.UTC
	}
	return &Mailer{payments: svc, emails: emails, mail: mail, from: from,
		baseURL: baseURL, loc: loc, log: log}
}

// Run drains the letter queues until the context ends.
func (m *Mailer) Run(ctx context.Context, interval time.Duration) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			m.Tick(ctx)
		}
	}
}

// Tick drains one batch of each queue. Run calls it on a cadence; it is exported so a test can
// drive one drain deterministically instead of waiting on a ticker.
func (m *Mailer) Tick(ctx context.Context) {
	m.sendPurchaseMails(ctx)
	m.sendReceiptMails(ctx, false)
	m.sendReceiptMails(ctx, true)
}

// sendPurchaseMails tells buyers their purchase went through.
func (m *Mailer) sendPurchaseMails(ctx context.Context) {
	pending, err := m.payments.PendingPurchaseMails(ctx, mailBatch)
	if err != nil {
		m.log.Error("purchase mail queue could not be read", zap.Error(err))
		return
	}
	for _, p := range pending {
		// Only the direct rail writes to buyers: a store purchase is confirmed by the store itself,
		// and no tax receipt of ours follows it.
		if p.Origin != string(payments.SourceDirect) || p.OrderID == uuid.Nil {
			m.stampPurchase(ctx, p.EventID)
			continue
		}
		to, ok, err := m.emails.ConfirmedEmail(ctx, p.AccountID)
		if err != nil {
			m.log.Error("purchase mail address lookup failed",
				zap.String("event", p.EventID.String()), zap.Error(err))
			continue
		}
		if !ok {
			// A direct purchase requires a confirmed address, so this should not happen — but a
			// later address removal must not leave the event undecided forever.
			m.stampPurchase(ctx, p.EventID)
			continue
		}
		msg := account.Message{
			From:    m.from,
			To:      to,
			Subject: "Эрудит — покупка совершена",
			Text:    purchaseText(p),
		}
		if err := m.mail.Send(ctx, msg); err != nil {
			m.log.Error("purchase mail could not be delivered",
				zap.String("event", p.EventID.String()), zap.Error(err))
			continue // left undecided on purpose: the next tick tries again
		}
		m.stampPurchase(ctx, p.EventID)
	}
}

// stampPurchase records that the decision for an event has been made, whether or not a letter went.
func (m *Mailer) stampPurchase(ctx context.Context, eventID uuid.UUID) {
	if err := m.payments.MarkPurchaseMailed(ctx, eventID); err != nil {
		m.log.Error("purchase mail could not be marked",
			zap.String("event", eventID.String()), zap.Error(err))
	}
}

// sendReceiptMails delivers the tax-receipt letter, or the annulment letter when cancelled is true.
func (m *Mailer) sendReceiptMails(ctx context.Context, cancelled bool) {
	read := m.payments.PendingReceiptMails
	mark := m.payments.MarkReceiptMailed
	if cancelled {
		read = m.payments.PendingCancelMails
		mark = m.payments.MarkCancelMailed
	}
	pending, err := read(ctx, mailBatch)
	if err != nil {
		m.log.Error("receipt mail queue could not be read", zap.Error(err))
		return
	}
	if len(pending) == 0 {
		return
	}
	stored, ok, err := m.payments.MyNalogSession(ctx)
	if err != nil || !ok {
		// The taxpayer id lives on the session and the receipt link cannot be built without it.
		// Signing back in makes these letters go out; until then they simply wait.
		return
	}
	for _, r := range pending {
		to, ok, err := m.emails.ConfirmedEmail(ctx, r.AccountID)
		if err != nil {
			m.log.Error("receipt mail address lookup failed",
				zap.String("ledger", r.LedgerID.String()), zap.Error(err))
			continue
		}
		if !ok {
			m.stampReceipt(ctx, mark, r.LedgerID)
			continue
		}
		subject, body := "Эрудит — Ваш чек покупки", receiptText(r, stored.INN, m.baseURL, m.loc)
		if cancelled {
			subject, body = "Эрудит — чек аннулирован", cancelText(r, m.loc)
		}
		if err := m.mail.Send(ctx, account.Message{From: m.from, To: to, Subject: subject, Text: body}); err != nil {
			m.log.Error("receipt mail could not be delivered",
				zap.String("ledger", r.LedgerID.String()), zap.Error(err))
			continue
		}
		m.stampReceipt(ctx, mark, r.LedgerID)
	}
}

// stampReceipt records that the letter decision for an income has been made.
func (m *Mailer) stampReceipt(ctx context.Context, mark func(context.Context, uuid.UUID) error, ledgerID uuid.UUID) {
	if err := mark(ctx, ledgerID); err != nil {
		m.log.Error("receipt mail could not be marked",
			zap.String("ledger", ledgerID.String()), zap.Error(err))
	}
}

// describe renders what was bought in one line: the catalog title exactly as the buyer saw it on
// the storefront, then the price.
//
// The pack size is deliberately NOT appended. Catalog titles already name it ("50 фишек"), so
// adding it produced "50 фишек — 50 фишек". The title is the single source of that wording; the
// tax receipt states the quantity separately in the form the tax service wants (see
// mynalog.IncomeName), which is a different surface with different requirements.
func describe(title string, amountMinor int64, currency string) string {
	what := strings.TrimSpace(title)
	if what == "" {
		what = "покупка в игре"
	}
	return fmt.Sprintf("%s, %s", what, formatAmount(amountMinor, currency))
}

// formatAmount renders a minor-unit amount for a buyer, in roubles where that is the currency.
func formatAmount(minor int64, currency string) string {
	money, err := payments.MoneyFromMinor(minor, payments.Currency(currency))
	if err != nil {
		return ""
	}
	if money.Currency() == payments.CurrencyRUB {
		return money.Major() + " ₽"
	}
	return money.String()
}

// purchaseText is the confirmation sent the moment chips are credited.
//
// It says plainly that it is not the fiscal receipt. The tax receipt is issued by a separate
// service on its own schedule, and a buyer who took this letter for the receipt would be misled —
// so the wait is stated up front rather than left to be discovered.
func purchaseText(p payments.PurchaseMail) string {
	return fmt.Sprintf(`Здравствуйте!

Покупка совершена: %s.
Номер заказа: %s

Это письмо — подтверждение покупки, а не чек. Чек от налоговой службы придёт
отдельным письмом: обычно в течение часа, но иногда это занимает до нескольких дней.

Спасибо, что играете в «Эрудит».
`, describe(p.Title, p.AmountMinor, p.Currency), p.OrderID)
}

// receiptText is the fiscal receipt letter, which is how the receipt is actually handed to the
// buyer as the professional-income tax regime requires.
//
// It is deliberately bare: the amount, the moment of payment with its offset, and the link. The
// receipt itself is the document and it opens without authentication, so repeating its contents
// here would only add something for the two to disagree about.
func receiptText(r payments.ReceiptMail, inn, baseURL string, loc *time.Location) string {
	return fmt.Sprintf(`Здравствуйте!

Покупка на сумму: %s от %s
Ссылка на чек: %s

Спасибо, что играете в «Эрудит».
`, formatAmount(r.AmountMinor, r.Currency),
		r.OperationTime.In(loc).Format("02.01.2006 15:04 -07:00"),
		mynalog.ReceiptURL(baseURL, inn, r.ReceiptUUID))
}

// cancelText tells a buyer their receipt has been annulled after a refund, so that the link they
// were sent earlier does not simply stop working without explanation.
func cancelText(r payments.ReceiptMail, loc *time.Location) string {
	return fmt.Sprintf(`Здравствуйте!

Деньги по покупке возвращены, поэтому чек в налоговой службе аннулирован —
ранее присланная ссылка на него больше не действует.

%s
Дата покупки: %s
`, describe(r.Title, r.AmountMinor, r.Currency),
		r.OperationTime.In(loc).Format("02.01.2006 15:04"))
}
