"""Deterministic demo data for the mobile statement screen.

`python manage.py seed_demo` creates the `acct-demo` account that
`mobile/src/StatementScreen.js` requests by default (period `2026-06`), with
an issued invoice and a handful of corrections chosen to exercise the
interesting cases `docs/STATEMENT.md`'s audit-history section calls out:

  * an ordinary correction, as a baseline;
  * a correction that nets to a 0.00 signed effect, which is then withdrawn --
    this reproduces Tomas's "deleted withdrawal" ticket
    (docs/chat/billing-recon.md, 2026-07-09 11:58): the withdrawal record
    itself must stay visible even though its net effect is 0.00;
  * a correction that is later superseded by a replacement version -- both
    versions must remain visible, the older one marked superseded.

Without it a fresh database has no such account, so
`GET /api/accounts/acct-demo/statement/2026-06` 404s and the statement screen
has nothing real to render.

Idempotent: corrections are delivered through the same
`adjustments.services.deliver` path production traffic uses, which already
absorbs a re-delivered `adjustment_id` (see its docstring), and the
account/invoice/line-item rows are created with get_or_create. Running this
command twice in a row -- or after `make demo` was already run once against
the same database -- does not error and does not duplicate anything.

Resolution is invoked synchronously: `adjustments.tasks.apply_correction` is
called directly (the same thing `tests/test_visible.py` does) rather than
relying on its `.delay()` making it onto the broker and a worker picking it
up before this command exits. `deliver()`'s own internal `.delay()` call is
patched out for the duration of this command for the same reason `deliver`'s
worker interaction is undesirable here: it would enqueue the exact same
resolution work a second time for no benefit, and it would make this
command's result depend on the Redis broker being reachable the moment it
runs, rather than only on the Django cache it already needs for acceptance
bookkeeping. Either way the outcome is identical and safe -- if the `worker`
container also happens to be up and dequeues the same task later, it finds
the checkpoint already `posted` and no-ops.
"""

from __future__ import annotations

from decimal import Decimal
from unittest import mock

from django.core.management.base import BaseCommand

from adjustments import services, tasks
from adjustments.models import Adjustment, Account, Invoice, LineItem

ACCOUNT_ID = "acct-demo"
PERIOD_KEY = "2026-06"
INVOICE_ID = f"INV-{ACCOUNT_ID}-{PERIOD_KEY}"


def _deliver_and_resolve(account_id: str, payload: dict) -> None:
    """Deliver one correction and resolve it before returning.

    `services.deliver` returns `None` both when the delivery is a brand new
    acceptance it just queued and when the identifier was already accepted on
    a prior run of this command -- in the latter case there is nothing new to
    resolve, so only a non-`None`, freshly created `Adjustment` is resolved
    here.
    """

    try:
        adjustment = services.deliver(account_id, payload)
    except services.RefusedError:
        # `deliver` absorbs a re-delivered identifier by consulting the accepted
        # set, which lives in Redis -- and Redis here runs with persistence off.
        # So after `docker compose down -v`, or any restart that empties the
        # cache while MySQL keeps its rows, a second `make demo` re-offers a
        # correction the durable store already has and intake refuses it. That
        # is correct behaviour from `deliver`; it is this command's job to be
        # re-runnable regardless, so a refusal is only fatal if the row is NOT
        # already there.
        if not Adjustment.objects.filter(
            adjustment_id=payload["adjustment_id"]
        ).exists():
            raise
        return
    if adjustment is not None:
        tasks.apply_correction(adjustment.pk)


class Command(BaseCommand):
    help = (
        "Seed the deterministic acct-demo account (an issued invoice plus a "
        "few corrections, including a correction withdrawn to a 0.00 net "
        "effect and a superseded version) so the mobile statement screen "
        "has real data to render against. Safe to run more than once."
    )

    def handle(self, *args, **options) -> None:
        with mock.patch.object(tasks.apply_correction, "delay", lambda *a, **k: None):
            self._seed()

    def _seed(self) -> None:
        account, _ = Account.objects.get_or_create(account_id=ACCOUNT_ID)
        invoice, invoice_created = Invoice.objects.get_or_create(
            invoice_id=INVOICE_ID,
            defaults=dict(
                account=account,
                period_key=PERIOD_KEY,
                total=Decimal("500.00"),
                issued_total=Decimal("500.00"),
                issued=True,
            ),
        )
        if invoice_created:
            LineItem.objects.create(
                invoice=invoice,
                usage_key="base-plan",
                description="Base plan",
                quantity=1,
                base=Decimal("300.00"),
                net=Decimal("300.00"),
                tax=Decimal("0.00"),
                total=Decimal("300.00"),
            )
            LineItem.objects.create(
                invoice=invoice,
                usage_key="metered-usage",
                description="Metered usage",
                quantity=1,
                base=Decimal("200.00"),
                net=Decimal("200.00"),
                tax=Decimal("0.00"),
                total=Decimal("200.00"),
            )
            self.stdout.write(f"created invoice {INVOICE_ID}")
        else:
            self.stdout.write(f"invoice {INVOICE_ID} already exists")

        # 1. An ordinary correction -- the baseline case.
        _deliver_and_resolve(
            ACCOUNT_ID,
            {
                "adjustment_id": "adj-demo-credit",
                "period_key": PERIOD_KEY,
                "kind": "credit",
                "amount": Decimal("10.00"),
                "effective_at": "2026-07-01T00:00:00Z",
                "received_at": "2026-07-01T00:05:00Z",
            },
        )

        # 2. A correction that nets to 0.00, then withdrawn. The withdrawal
        #    record must remain visible on the statement even though its
        #    net effect is 0.00 -- see docs/STATEMENT.md's "Visible
        #    audit history".
        _deliver_and_resolve(
            ACCOUNT_ID,
            {
                "adjustment_id": "adj-demo-zero",
                "period_key": PERIOD_KEY,
                "kind": "credit",
                "amount": Decimal("0.00"),
                "effective_at": "2026-07-02T00:00:00Z",
                "received_at": "2026-07-02T00:05:00Z",
            },
        )
        _deliver_and_resolve(
            ACCOUNT_ID,
            {
                "adjustment_id": "adj-demo-zero-withdrawal",
                "period_key": PERIOD_KEY,
                "kind": "withdrawal",
                "withdraws_adjustment_id": "adj-demo-zero",
                "effective_at": "2026-07-03T00:00:00Z",
                "received_at": "2026-07-03T00:05:00Z",
            },
        )

        # 3. A correction later superseded by a replacement version. Both
        #    versions must remain visible; the older one is marked
        #    superseded and names its replacement.
        _deliver_and_resolve(
            ACCOUNT_ID,
            {
                "adjustment_id": "adj-demo-super-v1",
                "period_key": PERIOD_KEY,
                "kind": "credit",
                "amount": Decimal("3.00"),
                "subject_key": "demo-superseded-subject",
                "effective_at": "2026-07-04T00:00:00Z",
                "received_at": "2026-07-04T00:05:00Z",
            },
        )

        # A statement run settles everything accepted so far, so the demo
        # shows both a settled group and -- once the replacement below lands
        # unsettled -- a pending one. Re-running this command repeats the
        # same operation_id, which issue_statement_run treats as a retry of
        # the same run rather than issuing a second one.
        services.issue_statement_run(ACCOUNT_ID, "op-demo-1")

        _deliver_and_resolve(
            ACCOUNT_ID,
            {
                "adjustment_id": "adj-demo-super-v2",
                "period_key": PERIOD_KEY,
                "kind": "credit",
                "amount": Decimal("5.00"),
                "subject_key": "demo-superseded-subject",
                "replaces_adjustment_id": "adj-demo-super-v1",
                "effective_at": "2026-07-05T00:00:00Z",
                "received_at": "2026-07-05T00:05:00Z",
            },
        )

        self.stdout.write(
            self.style.SUCCESS(
                f"seeded {ACCOUNT_ID}: {account.adjustments.count()} "
                f"adjustment(s), {account.records.count()} record(s) on "
                f"invoice {INVOICE_ID}"
            )
        )
        self.stdout.write(
            "try: curl "
            f"http://localhost:$PORT/api/accounts/{ACCOUNT_ID}/statement/{PERIOD_KEY}"
            "  (whatever host port you mapped web to; 8000 by default)"
        )
