package adminconsole

import "html/template"

// The *View types are the display models the gin handlers fill and the templates
// render. Time values are pre-formatted to strings by the handlers so the
// templates stay logic-free.

// Pager is the shared list pagination state.
type Pager struct {
	Page     int
	PageSize int
	Total    int
	HasPrev  bool
	HasNext  bool
	PrevPage int
	NextPage int
}

// NewPager builds the pagination state for a 1-based page of pageSize over total
// items.
func NewPager(page, pageSize, total int) Pager {
	if page < 1 {
		page = 1
	}
	p := Pager{Page: page, PageSize: pageSize, Total: total, PrevPage: page - 1, NextPage: page + 1}
	p.HasPrev = page > 1
	p.HasNext = page*pageSize < total
	return p
}

// VariantVersions lists the dictionary versions resident for one variant.
type VariantVersions struct {
	Variant  string
	Latest   string
	Versions []string
}

// DashboardView is the landing-page summary.
type DashboardView struct {
	Accounts       int
	Games          int
	ActiveGames    int
	OpenComplaints int
	OpenFeedback   int
	PendingChanges int
	// ActiveVersion is the dictionary version new games pin (the persisted active
	// version), distinct from the per-variant resident versions.
	ActiveVersion string
	Variants      []VariantVersions
}

// UsersView is the paginated account list.
type UsersView struct {
	Items []UserRow
	Pager Pager
	// Robots is the active people/robots toggle; NameMask/ExternalIDMask are the current
	// glob filters; FilterQuery is those URL-encoded for the pager links. It is an
	// already-escaped query fragment (url.Values.Encode), so it is typed template.URL to
	// be emitted verbatim — interpolated as a plain string it would have its "=" and "&"
	// percent-encoded again by the contextual escaper.
	Robots         bool
	Deleted        bool
	NameMask       string
	ExternalIDMask string
	EmailExact     string
	FilterQuery    template.URL
}

// UserRow is one account row in the list. MoveMin/Avg/Max are the account's
// pre-formatted move-duration summary (empty when it has no timed move);
// FlaggedHighRate marks the soft high-rate badge.
type UserRow struct {
	ID              string
	DisplayName     string
	Kind            string
	Language        string
	Guest           bool
	Deleted         bool
	FlaggedHighRate bool
	CreatedAt       string
	HasMoveStats    bool
	MoveMin         string
	MoveAvg         string
	MoveMax         string
}

// MessagesView is the paginated chat-message moderation list. NameMask/ExtMask are the
// current sender glob filters; GameID/UserID pin the list to one game / sender (set from a
// game or user card); FilterQuery is the active filters URL-encoded for the pager and CSV
// links — an already-escaped query fragment, hence template.URL so it is not re-encoded
// inside the link (see UsersView.FilterQuery).
type MessagesView struct {
	Items       []MessageRow
	Pager       Pager
	NameMask    string
	ExtMask     string
	GameID      string
	UserID      string
	UnreadOnly  bool
	FilterQuery template.URL
}

// MessageRow is one chat message in the moderation list: its sender (linked to the user
// card), source, IP, body, game (linked to the game card), time, and whether it is still
// unread by at least one recipient.
type MessageRow struct {
	ID         string
	SenderID   string
	SenderName string
	Source     string
	IP         string
	Body       string
	GameID     string
	CreatedAt  string
	Unread     bool
}

// ChatMessageDetailView is one chat message with its per-seat read breakdown, for the
// console message card.
type ChatMessageDetailView struct {
	ID         string
	GameID     string
	SenderID   string
	SenderName string
	Source     string
	Kind       string
	Body       string
	IP         string
	CreatedAt  string
	Unread     bool
	Seats      []ChatSeatStatusRow
}

// ChatSeatStatusRow is one seat's read status on the message card: the seat index, the
// occupant (linked to the user card) and its role ("sender", "read" or "unread").
type ChatSeatStatusRow struct {
	Seat        int
	AccountID   string
	DisplayName string
	Role        string
}

// UserDetailView is one account with its stats, identities and recent games.
type UserDetailView struct {
	ID                     string
	DisplayName            string
	Language               string
	TimeZone               string
	Guest                  bool
	NotificationsInAppOnly bool
	// MergedInto is the primary account id when this account has been retired by a
	// merge, or empty for a live account.
	MergedInto string
	// The account-deletion dossier. Deleted marks a tombstoned account; DeletedAt and
	// DeletedName are its deletion time and retained real name; LastLoginAt/IP are the
	// last cold-load stamp (shown for any account); Retained is the credential journal.
	Deleted     bool
	DeletedAt   string
	DeletedName string
	LastLoginAt string
	LastLoginIP string
	Retained    []RetainedRow
	// FlaggedHighRateAt is the pre-formatted soft high-rate marker timestamp,
	// empty for an unflagged account; the card shows it with the Clear action.
	FlaggedHighRateAt string
	CreatedAt         string
	HasStats          bool
	Stats             StatsRow
	Identities        []IdentityRow
	// HasEmail gates the "Erase email" action; set when the account carries an email identity.
	HasEmail bool
	Games    []GameRow
	// TelegramID and VKID are the account's platform external ids (empty when absent).
	// TelegramID gates the "Send Telegram message" operator action; VKID surfaces the VK
	// user id with a link to the VK profile (there is no VK messaging to drive).
	TelegramID       string
	VKID             string
	ConnectorEnabled bool
	// MoveChart is the pre-rendered inline SVG of the account's per-move-number think
	// time (min/mean/max), empty when the account has no timed move.
	MoveChart template.HTML
	// Suspension is the account's current manual-block state, shown with the block/unblock
	// form; Reasons is the operator-editable reason picklist offered in the block form.
	Suspension SuspensionView
	Reasons    []ReasonOption
	// Roles is the account's current roles (each revocable); KnownRoles is the set the
	// grant form offers. The first role is the feedback ban (see internal/account).
	Roles      []string
	KnownRoles []string
	// Blocks, BlockedBy and Friends are the social graph on the card: who this account has
	// blocked, who currently blocks it, and its mutual friendships — each cross-linked to the
	// other account with the date it happened. They are the full truth; the asymmetric block
	// suppression that hides relationships from players never applies to the console.
	Blocks    []RelationRow
	BlockedBy []RelationRow
	Friends   []RelationRow
	// Finance is the account's payments picture (balances, benefits, refund risk, ledger). Present
	// is false when the payments domain is unwired.
	Finance FinanceView
	// PurchaseOverride is the account's per-account purchase override ("default"/"allow"/"deny"),
	// shown in and edited from the user card's payment-override control.
	PurchaseOverride string
	// Grant is the admin-grant panel (origin picker + grantable products). Present is false when the
	// payments domain is unwired.
	Grant GrantFormView
}

// FinanceView is the account's payments picture on the user card: chip balances per funding
// segment, benefits per origin, the recorded refund risk, and a short lifetime summary. The
// operations themselves live in the ledger section, which this links to pre-filtered to the
// account — the card answers "where does this account stand", not "what happened when".
// Present is false when the payments domain is unwired.
type FinanceView struct {
	Present  bool
	Segments []SegmentRow
	Benefits []BenefitRow
	// Abuse is the refund abuse flag; Loss is the unrecoverable chip loss from floor-0 refunds.
	Abuse bool
	Loss  int
	// Paid is the money the account has spent, per currency, already formatted; Refunded is what
	// came back. ChipsBought counts every chip ever credited (purchases, rewarded views, grants) and
	// ChipsSpent every chip spent on a value.
	Paid        []string
	Refunded    []string
	ChipsBought int
	ChipsSpent  int
	// LedgerQuery is the ledger-section query string that shows this account's operations.
	LedgerQuery template.URL
}

// SegmentRow is one funding segment's chip balance.
type SegmentRow struct {
	Source string
	Chips  int
}

// BenefitRow is one origin's benefit: the hint wallet, the ad-free expiry (pre-formatted, empty
// when none) and the lifetime ad-free flag.
type BenefitRow struct {
	Origin   string
	Hints    int
	AdsUntil string
	Forever  bool
}

// RelationRow is one cross-linked account in the user card's blocks / blocked-by / friends
// lists: the other account's id (the link target), its display name, and the pre-formatted date.
type RelationRow struct {
	AccountID   string
	DisplayName string
	Date        string
}

// SuspensionView is an account's current manual-block state shown on the user card: whether it
// is blocked, whether the block is permanent, the pre-formatted expiry (empty when permanent),
// when it was applied, and the reason snapshot in both languages (empty when none was cited).
type SuspensionView struct {
	Blocked   bool
	Permanent bool
	Until     string
	BlockedAt string
	ReasonEn  string
	ReasonRu  string
}

// ReasonOption is one suspension-reason picklist entry offered in the block form's dropdown.
type ReasonOption struct {
	ID     string
	TextEn string
	TextRu string
}

// StatsRow is an account's lifetime statistics.
type StatsRow struct {
	Wins          int
	Losses        int
	Draws         int
	MaxGamePoints int
	MaxWordPoints int
	Moves         int
	HintsUsed     int
}

// IdentityRow is one platform/email identity of an account.
type IdentityRow struct {
	Kind       string
	ExternalID string
	Confirmed  bool
	CreatedAt  string
}

// RetainedRow is one credential in the account-deletion retention journal (the legal
// dossier of detached credentials): what was detached, when, and why.
type RetainedRow struct {
	Kind       string
	ExternalID string
	Reason     string
	Confirmed  bool
	LinkedAt   string
	DetachedAt string
}

// GameRow is one game row in a list.
type GameRow struct {
	ID        string
	Variant   string
	Status    string
	Players   int
	UpdatedAt string
	// VsAI marks an honest-AI game (rendered as 🤖 in the list's AI column).
	VsAI bool
	// Kind is the game's origin tag label: vs_ai / random / friends / unknown.
	Kind string
}

// GamesView is the paginated games list, optionally filtered by status.
type GamesView struct {
	Items  []GameRow
	Status string
	Pager  Pager
}

// GameLimitsView is the per-tier, per-kind active-game limit form: each field is a cap where -1
// is unlimited, 0 blocks the kind, and a positive value caps concurrent games of that kind.
type GameLimitsView struct {
	GuestVsAI      int
	GuestRandom    int
	GuestFriends   int
	DurableVsAI    int
	DurableRandom  int
	DurableFriends int
}

// GameDetailView is one game with its seats.
type GameDetailView struct {
	ID          string
	Variant     string
	DictVersion string
	Status      string
	Players     int
	ToMove      int
	EndReason   string
	MoveCount   int
	CreatedAt   string
	UpdatedAt   string
	FinishedAt  string
	// VsAI marks an honest-AI game (shown as a 🤖 flag in the summary).
	VsAI bool
	// MultipleWordsPerTurn is the game's cross-word rule: true = standard Scrabble (every cross-word
	// is validated and scored), false = the single-word rule (only the main word along the play
	// direction counts). Shown in the summary so an operator can tell the rule at a glance.
	MultipleWordsPerTurn bool
	Seats                []SeatRow
	// HasRobot is true when any seat is a robot, gating the robot-target caption;
	// RobotTargetPct is the configured global play-to-win rate, in percent.
	HasRobot       bool
	RobotTargetPct int
	// ReplayJSON is the game-replay payload (board, seats, per-step racks/scores/bag) the
	// game_detail page feeds to its vanilla-JS stepper; HasReplay gates the replay section.
	ReplayJSON template.JS
	HasReplay  bool
	// SetupDraws is the first-move draw — one row per tile drawn (docs/ARCHITECTURE.md §6) —
	// and FirstMover is the resolved name of the seat-0 player the draw elected.
	SetupDraws []SetupDrawRow
	FirstMover string
}

// SetupDrawRow is one tile drawn in the first-move seeding (docs/ARCHITECTURE.md §6): the
// round, the player (Name/AccountID, or "(opponent)" with an empty AccountID for an
// auto-match synthetic draw not yet back-filled), the drawn letter (upper-cased; "?" for a
// blank) and its draw rank.
type SetupDrawRow struct {
	Round     int
	Name      string
	AccountID string
	Letter    string
	Blank     bool
	Rank      int
}

// SeatRow is one seat of a game. For a robot seat (IsRobot) RobotIntent is the game's
// deterministic play-to-win decision ("play to win"/"play to lose"), and NextMove is the
// scheduled next-move ETA shown only while it is that robot's turn in an active game.
type SeatRow struct {
	Seat        int
	DisplayName string
	AccountID   string
	Score       int
	HintsUsed   int
	Winner      bool
	IsRobot     bool
	RobotIntent string
	NextMove    string
}

// ComplaintsView is the paginated complaint review queue.
type ComplaintsView struct {
	Items  []ComplaintRow
	Status string
	Pager  Pager
}

// ComplaintRow is one complaint row in the queue.
type ComplaintRow struct {
	ID          string
	Word        string
	Variant     string
	WasValid    bool
	Status      string
	Disposition string
	CreatedAt   string
}

// ComplaintDetailView is one complaint with its resolution state and form.
type ComplaintDetailView struct {
	ID             string
	Word           string
	Variant        string
	DictVersion    string
	WasValid       bool
	Note           string
	Status         string
	Disposition    string
	ResolutionNote string
	CreatedAt      string
	ResolvedAt     string
	GameID         string
	Resolved       bool
}

// DictionaryView lists the resident versions per variant, the active version new
// games pin, and the pending wordlist changes from accepted complaints.
type DictionaryView struct {
	// ActiveVersion is the dictionary version new games pin; the update form sets it.
	ActiveVersion string
	Variants      []VariantVersions
	Changes       []DictChangeRow
}

// DictChangeRow is one pending wordlist edit.
type DictChangeRow struct {
	Variant    string
	Word       string
	Action     string
	ResolvedAt string
}

// DictionaryPreviewView is the second step of a dictionary update: the version
// parsed from the uploaded archive (editable before confirming), the staging token
// that names the uploaded files on disk, the active version the diff is against, and
// the per-variant word diff.
type DictionaryPreviewView struct {
	Version       string
	Token         string
	ActiveVersion string
	Variants      []VariantDiffRow
}

// VariantDiffRow summarises one variant's word diff in the update preview.
// AddedCount/RemovedCount are the full totals; AddedSample/RemovedSample are the
// first words shown (capped); AddedTruncated/RemovedTruncated mark a capped list;
// LargeRemoval flags a removal large enough to warrant caution before confirming.
type VariantDiffRow struct {
	Variant          string
	AddedCount       int
	RemovedCount     int
	AddedSample      []string
	RemovedSample    []string
	AddedTruncated   bool
	RemovedTruncated bool
	LargeRemoval     bool
}

// BroadcastView is the operator-broadcast form page.
type BroadcastView struct {
	ConnectorEnabled bool
}

// ThrottledView is the rate-limit observability page: the temporary IP bans the
// gateway is currently enforcing, the recent gateway-reported throttle episodes
// (in-memory, reset on restart) and the accounts currently carrying the high-rate
// flag. FlagThreshold and FlagWindow caption the active auto-flag tuning.
type ThrottledView struct {
	Bans          []BanRow
	Episodes      []ThrottleEpisodeRow
	Flagged       []FlaggedAccountRow
	FlagThreshold int
	FlagWindow    string
}

// BanRow is one temporary IP ban the gateway is enforcing, with its reason and its
// since/expiry timestamps; the row carries an unban action.
type BanRow struct {
	IP      string
	Reason  string
	Since   string
	Expires string
}

// ThrottleEpisodeRow is one recently throttled limiter key. UserID links to the
// user card and is set only for the user class (the other classes key by IP).
type ThrottleEpisodeRow struct {
	Class     string
	Key       string
	UserID    string
	Rejected  int
	FirstSeen string
	LastSeen  string
}

// FlaggedAccountRow is one account carrying the high-rate flag.
type FlaggedAccountRow struct {
	ID          string
	DisplayName string
	FlaggedAt   string
}

// ReasonsView is the suspension-reason picklist management page: every editable reason entry.
type ReasonsView struct {
	Items []ReasonRow
}

// ReasonRow is one editable suspension-reason entry, with its English and Russian text.
type ReasonRow struct {
	ID        string
	TextEn    string
	TextRu    string
	CreatedAt string
}

// MessageView is the result page shown after a POST action.
type MessageView struct {
	Heading string
	Body    string
	Back    string
}

// BannersView is the advertising-campaign list page.
type BannersView struct {
	Items []BannerCampaignRow
}

// BannerCampaignRow is one campaign in the list. Window is a human-readable
// validity window ("perpetual" for the default); ActiveNow reports whether it
// would rotate right now (enabled and within its window).
type BannerCampaignRow struct {
	ID        string
	Name      string
	Weight    int
	IsDefault bool
	Enabled   bool
	Window    string
	Messages  int
	ActiveNow bool
}

// BannerDetailView is the campaign detail/edit page. StartsAt/EndsAt are the
// "YYYY-MM-DDTHH:MM" (UTC) values for the datetime-local inputs, empty when open.
//
// The colour-override and urgent fields drive the non-default campaign's editor:
// OverrideAllOn/OverrideDarkOn report whether each colour set is active, and the
// six *Bg/*Fg/*Link values seed the native colour inputs — the stored override
// when a set is on, otherwise the neutral theme token so the picker starts from a
// sensible colour and the live preview shows the real fallback.
type BannerDetailView struct {
	ID             string
	Name           string
	Weight         int
	IsDefault      bool
	Enabled        bool
	StartsAt       string
	EndsAt         string
	Urgent         bool
	OverrideAllOn  bool
	AllBg          string
	AllFg          string
	AllLink        string
	OverrideDarkOn bool
	DarkBg         string
	DarkFg         string
	DarkLink       string
	Messages       []BannerMessageRow
}

// BannerMessageRow is one bilingual message of a campaign. First/Last drive the
// reorder buttons (disabled at the ends).
type BannerMessageRow struct {
	ID     string
	BodyEn string
	BodyRu string
	First  bool
	Last   bool
}

// BannerSettingsView is the global display-timings form.
type BannerSettingsView struct {
	HoldMs         int
	EdgePauseMs    int
	ScrollPxPerSec int
	FadeOutMs      int
	GapMs          int
	FadeInMs       int
}

// FeedbackView is the paginated user-feedback queue. Status is the active
// unread/read/archived filter; NameMask/ExtMask are the sender glob filters;
// UserID pins the list to one account (the per-user link from /users);
// FilterQuery is the active filters URL-encoded for the pager links (already
// escaped, hence template.URL — see UsersView.FilterQuery).
type FeedbackView struct {
	Items       []FeedbackRow
	Status      string
	NameMask    string
	ExtMask     string
	UserID      string
	Pager       Pager
	FilterQuery template.URL
}

// FeedbackRow is one feedback message in the queue: its sender (linked to the user
// card), source, channel, whether it has an attachment / a reply, its state and
// time.
type FeedbackRow struct {
	ID            string
	AccountID     string
	SenderName    string
	Source        string
	Channel       string
	HasAttachment bool
	Read          bool
	Replied       bool
	Archived      bool
	CreatedAt     string
}

// FeedbackDetailView is one feedback message with its body, attachment, state and
// the reply / archive / delete forms. Body, AttachmentName, SenderName and IP are
// user-controlled and rendered as plain auto-escaped text. IsImage gates the inline
// <img> preview; Banned shows whether the sender already holds the feedback ban.
type FeedbackDetailView struct {
	ID         string
	AccountID  string
	SenderName string
	Source     string
	Channel    string
	// InterfaceLanguage is the sender's interface language (account preference).
	InterfaceLanguage string
	IP                string
	Body              string
	HasAttachment     bool
	AttachmentName    string
	IsImage           bool
	Read              bool
	Archived          bool
	Replied           bool
	ReplyBody         string
	RepliedAt         string
	CreatedAt         string
	// Version is the client app build the report was sent from (empty for rows that predate it).
	Version string
	// The Filed time is shown in three zones so the operator can tell what is certainly known from
	// what is merely defaulted. CreatedAt is the authoritative UTC time. CreatedAtBrowser is that
	// instant in the client's UTC offset detected at submit (BrowserTZ its "±HH:MM" label), empty
	// when the client reported none (an older build). CreatedAtUser is that instant in the sender's
	// saved profile zone (UserTZ its label), empty when the account has no zone beyond the UTC
	// default — the template then shows "N/A" so the missing datum is explicit.
	CreatedAtBrowser string
	BrowserTZ        string
	CreatedAtUser    string
	UserTZ           string
	Banned           bool
}

// CatalogView is the product-catalog list page.
type CatalogView struct {
	Products []ProductRow
	// ShowAll reports whether archived products are listed alongside active ones (the active/all
	// toggle); it drives the toggle link and the list heading.
	ShowAll bool
	// RewardPayout / RewardDailyCap / RewardHourlyCap are the rewarded-video config (chips earned per
	// view and the per-day / per-hour anti-abuse caps), shown in and edited from the page's
	// rewarded-ads form. A 0 payout means rewarded is inert.
	RewardPayout    int
	RewardDailyCap  int
	RewardHourlyCap int
	// Rails is the per-rail operational kill-switch state (enabled + per-language off-message), shown
	// in and edited from the page's payment-availability form.
	Rails []RailStatusRow
}

// RailStatusRow is one payment rail's operational availability in the kill-switch editor: the rail
// key, whether purchases are enabled, and the operator's per-language off-message shown to the user.
type RailStatusRow struct {
	Rail      string
	Enabled   bool
	MessageRU string
	MessageEN string
}

// ProductRow is one product in the catalog list: its composition, prices, the archived flag
// (Active) and the transacted flag (which forbids a hard delete).
type ProductRow struct {
	ID         string
	Title      string
	Active     bool
	Atoms      []AtomRow
	Prices     []PriceRow
	Transacted bool
}

// AtomRow is one atom line of a product row.
type AtomRow struct {
	Atom     string
	Quantity int
}

// PriceRow is one price of a product: the method ("" for a value's CHIP price), the currency, and
// the amount in that currency's minor units.
type PriceRow struct {
	Method   string
	Currency string
	Amount   int64
}

// ProductFormView is the product edit form, pre-filled from the current composition. Atom quantities
// and prices are flattened to the fixed fields the form offers (0 = absent); Transacted disables the
// delete action.
type ProductFormView struct {
	ID         string
	Title      string
	Active     bool
	Chips      int
	Hints      int
	NoAds      int
	Tournament int
	PriceRUB   int64
	PriceVote  int64
	PriceStar  int64
	PriceChip  int64
	Transacted bool
}

// GrantFormView is the admin-grant panel on the user card: the origin picker and the grantable
// products (value bundles — hints / no-ads days — including archived ones; chips and tournament
// products are excluded). Present is false when the payments domain is unwired.
type GrantFormView struct {
	Present  bool
	Origins  []string
	Products []GrantProductOption
}

// GrantProductOption is one grantable product in the by-product picker: its id, title, an atom
// summary, and whether it is archived (the common case for a non-public reward bundle).
type GrantProductOption struct {
	ID       string
	Title    string
	Summary  string
	Archived bool
}

// LedgerView is the all-accounts financial ledger: one page of operations, the totals for
// everything the current filter matches (not merely the page), and the filter state itself so the
// form renders back what the operator asked for. FilterQuery is those filters URL-encoded, carried
// into the pager, the CSV export and the refund's return link so every one of them stays on the
// same slice of the ledger (see MessagesView.FilterQuery).
type LedgerView struct {
	Rows        []LedgerRow
	Pager       Pager
	Totals      LedgerTotalsRow
	From        string
	To          string
	UserID      string
	Kind        string
	Wallet      string
	Provider    string
	Kinds       []string
	Wallets     []string
	Providers   []string
	FilterQuery template.URL
}

// LedgerRow is one operation in the ledger list. Money is the amount the customer paid or was
// refunded, already formatted with its currency, and empty for a row that moved no money (a chip
// spend, an admin grant, a rewarded-video credit). Refundable marks a funded order the operator may
// still reverse, which is what puts the Refund button on that row.
type LedgerRow struct {
	At string
	// AccountID is the link target; AccountShort is what the cell shows, because a ledger row has
	// no room for a full uuid and the operator mostly needs to spot two rows from the same buyer.
	AccountID    string
	AccountShort string
	Kind         string
	Source       string
	Origin       string
	ChipsDelta   int
	Money        string
	Title        string
	Order        string
	Provider     string
	Shop         string
	Refundable   bool
}

// LedgerTotalsRow is the summary above the table, over everything the filter matches. Money is
// listed per currency because the rails settle in different ones and one sum across them would mean
// nothing.
type LedgerTotalsRow struct {
	MoneyIn       []string
	MoneyRefunded []string
	ChipsIn       int
	ChipsOut      int
}

// MyNalogView is the professional-income tax export section: the state of the tax-cabinet session,
// what is still owed to the tax register, and the manual fallback for when its API is unavailable.
type MyNalogView struct {
	SignedIn    bool
	INN         string
	AutoEnabled bool
	Paused      bool
	PauseReason string
	LastOK      string
	TimeZone    string
	// Backlog is what a closed month still owes, and is empty when nothing does.
	Backlog MyNalogBacklogRow
	// Owed, Cancels and Attention are the three working lists: income to file, receipts to annul,
	// and rows whose outcome nobody could establish.
	Owed      []MyNalogRow
	Cancels   []MyNalogCancelRow
	Attention []MyNalogRow
}

// MyNalogBacklogRow summarises unfiled income from a closed month.
type MyNalogBacklogRow struct {
	Count  int
	Amount string
	Oldest string
	// Overdue reports that the statutory filing date for that month has passed.
	Overdue bool
}

// MyNalogRow is one income on the export page. Name is the exact service description that would be
// sent, which is also what an operator must type when filing by hand — the two have to match, or a
// later automatic recovery would not recognise the receipt.
type MyNalogRow struct {
	LedgerID     string
	AccountID    string
	AccountShort string
	At           string
	Amount       string
	Name         string
	Status       string
	Attempts     int
	LastError    string
	ReceiptUUID  string
	ReceiptURL   string
}

// MyNalogCancelRow is one registered receipt whose income has been refunded and which is therefore
// owed an annulment.
type MyNalogCancelRow struct {
	LedgerID     string
	AccountID    string
	AccountShort string
	ReceiptUUID  string
	Failed       bool
}
