// ============================================================
// hledger.pest — PEG grammar for the hledger journal format.
//
// Conventions from pest (https://pest.rs/):
//   UPPERCASE  — built-in rules (NEWLINE, EOI, ANY, ASCII_DIGIT, …)
//   _{ … }    — silent rule: matched but not included in the pair tree
//   @{ … }    — atomic rule: inner tokens are NOT individually paired
//   ${ … }    — compound-atomic: inner named rules ARE paired but
//               implicit whitespace skipping is disabled
//
// Key differences from ledger.pest:
//   - Date separators include `.` and `/` in addition to `-`
//   - Comment lines also start with `#` (in addition to `;`)
//   - `description` is optional (hledger allows bare `DATE *` lines)
//   - `account` directives may have a type comment on the header line
//   - `commodity` directive accepts a commodity-format string, not just
//     a bare commodity name
//   - Automated posting rule bodies may reference `*N` multipliers;
//     parsing of the multiplier expression is STUBBED out (see TODO below)
//
// NOT YET SUPPORTED (stubbed / ignored):
//   - Automated posting arithmetic bodies (`= query\n    account  *1.1`):
//     the auto_rule rule captures the shape but the posting amounts inside
//     automated rules that use `*N` will fail to parse.  See TODO(#103).
//   - Full hledger date inference from a preceding `Y year` directive.
//   - Directive-attached tags (metadata on the same line as a directive).
// ============================================================

// --- Top Level ---

journal = ${ (entry | NEWLINE)* ~ EOI }

entry = _{
    transaction
    | periodic_transaction
    | auto_rule
    | historical_price
    | account_directive
    | commodity_directive
    | default_directive
    | include_directive
    | block_comment
    | comment_line
}

// --- Transactions ---

// A transaction header followed by optional indented notes and postings.
transaction = ${
    header ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}

// hledger header:
//   DATE [STATUS] [CODE] [DESCRIPTION] [; COMMENT]
//
// Description is optional in hledger (bare `2024-01-01 *` is valid).
header = ${
    date
    ~ (ws+ ~ status)?
    ~ (ws+ ~ code)?
    ~ (ws+ ~ description)?
    ~ (ws* ~ note)?
}

transaction_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }
empty_indented_line = _{ indent+ ~ NEWLINE }

// --- Periodic transactions ---
//
// `~ PERIOD_EXPR\n POSTINGS…`
// The period expression is the rest of the header line; it is stored as a raw
// string and not elaborated (same as the ledger frontend budget entries).
periodic_transaction = ${
    "~" ~ ws+ ~ period_expr ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}
period_expr = @{ (!(NEWLINE | ";") ~ ANY)+ }

// --- Automated posting rules ---
//
// `= QUERY\n POSTINGS…`
//
// TODO(#103): Automated posting amounts may use `*N` multiplier expressions
// that reference the matched posting's amount.  Parsing these is not yet
// implemented.  For now the rule captures the outer shape; any posting whose
// amount body uses `*N` will produce a parse error with a clear message.
auto_rule = ${
    "=" ~ ws+ ~ rule_query ~ NEWLINE ~
    (transaction_note | posting | empty_indented_line)*
}
rule_query = @{ (!(NEWLINE | ";") ~ ANY)+ }

// --- Postings ---

posting = ${
    indent+ ~ !NEWLINE ~
    (status ~ ws+)? ~
    posting_account ~
    (amount_logic)? ~
    (ws* ~ note)? ~
    (NEWLINE | EOI) ~
    posting_note*
}
posting_note = ${ indent+ ~ note ~ (NEWLINE | EOI) }

// The account portion of a posting: bare name or a virtual-posting marker.
posting_account = ${
    virtual_unbalanced_account
    | virtual_balanced_account
    | account
}

// `(Account)` — virtual unbalanced posting.
virtual_unbalanced_account = ${ "(" ~ virtual_account_inner ~ ")" }

// `[Account]` — virtual balanced posting.
virtual_balanced_account = ${ "[" ~ virtual_account_inner ~ "]" }

// Bare account name inside a virtual posting marker (parens/brackets stripped).
virtual_account_inner = @{ (!("  " | ";" | "\t" | "=" | NEWLINE | ")" | "]") ~ ANY)+ }

// The amount field of a posting.
// Two or more spaces (or a tab) separate the account from the amount —
// identical to the ledger-cli convention so that account names containing a
// single space are parsed correctly.
amount_logic = ${
    (ws{2,} | "\t") ~ (
        value_logic
        | assertion
    )
}

// A value expression, optionally followed by lot annotations and/or lot pricing
// and/or a balance assertion check. Mirrors the ledger-cli grammar.
value_logic = ${ value_expr ~ (ws* ~ lot_annotation_or_price)* ~ (ws* ~ assertion)? }
lot_annotation_or_price = ${ lot_annotation | lot_price }
lot_annotation = ${ lot_cost | lot_date | lot_note }
// Double-brace `{{expr}}` means total cost; single-brace `{expr}` means per-unit
// cost. Try `{{` before `{` so the longer token wins.
lot_cost = ${ ("{{" ~ ws* ~ value_expr ~ ws* ~ "}}") | ("{" ~ ws* ~ value_expr ~ ws* ~ "}") }
// `[date]` — lot acquisition date.
lot_date = ${ "[" ~ date ~ "]" }
// `((note))` — free-form lot note.
lot_note = ${ "((" ~ lot_note_inner ~ "))" }
lot_note_inner = @{ (!"))" ~ ANY)* }

// hledger uses `==` for "strict" balance assertions and `=` for "weak"
// assertions; an optional trailing `*` qualifier ("== *" / "= *") makes
// the assertion span EVERY commodity the account holds rather than just
// the one written. The combined form `==* 0` is the standard
// fiscal-year retained-earnings idiom: assert that the account has
// zero across all commodities and synthesize whatever postings are
// needed to make that true. The longer token must be matched first
// (the `==` `*` ordering is enforced by the rule below).
assertion = ${ assertion_op ~ ws* ~ value_expr }
assertion_op = @{ ("==" | "=") ~ "*"? }

// --- Atoms ---

// hledger accepts YYYY-MM-DD, YYYY/MM/DD, and YYYY.MM.DD.
date = ${ year ~ date_sep ~ monthdate ~ date_sep ~ monthdate }
year = @{ ASCII_DIGIT{4} }
monthdate = @{ ASCII_DIGIT{1,2} }
date_sep = _{ "-" | "/" | "." }

// Cleared (*) or pending (!) state marker.
status = { "*" | "!" }

// An optional reference code in parentheses, e.g. "(INV-042)".
code = { "(" ~ (!")" ~ ANY)* ~ ")" }

// Everything after the header fields up to a semicolon or newline.
// hledger trims trailing whitespace from the description at display time.
description = @{ (!(";" | NEWLINE) ~ ANY)+ }

// An account name: stops at double-space, semicolon, tab, equals, or newline.
account = @{ (!("  " | ";" | "\t" | "=" | NEWLINE) ~ ANY)+ }

// Lot pricing: `@ unit_price` or `@@ total_price`.
lot_price = ${ ("@@" | "@") ~ ws* ~ value_expr }

indent = _{ " " | "\t" }
ws = _{ " " | "\t" }

// hledger comments start with `;` or `#`.
note = ${ ";" ~ note_str }
note_str = ${ (!NEWLINE ~ ANY)* }
comment_line = @{ (";" | "#") ~ (!NEWLINE ~ ANY)* }

// `comment` ... `end comment` block-comment form. hledger ignores
// every line in between; we capture the inner text verbatim and
// pass it through as Entry::Comment so resolution discards it like
// any other comment. The opening `comment` must appear on its own
// line; an unclosed block (no matching `end comment`) is implicitly
// terminated by EOF, matching hledger's behaviour.
//
// The body lookahead requires `end comment` to be followed by
// NEWLINE or EOI so that a literal `end comment` *substring* inside
// the body's prose (e.g. inside backticks) does not prematurely
// terminate the block.
block_comment = ${
    "comment" ~ NEWLINE ~
    block_comment_body ~
    (("end comment" ~ (NEWLINE | EOI)) | EOI)
}
block_comment_body = @{ (!("end comment" ~ (NEWLINE | EOI)) ~ ANY)* }

// --- Historical prices ---

historical_price = ${
    "P" ~ ws+ ~ date ~ ws+ ~ commodity ~ ws+ ~ value_expr ~ (NEWLINE | EOI)
}

// --- Account directive ---
//
// hledger's account directive optionally has indented sub-directives
// (note, alias, type, …) which are treated as key/value account items,
// mirroring the structure of the ledger-cli account block.
account_directive = ${
    "account" ~ ws+ ~ account ~ (ws* ~ note)? ~ NEWLINE ~
    ((indent+ ~ note ~ NEWLINE) | account_item)*
}

account_item = ${
    indent+ ~ account_key ~ (ws+ ~ account_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}
account_key = @{ "note" | "alias" | "type" | identifier }
account_val = @{ (!NEWLINE ~ ANY)* }

// --- Commodity directive ---
//
// hledger accepts:
//   commodity SYMBOL           (bare symbol)
//   commodity FORMAT_STRING    (e.g. `commodity $1,000.00`)
//
// Both forms are captured as a raw format string; parsing the format is done
// in the Rust layer by inspecting the string.
commodity_directive = ${
    "commodity" ~ ws+ ~ commodity_format ~ (ws* ~ note)? ~ (NEWLINE | EOI) ~
    ((indent+ ~ note ~ NEWLINE) | commodity_item)*
}

// A commodity format string is the rest of the line up to a comment.
commodity_format = @{ (!(";" | NEWLINE) ~ ANY)+ }

commodity_item = ${
    indent+ ~ commodity_key ~ (ws+ ~ commodity_val)? ~ (ws* ~ note)? ~ (NEWLINE | EOI)
}
commodity_key = @{ "alias" | "format" | "nomarket" | "default" | "note" | identifier }
commodity_val = @{ (!NEWLINE ~ ANY)* }

// --- Default commodity directive ---
//
// hledger supports the same `D <amount>` form as ledger-cli:
//   D $1,000.00
// This declares the default commodity and its display format simultaneously.
// It is lowered to the same `Directive::Commodity` representation as a
// full `commodity` block with `default` and `format` sub-directives.
default_directive = ${ "D" ~ ws+ ~ value_expr ~ (NEWLINE | EOI) }

// --- Include directive ---

include_directive = ${ "include" ~ ws+ ~ filename ~ (NEWLINE | EOI) }
filename = @{ (!NEWLINE ~ ANY)+ }

// --- Value expressions ---
//
// Identical in shape to ledger.pest.  hledger's posting-amount syntax is a
// subset of ledger-cli's (no `define`, no boolean-expressions in directives),
// so the same Pratt-parsed expression grammar is fully sufficient.

value_expr = ${ expr ~ (ws+ ~ commodity)? }
expr = ${ term ~ (ws* ~ infix_op ~ ws* ~ term)* }
term = ${ (prefix_op ~ ws*)? ~ primary }
primary = { base_primary }

base_primary = _{
    "(" ~ expr ~ ")"
    | amount
    | commodity
}

amount = ${
    (commodity ~ ws* ~ number)
    | (number ~ ws+ ~ commodity)
    | number
}

number = @{
    ASCII_DIGIT+ ~ ("," ~ ASCII_DIGIT{3})* ~ ("." ~ ASCII_DIGIT+)?
    | "." ~ ASCII_DIGIT+
}

commodity = @{
    symbol+
    | (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "." | ":" | "#")*
}
symbol = _{ "$" | "€" | "£" | "¥" }

// A plain identifier: letter or underscore, then alphanumeric or underscore.
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }

infix_op = _{ add | sub | mul | div }
    add = { "+" }
    sub = { "-" }
    mul = { "*" }
    div = { "/" }
prefix_op = { "-" | "+" }
