"""Idempotent finance migrations run at server startup.

The receipt → draft → ledger pipeline added the
``finance/library/YYYY-MM/`` attachment layout in 2026-04 (see
``CLAUDE.md`` "Finance" notes). Pre-migration vaults have transactions
whose ``source_file`` still points at ``finance/inbox/``; this module
sweeps them on startup, moves the file, updates the ledger row + the
``transactions/YYYY-MM/`` markdown, and emits an audit-trail event.

The migration is:
  - idempotent — already-migrated rows are skipped, missing source files
    leave the row alone (we don't fabricate a path)
  - bounded — caps at ``max_rows`` per pass so a huge backlog can't
    block startup; remaining rows pick up on next boot
  - non-fatal — every per-row exception is logged + counted but never
    propagated; the server boots regardless
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from agntspace.finance.storage import (
    attach_receipt_to_library,
    draft_path,
    transaction_path,
    write_draft,
    write_transaction,
)
from agntspace.finance.types import (
    TransactionDraft,
)

if TYPE_CHECKING:
    from agntspace.finance.ledger import FinanceLedger
    from agntspace.vault.paths import VaultPaths
    from agntspace.vault.reader import VaultReader
    from agntspace.vault.writer import VaultWriter

log = logging.getLogger(__name__)


async def migrate_inbox_attachments(
    ledger: FinanceLedger,
    paths: VaultPaths,
    writer: VaultWriter,
    *,
    max_rows: int = 500,
    reader: VaultReader | None = None,
) -> dict:
    """Move existing transactions' originals from inbox/ to library/.

    Returns a counters dict: ``{scanned, migrated, skipped_missing,
    skipped_already_in_library, markdown_rewrites, errors}``. Safe to
    call on every server boot — already-attached rows are skipped
    without I/O, and stale on-disk markdown frontmatter (where a previous
    migration ran but didn't refresh the markdown) is healed via the
    ``markdown_rewrites`` path.

    ``reader`` enables stale-markdown detection. When omitted, the
    "already in library" branch trusts the ledger and never inspects
    the markdown — the file move still happened on the original pass,
    so the receipt is reachable; only the on-disk frontmatter could be
    out of date.
    """
    counters = {
        "scanned": 0,
        "migrated": 0,
        "skipped_missing": 0,
        "skipped_already_in_library": 0,
        "markdown_rewrites": 0,
        "errors": 0,
    }

    if ledger is None or paths is None or writer is None:
        return counters

    try:
        # ``status=""`` returns visible (non-archived) rows. We migrate
        # rejected too — the original is part of the audit trail.
        rows = await ledger.list_transactions(status="", limit=max_rows)
    except Exception:
        log.exception("inbox-attachment migration: list_transactions failed")
        counters["errors"] += 1
        return counters

    for txn in rows:
        counters["scanned"] += 1
        source = (txn.source_file or "").replace("\\", "/").strip()
        if not source:
            continue
        if source.startswith("finance/library/"):
            counters["skipped_already_in_library"] += 1
            # Heal stale markdown: a previous boot may have updated the
            # ledger pointer but not the on-disk frontmatter (e.g. the
            # rewrite step crashed, or the rewrite-for-drafts logic
            # didn't exist yet). Rewriting is cheap + idempotent — but
            # we only do it when ``reader`` is wired AND we can prove
            # the markdown is stale, to avoid a no-op write storm.
            if reader is not None:
                try:
                    if _markdown_pointer_is_stale(reader, txn, source):
                        _rewrite_markdown_for_pointer(writer, txn)
                        counters["markdown_rewrites"] += 1
                except Exception:
                    log.exception(
                        "inbox-attachment migration: stale-markdown heal failed for %s",
                        txn.txn_id,
                    )
            continue
        if not source.startswith("finance/inbox/"):
            # Anything else (manual entry pointing at a custom location)
            # is left alone — we don't move what we don't own.
            continue

        try:
            new_logical = attach_receipt_to_library(
                paths,
                source_logical_path=source,
                source_physical_path=None,
                draft_id=txn.txn_id,
                txn_date=txn.txn_date,
            )
        except Exception:
            log.exception("inbox-attachment migration: attach failed for %s", txn.txn_id)
            counters["errors"] += 1
            continue

        if not new_logical or new_logical == source:
            # Either the file was missing or attach refused. Either way
            # the row stays as-is and we count it.
            counters["skipped_missing"] += 1
            continue

        try:
            updated = await ledger.update_source_file(txn.txn_id, new_logical)
        except Exception:
            log.exception("inbox-attachment migration: ledger update failed for %s",
                          txn.txn_id)
            counters["errors"] += 1
            continue
        if not updated:
            counters["errors"] += 1
            continue

        # Re-fetch to get the updated row, then rewrite the markdown so
        # the on-disk source_file frontmatter matches the ledger truth.
        # Both verified transactions (transactions/YYYY-MM/) AND pending
        # drafts (pending/{draft_id}.md) need the rewrite — anything reading
        # the markdown directly (Obsidian, manual scripts, the rejected
        # archive helper) would otherwise see a broken inbox pointer.
        try:
            refreshed = await ledger.get_transaction(txn.txn_id)
            if refreshed is not None and refreshed.txn_date:
                status_val = refreshed.status.value
                if status_val == "verified":
                    write_transaction(writer, refreshed)
                    log.info(
                        "inbox-attachment migration: re-wrote %s to %s",
                        transaction_path(refreshed.txn_id, refreshed.txn_date),
                        new_logical,
                    )
                elif status_val == "draft":
                    draft = _txn_to_draft(refreshed)
                    write_draft(writer, draft)
                    log.info(
                        "inbox-attachment migration: re-wrote %s to %s",
                        draft_path(refreshed.txn_id),
                        new_logical,
                    )
                # rejected/archived/superseded: file already in library,
                # markdown lives elsewhere or is intentionally frozen — skip.
        except Exception:
            log.exception(
                "inbox-attachment migration: markdown rewrite failed for %s",
                txn.txn_id,
            )
            # File + ledger are migrated already — markdown drift is not
            # fatal, the next write to that row will refresh frontmatter.

        counters["migrated"] += 1

    if counters["migrated"] or counters["errors"]:
        log.info("inbox-attachment migration: %s", counters)
    return counters


def _markdown_pointer_is_stale(reader: VaultReader, txn, ledger_source: str) -> bool:
    """Return True iff the on-disk markdown frontmatter still records an
    older ``source_file`` than the ledger.

    Conservative: returns False when the markdown can't be read (we don't
    know it's stale), True only when there's a clear mismatch.
    """
    if reader is None or txn is None or not ledger_source:
        return False
    status_val = txn.status.value
    if status_val == "verified":
        path = transaction_path(txn.txn_id, txn.txn_date)
    elif status_val == "draft":
        path = draft_path(txn.txn_id)
    else:
        return False
    try:
        doc = reader.read(path)
    except FileNotFoundError:
        return False
    except Exception:
        return False
    if doc is None or not getattr(doc, "metadata", None):
        return False
    on_disk = str(doc.metadata.get("source_file") or "").replace("\\", "/").strip()
    return bool(on_disk) and on_disk != ledger_source


def _rewrite_markdown_for_pointer(writer: VaultWriter, txn) -> None:
    """Rewrite a transaction's markdown so frontmatter ``source_file``
    matches the ledger row. Idempotent — overwrites whatever's there.
    """
    status_val = txn.status.value
    if status_val == "verified":
        write_transaction(writer, txn)
    elif status_val == "draft":
        write_draft(writer, _txn_to_draft(txn))


def _txn_to_draft(txn) -> TransactionDraft:
    """Build a TransactionDraft from a ledger Transaction row.

    Used by the migration to rewrite a draft's pending markdown when its
    ledger source_file pointer changes. The draft markdown can't be
    derived from the Transaction shape directly because TransactionDraft
    has a few extra fields (``merchant_raw``, ``audit_flags``,
    ``extracted_at``) that Transaction lacks. We populate them from
    sensible defaults — the markdown body has always been agent-generated
    boilerplate, the load-bearing data is in the frontmatter and that's
    what we want correct.
    """
    return TransactionDraft(
        draft_id=txn.txn_id,
        txn_date=txn.txn_date,
        merchant_slug=txn.merchant_slug,
        merchant_raw=txn.merchant_slug or "",  # canonical name is closest fallback
        currency=txn.currency,
        total=txn.total,
        tax=txn.tax,
        project_slug=txn.project_slug,
        items=txn.items,
        payment_method=txn.payment_method,
        source_file=txn.source_file,
        content_hash=txn.content_hash,
        correlation_id=txn.correlation_id,
        notes=txn.notes,
        audit_flags=(),
        extracted_at=txn.created_at,
        receipt_number=txn.receipt_number,
        document_type=txn.document_type,
        subtotal=txn.subtotal,
        vendor_tax_id=txn.vendor_tax_id,
        vendor_address=txn.vendor_address,
        due_date=txn.due_date,
        discount_total=txn.discount_total,
        discount_description=txn.discount_description,
        tip=txn.tip,
        tax_breakdown=txn.tax_breakdown,
        buyer_tax_id=txn.buyer_tax_id,
        reverse_charge=txn.reverse_charge,
    )


__all__ = ["migrate_inbox_attachments"]
