"""What this deployment tells the CMS about itself.

The CMS cannot find any of this out. ADR-0005 makes the Agent API the only
channel between the two products and this side the client, so whether WhatsApp
is connected, whether OCR loaded, how many runs are in flight — all of it is on
the far side of a boundary the CMS does not cross. Its only signal today is
`last_seen_at`, which answers "alive" and nothing else.

So this reports. Same signed channel, same authentication, no new direction of
traffic and no credential handed to the CMS.

WHAT IS NOT IN IT

Anything about a client. ADR-0002 keeps CNICs, documents, names and phone
numbers on the installation side, and this payload ends up rendered on an
operator's screen in a browser. So it carries component states, counts and
averages — never a number, never a name, never a line of a document.

That is enforced twice: assembled from allow-listed sources here, and validated
against an allow-list at the CMS, which stores only what it validated. Two
checks because one of them is on the other side of a release boundary, and the
day they disagree should be a day nothing leaks.

WHY IT REUSES readiness()

The components reported are the ones `/health/ready` already computes. A status
report assembled from its own separate inspection would be a second opinion —
and the day the two disagreed, nobody would know which screen to believe.
"""

from __future__ import annotations

import logging
import time
from typing import Any

logger = logging.getLogger(__name__)

#: How much of the workflow history to summarise.
#:
#: Seven days matches what /metrics reports, so an operator comparing the CMS
#: screen with the Prometheus series is comparing the same window.
WINDOW_DAYS = 7

#: Component names the CMS accepts, and therefore the only ones worth sending.
#:
#: Named rather than inlined so scripts/emit_status.py can build the contract
#: fixture from the same list the daemon reports from. Anything outside it the
#: CMS drops on arrival, and sending it would suggest it was being recorded.
REPORTED_COMPONENTS = frozenset(
    {"database", "ocr", "whatsapp", "memory", "cms", "compatibility"}
)

_STARTED_AT = time.monotonic()


def build(container, daemon=None) -> dict[str, Any]:
    """Assemble the report. Never raises — a status report is not worth an outage."""
    from app.release.version import VERSION

    report: dict[str, Any] = {
        "version": VERSION,
        "uptime_seconds": int(time.monotonic() - _STARTED_AT),
        "components": _components(container, daemon),
    }

    whatsapp = _whatsapp(container)

    if whatsapp:
        report["whatsapp"] = whatsapp

    workflows = _workflows(container)

    if workflows:
        report["workflows"] = workflows

    ocr = _ocr(container)

    if ocr:
        report["ocr"] = ocr

    classification = _classification()

    if classification:
        report["classification"] = classification

    process = _process()

    if process:
        report["process"] = process

    return report


def _process() -> dict[str, Any]:
    """What this process is costing the machine it runs on.

    The one component of health the CMS could previously only guess at. Its own
    memory check reports a PHP request's peak against PHP's limit, which is a
    different process with a different lifetime and no OCR models in it.

    Omitted rather than zeroed when it cannot be read, so the page can say
    "not reported" instead of showing a deployment using no memory at all.
    """
    from app.runtime.process_memory import usage

    return usage()


def _classification() -> dict[str, Any]:
    """How the classifier has been doing since this process started.

    Omitted entirely until something has been classified. A screen reporting
    "0% unknown" before any document has arrived is a comfortable lie, and the
    CMS renders an absent block as "nothing yet" rather than as zeros.

    Level 1 throughout: counts, rates and stage names. Nothing here identifies a
    person or a document (ADR-0002).
    """
    from app.documents.statistics import STATS

    return STATS.report()


def send(container, daemon=None) -> bool:
    """Report to the CMS. Returns whether it landed.

    Failure is logged and swallowed. This is telemetry: a CMS that is briefly
    unreachable must not take down the process that reads clients' documents,
    and the next report is a few minutes away.
    """
    try:
        container.cms.report_status(build(container, daemon))

        return True
    except Exception as exc:  # noqa: BLE001 - telemetry never breaks the caller
        logger.warning("Could not report status to the CMS: %s", exc)

        return False


# ── Assembling ────────────────────────────────────────────────────────────


def _components(container, daemon) -> dict[str, str]:
    """The same components /health/ready reports, as plain states."""
    from app.runtime import health

    try:
        report = health.readiness(container, daemon)
    except Exception:  # noqa: BLE001
        logger.warning("Could not read health for the status report.", exc_info=True)

        return {}

    return {
        component.name: component.status.value
        for component in report.components
        if component.name in REPORTED_COMPONENTS
    }


def _whatsapp(container) -> dict[str, Any]:
    """Which transport, whether it can actually send, and what it turned away.

    `can_send` rather than "connected", because nothing here opens a connection
    to find out — Meta is only constructed when credentials exist, and Evolution
    records instead of transmitting without a base URL. Reporting a boolean
    nothing measures would be worse than reporting none.

    The two counts matter because "the AI is not seeing my documents" and "the
    AI is reading conversations it should ignore" are the two real complaints,
    and they look identical without them.

    Note what is NOT sent: `policy.describe()` lists the allow-listed numbers,
    which are client identifiers under ADR-0002 — and this payload is stored in
    the CMS and rendered in a browser.
    """
    provider = getattr(container, "whatsapp", None)
    inbox = getattr(container, "inbox", None)

    name = str(getattr(provider, "name", "none")) if provider is not None else "none"
    name = name if name in {"meta", "evolution"} else "none"

    from app.whatsapp.session import status_of, supports_linking

    session = status_of(provider) if provider is not None else None

    report: dict[str, Any] = {
        "provider": name,
        "can_send": _can_send(provider, name),
        # Now means "the connected number is known", which is the only thing the
        # inbox can be missing. There is nothing to configure.
        #
        # Resolved rather than read: the owner is discovered on demand, so a
        # linked account that has not yet been sent anything would otherwise
        # report itself as having no account linked.
        "inbox_configured": bool(getattr(_inbox_policy(inbox), "is_configured", False)),
        # Which rule is in force. Sent so the CMS states the guarantee from what
        # the agent is actually running rather than from what its own release
        # happens to believe — the two are separately deployed, and a page
        # promising "self-chat only" over an agent that does something else is
        # the worst possible way to be wrong about this.
        "inbox_policy": "self_chat",
        # Whether an account can be linked by scanning a QR at all. The dashboard
        # needs this BEFORE it draws a Connect button: Meta has no QR flow, and
        # offering one would be offering something that cannot exist.
        "can_link": supports_linking(provider) if provider is not None else False,
    }

    if session is not None:
        report["session_state"] = session.state.value
        report["connected_number"] = session.number

    # How long since a message last proved the pipe works. Surfaced rather than
    # alerted on, because silence is ambiguous — a quiet evening and a dead
    # stream look identical from here — but a person reading "last event 4h
    # ago" against "connected" has exactly the contradiction that found the
    # 2026-08-03 outage. Absent until anything has arrived: "0 seconds ago" on
    # a fresh start would be a claim, not a measurement.
    last_event = getattr(inbox, "last_event_at", None)

    if last_event is not None:
        from datetime import UTC, datetime

        report["last_event_seconds"] = max(0, int((datetime.now(UTC) - last_event).total_seconds()))

    counters = (
        ("messages_accepted", "allowed"),
        ("messages_ignored", "ignored"),
        # Forwarded attachments taken, refused, and never fetched. These are the
        # only record that anything but the first ever existed: neither produces
        # a proposal, so the Approval Queue shows nothing and whoever sent it is
        # waiting for a document that is not coming.
        #
        # Refused and failed are separate because the answer differs. Refused is
        # about the file — ask the sender. Failed is about the provider.
        ("forwards_accepted", "forwards_accepted"),
        ("forwards_rejected", "forwards_rejected"),
        ("forwards_failed", "forwards_failed"),
    )

    for key, attribute in counters:
        value = getattr(inbox, attribute, None)

        if isinstance(value, int):
            report[key] = value

    return report


def _inbox_policy(inbox):
    """The inbox's policy, asking it to resolve the owner if that is due.

    Tolerant of a stub inbox, because this is telemetry and a status report is
    never worth an outage.
    """
    resolve = getattr(inbox, "current_policy", None)

    if callable(resolve):
        try:
            return resolve()
        except Exception:  # noqa: BLE001
            logger.debug("Could not resolve the inbox owner for the status report.")

    return getattr(inbox, "policy", None)


def _can_send(provider, name: str) -> bool:
    if provider is None or name == "none":
        return False

    if name == "meta":
        # Only constructed when credentials are present.
        return True

    # Evolution records rather than transmits without a base URL, which is a
    # deployment that looks healthy and sends nothing.
    return bool(getattr(provider, "_base_url", ""))


def _workflows(container) -> dict[str, Any]:
    connect = getattr(container, "connect", None)

    if connect is None:
        # In-memory runs. There is nothing durable to count, and reporting zeros
        # would read as "nothing happened" rather than "nothing is recorded".
        return {}

    try:
        from app.runtime.workflow_stats import workflow_stats

        stats = workflow_stats(connect, days=WINDOW_DAYS)
    except Exception:  # noqa: BLE001
        logger.warning("Could not read workflow statistics for the status report.", exc_info=True)

        return {}

    if stats is None:
        return {}

    report = {
        "completed": stats.completed,
        "failed": stats.failed,
        "awaiting": stats.awaiting,
        "retried": stats.retried,
    }

    if stats.median_seconds is not None:
        report["median_seconds"] = stats.median_seconds

    return report


def _ocr(container) -> dict[str, Any]:
    engine = getattr(container, "ocr", None)

    if engine is None:
        return {}

    return {"engine": str(getattr(engine, "name", "unknown"))}
