"""Shared structured JSON logging (E-CC-LOG / AZ-245 / AZ-266).

Implements the `log_record_schema` v1.0.0 contract at
`_docs/02_document/contracts/shared_logging/log_record_schema.md`:

- One JSON object per log line.
- Stable field order: ``ts, level, component, frame_id, kind, msg, kv, exc``.
- Level normalisation: ``WARNING`` -> ``WARN``.
- ``frame_id`` is explicit ``null`` for non-frame records.
- Formatter never raises into the caller: a serialisation failure replaces
  the offending record's ``kv`` payload with
  ``{"_format_error": "<reason>"}`` and adds an internal WARN to the
  emitted bytes; the rest of the record still goes out.

Public surface (consumed by every onboard component):

- ``get_logger(component_id)`` -> ``logging.Logger`` (cached).
- ``configure_logging(tier, level)`` -> attach the handler topology for
  the active deployment tier without duplicating handlers on re-init.
"""

from __future__ import annotations

import datetime as _dt
import json
import logging
import os
import sys
import threading
from collections.abc import Iterable
from typing import Any, Final

# Schema field order (REQUIRED by contract; verified by AC-2).
_CONTRACT_FIELDS: Final[tuple[str, ...]] = (
    "ts",
    "level",
    "component",
    "frame_id",
    "kind",
    "msg",
    "kv",
    "exc",
)

# Default `kind` for records that don't pass one explicitly (startup / shutdown / unclassified).
_DEFAULT_KIND: Final[str] = "log.diag"

# Python stdlib LogRecord attributes that must not leak into `kv` when we
# auto-collect record extras. Kept as a frozenset for O(1) lookup.
_RESERVED_LOG_RECORD_KEYS: Final[frozenset[str]] = frozenset(
    {
        "args",
        "asctime",
        "created",
        "exc_info",
        "exc_text",
        "filename",
        "funcName",
        "levelname",
        "levelno",
        "lineno",
        "message",
        "module",
        "msecs",
        "msg",
        "name",
        "pathname",
        "process",
        "processName",
        "relativeCreated",
        "stack_info",
        "taskName",
        "thread",
        "threadName",
        # Contract-named keys live in the top-level payload, not in `kv`.
        "frame_id",
        "kind",
        "kv",
        "component",
    }
)


def _iso_utc(created_epoch: float) -> str:
    """Return RFC 3339 UTC timestamp with microsecond precision and ``Z`` suffix."""
    dt = _dt.datetime.fromtimestamp(created_epoch, tz=_dt.timezone.utc)
    return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond:06d}Z"


def _normalise_level(stdlib_levelname: str) -> str:
    """Translate Python stdlib level names to the contract enum."""
    if stdlib_levelname == "WARNING":
        return "WARN"
    return stdlib_levelname


def _coerce_jsonable(value: Any) -> Any:
    """Coerce arbitrary kv payloads into JSON-safe primitives.

    Raises ``TypeError`` / ``ValueError`` on un-serialisable content; the
    caller (the formatter) is responsible for replacing offending payloads
    with ``{"_format_error": ...}``.
    """
    if isinstance(value, (str, int, float, bool)) or value is None:
        return value
    if isinstance(value, dict):
        return {str(k): _coerce_jsonable(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        return [_coerce_jsonable(v) for v in value]
    raise TypeError(f"unserialisable kv payload type: {type(value).__name__}")


class JsonFormatter(logging.Formatter):
    """Emit one schema-compliant JSON object per log record.

    Field order is locked by the contract — we build the payload via an
    ordered ``dict`` and rely on Python 3.7+ insertion-order preservation
    plus ``json.dumps(..., sort_keys=False)``.
    """

    def format(self, record: logging.LogRecord) -> str:
        rec_dict = record.__dict__

        frame_id = rec_dict.get("frame_id")
        kind = rec_dict.get("kind") or _DEFAULT_KIND

        explicit_kv = rec_dict.get("kv")
        if explicit_kv is None:
            kv_raw: dict[str, Any] = {
                k: v
                for k, v in rec_dict.items()
                if k not in _RESERVED_LOG_RECORD_KEYS and not k.startswith("_")
            }
        else:
            kv_raw = dict(explicit_kv)

        try:
            kv_safe = _coerce_jsonable(kv_raw)
        except (TypeError, ValueError) as exc:
            kv_safe = {"_format_error": f"{type(exc).__name__}: {exc}"}

        if record.exc_info:
            exc_text: str | None = self.formatException(record.exc_info)
        else:
            exc_text = None

        component = rec_dict.get("component") or record.name

        payload: dict[str, Any] = {
            "ts": _iso_utc(record.created),
            "level": _normalise_level(record.levelname),
            "component": component,
            "frame_id": frame_id,
            "kind": kind,
            "msg": record.getMessage().replace("\n", " "),
            "kv": kv_safe,
            "exc": exc_text,
        }

        ordered_payload = {field: payload[field] for field in _CONTRACT_FIELDS}
        return json.dumps(ordered_payload, separators=(",", ":"), default=str, sort_keys=False)


# Module-level guard for handler-topology re-initialisation idempotency (AC-4).
_LOGGING_LOCK = threading.Lock()
_HANDLER_MARKER_ATTR: Final[str] = "_gps_denied_handler_kind"


def _make_tier1_handler() -> logging.Handler:
    """Tier-1: stdout handler (Docker captures stdout)."""
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    setattr(handler, _HANDLER_MARKER_ATTR, "tier1.stdout")
    return handler


def _make_tier2_handler() -> logging.Handler:
    """Tier-2: journald handler (Jetson with systemd).

    journald is reached via ``systemd.journal.JournalHandler``; if the
    ``systemd-python`` package is not installed on the active host, the
    factory raises so callers (and tests) get a clear prerequisite signal
    rather than a silent stderr fallback that would violate AC-4's
    "exactly one journald handler" invariant.
    """
    try:
        from systemd.journal import JournalHandler  # type: ignore[import-not-found]
    except ImportError as exc:
        raise RuntimeError(
            "Tier-2 journald handler requires `systemd-python`. Install it on the "
            "Jetson runtime or select `tier=1` for local/Docker deployments."
        ) from exc
    handler = JournalHandler()
    handler.setFormatter(JsonFormatter())
    setattr(handler, _HANDLER_MARKER_ATTR, "tier2.journald")
    return handler


_TIER_FACTORIES: Final[dict[int, Any]] = {
    1: _make_tier1_handler,
    2: _make_tier2_handler,
}


def configure_logging(
    *,
    tier: int,
    level: str = "INFO",
    target_loggers: Iterable[str] = ("",),
) -> None:
    """Install the handler topology for ``tier`` on the named loggers.

    Idempotent: re-calling with the same tier replaces any prior handler
    of that kind (no duplicates — AC-4). Switching tiers removes prior
    tier handlers and installs the new one.

    By default, the root logger (``""``) is configured so every named
    logger inherits the handler.
    """
    if tier not in _TIER_FACTORIES:
        raise ValueError(f"unsupported logging tier: {tier!r} (expected 1 or 2)")

    new_handler = _TIER_FACTORIES[tier]()
    level_value = getattr(logging, level.upper(), logging.INFO)

    with _LOGGING_LOCK:
        for name in target_loggers:
            target = logging.getLogger(name)
            target.handlers = [
                h for h in target.handlers if not getattr(h, _HANDLER_MARKER_ATTR, None)
            ]
            target.addHandler(new_handler)
            target.setLevel(level_value)
            target.propagate = name != ""


def _bootstrap_default_handler() -> None:
    """Attach a default Tier-1 handler if no schema handler is yet installed.

    Called lazily on first ``get_logger`` so importing a component that
    logs at module-import time still produces well-formed records before
    the composition root runs ``configure_logging``.
    """
    root = logging.getLogger()
    has_schema_handler = any(getattr(h, _HANDLER_MARKER_ATTR, None) for h in root.handlers)
    if has_schema_handler:
        return
    env_tier = os.getenv("GPS_DENIED_TIER", "1")
    tier = 2 if env_tier.strip() == "2" else 1
    env_level = os.getenv("LOG_LEVEL", "INFO")
    try:
        configure_logging(tier=tier, level=env_level)
    except RuntimeError:
        configure_logging(tier=1, level=env_level)


def get_logger(component_id: str) -> logging.Logger:
    """Return a `Logger` whose records satisfy `log_record_schema` v1.0.0.

    Repeated calls with the same `component_id` return the same cached
    `Logger` instance (Python stdlib's named-logger registry). The first
    call installs a default Tier-1 handler if the composition root has
    not yet run ``configure_logging``.
    """
    _bootstrap_default_handler()
    logger = logging.getLogger(component_id)
    logger.setLevel(logging.NOTSET)
    return logger


# Backwards-compat alias for the formatter's prior name (used by Tier-1 unit tests).
_JsonFormatter = JsonFormatter
