package payments

import (
	"context"
	"encoding/json"
	"time"

	"github.com/google/uuid"
)

// Export statuses of one income on its way into the professional-income tax register. They are
// deliberately not a subset of the order or ledger statuses: registering an income with the tax
// service is a separate obligation with its own failure modes, and the ledger is append-only, so
// this progress cannot live there.
const (
	// MyNalogSending is written before the registration call. It exists so that a process that dies
	// mid-call leaves evidence: on the next run the row is promoted to MyNalogUnknown rather than
	// being retried blindly, because the call is not idempotent.
	MyNalogSending = "sending"
	// MyNalogSent means the tax service issued a receipt and we hold its identifier.
	MyNalogSent = "sent"
	// MyNalogUnknown means the outcome could not be established: the call failed and the follow-up
	// probe did not find our receipt. It halts the queue until a human resolves it, because the only
	// alternatives are filing the income twice or losing it.
	MyNalogUnknown = "unknown"
	// MyNalogFailed is a rejection we understand and may retry later.
	MyNalogFailed = "failed"
	// MyNalogNotRequired marks an income the buyer got back before it was ever registered. Nothing
	// is filed and nothing is annulled — the two cancel out.
	MyNalogNotRequired = "not_required"
)

// Annulment statuses of a registered receipt whose money was returned.
const (
	MyNalogCancelNone   = "none"
	MyNalogCancelled    = "cancelled"
	MyNalogCancelFailed = "failed"
)

// myNalogProvider is the only rail this export covers: the direct rouble rail settled through
// YooKassa. Store rails are excluded on purpose — the ledger holds no rouble amount for them.
const myNalogProvider = "yookassa"

// MyNalogIncome is one rouble income the tax register may still be owed. Chips and Title come from
// the ledger snapshot, which is what the receipt description is built from. Status is empty when
// the income has never been acted on.
type MyNalogIncome struct {
	LedgerID      uuid.UUID
	OrderID       uuid.UUID
	AccountID     uuid.UUID
	Chips         int
	Title         string
	Amount        Money
	OperationTime time.Time

	Status      string
	ReceiptName string
	ReceiptUUID string
	Attempts    int
	LastError   string
}

// MyNalogCancel is one registered receipt whose income has since been refunded and which must
// therefore be annulled. LedgerID names the income row, RefundLedgerID the refund that voided it.
type MyNalogCancel struct {
	LedgerID       uuid.UUID
	RefundLedgerID uuid.UUID
	OrderID        uuid.UUID
	AccountID      uuid.UUID
	ReceiptUUID    string
	// CancelStatus is MyNalogCancelFailed when a previous attempt was refused, so the console can
	// show that this one is a retry rather than a first try.
	CancelStatus string
	LastError    string
}

// MyNalogSession is the stored tax-cabinet session and the rail's operating mode. RefreshTokenEnc
// is opaque here: the payments domain stores the sealed bytes without holding the key, so the
// credential never becomes readable through a query on this schema.
type MyNalogSession struct {
	INN             string
	DeviceID        string
	RefreshTokenEnc []byte
	AutoEnabled     bool
	PausedAt        *time.Time
	PauseReason     string
	LastOKAt        *time.Time
	LastAlertAt     *time.Time
	LastAlertKind   string
	UpdatedAt       time.Time
}

// Paused reports whether the rail has taken itself out of service.
func (s MyNalogSession) Paused() bool { return s.PausedAt != nil }

// MyNalogBacklog summarises income still owed to the tax register up to a cut-off. It is what the
// fiscal-period watchdog escalates on, so it carries enough to write a useful alert without a
// second query.
type MyNalogBacklog struct {
	Count       int
	AmountMinor int64
	Oldest      time.Time
}

// MyNalogQueue is everything one export run has to consider, read in a single pass so the console
// and the worker see the same picture.
type MyNalogQueue struct {
	Incomes     []MyNalogIncome
	Cancels     []MyNalogCancel
	NotRequired []MyNalogIncome
	Attention   []MyNalogIncome
}

// Empty reports whether there is nothing to do. The export engine checks this before authenticating,
// so an idle installation sends no traffic to the tax service at all.
func (q MyNalogQueue) Empty() bool {
	return len(q.Incomes) == 0 && len(q.Cancels) == 0 && len(q.NotRequired) == 0
}

// PurchaseMail is one credited purchase whose buyer has not yet been told. It rides the existing
// payment-event outbox on its own cursor, so no payment path had to be modified to send it.
type PurchaseMail struct {
	EventID     uuid.UUID
	AccountID   uuid.UUID
	OrderID     uuid.UUID
	Origin      string
	Title       string
	AmountMinor int64
	Currency    string
	CreatedAt   time.Time
}

// ReceiptMail is one registered (or annulled) receipt whose buyer has not yet been told.
type ReceiptMail struct {
	LedgerID      uuid.UUID
	AccountID     uuid.UUID
	ReceiptUUID   string
	Title         string
	AmountMinor   int64
	Currency      string
	OperationTime time.Time
}

// myNalogSnapshot is the part of a fund ledger row's snapshot the receipt description needs.
type myNalogSnapshot struct {
	Title string `json:"title"`
	Chips int    `json:"chips"`
}

// describeSnapshot recovers the product title and pack size a fund row recorded. A snapshot that
// cannot be read yields zeroes, and the caller falls back to a generic description rather than
// refusing to file the income.
func describeSnapshot(snapshot string) (title string, chips int) {
	if snapshot == "" {
		return "", 0
	}
	var snap myNalogSnapshot
	if err := json.Unmarshal([]byte(snapshot), &snap); err != nil {
		return "", 0
	}
	return snap.Title, snap.Chips
}

// MyNalogQueue reads everything an export run must consider, capped at limit incomes and limit
// cancels. Nothing here calls out to the tax service, so it is safe to run on every tick.
func (s *Service) MyNalogQueue(ctx context.Context, limit int) (MyNalogQueue, error) {
	return s.store.myNalogQueue(ctx, limit)
}

// MyNalogIncomeAt reads one taxable income by its ledger row, reporting false when that row is not
// one this export is responsible for. It backs the per-row console actions, which must not depend on
// the row still being inside the paged queue.
func (s *Service) MyNalogIncomeAt(ctx context.Context, ledgerID uuid.UUID) (MyNalogIncome, bool, error) {
	return s.store.myNalogIncomeByLedger(ctx, ledgerID)
}

// MyNalogBacklogBefore summarises income received before the cut-off that is still not registered.
// The fiscal-period watchdog uses it to escalate as the filing deadline approaches.
func (s *Service) MyNalogBacklogBefore(ctx context.Context, before time.Time) (MyNalogBacklog, error) {
	return s.store.myNalogBacklogBefore(ctx, before)
}

// PromoteStrandedSending turns rows left in MyNalogSending by a process that died mid-call into
// MyNalogUnknown, and reports how many. The caller holds the export lock, so any such row is
// genuinely orphaned rather than in flight — and since the registration call is not idempotent, a
// blind retry is exactly what must not happen.
func (s *Service) PromoteStrandedSending(ctx context.Context) (int, error) {
	return s.store.promoteStrandedSending(ctx, s.clock())
}

// BeginMyNalogSend records the intent to register income, freezing the receipt name before the call
// leaves. The name is the only handle the recovery probe has, so it must be durable before the
// request, not after it.
func (s *Service) BeginMyNalogSend(ctx context.Context, in MyNalogIncome, receiptName string) error {
	return s.store.beginMyNalogSend(ctx, in, receiptName, s.clock())
}

// MarkMyNalogSent records a receipt the tax service issued. manual is true when an operator filed
// the income by hand and typed the identifier back in — such a row is as good as an automatic one,
// and in particular its receipt can still be annulled automatically on a refund.
func (s *Service) MarkMyNalogSent(ctx context.Context, ledgerID uuid.UUID, receiptUUID string, manual bool) error {
	return s.store.markMyNalogSent(ctx, ledgerID, receiptUUID, manual, s.clock())
}

// MarkMyNalogManual records an income an operator filed by hand, creating the row when the export
// never got as far as attempting it.
func (s *Service) MarkMyNalogManual(ctx context.Context, in MyNalogIncome, receiptName, receiptUUID string) error {
	return s.store.markMyNalogManual(ctx, in, receiptName, receiptUUID, s.clock())
}

// MarkMyNalogOutcome records a non-final outcome: MyNalogUnknown when the result could not be
// established, MyNalogFailed when it was understood and may be retried.
func (s *Service) MarkMyNalogOutcome(ctx context.Context, ledgerID uuid.UUID, status, reason string) error {
	return s.store.markMyNalogOutcome(ctx, ledgerID, status, reason, s.clock())
}

// MarkMyNalogNotRequired records that an income needs no receipt because it was refunded before one
// was ever issued.
func (s *Service) MarkMyNalogNotRequired(ctx context.Context, in MyNalogIncome, reason string) error {
	return s.store.markMyNalogNotRequired(ctx, in, reason, s.clock())
}

// MarkMyNalogCancelled records that a receipt was annulled for the given refund.
func (s *Service) MarkMyNalogCancelled(ctx context.Context, ledgerID, refundLedgerID uuid.UUID) error {
	return s.store.markMyNalogCancel(ctx, ledgerID, refundLedgerID, MyNalogCancelled, "", s.clock())
}

// MarkMyNalogCancelFailed records that an annulment was refused, leaving the receipt standing.
func (s *Service) MarkMyNalogCancelFailed(ctx context.Context, ledgerID, refundLedgerID uuid.UUID, reason string) error {
	return s.store.markMyNalogCancel(ctx, ledgerID, refundLedgerID, MyNalogCancelFailed, reason, s.clock())
}

// SaveMyNalogSession stores the tax-cabinet session, preserving the operating mode across a
// re-login and clearing any self-imposed pause — signing in again is the operator saying the
// problem is dealt with.
func (s *Service) SaveMyNalogSession(ctx context.Context, inn, deviceID string, refreshTokenEnc []byte) error {
	return s.store.saveMyNalogSession(ctx, inn, deviceID, refreshTokenEnc, s.clock())
}

// UpdateMyNalogRefreshToken replaces the stored refresh token after the service rotated it. Losing
// a rotated token would strand the next renewal and force a password login.
func (s *Service) UpdateMyNalogRefreshToken(ctx context.Context, refreshTokenEnc []byte) error {
	return s.store.updateMyNalogRefreshToken(ctx, refreshTokenEnc, s.clock())
}

// MyNalogSession reads the stored session, reporting false when the rail has never been signed in.
func (s *Service) MyNalogSession(ctx context.Context) (MyNalogSession, bool, error) {
	return s.store.myNalogSession(ctx)
}

// DropMyNalogSession signs the rail out. The operating mode goes with it: without a session there
// is nothing for the automatic mode to run on.
func (s *Service) DropMyNalogSession(ctx context.Context) error {
	return s.store.dropMyNalogSession(ctx)
}

// SetMyNalogAuto arms or disarms the automatic export.
func (s *Service) SetMyNalogAuto(ctx context.Context, enabled bool) error {
	return s.store.setMyNalogAuto(ctx, enabled, s.clock())
}

// PauseMyNalog takes the rail out of service with a reason an operator can read. It is how a run
// reacts to a fault that repeating cannot fix, so that a changed API cannot turn into thousands of
// futile requests overnight.
func (s *Service) PauseMyNalog(ctx context.Context, reason string) error {
	return s.store.pauseMyNalog(ctx, reason, s.clock())
}

// ResumeMyNalog clears a self-imposed pause after an operator has dealt with the cause.
func (s *Service) ResumeMyNalog(ctx context.Context) error {
	return s.store.resumeMyNalog(ctx, s.clock())
}

// NoteMyNalogOK records a successful exchange with the tax service, which is what the "has it been
// down for a day?" test measures against.
func (s *Service) NoteMyNalogOK(ctx context.Context) error {
	return s.store.noteMyNalogOK(ctx, s.clock())
}

// NoteMyNalogAlert records that an alert of the given kind was sent, so a persistent fault produces
// one letter a day rather than one per run.
func (s *Service) NoteMyNalogAlert(ctx context.Context, kind string) error {
	return s.store.noteMyNalogAlert(ctx, kind, s.clock())
}

// TryLockMyNalog takes the advisory lock guarding an export run, reporting false when another run
// already holds it. Releasing it is the caller's job; the returned release function is safe to call
// exactly once.
func (s *Service) TryLockMyNalog(ctx context.Context) (release func(), ok bool, err error) {
	return s.store.tryLockMyNalog(ctx)
}

// PendingPurchaseMails reads credited purchases whose buyer has not been written to yet.
func (s *Service) PendingPurchaseMails(ctx context.Context, limit int) ([]PurchaseMail, error) {
	return s.store.pendingPurchaseMails(ctx, limit)
}

// MarkPurchaseMailed records that the mail decision for an event has been made. It is stamped even
// when no letter was sent — a store-rail purchase, or a buyer with no confirmed address — because
// the column tracks the decision, not the delivery, and an undecided event would be reconsidered
// forever.
func (s *Service) MarkPurchaseMailed(ctx context.Context, eventID uuid.UUID) error {
	return s.store.markPurchaseMailed(ctx, eventID, s.clock())
}

// PendingReceiptMails reads registered receipts whose buyer has not been sent the receipt yet.
func (s *Service) PendingReceiptMails(ctx context.Context, limit int) ([]ReceiptMail, error) {
	return s.store.pendingMyNalogMails(ctx, limit, false)
}

// PendingCancelMails reads annulled receipts whose buyer has not been told the receipt is void.
func (s *Service) PendingCancelMails(ctx context.Context, limit int) ([]ReceiptMail, error) {
	return s.store.pendingMyNalogMails(ctx, limit, true)
}

// MarkReceiptMailed records that the receipt letter decision for an income has been made.
func (s *Service) MarkReceiptMailed(ctx context.Context, ledgerID uuid.UUID) error {
	return s.store.markMyNalogMailed(ctx, ledgerID, false, s.clock())
}

// MarkCancelMailed records that the annulment letter decision for an income has been made.
func (s *Service) MarkCancelMailed(ctx context.Context, ledgerID uuid.UUID) error {
	return s.store.markMyNalogMailed(ctx, ledgerID, true, s.clock())
}
