//! Snapshot record types - the wire format for `history/<date>-portfolio.srf`.
//!
//! Each record kind below is a plain struct suitable for `srf.fmt`
//! on the write side and `srf.iterator` + `FieldIterator.to(Union)` on
//! the read side (see `src/history.zig`, which demuxes via a tagged
//! `SnapshotRecord` union whose `srf_tag_field = "kind"`). Field order
//! in the struct declaration IS the on-disk order: srf's `Record.from`
//! iterates `inline for (info.fields)`.
//!
//! Lives in `src/models/` because these types describe the data format
//! itself - they're consumed by the snapshot writer command
//! (`src/commands/snapshot.zig`), the history reader (`src/history.zig`),
//! and the timeline analytics (`src/analytics/timeline.zig`). Putting
//! them in `commands/` would force analytics to depend on a command
//! module, which would be backwards.
//!
//! ## What a snapshot IS: a valuation record, in two layers
//!
//! A snapshot answers exactly one question: "what was this portfolio
//! worth on date D, decomposed by symbol / account / tax-type." It is
//! deliberately NOT a faithful lot record. Two layers, and the
//! distinction is load-bearing:
//!
//!   - `total` / `tax_type` / `account` rows are the AUTHORITATIVE
//!     valuation layer. They come from `valuation.portfolioSummary`,
//!     which applies portfolio-level adjustments.
//!   - `lot` rows are a DECOMPOSITION AID. They are per-lot marks
//!     produced by a separate direct walk of `portfolio.lots`, and they
//!     deliberately do NOT carry portfolio-level adjustments.
//!
//! Consequence, and the one cross-layer gotcha: `sum(stock lot.value)`
//! can EXCEED `total::liquid`. `portfolioSummary` calls
//! `adjustForCoveredCalls` (`analytics/valuation.zig`), capping an
//! underlying's market value at the strike for open in-the-money sold
//! calls; `buildSnapshot`'s per-lot loop does not. When the two
//! disagree, `total::liquid` wins. The gap is not recoverable from the
//! file, because `strike` is not emitted (see the omissions list).
//!
//! If you ever need to key lots ACROSS snapshots, this is the wrong
//! format and the answer is not to extend it. zfin already has a
//! faithful lot record with full temporal identity: the git history of
//! `portfolio.srf`, which `commands/contributions.zig` reads revision
//! by revision, deserializing complete 22-field `Lot`s and keying them
//! with `lotKey` + `secondaryKey`. Extend that pipeline instead. Full
//! rationale (including why `open_date` and `split_factor` were each
//! proposed and rejected) in `docs/explanation/snapshots-model.md`.
//!
//! ## Deliberately omitted `Lot` fields
//!
//! `Lot` has 22 fields. `LotRow` carries 5 of them straight through
//! (`symbol`, `shares`, `open_price`, `account`, `security_type`),
//! FOLDS 5 more into computed values, and DROPS the remaining 12
//! outright. Grouped by why, so the next person who wants one back
//! knows which argument they have to beat:
//!
//! DROPPED (12):
//!
//!   Lot identity / provenance - belongs to the git-revision pipeline,
//!   not here: `open_date`, `close_date`, `close_price`, `drip`,
//!   `note`, `label`.
//!
//!   Hand-edit inputs consumed only by `zfin import` (see
//!   `Lot.hand_edited_fields`, which has zero influence on this
//!   format): `price_date`, `rate`, `maturity_date`.
//!
//!   No valuation consumer: `strike`, `underlying`, `option_type`.
//!
//! FOLDED IN (5) - not lost, but already accounted for in an emitted
//! value, so re-emitting them would create a second source of truth:
//!
//!     `ticker`       -> `symbol`, via `Lot.priceSymbol()`
//!     `price`        -> `price`, via `buildFallbackPrices` (the manual
//!                      override becomes the mark when no quote exists)
//!     `price_ratio`  -> `price`, via `Lot.effectivePrice()`
//!     `split_factor` -> `value`, via `Lot.effectiveShares()`
//!     `multiplier`   -> option `value` / `cost_basis`
//!
//!   Each is recoverable by algebra when genuinely needed;
//!   `compare.aggregateSnapshotStocks` documents the sanctioned
//!   `value / price` recovery for split-adjusted shares.
//!
//! NAMING HAZARD: `Lot.price` and `LotRow.price` are different things
//! that share a name. `Lot.price` is the user's hand-entered manual
//! override INPUT; `LotRow.price` is the resulting per-share market
//! price actually used for the mark.
//!
//! ## Per-security_type field semantics
//!
//! `LotRow` fields mean different things per `security_type`, and the
//! invariants that hold are NOT uniform. Absence of this table is how
//! that became a surprise. `buildSnapshot` is the authority:
//!
//!   Stock:
//!     shares      raw, as-transacted (NOT split-adjusted)
//!     open_price  raw
//!     cost_basis  shares * open_price (split-invariant, so raw is fine)
//!     price       share-class-adjusted market price
//!     value       shares * split_factor * price  (split-ADJUSTED)
//!     => `shares * price != value` on any split-affected lot. Do not
//!        assume that identity. Effective shares are `value / price`.
//!
//!   Cash / CD / Illiquid:
//!     shares      the face / dollar value
//!     open_price  0, meaningless
//!     cost_basis  0, meaningless
//!     price       null (no per-share price exists)
//!     value       == shares
//!
//!   Option:
//!     shares      contract count, signed (negative = short)
//!     open_price  premium per share
//!     cost_basis  |shares| * open_price * multiplier
//!     price       null
//!     value       == cost_basis. Options are carried AT COST and are
//!                 never marked to market in a snapshot.
//!
//!   Watch: not emitted at all - watchlist lots aren't positions.
//!
//! IMPORTANT: `kind` uses `= ""` as the default - a sentinel that never
//! matches any real discriminator value. This satisfies two constraints
//! simultaneously:
//!   - On write, srf elides fields whose value matches the default. Since
//!     real tags are "meta"/"total"/... (never `""`), `kind` is always
//!     emitted.
//!   - On read via the `SnapshotRecord` tagged union (see `history.zig`),
//!     srf's union dispatch consumes the `kind` field itself before
//!     coercing into a variant struct, so `kind` is absent from the
//!     variant's field stream. The default value prevents a
//!     `FieldNotFoundOnFieldWithoutDefaultValue` error. The post-parse
//!     `kind` field on variant rows is `""` and should not be consulted -
//!     the union tag carries the discriminator.
//!
//! Optional fields default to `null` so they're elided on null values -
//! that's the behavior we want for `price`, `quote_date`, etc.
//!
//! ## Adding a field (read this first)
//!
//! srf's compatibility rules, verified against the pinned srf:
//!   - Every field MUST have a default. srf coerces by NAME, filling
//!     absent fields from their default; a field with no default raises
//!     `FieldNotFoundOnFieldWithoutDefaultValue`, which would break
//!     every previously-written snapshot on read.
//!   - `kind` MUST stay at declaration index 0. Declaration order is
//!     on-disk order, and the `SnapshotRecord` union dispatch in
//!     `history.zig` reads exactly one field and requires it to be the
//!     tag, else `ActiveTagNotFirstField`.
//!   - Unknown fields are silently skipped on read and default-valued
//!     fields are elided on write, so old readers tolerate new files
//!     and new readers tolerate old files. Both directions hold.
//!   - Prefer APPENDING. Nothing compares snapshots byte-wise (the
//!     render tests are `std.mem.indexOf` substring assertions, not
//!     golden files), but several of those substrings pin the field
//!     immediately following `kind` - e.g. `"kind::lot,symbol::VTI"`.
//!     Appending is safe; inserting right after `kind` is not.
//!
//! `snapshot_version` is currently written as 1 and never read to gate
//! behavior, so bumping it is documentary - EXCEPT in one case that
//! makes it load-bearing. Because srf elides default-valued fields, a
//! new field whose value equals its default is absent from the file,
//! and a reader cannot distinguish "the writer predates this field"
//! from "the value is the default." Bump `snapshot_version` when, and
//! only when, a new field's ABSENCE is semantically ambiguous. Novelty
//! alone is not a reason. (Worked example: `drip: bool = false` and
//! `open_date: ?Date = null` would both have required a bump; a
//! `split_factor: f64 = 1.0` would not, since 1.0 means "no
//! adjustment" whether elided by a new writer or missing from an old
//! file.)
//!
//! Note also that snapshot writes and snapshot reads are separate
//! deployments in practice: a scheduled job may run an older zfin than
//! the interactive commands that read what it wrote. `zfin snapshot`
//! never reads existing snapshots, so the write side is old-binary-
//! safe, but do not assume a new field appears in files written before
//! that job's build was refreshed.

const std = @import("std");
const Date = @import("../Date.zig");

pub const MetaRow = struct {
    kind: []const u8 = "",
    snapshot_version: u32,
    as_of_date: Date,
    captured_at: i64,
    zfin_version: []const u8,
    quote_date_min: ?Date = null,
    quote_date_max: ?Date = null,
    stale_count: usize,
};

pub const TotalRow = struct {
    kind: []const u8 = "",
    scope: []const u8,
    value: f64,
};

pub const TaxTypeRow = struct {
    kind: []const u8 = "",
    label: []const u8,
    value: f64,
};

pub const AccountRow = struct {
    kind: []const u8 = "",
    name: []const u8,
    value: f64,
};

/// One open lot's valuation on the snapshot date. See the
/// "Per-security_type field semantics" block at the top of this file
/// before reading any of `shares` / `open_price` / `cost_basis` /
/// `value` - what they mean, and which invariants relate them, differ
/// by `security_type`.
pub const LotRow = struct {
    kind: []const u8 = "",
    symbol: []const u8,
    lot_symbol: []const u8,
    account: []const u8,
    security_type: []const u8,
    /// RAW, as-transacted share count for stocks - NOT split-adjusted.
    /// Face/dollar value for cash/CD/illiquid; signed contract count
    /// for options. Effective (split-adjusted) shares are `value /
    /// price`, never this field; see `compare.aggregateSnapshotStocks`.
    shares: f64,
    open_price: f64,
    cost_basis: f64,
    value: f64,
    /// Null for non-stock lots (cash/CD/illiquid have no per-share price).
    price: ?f64 = null,
    /// Null for non-stock lots.
    quote_date: ?Date = null,
    /// Emitted only when true (default is false, which srf skips).
    quote_stale: bool = false,
};

/// In-memory portfolio snapshot. `meta` is a single record; the four
/// slices are per-section record collections.
///
/// When constructed by `history.parseSnapshotBytes`, all string fields
/// inside the rows (`symbol`, `label`, etc.) are slices into the
/// caller-owned `bytes` buffer, NOT independently-allocated copies.
/// The caller is responsible for keeping that buffer alive at least as
/// long as the `Snapshot` - typical pattern is `defer allocator.free(bytes)`
/// placed AFTER the `defer snap.deinit(allocator)`.
pub const Snapshot = struct {
    meta: MetaRow,
    totals: []TotalRow,
    tax_types: []TaxTypeRow,
    accounts: []AccountRow,
    lots: []LotRow,

    pub fn deinit(self: *Snapshot, allocator: std.mem.Allocator) void {
        allocator.free(self.totals);
        allocator.free(self.tax_types);
        allocator.free(self.accounts);
        allocator.free(self.lots);
    }
};
