package mynalogsync

import (
	"context"
	"fmt"
	"sync"
	"time"

	"go.uber.org/zap"

	"scrabble/backend/internal/payments"
)

// WatchdogInterval is how often the filing deadline is checked. It reads only local tables, so the
// cost is one query a day.
const WatchdogInterval = 24 * time.Hour

// Days of the month at which unfiled income of the previous month escalates.
//
// The professional-income statute gives until the 9th of the following month only for settlements
// that do NOT involve an electronic means of payment; a card payment is one, so the receipt is
// strictly due at the moment of settlement. The automatic export satisfies both readings, and this
// watchdog is the backstop for when it is switched off or has stopped — which is exactly when the
// deadline stops taking care of itself.
const (
	warnDay    = 1
	alarmDay   = 5
	overdueDay = 9
)

// Watchdog warns when income from a closed month is still not registered with the tax service.
//
// It runs whether or not the automatic export does, because the case it exists for is the automatic
// export being off, paused or broken. Nothing here contacts the tax service.
type Watchdog struct {
	payments *payments.Service
	alert    Alerter
	loc      *time.Location
	log      *zap.Logger
	now      func() time.Time

	// mu guards sent, an in-process record of what has already been raised. The watchdog ticks
	// daily, so this only prevents a restart from re-alerting within the same day.
	mu   sync.Mutex
	sent map[string]time.Time
}

// NewWatchdog builds the deadline watchdog. It returns nil without a payments service or an alert
// channel, since it has nothing to watch or nowhere to say it.
func NewWatchdog(svc *payments.Service, alert Alerter, loc *time.Location, log *zap.Logger) *Watchdog {
	if svc == nil || alert == nil {
		return nil
	}
	if loc == nil {
		loc = time.UTC
	}
	return &Watchdog{
		payments: svc, alert: alert, loc: loc, log: log,
		now:  func() time.Time { return time.Now().UTC() },
		sent: map[string]time.Time{},
	}
}

// Run checks the deadline now and then once per interval, until the context ends. It checks on
// start too: a deployment that lands on the 6th should not wait a day to mention a month of
// unfiled income.
func (w *Watchdog) Run(ctx context.Context, interval time.Duration) {
	w.tick(ctx)
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			w.tick(ctx)
		}
	}
}

// tick raises the appropriate alert for whatever of the previous month is still unfiled.
func (w *Watchdog) tick(ctx context.Context) {
	now := w.now().In(w.loc)
	kind, heading := escalation(now.Day())
	if kind == "" {
		return
	}
	monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, w.loc)
	backlog, err := w.payments.MyNalogBacklogBefore(ctx, monthStart)
	if err != nil {
		w.log.Error("fiscal deadline check failed", zap.Error(err))
		return
	}
	if backlog.Count == 0 {
		return
	}
	w.raise(ctx, kind, "«Мой налог»: "+heading, w.body(heading, backlog, monthStart))
}

// escalation maps the day of the month onto an alert kind and its heading, or an empty kind on the
// days between the warning and the alarm, when there is nothing new to say.
func escalation(day int) (kind, heading string) {
	switch {
	case day >= overdueDay:
		return alertBacklogOverdue, "срок подачи вышел"
	case day >= alarmDay:
		return alertBacklogDue, "чеки за прошлый месяц не отправлены"
	case day >= warnDay && day < alarmDay:
		if day == warnDay {
			return alertBacklog, "остались неотправленные чеки за прошлый месяц"
		}
	}
	return "", ""
}

// body renders the alert, naming what is owed and by when.
func (w *Watchdog) body(heading string, backlog payments.MyNalogBacklog, monthStart time.Time) string {
	deadline := time.Date(monthStart.Year(), monthStart.Month(), overdueDay, 0, 0, 0, 0, w.loc)
	return fmt.Sprintf(`%s.

Не зарегистрировано в «Мой налог»: %d приход(ов) на сумму %s.
Самый старый — от %s.

Крайний срок по закону — %s. Оплата картой считается расчётом электронным средством
платежа, поэтому чек по-хорошему формируется в момент расчёта, а не к этой дате.

Что сделать: в админ-консоли, раздел «Мой налог», войти в кабинет и запустить выгрузку
(или включить автоматический режим, если он выключен).`,
		heading, backlog.Count, formatAmount(backlog.AmountMinor, string(payments.CurrencyRUB)),
		backlog.Oldest.In(w.loc).Format("02.01.2006"), deadline.Format("02.01.2006"))
}

// raise sends an alert unless one of the same kind already went out within the day.
func (w *Watchdog) raise(ctx context.Context, kind, subject, body string) {
	w.mu.Lock()
	last, seen := w.sent[kind]
	now := w.now()
	if seen && now.Sub(last) < 24*time.Hour {
		w.mu.Unlock()
		return
	}
	w.sent[kind] = now
	w.mu.Unlock()

	if err := w.alert.Alert(ctx, kind, subject, body); err != nil {
		w.log.Error("fiscal deadline alert could not be delivered", zap.String("kind", kind), zap.Error(err))
	}
}
