"""App-layer session service contracts.

Responsibility: define attach/session projections that combine mutable agent
state, immutable execution-contract truth, and ephemeral session transport
state for interactive talk/chat adapters.

Non-responsibility: CLI presentation rendering, MCP/socket inspection,
filesystem access, or interactive session runtime loops.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Protocol

from returns.result import Failure, Result, Success

from anima.core.execution import (
    AgentMutableStateView,
    ExecutionControlView,
    ExecutionSessionState,
)
from anima.core.schemas import AgentState, AnimaError, CompletionPolicy, LifecycleType

SessionAttachFlow = Literal["talk", "chat"]
SessionPresentationKind = Literal[
    "interactive_ready",
    "terminal_state",
    "non_interactive_state",
    "transport_failure",
]
TerminalStatePrecedence = Literal["terminal_state_first"]
SessionTransportFailureCode = Literal[
    "socket_not_found",
    "transport_disconnected",
    "transport_error",
]
SessionCompletionExpectationCode = Literal[
    "daemon_session_persists_until_external_terminal",
    "auto_completion_may_end_while_attached",
    "explicit_completion_waits_for_finish_signal",
]


@dataclass(frozen=True)
class TalkAttachRequest:
    """Attach projection request for an existing interactive talk session."""

    instance_id: str


@dataclass(frozen=True)
class ChatAttachRequest:
    """Attach projection request for an interactive chat session."""

    instance_id: str


@dataclass(frozen=True)
class SessionTransportFailure:
    """Transport-layer failure observed while evaluating attach viability."""

    code: SessionTransportFailureCode
    message: str
    retryable: bool


@dataclass(frozen=True)
class SessionAttachPresentation:
    """Adapter-facing presentation rule for one attach evaluation.

    Invariants:
    - Terminal mutable state takes precedence over observed transport failure.
    - ``transport_failure`` may be populated even when ``kind`` resolves to
      ``"terminal_state"``.
    - ``session_state`` remains ephemeral transport/session context only and
      must not be treated as immutable execution-contract truth.
    """

    kind: SessionPresentationKind
    terminal_state_precedence: TerminalStatePrecedence = "terminal_state_first"
    terminal_state: AgentState | None = None
    transport_failure: SessionTransportFailure | None = None


@dataclass(frozen=True)
class SessionCompletionExpectation:
    """Contract-derived completion expectations for attachable interactive sessions.

    Invariants:
    - ``LifecycleType.daemon`` sessions stay attachable across detach until an
      external terminal transition occurs.
    - ``CompletionPolicy.auto`` sessions may complete while attached without an
      explicit finish signal from the user.
    - ``CompletionPolicy.explicit`` sessions remain attachable until the agent
      or an external controller issues the terminal transition.
    """

    lifecycle: LifecycleType
    completion_policy: CompletionPolicy
    expectation: SessionCompletionExpectationCode
    detach_preserves_execution: bool
    may_complete_while_attached: bool


@dataclass(frozen=True)
class SessionAttachProjection:
    """Attach projection consumed by talk/chat adapter layers.

    The fields intentionally separate three state strata:
    - ``mutable_state``: live mutable execution facts owned by agent state.
    - ``contract``: immutable execution-contract truth.
    - ``session_state``: ephemeral attach/transport observations only.
    """

    flow: SessionAttachFlow
    mutable_state: AgentMutableStateView
    contract: ExecutionControlView
    session_state: ExecutionSessionState
    presentation: SessionAttachPresentation
    completion: SessionCompletionExpectation


class SessionProjectionPort(Protocol):
    """Adapter port for loading the three session-attach state strata."""

    def read_state(
        self, instance_id: str
    ) -> Result[AgentMutableStateView, AnimaError]: ...

    def load_contract(
        self, instance_id: str
    ) -> Result[ExecutionControlView, AnimaError]: ...

    def load_session_state(
        self, instance_id: str
    ) -> Result[ExecutionSessionState, AnimaError]: ...


class SessionService(Protocol):
    """App-layer boundary for talk/chat attach semantics.

    Invariants:
    - Adapters must consume ``SessionAttachProjection`` instead of inferring
      attach truth independently from raw mutable state or socket inspection.
    - When terminal state and transport failure are both observable,
      ``presentation.kind`` must resolve to ``"terminal_state"``.
    - Completion expectations come from immutable contract truth plus mutable
      state category, never from ephemeral ``ExecutionSessionState``.
    """

    def project_talk_attach(
        self, request: TalkAttachRequest
    ) -> Result[SessionAttachProjection, AnimaError]: ...

    def project_chat_attach(
        self, request: ChatAttachRequest
    ) -> Result[SessionAttachProjection, AnimaError]: ...


@dataclass(frozen=True)
class DefaultSessionService:
    """Default app-layer implementation for attach/session projection."""

    projection_port: SessionProjectionPort

    def project_talk_attach(
        self, request: TalkAttachRequest
    ) -> Result[SessionAttachProjection, AnimaError]:
        """Build a talk attach projection from mutable/contract/session strata."""
        return self._project_attach(flow="talk", instance_id=request.instance_id)

    def project_chat_attach(
        self, request: ChatAttachRequest
    ) -> Result[SessionAttachProjection, AnimaError]:
        """Build a chat attach projection from mutable/contract/session strata."""
        return self._project_attach(flow="chat", instance_id=request.instance_id)

    def _project_attach(
        self,
        *,
        flow: SessionAttachFlow,
        instance_id: str,
    ) -> Result[SessionAttachProjection, AnimaError]:
        mutable_result = self.projection_port.read_state(instance_id)
        if isinstance(mutable_result, Failure):
            return mutable_result
        mutable_state = mutable_result.unwrap()

        contract_result = self.projection_port.load_contract(instance_id)
        if isinstance(contract_result, Failure):
            return contract_result
        contract = contract_result.unwrap()

        session_result = self.projection_port.load_session_state(instance_id)
        if isinstance(session_result, Failure):
            return session_result
        session_state = session_result.unwrap()

        transport_failure = _resolve_transport_failure(session_state)
        presentation = _build_attach_presentation(
            mutable_state=mutable_state,
            transport_failure=transport_failure,
        )

        completion = _completion_expectation(
            lifecycle=contract.lifecycle_policy.lifecycle,
            completion_policy=contract.lifecycle_policy.completion_policy,
        )

        return Success(
            SessionAttachProjection(
                flow=flow,
                mutable_state=mutable_state,
                contract=contract,
                session_state=session_state,
                presentation=presentation,
                completion=completion,
            )
        )


# @invar:allow shell_result: App-layer projection helper returns domain value, not shell Result.
def _resolve_transport_failure(
    session_state: ExecutionSessionState,
) -> SessionTransportFailure | None:
    """Evaluate transport/socket observability into one failure descriptor."""
    transport = session_state.transport
    socket_path_raw = transport.socket_path
    if not socket_path_raw:
        return SessionTransportFailure(
            code="socket_not_found",
            message="Agent MCP socket path is not available for this session.",
            retryable=True,
        )

    socket_path = Path(socket_path_raw)
    if not socket_path.exists():
        return SessionTransportFailure(
            code="socket_not_found",
            message=f"Agent MCP socket not found at {socket_path}",
            retryable=True,
        )

    if not transport.transport_connected:
        return SessionTransportFailure(
            code="transport_disconnected",
            message="Session transport is currently disconnected.",
            retryable=True,
        )

    return None


# @invar:allow shell_result: App-layer projection helper returns presentation model.
def _build_attach_presentation(
    *,
    mutable_state: AgentMutableStateView,
    transport_failure: SessionTransportFailure | None,
) -> SessionAttachPresentation:
    """Resolve adapter-facing attach presentation with terminal precedence."""
    terminal_states = {
        AgentState.completed,
        AgentState.failed,
        AgentState.killed,
    }
    interactive_states = {
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }

    if mutable_state.state in terminal_states:
        return SessionAttachPresentation(
            kind="terminal_state",
            terminal_state=mutable_state.state,
            transport_failure=transport_failure,
        )

    if mutable_state.state not in interactive_states:
        return SessionAttachPresentation(
            kind="non_interactive_state",
            transport_failure=transport_failure,
        )

    if transport_failure is not None:
        return SessionAttachPresentation(
            kind="transport_failure",
            transport_failure=transport_failure,
        )

    return SessionAttachPresentation(kind="interactive_ready")


# @invar:allow shell_result: App-layer projection helper returns completion expectation model.
def _completion_expectation(
    *,
    lifecycle: LifecycleType,
    completion_policy: CompletionPolicy,
) -> SessionCompletionExpectation:
    """Project immutable completion expectations for attach UX messaging."""
    if lifecycle == LifecycleType.daemon:
        return SessionCompletionExpectation(
            lifecycle=lifecycle,
            completion_policy=completion_policy,
            expectation="daemon_session_persists_until_external_terminal",
            detach_preserves_execution=True,
            may_complete_while_attached=(completion_policy == CompletionPolicy.auto),
        )

    if completion_policy == CompletionPolicy.auto:
        return SessionCompletionExpectation(
            lifecycle=lifecycle,
            completion_policy=completion_policy,
            expectation="auto_completion_may_end_while_attached",
            detach_preserves_execution=False,
            may_complete_while_attached=True,
        )

    return SessionCompletionExpectation(
        lifecycle=lifecycle,
        completion_policy=completion_policy,
        expectation="explicit_completion_waits_for_finish_signal",
        detach_preserves_execution=False,
        may_complete_while_attached=False,
    )
