Private/AnnualReportInput.ps1

# Helpers for per-fiscal-year annual report input stored as report.txt in a
# fiscal year directory. This holds the year-specific narrative and decision
# data that cannot be derived from the bookkeeping itself (significant events,
# proposed dividend, average number of employees, securities market value and
# the signing place/date). It mirrors the plain-text, one-optional-file-per-year
# ethos of ib.txt (the opening balance metadata).
#
# The file format is line based and UTF-8:
# * "# ..." - a comment (ignored on read).
# * "Key: value" - a scalar field on a single line.
# * "## Key" - starts a multi-line text block; every following line
# belongs to the block until the next "## " header or EOF.
# Scalars are always written before any block so a value that happens to contain
# a "Word:" prefix inside a block is never mistaken for a new scalar field.

# The canonical file name for annual report input inside a fiscal year directory.
$script:LedgerReportInputFileName = 'report.txt'

function Get-LedgerReportInputPath {
    <#
    .SYNOPSIS
    Returns the full path to the report.txt input file for a fiscal year.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$JournalPath,

        [Parameter(Mandatory)]
        [string]$FiscalYear
    )

    Join-Path (Join-Path $JournalPath $FiscalYear) $script:LedgerReportInputFileName
}

function Read-LedgerReportInput {
    <#
    .SYNOPSIS
    Reads a report.txt annual report input file into an ordered dictionary.

    .DESCRIPTION
    Parses the plain-text report.txt format (scalars as "Key: value" and
    multi-line "## Key" blocks) into an ordered dictionary of field name to
    value. Multi-line block values keep their internal newlines (joined with
    "`n") with trailing blank lines trimmed. Returns an empty ordered
    dictionary when the file does not exist.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path
    )

    $fields = [ordered]@{}
    if (-not (Test-Path $Path)) {
        return $fields
    }

    $currentSection = $null
    $sectionLines = $null

    $flush = {
        if ($null -ne $currentSection) {
            # Trim trailing blank lines from the collected block.
            $end = $sectionLines.Count - 1
            while ($end -ge 0 -and [string]::IsNullOrWhiteSpace($sectionLines[$end])) {
                $end--
            }
            $text = if ($end -ge 0) { ($sectionLines[0..$end] -join "`n") } else { '' }
            $fields[$currentSection] = $text
        }
    }

    foreach ($line in (Get-Content -Path $Path -Encoding UTF8)) {
        if ($line -match '^##\s+(.+?)\s*$') {
            & $flush
            $currentSection = $Matches[1]
            $sectionLines = [System.Collections.Generic.List[string]]::new()
            continue
        }

        if ($null -ne $currentSection) {
            $sectionLines.Add($line)
            continue
        }

        if ($line -match '^\s*#') {
            continue
        }
        if ($line -match '^([A-Za-z][A-Za-z0-9_]*):\s?(.*)$') {
            $fields[$Matches[1]] = $Matches[2]
        }
    }

    & $flush
    return $fields
}

function Write-LedgerReportInput {
    <#
    .SYNOPSIS
    Writes an ordered dictionary of annual report input fields to report.txt.

    .DESCRIPTION
    Serialises the supplied fields to the plain-text report.txt format as UTF-8.
    A field whose value contains a newline is written as a multi-line "## Key"
    block; every other field is written as a single "Key: value" scalar line.
    Scalars are written first, then blocks, so the file round-trips cleanly.
    Fields with a $null or empty value are omitted.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Fields
    )

    $scalars = [System.Collections.Generic.List[string]]::new()
    $blocks = [System.Collections.Generic.List[string]]::new()

    foreach ($key in $Fields.Keys) {
        $value = $Fields[$key]
        if ($null -eq $value -or [string]::IsNullOrEmpty([string]$value)) {
            continue
        }
        $text = [string]$value
        if ($text -match '\r?\n') {
            $normalised = $text -replace '\r?\n', "`n"
            $blocks.Add('')
            $blocks.Add("## $key")
            foreach ($blockLine in ($normalised -split "`n")) {
                $blocks.Add($blockLine)
            }
        }
        else {
            $scalars.Add("${key}: $text")
        }
    }

    $lines = [System.Collections.Generic.List[string]]::new()
    $lines.Add('# PSLedger annual report input')
    foreach ($s in $scalars) { $lines.Add($s) }
    foreach ($b in $blocks) { $lines.Add($b) }

    $dir = Split-Path -Parent $Path
    if ($dir -and -not (Test-Path $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }

    Set-LedgerFileContent -Path $Path -Value $lines
}