"""App-identity cache for the mgf.common observability stack.

Set once at startup via :func:`mgf.common.bootstrap`. Subsequent
calls in the observability layer (``setup_logging``, ``setup_otel``,
``report_now``, ``install_excepthook``, ...) read the identity
from the module-level cache here.

Why module-level state and not a contextvar:
- The identity is process-scoped, not request-scoped.
- A contextvar would suggest "different apps in the same process"
  which is not a real use case for any consumer of this library.
- Module-level state is faster (no descriptor lookup) and simpler
  to reason about.

The defaults below are intentionally placeholder strings — every
production caller MUST call :func:`mgf.common.bootstrap` before any
observability function runs. The defaults exist only so unit tests
that touch the observability layer don't trip on uninitialised
state.

v0.4 Phase B: :func:`_derive_identity_from_metadata` is a
conservative auto-derivation helper that lets ``bootstrap()`` be
called with no args. It looks at ``sys.modules["__main__"]`` and
derives ``(name, version)`` from the launching package's
distribution metadata when the app was started via ``python -m
<pkg>`` or a console-script entry that preserves ``__spec__``.

History: the legacy ``configure(app_name, app_version)`` setter
was removed in v0.33.0 per its AP-12 deprecation cycle. The
internal :func:`_set_identity` helper below is what
:func:`mgf.common.bootstrap` (and the test-only
:func:`mgf.common.bootstrap_for_test`) call to populate the cache.
"""

from __future__ import annotations

import sys
import warnings
from typing import Any

# Defaults are placeholders. Real consumers MUST call bootstrap().
_APP_NAME: str = "app"
_APP_VERSION: str = "unknown"

# Tracks whether bootstrap() has been called this process. Reading
# identity (app_name() / app_version()) before bootstrap() emits a
# one-shot UserWarning so the silent-wrong-default failure mode
# (logs land at ``<state>/app/logs/app.log``, crash reports'
# ``app`` field reads ``"app"``) becomes loud.
#
# Closes FEEDBACK API-08. Warning, not error — third-party test
# harnesses that transitively import ``mgf.common.observability``
# must NOT be broken by a missing bootstrap().
_CONFIGURED: bool = False
_UNCONFIGURED_WARNED: bool = False


class UnconfiguredWarning(UserWarning):
    """Identity was read before :func:`bootstrap` was called.

    Raised once per process, not per call, to avoid spam. Inherits
    from :class:`UserWarning` so it shows by default but does not
    break test harnesses that have ``filterwarnings = ["error"]``
    set against the project's own warning categories (consumers
    that want this loud can ``warnings.filterwarnings("error",
    category=UnconfiguredWarning)``).
    """


def _set_identity(app_name: str, app_version: str) -> None:
    """Internal: set the cached identity.

    Called by :func:`mgf.common.bootstrap` (via ``_bootstrap_inner``)
    and by the test-only :func:`mgf.common.bootstrap_for_test`. Also
    used directly by tests under ``tests/unit/`` that need identity
    isolation without the full wiring stack (e.g.,
    ``tests/unit/test_logging_perf.py``,
    ``tests/unit/observability/conftest.py``).

    History: the public ``configure(app_name, app_version)`` setter
    that wrapped this helper (and emitted ``MgfDeprecationWarning``)
    was removed in v0.33.0 per its AP-12 cycle.
    """
    global _APP_NAME, _APP_VERSION, _CONFIGURED
    _APP_NAME = app_name
    _APP_VERSION = app_version
    _CONFIGURED = True


def _warn_if_unconfigured() -> None:
    """Emit ``UnconfiguredWarning`` once on first read before bootstrap().

    The one-shot guard (``_UNCONFIGURED_WARNED``) prevents spam.
    The warning is loud enough that operators see it in the first
    log line; it is not an exception so transitive imports don't
    break.
    """
    global _UNCONFIGURED_WARNED
    if _CONFIGURED or _UNCONFIGURED_WARNED:
        return
    _UNCONFIGURED_WARNED = True
    warnings.warn(
        "mgf.common: app identity not configured — reading the placeholder "
        "default 'app'. Call mgf.common.bootstrap(app_name=..., "
        "app_version=...) at your entry point before any observability "
        "function. See PUBLIC_API.md for details.",
        UnconfiguredWarning,
        stacklevel=3,  # past the wrapper + past app_name/app_version
    )


def set_identity(app_name: str, app_version: str) -> None:
    """Set the process-wide identity (app name + version) directly, WITHOUT the
    full :func:`mgf.common.bootstrap` wiring stack.

    For CLIs and short-lived tools that want their logs/records stamped with an
    identity but don't need the observability / lifespan / excepthook wiring.
    Long-running services use :func:`bootstrap` instead. (Public seam over the
    internal ``_set_identity`` — promoted in 0.49 so consumers stop reaching
    into ``mgf.common._identity``.)
    """
    _set_identity(app_name, app_version)


def app_name() -> str:
    """Return the current app name.

    Resolution order (FEEDBACK §6 Option C / closed-box redesign §4.1):

    1. The active :class:`mgf.common.AppContext` if one is set
       (via :func:`mgf.common.bootstrap` or per-task contextvar).
    2. The module-level global set by the legacy
       :func:`configure` path.
    3. The placeholder ``"app"`` (with a one-shot
       :class:`UnconfiguredWarning` — closes FEEDBACK API-08).
    """
    # Lazy import to break a startup cycle: lifecycle imports from
    # settings → exceptions → exceptions imports nothing from us,
    # but lifecycle ALSO imports from this module's neighbour
    # (logging_setup which calls app_name()). The lazy import keeps
    # the cycle dormant until first call.
    from mgf.common._lifecycle import current_context

    ctx = current_context()
    if ctx is not None:
        return ctx.app_name
    _warn_if_unconfigured()
    return _APP_NAME


def app_version() -> str:
    """Return the current app version.

    Resolution order (same as :func:`app_name`).
    """
    from mgf.common._lifecycle import current_context

    ctx = current_context()
    if ctx is not None:
        return ctx.app_version
    _warn_if_unconfigured()
    return _APP_VERSION


def _derive_identity_from_metadata(
    main_module: Any | None = None,
) -> tuple[str | None, str | None]:
    """Try to derive ``(app_name, app_version)`` from the launching
    package's distribution metadata.

    Conservative — only derives when ``sys.modules["__main__"]`` (or
    the explicit ``main_module`` arg, used by tests) has a
    ``__spec__`` whose ``parent`` resolves to an installed
    distribution. That covers the high-confidence cases:

      * ``python -m mypackage`` — ``__main__.__spec__.parent`` is
        ``"mypackage"``.
      * Console-script entries that preserve ``__spec__`` (modern
        ``pyproject.toml``-generated entry scripts on most
        platforms).

    Ad-hoc scripts (``python script.py``) and test-runner contexts
    (pytest, unittest) typically have no ``__spec__`` or an empty
    ``parent`` — those return ``(None, None)`` rather than guessing.
    Callers in those contexts SHOULD pass ``app_name`` /
    ``app_version`` explicitly.

    Parameters
    ----------
    main_module
        Optional override (test-only). When ``None``, reads
        ``sys.modules["__main__"]``. Tests inject a stub here to
        drive each branch deterministically.

    Returns
    -------
    tuple[str | None, str | None]
        ``(name, version)`` from the resolved distribution metadata,
        or ``(None, None)`` if no derivation is possible. NEVER
        raises — every failure mode falls through to ``(None, None)``.
    """
    if main_module is None:
        main_module = sys.modules.get("__main__")
    if main_module is None:
        return None, None
    spec = getattr(main_module, "__spec__", None)
    if spec is None:
        return None, None
    parent = getattr(spec, "parent", None)
    if not parent:
        return None, None
    try:
        # Lazy import to keep ``from mgf.common import ...`` cheap
        # for callers that pass app_name/app_version explicitly.
        from importlib.metadata import distribution

        dist = distribution(parent)
        return dist.name, dist.version
    except Exception:
        # PackageNotFoundError, transient I/O — return None/None and
        # let the caller decide whether to warn.
        return None, None


__all__ = ["UnconfiguredWarning", "app_name", "app_version"]
