"""The :class:`TelematicsProvider` device-session seam (rule DP-02).

Two-layer abstraction (the `mgf-tele-*` family shape):

  * :class:`TelematicsProvider` (here) — owns the long-lived device
    session: vendor-specific handshake, packet reception, command
    dispatch, teardown.
  * a per-vendor **codec** (a separate `mgf-tele-<vendor>` package) —
    a pure ``bytes → DecodedAvlRecord`` function the provider depends
    on. The codec is not part of the provider's identity.

The seam's data vocabulary — :class:`SessionHandle`,
:class:`TrackerCommand`, :class:`CommandAck` — lives here because it's
the language of the seam itself; the AVL frame types
(:class:`~mgf.tele.core.ReceivedPacket` etc.) live in
:mod:`mgf.tele.core._frames`.

Per rule **DP-09**, long-running sessions take cancellation via the
concrete provider's constructor (different vendors have different
cancellation needs), not via these signatures.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import asyncio
    from datetime import datetime

    from mgf.tele.core._frames import ReceivedPacket

__all__ = [
    "CommandAck",
    "SessionHandle",
    "TelematicsProvider",
    "TrackerCommand",
]


@dataclass(frozen=True, slots=True)
class SessionHandle:
    """Opaque per-device-session token.

    Carried between :meth:`TelematicsProvider.open_session` and the
    receive / send / close methods. A provider MAY subclass this to
    carry private state (the reader/writer pair, parser state machines,
    retry budgets); consumer code MUST treat it as opaque. The base
    shape carries the identity every consumer needs regardless of
    vendor.
    """

    imei: str
    opened_at: datetime


@dataclass(frozen=True, slots=True)
class TrackerCommand:
    """A command to send to the device's control plane.

    Placeholder shape: an opaque bytes payload + optional correlation
    id. Concrete command vocabulary (immobilise, remote DOUT, config
    update) is defined by the consumer when its control plane lands.
    """

    payload: bytes
    correlation_id: str | None = None


@dataclass(frozen=True, slots=True)
class CommandAck:
    """The device's response to a :class:`TrackerCommand`."""

    success: bool
    detail: bytes = b""


class TelematicsProvider(ABC):
    """Device-session seam. Owns the long-lived TCP/UDP session per
    device: vendor handshake, packet reception, command dispatch,
    teardown. The codec is a dependency injected by the subclass."""

    @abstractmethod
    async def open_session(
        self,
        reader: asyncio.StreamReader,
        writer: asyncio.StreamWriter,
    ) -> SessionHandle:
        """Read the vendor handshake and return a session handle.

        Raises :class:`~mgf.tele.core.MalformedHandshakeError` /
        :class:`~mgf.tele.core.UnknownTrackerError` on rejection. The
        reader/writer remain owned by the provider; the supervisor
        owns the underlying socket close.
        """

    @abstractmethod
    async def receive_packet(self, session: SessionHandle) -> ReceivedPacket:
        """Read one packet; return its raw bytes + decoded records + status.

        Always returns a :class:`~mgf.tele.core.ReceivedPacket` carrying
        the wire bytes — even on decode failure (the consumer persists
        the raw packet regardless, for the forensic trail). On
        ``parse_status='ok'`` the provider has already sent the
        protocol's per-packet ack. Socket-level failures raise
        :class:`~mgf.tele.core.SessionClosedError`; decode failures do
        NOT raise — they're encoded in ``parse_status`` / ``parse_error``.
        """

    @abstractmethod
    async def send_command(self, session: SessionHandle, cmd: TrackerCommand) -> CommandAck:
        """Dispatch a command and await its ack. A read-only consumer's
        provider MAY raise ``NotImplementedError`` until its control
        plane lands."""

    @abstractmethod
    async def close_session(self, session: SessionHandle) -> None:
        """Tear the session down cleanly. Idempotent."""
