"""Emit the widest status report this agent can produce, as a fixture.

WHY THIS EXISTS

The CMS validates the status payload against an allow-list and stores only what
it validated. That is deliberate — it stops a future agent widening what lands
in a customer's database (ADR-0002) — but it fails in one direction silently: a
field with no rule on the CMS side is accepted with a 200 and then dropped. The
agent looks healthy, the CMS looks current, and the value is simply gone.

That happened. `can_link` shipped on both sides, the CMS had no rule for it, and
the WhatsApp settings page could not draw a Connect button. Nothing logged an
error, because as far as either half was concerned nothing went wrong.

So this writes every field the agent can send into a fixture the CMS test suite
replays. Add a field to the report, regenerate, and the CMS test fails until a
validation rule exists for it. The fixture is committed to the CMS repository so
that suite needs no Python.

    python scripts/emit_status.py > ../life associate/tests/Feature/Ai/fixtures/agent_status_report.json

WHAT IT FAKES, AND WHAT IT DOES NOT

The container is a stub — there is no database, no OCR engine and no WhatsApp
here. But the *assembly* is the real thing: `_whatsapp`, `_workflows` and `_ocr`
are the functions the daemon runs, so a field added there appears here without
anybody remembering to update this script. That is the whole point; a fixture
maintained by hand would drift the same way the validator did.

Components are the one exception. `health.readiness` wants a working deployment,
so they are built from REPORTED_COMPONENTS — the same list `_components` filters
against, which is why it is a module constant rather than a local.
"""

from __future__ import annotations

import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from app.runtime import status_report  # noqa: E402
from app.whatsapp.session import SessionState, SessionStatus  # noqa: E402


class _Provider:
    """A linked Evolution account, reporting everything a provider can."""

    name = "evolution"

    # `_can_send` reads this directly: Evolution records rather than transmits
    # without a base URL.
    _base_url = "http://127.0.0.1:8081"

    def session_status(self) -> SessionStatus:
        return SessionStatus(
            SessionState.CONNECTED,
            detail="linked",
            number="923001234567",
            since=datetime(2026, 7, 30, 9, 0, tzinfo=timezone.utc),
        )

    # start_link and unlink are never called here. They exist because
    # supports_linking() is an isinstance check against the LinkableProvider
    # protocol, so a stub missing either one reports can_link false — which is
    # exactly the bug this fixture guards against, and worth having the
    # generator itself subject to.
    def start_link(self):  # pragma: no cover
        raise NotImplementedError

    def unlink(self) -> SessionStatus:  # pragma: no cover
        raise NotImplementedError


class _Policy:
    is_configured = True


class _Inbox:
    policy = _Policy()
    allowed = 42
    ignored = 7

    # Far enough in the past that the computed last_event_seconds is a real
    # number rather than a racy zero — the CMS contract test replays the exact
    # value, so it only needs to be deterministic within one generation.
    from datetime import UTC as _UTC, datetime as _dt, timedelta as _td
    last_event_at = _dt.now(_UTC) - _td(seconds=321)

    # Every counter the real InboxFilter carries. Distinct values on purpose:
    # there are five of these now, which is exactly the point at which a rule
    # gets copied on the CMS side and left pointing at the wrong field — and two
    # counters sharing a value would let that pass.
    forwards_accepted = 5
    forwards_rejected = 2
    forwards_failed = 1


class _Ocr:
    name = "paddleocr"


class _Stats:
    completed = 12
    failed = 1
    awaiting = 3
    retried = 2
    median_seconds = 18.5


class _Container:
    whatsapp = _Provider()
    inbox = _Inbox()
    ocr = _Ocr()
    connect = object()

    # Present but useless, which is what the health checks are built to survive.
    # Absent entirely, `_compatibility` raises before its own guard and the
    # drift check below prints a traceback that looks like a real failure.
    cms = None


def _report() -> dict:
    """The maximal payload, assembled by the daemon's own code."""
    container = _Container()

    # _workflows imports this inside the function, so replacing the module
    # attribute is enough and no database is needed.
    import app.runtime.workflow_stats as workflow_stats_module

    workflow_stats_module.workflow_stats = lambda connect, days: _Stats()

    return {
        "version": "1.0.0",
        "uptime_seconds": 3600,
        # Every component the CMS accepts, so a name added on this side without
        # a matching rule shows up as a failing CMS test.
        "components": {name: "ok" for name in sorted(status_report.REPORTED_COMPONENTS)},
        "whatsapp": status_report._whatsapp(container),
        "workflows": status_report._workflows(container),
        "ocr": status_report._ocr(container),
        # Recorded through the real tally, so a field added to its report
        # appears here and fails the CMS test until a rule exists for it.
        "classification": _classification(),
        # Read from this very process, which is the honest thing to put in a
        # fixture about memory: whatever number lands here is one the reporting
        # code actually produced.
        "process": status_report._process(),
    }


def _check_for_drift(report: dict) -> None:
    """Refuse to emit a fixture that has fallen behind the report it describes.

    This script protects the CMS from a field with no validation rule. Nothing
    protected it from the same bug one level up: the payload above is assembled
    block by block, so a block added to `status_report.build` and not added here
    is missing from the fixture, the CMS test passes, and the field is silently
    dropped in production exactly as `can_link` was.

    That is not hypothetical either — `process` was added to the report and this
    list did not know about it, which is why this function exists.

    Only additions in `build` are an error. The fixture is deliberately *wider*:
    it carries blocks a stub container cannot produce.
    """
    # Quietly, because this is expected to fail in one specific way: the stub
    # cannot satisfy `health.readiness`, which is exactly why `components` is
    # built by hand above rather than from it. `build` swallows that and logs a
    # traceback, which on a script whose job is to succeed reads like a fault.
    logging.disable(logging.CRITICAL)

    try:
        produced = status_report.build(_Container())
    finally:
        logging.disable(logging.NOTSET)

    missing = sorted(set(produced) - set(report))

    if missing:
        raise SystemExit(
            "The fixture is out of date. status_report.build() now reports "
            f"{', '.join(missing)}, which _report() does not include. Add it "
            "above, then add a validation rule in the CMS's StatusApiController "
            "— without the rule the field is accepted and silently discarded."
        )


def _classification() -> dict:
    """A tally with one of each stage in it.

    Distinct counts per stage, so a CMS rule copied from the line above and left
    pointing at the wrong key shows up as a mismatch rather than passing because
    two stages happened to hold the same number.
    """
    from app.documents.statistics import ClassificationStats

    stats = ClassificationStats()

    for method, times, known, read in (
        ("caption", 5, True, False),
        ("filename", 3, True, False),
        ("rules", 2, True, True),
        ("none", 1, False, True),
    ):
        for _ in range(times):
            # Deliberately not a round number. A mean of exactly 250.0 encodes
            # as `250.0` here and decodes to an integer on the PHP side, so the
            # contract test compared 250 against 250.0 and failed on a
            # difference that does not exist. A fractional value round-trips
            # unambiguously — and is what a real mean looks like anyway.
            stats.record(method, known=known, was_read=read, seconds=0.2345)

    return stats.report()


if __name__ == "__main__":
    report = _report()
    _check_for_drift(report)

    print(json.dumps(report, indent=4, sort_keys=True))
