"""App-layer launch service contracts.

Responsibility: own launch orchestration for run/spawn/chat style entrypoints,
including resolved-contract consumption, initial mutable-state creation, and
delegation to runtime-launch adapters.

Non-responsibility: parse CLI/MCP payloads, format transport responses, or
perform subprocess/filesystem/network work directly.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal, Protocol

from returns.result import Failure, Result, Success

from anima.core.execution import (
    ExecutionContractResolver,
    ExecutionIntent,
    ExecutionLaunchView,
    ResolvedExecutionContract,
)
from anima.core.schemas import AnimaError, InvalidInputError, JobSpec, PersonaSpec
from anima.core.schemas.models import AgentStateData

LaunchOrigin = Literal["run", "spawn", "chat", "serve_spawn", "subagent"]


@dataclass(frozen=True)
class LaunchRequest:
    """Normalized app-layer request for top-level execution launch."""

    origin: LaunchOrigin
    task: str
    execution_intent: ExecutionIntent
    instance_id: str
    trace_id: str
    parent_instance_id: str | None = None


@dataclass(frozen=True)
class LaunchSubagentRequest:
    """Delegated child-launch request emitted after control-layer checks pass."""

    task: str
    instance_id: str
    trace_id: str
    parent_instance_id: str
    child_persona_ref: str
    child_persona: PersonaSpec
    child_job: JobSpec
    resolved_contract: ResolvedExecutionContract
    context_bundle_ref: str | None = None


@dataclass(frozen=True)
class LaunchResult:
    """Service-owned launch output consumed by transport adapters only."""

    instance_id: str
    trace_id: str
    resolved_contract: ResolvedExecutionContract
    initial_state: AgentStateData


class AgentLaunchStorePort(Protocol):
    """Adapter port for initial mutable-state persistence."""

    def create_agent_state(
        self,
        *,
        instance_id: str,
        persona_name: str,
        job: JobSpec,
        parent_id: str | None = None,
        trace_id: str | None = None,
        spec_id: str | None = None,
        spec_digest: str | None = None,
        model: str | None = None,
        resolved_contract: ResolvedExecutionContract | None = None,
    ) -> Result[AgentStateData, AnimaError]: ...


class SubprocessLaunchPort(Protocol):
    """Adapter port for runtime bootstrap after state persistence succeeds."""

    def launch_agent_subprocess(
        self,
        *,
        instance_id: str,
        task: str,
        persona: PersonaSpec,
        job: JobSpec,
        trace_id: str | None = None,
    ) -> Result[int, AnimaError]: ...


class SharedContextBundlePort(Protocol):
    """Adapter port for optional shared-context export during child launch."""

    def export_context_bundle(
        self,
        *,
        parent_instance_id: str,
        child_instance_id: str,
    ) -> Result[str | None, AnimaError]: ...


class ContractSnapshotLoaderPort(Protocol):
    """Adapter port for reading persisted resolved contracts when needed."""

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


class LaunchService(Protocol):
    """App-layer orchestration boundary for launch and child bootstrap flows.

    Invariants:
    - This service owns contract resolution and launch orchestration.
    - Adapters own only transport parsing/presentation and side-effect execution.
    - Consumers should depend on ``ExecutionLaunchView`` where full contract
      access is unnecessary.
    """

    contract_resolver: ExecutionContractResolver

    def launch(self, request: LaunchRequest) -> Result[LaunchResult, AnimaError]: ...

    def launch_subagent(
        self, request: LaunchSubagentRequest
    ) -> Result[LaunchResult, AnimaError]: ...

    def contract_view(self, result: LaunchResult) -> ExecutionLaunchView: ...


@dataclass(frozen=True)
class DefaultLaunchService:
    """Default app-layer launch orchestration implementation.

    This service resolves immutable execution truth exactly once per launch call,
    persists initial mutable state beside the immutable contract snapshot, and
    optionally delegates subprocess bootstrap to adapter ports.
    """

    contract_resolver: ExecutionContractResolver
    launch_store: AgentLaunchStorePort
    subprocess_launcher: SubprocessLaunchPort | None = None
    shared_context_exporter: SharedContextBundlePort | None = None
    contract_snapshot_loader: ContractSnapshotLoaderPort | None = None

    def launch(self, request: LaunchRequest) -> Result[LaunchResult, AnimaError]:
        """Resolve and persist launch state for run/spawn/chat entrypoints.

        Args:
            request: Normalized launch request from adapter layer.

        Returns:
            Success with immutable contract + initial mutable state, or Failure
            when resolution, persistence, or optional subprocess launch fails.
        """
        resolved_contract = self.contract_resolver(request.execution_intent)
        persona = request.execution_intent.persona_spec
        job = request.execution_intent.job_spec
        if persona is None or job is None:
            return Failure(
                InvalidInputError(
                    "LaunchRequest.execution_intent requires persona_spec and job_spec"
                )
            )

        persisted = self.launch_store.create_agent_state(
            instance_id=request.instance_id,
            persona_name=persona.id,
            job=job,
            parent_id=request.parent_instance_id,
            trace_id=request.trace_id,
            spec_id=persona.id,
            spec_digest=persona.spec_digest,
            model=resolved_contract.runtime.effective_model,
            resolved_contract=resolved_contract,
        )
        if isinstance(persisted, Failure):
            return persisted

        if request.origin in {"spawn", "chat", "serve_spawn"}:
            launch_result = self._launch_subprocess_if_enabled(
                instance_id=request.instance_id,
                task=request.task,
                persona=persona,
                job=job,
                trace_id=request.trace_id,
            )
            if isinstance(launch_result, Failure):
                return launch_result

        return Success(
            LaunchResult(
                instance_id=request.instance_id,
                trace_id=request.trace_id,
                resolved_contract=resolved_contract,
                initial_state=persisted.unwrap(),
            )
        )

    def launch_subagent(
        self, request: LaunchSubagentRequest
    ) -> Result[LaunchResult, AnimaError]:
        """Persist and launch a child agent from control-layer request.

        Args:
            request: Child launch request after spawnability checks pass.

        Returns:
            Success with launch result, or Failure from persistence/export/launch.
        """
        child_job = request.child_job
        if (
            request.context_bundle_ref is None
            and self.shared_context_exporter is not None
        ):
            exported = self.shared_context_exporter.export_context_bundle(
                parent_instance_id=request.parent_instance_id,
                child_instance_id=request.instance_id,
            )
            if isinstance(exported, Failure):
                return exported
            bundle_ref = exported.unwrap()
            if bundle_ref is not None:
                child_job = request.child_job.model_copy(
                    update={"context_bundle_ref": bundle_ref}
                )

        persisted = self.launch_store.create_agent_state(
            instance_id=request.instance_id,
            persona_name=request.child_persona.id,
            job=child_job,
            parent_id=request.parent_instance_id,
            trace_id=request.trace_id,
            spec_id=request.child_persona.id,
            spec_digest=request.child_persona.spec_digest,
            model=request.resolved_contract.runtime.effective_model,
            resolved_contract=request.resolved_contract,
        )
        if isinstance(persisted, Failure):
            return persisted

        launch_result = self._launch_subprocess_if_enabled(
            instance_id=request.instance_id,
            task=request.task,
            persona=request.child_persona,
            job=child_job,
            trace_id=request.trace_id,
        )
        if isinstance(launch_result, Failure):
            return launch_result

        return Success(
            LaunchResult(
                instance_id=request.instance_id,
                trace_id=request.trace_id,
                resolved_contract=request.resolved_contract,
                initial_state=persisted.unwrap(),
            )
        )

    def contract_view(self, result: LaunchResult) -> ExecutionLaunchView:
        """Project launch result into execution launch view."""
        return result.resolved_contract

    def _launch_subprocess_if_enabled(
        self,
        *,
        instance_id: str,
        task: str,
        persona: PersonaSpec,
        job: JobSpec,
        trace_id: str,
    ) -> Result[None, AnimaError]:
        if self.subprocess_launcher is None:
            return Success(None)

        launched = self.subprocess_launcher.launch_agent_subprocess(
            instance_id=instance_id,
            task=task,
            persona=persona,
            job=job,
            trace_id=trace_id,
        )
        if isinstance(launched, Failure):
            return launched
        return Success(None)
