"""Session — the first-class handle for multi-turn agent conversations.

Example::

    async with MeerkatClient() as client:
        session = await client.create_session("Summarise this repo")
        print(session.text)

        # Multi-turn (non-streaming)
        result = await session.turn("Now list the open issues")
        print(result.text)

        # Streaming
        async with session.stream("Explain the CI pipeline") as events:
            async for event in events:
                match event:
                    case TextDelta(delta=chunk):
                        print(chunk, end="", flush=True)
            print()
            print(f"Tokens: {events.result.usage.input_tokens}")

        await session.archive()
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from .types import (
    ContentBlock,
    PeerCorrelationId,
    PeerId,
    RunResult,
    SessionForkResult,
    SessionHistory,
    SkillKey,
    SkillRef,
    TranscriptEditRunningBehavior,
    TranscriptReplacement,
)

if TYPE_CHECKING:
    from .client import MeerkatClient
    from .streaming import EventStream, EventSubscription


def _normalize_skill_ref(skill_ref: SkillRef) -> SkillKey:
    """Validate a structured skill reference."""
    if isinstance(skill_ref, SkillKey):
        return skill_ref

    raise TypeError("Skill references must be SkillKey objects")


class Session:
    """A live agent session.

    Constructed by :meth:`~meerkat.MeerkatClient.create_session` — not
    directly instantiated.

    The most recent :class:`~meerkat.RunResult` is always accessible via
    :attr:`last_result` and the convenience shortcuts :attr:`text`,
    :attr:`usage`, etc.
    """

    __slots__ = ("_client", "_id", "_ref", "_last_result")

    def __init__(
        self,
        client: MeerkatClient,
        result: RunResult,
    ) -> None:
        self._client = client
        self._id = result.session_id
        self._ref = result.session_ref
        self._last_result = result

    # -- Identity ----------------------------------------------------------

    @property
    def id(self) -> str:
        """The stable UUID for this session."""
        return self._id

    @property
    def ref(self) -> str | None:
        """Optional human-readable session reference."""
        return self._ref

    # -- Last result shortcuts ---------------------------------------------

    @property
    def last_result(self) -> RunResult:
        """The most recent :class:`RunResult`."""
        return self._last_result

    @property
    def text(self) -> str:
        """The assistant's text from the last turn."""
        return self._last_result.text

    @property
    def usage(self) -> Any:
        """Token usage from the last turn."""
        return self._last_result.usage

    @property
    def turns(self) -> int:
        """Number of LLM turns in the last run."""
        return self._last_result.turns

    @property
    def tool_calls(self) -> int:
        """Number of tool calls in the last run."""
        return self._last_result.tool_calls

    @property
    def structured_output(self) -> Any:
        """Structured output from the last run, if requested."""
        return self._last_result.structured_output

    # -- Multi-turn --------------------------------------------------------

    async def turn(
        self,
        prompt: str | list[ContentBlock],
        *,
        skill_refs: list[SkillRef] | None = None,
        flow_tool_overlay: dict[str, Any] | None = None,
        additional_instructions: list[str] | None = None,
        keep_alive: bool | None = None,
        model: str | None = None,
        provider: str | None = None,
        max_tokens: int | None = None,
        system_prompt: str | None = None,
        output_schema: dict[str, Any] | None = None,
        structured_output_retries: int | None = None,
        provider_params: dict[str, Any] | None = None,
    ) -> RunResult:
        """Run another turn on this session (non-streaming).

        Updates :attr:`last_result` and returns the new
        :class:`~meerkat.RunResult`.
        """
        result = await self._client._start_turn(  # noqa: SLF001
            self._id,
            prompt,
            skill_refs=skill_refs,
            flow_tool_overlay=flow_tool_overlay,
            additional_instructions=additional_instructions,
            keep_alive=keep_alive,
            model=model,
            provider=provider,
            max_tokens=max_tokens,
            system_prompt=system_prompt,
            output_schema=output_schema,
            structured_output_retries=structured_output_retries,
            provider_params=provider_params,
        )
        self._last_result = result
        return result

    def stream(
        self,
        prompt: str | list[ContentBlock],
        *,
        skill_refs: list[SkillRef] | None = None,
        flow_tool_overlay: dict[str, Any] | None = None,
        additional_instructions: list[str] | None = None,
        keep_alive: bool | None = None,
        model: str | None = None,
        provider: str | None = None,
        max_tokens: int | None = None,
        system_prompt: str | None = None,
        output_schema: dict[str, Any] | None = None,
        structured_output_retries: int | None = None,
        provider_params: dict[str, Any] | None = None,
    ) -> EventStream:
        """Run another turn on this session with streaming events.

        Returns an :class:`~meerkat.EventStream` async context manager::

            async with session.stream("prompt") as events:
                async for event in events:
                    ...
                result = events.result

        Updates :attr:`last_result` when the stream completes.
        """
        return self._client._start_turn_streaming(  # noqa: SLF001
            self._id,
            prompt,
            skill_refs=skill_refs,
            flow_tool_overlay=flow_tool_overlay,
            additional_instructions=additional_instructions,
            keep_alive=keep_alive,
            model=model,
            provider=provider,
            max_tokens=max_tokens,
            system_prompt=system_prompt,
            output_schema=output_schema,
            structured_output_retries=structured_output_retries,
            provider_params=provider_params,
            _session=self,
        )

    # -- Lifecycle ---------------------------------------------------------

    async def interrupt(self) -> None:
        """Cancel the currently running turn, if any."""
        await self._client._interrupt(self._id)  # noqa: SLF001

    async def archive(self) -> None:
        """Archive (remove) this session from the server."""
        await self._client._archive(self._id)  # noqa: SLF001


    async def inject_context(
        self,
        text: str,
        *,
        source: str | None = None,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]:
        """Inject system context into this session."""
        return await self._client.inject_context(
            self._id,
            text,
            source=source,
            idempotency_key=idempotency_key,
        )

    async def send_external_event(
        self,
        event_type: str,
        payload: Any,
        *,
        blocks: list[ContentBlock] | None = None,
    ) -> dict[str, Any]:
        """Append a durable external event input into this session runtime."""
        return await self._client.send_external_event(
            self._id,
            event_type,
            payload,
            blocks=blocks,
        )

    async def send_peer_response_terminal(
        self,
        peer_id: PeerId,
        request_id: PeerCorrelationId,
        status: str,
        result: Any,
        *,
        display_name: str | None = None,
    ) -> dict[str, Any]:
        """Admit a correlated terminal peer response into this session runtime."""
        return await self._client.send_peer_response_terminal(
            self._id,
            peer_id,
            request_id,
            status,
            result,
            display_name=display_name,
        )

    async def history(
        self,
        *,
        offset: int = 0,
        limit: int | None = None,
    ) -> SessionHistory:
        """Read paginated transcript history for this session."""
        return await self._client.read_session_history(  # noqa: SLF001
            self._id,
            offset=offset,
            limit=limit,
        )

    async def fork_at(
        self,
        message_index: int,
        *,
        running_behavior: TranscriptEditRunningBehavior | None = None,
    ) -> SessionForkResult:
        """Fork this idle session at a transcript message index."""
        return await self._client.fork_session_at(  # noqa: SLF001
            self._id,
            message_index,
            running_behavior=running_behavior,
        )

    async def fork_replace(
        self,
        message_index: int,
        replacement: TranscriptReplacement,
        *,
        running_behavior: TranscriptEditRunningBehavior | None = None,
    ) -> SessionForkResult:
        """Fork this idle session and apply a typed transcript replacement."""
        return await self._client.fork_session_replace(  # noqa: SLF001
            self._id,
            message_index,
            replacement,
            running_behavior=running_behavior,
        )

    async def subscribe_events(self) -> EventSubscription:
        """Open a standalone session-wide event subscription."""
        return await self._client.subscribe_session_events(self._id)  # noqa: SLF001

    # -- Skills convenience ------------------------------------------------

    async def invoke_skill(
        self,
        skill_ref: SkillRef,
        prompt: str | list[ContentBlock],
    ) -> RunResult:
        """Invoke a skill in this session.

        Sends the structured ``skill_refs`` parameter to the runtime.
        """
        self._client.require_capability("skills")
        canonical = _normalize_skill_ref(skill_ref)
        return await self.turn(
            prompt,
            skill_refs=[canonical],
        )

    # -- Comms convenience -------------------------------------------------

    async def send(self, **kwargs: Any) -> dict[str, Any]:
        """Send a comms command scoped to this session."""
        kwargs.pop("session_id", None)
        return await self._client._send(self._id, **kwargs)  # noqa: SLF001

    async def peers(self) -> list[dict[str, Any]]:
        """List peers visible to this session's comms runtime."""
        result = await self._client._peers(self._id)  # noqa: SLF001
        return result.get("peers", [])

    # -- Dunder ------------------------------------------------------------

    def __repr__(self) -> str:
        ref = f" ref={self._ref!r}" if self._ref else ""
        return f"<Session id={self._id!r}{ref}>"


class DeferredSession:
    """A session created with ``initial_turn="deferred"``.

    Unlike :class:`Session`, no first turn has been executed yet. Use
    :meth:`start_turn` to run the first turn, which returns a
    :class:`~meerkat.RunResult`.

    Example::

        deferred = await client.create_deferred_session(
            "Summarise this repo",
            model="claude-sonnet-4-5",
        )
        result = await deferred.start_turn("Begin analysis")
    """

    __slots__ = ("_client", "_id", "_ref")

    def __init__(
        self,
        client: MeerkatClient,
        session_id: str,
        session_ref: str | None = None,
    ) -> None:
        self._client = client
        self._id = session_id
        self._ref = session_ref

    @property
    def id(self) -> str:
        """The stable UUID for this session."""
        return self._id

    @property
    def ref(self) -> str | None:
        """Optional human-readable session reference."""
        return self._ref

    async def start_turn(
        self,
        prompt: str | list[ContentBlock],
        *,
        skill_refs: list[SkillRef] | None = None,
        flow_tool_overlay: dict[str, Any] | None = None,
        additional_instructions: list[str] | None = None,
        keep_alive: bool | None = None,
        model: str | None = None,
        provider: str | None = None,
        max_tokens: int | None = None,
        system_prompt: str | None = None,
        output_schema: dict[str, Any] | None = None,
        structured_output_retries: int | None = None,
        provider_params: dict[str, Any] | None = None,
    ) -> RunResult:
        """Run the first turn on this deferred session.

        Accepts per-turn overrides (model, provider, etc.) that are applied
        before the session is materialized. Returns a :class:`~meerkat.RunResult`.
        """
        return await self._client._start_turn(  # noqa: SLF001
            self._id,
            prompt,
            skill_refs=skill_refs,
            flow_tool_overlay=flow_tool_overlay,
            additional_instructions=additional_instructions,
            keep_alive=keep_alive,
            model=model,
            provider=provider,
            max_tokens=max_tokens,
            system_prompt=system_prompt,
            output_schema=output_schema,
            structured_output_retries=structured_output_retries,
            provider_params=provider_params,
        )

    async def interrupt(self) -> None:
        """Cancel the currently running turn, if any."""
        await self._client._interrupt(self._id)  # noqa: SLF001

    async def archive(self) -> None:
        """Archive (remove) this session from the server."""
        await self._client._archive(self._id)  # noqa: SLF001


    async def inject_context(
        self,
        text: str,
        *,
        source: str | None = None,
        idempotency_key: str | None = None,
    ) -> dict[str, Any]:
        """Inject system context into this session."""
        return await self._client.inject_context(
            self._id,
            text,
            source=source,
            idempotency_key=idempotency_key,
        )

    async def send_external_event(
        self,
        event_type: str,
        payload: Any,
        *,
        blocks: list[ContentBlock] | None = None,
    ) -> dict[str, Any]:
        """Append a durable external event input into this deferred session."""
        return await self._client.send_external_event(
            self._id,
            event_type,
            payload,
            blocks=blocks,
        )

    async def send_peer_response_terminal(
        self,
        peer_id: PeerId,
        request_id: PeerCorrelationId,
        status: str,
        result: Any,
        *,
        display_name: str | None = None,
    ) -> dict[str, Any]:
        """Admit a correlated terminal peer response into this deferred session."""
        return await self._client.send_peer_response_terminal(
            self._id,
            peer_id,
            request_id,
            status,
            result,
            display_name=display_name,
        )

    async def history(
        self,
        *,
        offset: int = 0,
        limit: int | None = None,
    ) -> SessionHistory:
        """Read paginated transcript history for this deferred session."""
        return await self._client.read_session_history(  # noqa: SLF001
            self._id,
            offset=offset,
            limit=limit,
        )

    async def fork_at(
        self,
        message_index: int,
        *,
        running_behavior: TranscriptEditRunningBehavior | None = None,
    ) -> SessionForkResult:
        """Fork this idle session at a transcript message index."""
        return await self._client.fork_session_at(  # noqa: SLF001
            self._id,
            message_index,
            running_behavior=running_behavior,
        )

    async def fork_replace(
        self,
        message_index: int,
        replacement: TranscriptReplacement,
        *,
        running_behavior: TranscriptEditRunningBehavior | None = None,
    ) -> SessionForkResult:
        """Fork this idle session and apply a typed transcript replacement."""
        return await self._client.fork_session_replace(  # noqa: SLF001
            self._id,
            message_index,
            replacement,
            running_behavior=running_behavior,
        )

    def __repr__(self) -> str:
        ref = f" ref={self._ref!r}" if self._ref else ""
        return f"<DeferredSession id={self._id!r}{ref}>"
