"""Finance dataclasses — pure value types, no I/O.

These are the canonical shapes that flow through ingest → ledger → reports.
Wire-format (JSON, SQLite, markdown frontmatter) is converted at the boundaries
in ``ledger.py`` and ``ingest.py`` — keep this module dependency-free so it
can be imported from tests without spinning up the full subsystem.

Design notes:

- **Amounts as strings.** ``Decimal`` doesn't survive a JSON round-trip cleanly
  and ``float`` loses precision after a few thousand additions. Every amount
  field is a string in canonical form (``"42.50"``, never ``"42.5"`` or
  ``"42,50"``). Helpers in ``money.py`` (Phase 1.2) handle parsing/formatting;
  this module just stores the canonical string.
- **Currency as ISO 4217.** Three-letter uppercase code (``"USD"``, ``"SEK"``,
  ``"EUR"``). No symbols, no localized names. Rejected at validation time.
- **Dates as ISO ``YYYY-MM-DD``.** Time component dropped — receipts don't
  have meaningful sub-day precision and storing a TZ would be misleading.
- **Slugs are ASCII**, sanitised by ``_slugify()`` in ``merchants.py``. Display
  names live in ``canonical_name`` / ``description`` and may carry any script.
- **Status is closed enum.** Adding a status value is a schema migration —
  every consumer would have to handle it.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum


class TransactionStatus(str, Enum):
    """Lifecycle of a transaction record.

    ``draft``        — extracted from a receipt, waiting for user approval
    ``verified``     — user approved; canonical ledger entry, decrements budget
    ``rejected``     — user rejected; archived for audit but does NOT consume budget
    ``superseded``   — replaced by a corrected entry; old row kept for audit
    ``archived``     — soft-deleted; preserved in archived/ but excluded from queries
    """

    DRAFT = "draft"
    VERIFIED = "verified"
    REJECTED = "rejected"
    SUPERSEDED = "superseded"
    ARCHIVED = "archived"


@dataclass(frozen=True, slots=True)
class TaxLine:
    """One VAT/sales-tax bracket on a receipt.

    Sweden: receipts often show 25% / 12% / 6% breakdowns simultaneously
    (standard / food / books-and-transport). EU B2B reverse-charge
    invoices have an empty list and ``reverse_charge=True`` on the
    transaction.

    All amounts are positive decimal strings. Sum of ``amount`` across
    every TaxLine should equal ``Transaction.tax`` within
    ``currency_precision``; the auditor surfaces ``vat_breakdown_mismatch``
    when they don't match.
    """

    rate: str            # percent as string: "25", "12", "6", "0"
    amount: str          # VAT charged at this rate (decimal string)
    taxable_base: str    # net amount the VAT is calculated on (decimal string)


@dataclass(frozen=True, slots=True)
class LineItem:
    """One line on a receipt — a product, a service, a fee.

    ``amount`` is the EXTENDED amount AFTER ``discount`` is applied (i.e.
    the actual line total the customer paid for this item). The receipt's
    total should equal ``sum(items.amount) - discount_total + tax + tip``
    within ``currency_precision`` tolerance — the auditor reconciles.

    ``unit_price`` may be empty when the receipt only shows the extended
    amount (common for restaurants). Don't fabricate it.

    Slugs (``category_slug``, ``project_slug``) are populated by the
    ``transaction-categorize`` and ``project-attribution`` skills. Empty
    string means "unassigned" — the auditor flags transactions where every
    line is unassigned as a categorization gap.

    Phase 1.5 fields:
      - ``item_code`` — SKU / PLU / barcode as printed (free-form). Useful
        for return tracking, statement reconciliation, and "same SKU →
        same category" auto-rules in future phases.
      - ``discount`` — line-level discount amount (decimal string,
        positive). Captured for analytics; the line's ``amount`` already
        reflects the post-discount value, so this is informational.
    """

    item_id: str
    description: str
    quantity: str = ""           # decimal string, e.g. "2", "0.5"; empty = qty 1 implied
    unit_price: str = ""         # decimal string in transaction currency; empty = unknown
    amount: str = "0.00"         # extended amount AFTER any line discount (decimal string)
    category_slug: str = ""
    project_slug: str = ""
    tax_deductible: bool = False
    notes: str = ""
    # ── Phase 1.5 ──
    item_code: str = ""          # SKU / PLU / barcode as printed (free-form)
    discount: str = ""           # line-level discount amount (positive decimal); empty if none
    tax_rate: str = ""           # VAT % as printed on this line ("25", "12", "6", "0"); empty if not printed


@dataclass(frozen=True, slots=True)
class Transaction:
    """Verified ledger entry — the canonical record after user approval.

    Every transaction MUST have at least one line item. Receipts with no
    breakdown (just a total) get a single line item with the merchant name
    as ``description`` — never an empty ``items`` list.

    ``content_hash`` of the source file (or canonical receipt JSON when
    digital) provides idempotent dedup: the same receipt scanned twice
    produces the same ``txn_id`` because ingest short-circuits on a hit.

    ``correlation_id`` is the receipt-processing workflow's chain ID. Joins
    to ``decision_log`` so the user can answer "why did the auditor flag
    this transaction?" by clicking through to the decisions UI.

    Accounting fields (Phase 1.5+):
      - ``receipt_number`` — vendor's receipt/invoice number for
        statement reconciliation
      - ``document_type`` — receipt | invoice | credit_note | refund |
        other; affects tax/accounting treatment
      - ``subtotal`` — pre-tax amount (when printed); useful for
        per-item VAT recovery
      - ``vendor_tax_id`` — vendor's VAT/Org number (Sweden: org-nummer);
        also persisted on the Merchant record across receipts
      - ``vendor_address`` — billing address as printed; useful for
        cross-border tax determination
      - ``due_date`` — for invoices, when payment is owed; empty for
        same-day receipts
    """

    txn_id: str
    txn_date: str                # ISO YYYY-MM-DD
    merchant_slug: str
    currency: str                # ISO 4217, uppercase
    total: str                   # decimal string
    tax: str = "0.00"            # decimal string
    project_slug: str = ""       # may be empty for unassigned receipts
    status: TransactionStatus = TransactionStatus.VERIFIED
    items: tuple[LineItem, ...] = field(default_factory=tuple)
    payment_method: str = ""     # free-form: "card-ending-1234", "cash", "transfer"
    source_file: str = ""        # logical path under finance/library/
    content_hash: str = ""       # sha256 of source bytes; empty for manual entries
    correlation_id: str = ""
    notes: str = ""
    created_at: str = ""         # ISO 8601 with TZ
    verified_at: str = ""        # ISO 8601 with TZ; set on user approval
    # ── Accounting fields (Phase 1.5) ──
    receipt_number: str = ""     # vendor receipt/invoice number
    document_type: str = ""      # receipt | invoice | credit_note | refund | other
    subtotal: str = ""           # pre-tax amount (decimal string); empty if not printed
    vendor_tax_id: str = ""      # VAT/Org/EIN; also kept on Merchant record
    vendor_address: str = ""     # billing address as printed
    due_date: str = ""           # ISO YYYY-MM-DD; empty for same-day receipts
    # ── Phase 1.5 — discounts + tip ──
    # Reconciliation: sum(items.amount) - discount_total + tax + tip ≈ total
    discount_total: str = ""         # transaction-level discount (positive decimal)
    discount_description: str = ""   # free-form: "SUMMER25", "Loyalty 10%"
    tip: str = ""                    # gratuity / service charge (decimal); empty if none
    # ── Phase 1.5 — VAT breakdown ──
    # tax_breakdown sums to ``tax``; auditor flags ``vat_breakdown_mismatch``
    # when sum-of-amounts diverges. Empty tuple is the right default for
    # consumer receipts that print only a total VAT amount, OR for
    # reverse-charge invoices (which also set ``reverse_charge=True``).
    tax_breakdown: tuple[TaxLine, ...] = field(default_factory=tuple)
    buyer_tax_id: str = ""           # buyer's VAT/Org if printed on the invoice
    reverse_charge: bool = False     # EU B2B 0% reverse-charge marker

    def total_decimal_str(self) -> str:
        """Return the canonical total amount string (no formatting)."""
        return self.total or "0.00"


@dataclass(frozen=True, slots=True)
class TransactionDraft:
    """A pending transaction awaiting user approval.

    Same shape as ``Transaction`` minus the verification fields. Lives in
    ``agntspace-finance/pending/`` as markdown until the user clicks
    approve or reject. Reject moves it to ``rejected/`` with the user's
    reason; approve commits it to the ledger and emits the SPO triples.

    Carries the same Phase 1.5 accounting fields as ``Transaction`` so
    nothing extracted from the receipt is dropped at the draft → verified
    boundary.
    """

    draft_id: str
    txn_date: str
    merchant_slug: str
    merchant_raw: str            # what the receipt actually said; preserved for audit
    currency: str
    total: str
    tax: str = "0.00"
    project_slug: str = ""
    items: tuple[LineItem, ...] = field(default_factory=tuple)
    payment_method: str = ""
    source_file: str = ""
    content_hash: str = ""
    correlation_id: str = ""
    notes: str = ""
    audit_flags: tuple[str, ...] = field(default_factory=tuple)
    # ^ short codes like ("totals_mismatch", "duplicate_suspected"). Surfaced
    #   to the user during approval so they know what to check.
    extracted_at: str = ""
    # ── Accounting fields (Phase 1.5) — match Transaction shape ──
    receipt_number: str = ""
    document_type: str = ""
    subtotal: str = ""
    vendor_tax_id: str = ""
    vendor_address: str = ""
    due_date: str = ""
    discount_total: str = ""
    discount_description: str = ""
    tip: str = ""
    # ── Phase 1.5 — VAT (match Transaction shape) ──
    tax_breakdown: tuple[TaxLine, ...] = field(default_factory=tuple)
    buyer_tax_id: str = ""
    reverse_charge: bool = False


@dataclass(frozen=True, slots=True)
class Merchant:
    """Canonical merchant record. Deduplicated identity across receipts.

    ``aliases`` carries every observed raw form (POS strings, marketplace
    suffixes, foreign-language variants). Updated incrementally by
    ``MerchantResolver.observe_alias()`` — new aliases are surfaced for
    user approval per the session-107 entity-dedup memory (suggest, never
    auto-merge above the comma-prefix threshold).

    ``default_category`` is a hint for the categorize skill — when set, new
    receipts from this merchant get pre-filled. Empty means "no opinion".

    ``tax_id`` is the vendor's VAT/Org/EIN identifier (Sweden: org-nummer;
    EU: VAT number). Auto-populated from receipt extraction when seen the
    first time; subsequent receipts with the same tax_id confirm merchant
    identity (cross-receipt validation).
    """

    merchant_slug: str
    canonical_name: str
    aliases: tuple[str, ...] = field(default_factory=tuple)
    country: str = ""            # ISO 3166-1 alpha-2 ("US", "SE", "DE"); empty if unknown
    default_category: str = ""   # category slug
    notes: str = ""
    last_seen: str = ""          # ISO date of most recent transaction
    tax_id: str = ""             # VAT/Org/EIN; persistent across receipts


@dataclass(frozen=True, slots=True)
class Budget:
    """A spend cap for a (period, scope, category) tuple.

    ``period`` is one of ``"monthly"``, ``"quarterly"``, ``"yearly"``, or
    a literal ``"YYYY-MM"`` / ``"YYYY-Qn"`` / ``"YYYY"`` for one-off budgets.
    Recurring budgets reset each period; one-off budgets do not.

    ``scope_type`` distinguishes global / project / category-only budgets:
        ``"global"``    — applies across everything; ``scope_slug`` empty
        ``"project"``   — applies to one project; ``scope_slug`` is project slug
        ``"category"``  — applies to one category; ``scope_slug`` is category slug

    A budget can have BOTH a project scope and a category filter — that's
    the ``"project"`` scope_type with ``category_slug`` set (e.g. "AI infra
    spending on the Phoenix project capped at $200/month").

    ``amount`` is the cap; ``currency`` is the currency the cap is denominated
    in. Cross-currency consumption is converted at FX rate from the period's
    midpoint (cheap, deterministic, good enough for personal use). The exact
    FX policy lives in ``budget-track/config/`` for Phase 2.
    """

    budget_id: str
    period: str
    scope_type: str              # "global" | "project" | "category"
    scope_slug: str = ""
    category_slug: str = ""
    amount: str = "0.00"
    currency: str = ""
    notes: str = ""
    created_at: str = ""
    updated_at: str = ""
