"""App-layer control service contracts.

Responsibility: own control flows over existing executions, including status,
interact, approval, resume/kill actions, and child-agent spawning decisions
derived from resolved contract truth.

Non-responsibility: CLI/MCP syntax, response-envelope formatting, or direct MCP,
filesystem, or subprocess operations.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Literal, Protocol, TypeAlias

from returns.result import Failure, Result, Success

from anima.app.launch_service import LaunchResult, LaunchSubagentRequest
from anima.core.contracts import SpawnabilityReason
from anima.core.execution import (
    AgentMutableStateView,
    ExecutionIntent,
    ExecutionControlView,
    ExecutionContractResolver,
    ResolvedExecutionContract,
)
from anima.core.schemas import (
    AgentNotInteractiveError,
    AgentState,
    AgentTerminatedError,
    AnimaError,
    ContextMode,
    InvalidInputError,
    JobSpec,
    LifecycleType,
    MaxDepthExceededError,
    PersonaSpec,
    SideEffectPolicy,
)


@dataclass(frozen=True)
class StatusRequest:
    """Read-only control query for one execution."""

    instance_id: str


@dataclass(frozen=True)
class InteractRequest:
    """Transport-agnostic interaction request routed through control service."""

    instance_id: str
    prompt: str


@dataclass(frozen=True)
class ApprovalRequest:
    """Approval decision input from transport adapters."""

    instance_id: str
    approved: bool
    note: str | None = None


@dataclass(frozen=True)
class ResumeRequest:
    """Resume request for suspended or waiting executions."""

    instance_id: str


@dataclass(frozen=True)
class KillRequest:
    """Kill request for an existing execution."""

    instance_id: str
    reason: str | None = None


@dataclass(frozen=True)
class SpawnSubagentRequest:
    """Control-layer child-agent spawn request.

    ``persona_ref`` is an optional explicit child override. When omitted, the
    service must derive inheritance eligibility from resolved persona provenance
    rather than raw mutable-state ``persona_ref`` presence.
    """

    parent_instance_id: str
    task: str
    persona_ref: str | None = None
    context_mode: ContextMode = ContextMode.isolated
    lifecycle: LifecycleType | None = None
    max_turns: int | None = None
    max_tokens: int | None = None


@dataclass(frozen=True)
class ExistingReusablePersonaForInheritance:
    """Resolved reusable parent persona inherited without materialization."""

    reusable_persona_ref: str
    source: Literal["file", "snapshot"]


@dataclass(frozen=True)
class MaterializedReusablePersonaForInheritance:
    """Explicit success outcome for inline/synthesized parent inheritance."""

    reusable_persona_ref: str
    source: Literal["inline", "synthesized"]
    materialization_status: Literal["materialized"] = "materialized"


@dataclass(frozen=True)
class ExplicitChildPersonaOverride:
    """Explicit child persona selected by the caller instead of inheritance."""

    persona_ref: str


SpawnPersonaResolution: TypeAlias = (
    ExistingReusablePersonaForInheritance
    | MaterializedReusablePersonaForInheritance
    | ExplicitChildPersonaOverride
)


@dataclass(frozen=True)
class SpawnSubagentAccepted:
    """Spawn accepted and delegated to launch service with resolved persona truth."""

    kind: Literal["spawned"]
    launch_request: LaunchSubagentRequest
    launch_result: LaunchResult
    persona_resolution: SpawnPersonaResolution


@dataclass(frozen=True)
class SpawnSubagentRejected:
    """Spawn rejected for business-level spawnability reasons.

    Inline or synthesized parents without a reusable persona must surface this
    structured outcome unless the service materializes a reusable snapshot and
    returns ``MaterializedReusablePersonaForInheritance`` instead.
    """

    kind: Literal["spawnability_error"]
    reason: SpawnabilityReason


SpawnSubagentOutcome: TypeAlias = SpawnSubagentAccepted | SpawnSubagentRejected


class ContractStatePort(Protocol):
    """Adapter port for loading the authoritative control inputs."""

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

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


class ControlTransportPort(Protocol):
    """Adapter port for live transport actions owned by control service."""

    async def call_agent_tool(
        self,
        *,
        instance_id: str,
        tool_name: str,
        arguments: dict[str, Any] | None = None,
        timeout: float = 30.0,
    ) -> Result[dict[str, Any], AnimaError]: ...


class ApprovalPort(Protocol):
    """Adapter port for approval persistence or signaling."""

    def write_approval_response(
        self, instance_id: str, approved: bool, note: str | None = None
    ) -> Result[None, AnimaError]: ...


class LifecycleControlPort(Protocol):
    """Adapter port for mutable-state lifecycle transitions."""

    def approve(self, instance_id: str) -> Result[None, AnimaError]: ...

    def reject(
        self, instance_id: str, reason: str | None = None
    ) -> Result[None, AnimaError]: ...

    def resume(self, instance_id: str) -> Result[None, AnimaError]: ...

    def kill(
        self, instance_id: str, reason: str | None = None
    ) -> Result[None, AnimaError]: ...


class SubagentLaunchPort(Protocol):
    """App-to-app port used by control service to delegate child bootstrap."""

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


class SpawnPersonaPort(Protocol):
    """Adapter port for loading and validating spawn personas."""

    def load_persona(self, persona_ref: str) -> Result[PersonaSpec, AnimaError]: ...

    def validate_parent_spawn_permission(
        self,
        *,
        parent_persona: PersonaSpec,
        child_persona_name: str,
    ) -> Result[None, AnimaError]: ...


class ControlService(Protocol):
    """App-layer control orchestration boundary.

    Invariants:
    - Existing execution control reads resolved contract views instead of
      re-deriving lifecycle or spawnability semantics from mutable state.
    - ``ctl_spawn_subagent`` and equivalent MCP flows must consume
      ``ExecutionControlView.persona_provenance``.
    - Inline or synthesized parent inheritance must end in exactly one explicit
      business outcome: materialized reusable persona snapshot, or structured
      ``SpawnSubagentRejected``.
    """

    def status(
        self, request: StatusRequest
    ) -> Result[tuple[AgentMutableStateView, ExecutionControlView], AnimaError]: ...

    async def interact(
        self, request: InteractRequest
    ) -> Result[dict[str, Any], AnimaError]: ...

    def approve(self, request: ApprovalRequest) -> Result[None, AnimaError]: ...

    def reject(self, request: ApprovalRequest) -> Result[None, AnimaError]: ...

    def resume(self, request: ResumeRequest) -> Result[None, AnimaError]: ...

    def kill(self, request: KillRequest) -> Result[None, AnimaError]: ...

    def spawn_subagent(
        self, request: SpawnSubagentRequest
    ) -> Result[SpawnSubagentOutcome, AnimaError]: ...


@dataclass(frozen=True)
class DefaultControlService:
    """Default app-layer control-service implementation.

    Spec source:
    - docs/proposals/execution-architecture-refactor.md#repository-target-architecture/app
    - docs/proposals/execution-architecture-refactor.md#execution-contract-specific-design-rules/persona-provenance-and-subagent-inheritance
    - docs/proposals/execution-architecture-refactor.md#refactor-program/wave-2-launch-and-control-unification
    """

    contract_state_port: ContractStatePort
    approval_port: ApprovalPort
    lifecycle_port: LifecycleControlPort
    transport_port: ControlTransportPort | None = None
    launch_port: SubagentLaunchPort | None = None
    spawn_persona_port: SpawnPersonaPort | None = None
    contract_resolver: ExecutionContractResolver | None = None
    generate_instance_id: Callable[[], str] | None = None

    def status(
        self, request: StatusRequest
    ) -> Result[tuple[AgentMutableStateView, ExecutionControlView], AnimaError]:
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        contract_result = self.contract_state_port.load_contract(request.instance_id)
        if isinstance(contract_result, Failure):
            return contract_result
        return Success((state_result.unwrap(), contract_result.unwrap()))

    async def interact(
        self, request: InteractRequest
    ) -> Result[dict[str, Any], AnimaError]:
        if self.transport_port is None:
            return Failure(
                InternalControlError("Control transport port is not configured")
            )
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        state = state_result.unwrap()
        if state.state not in _INTERACTIVE_STATES:
            return Failure(
                AgentNotInteractiveError(
                    f"Agent not running (state: {state.state.value})",
                    details={
                        "instance_id": state.instance_id,
                        "current_state": state.state.value,
                    },
                )
            )
        return await self.transport_port.call_agent_tool(
            instance_id=request.instance_id,
            tool_name="anima.interact",
            arguments={"message": request.prompt},
        )

    def approve(self, request: ApprovalRequest) -> Result[None, AnimaError]:
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        state = state_result.unwrap()
        if state.state != AgentState.awaiting_approval:
            return Failure(
                InvalidInputError(
                    f"Agent is not awaiting approval (state: {state.state.value})",
                    {
                        "instance_id": state.instance_id,
                        "state": state.state.value,
                    },
                )
            )
        write_result = self.approval_port.write_approval_response(
            request.instance_id,
            approved=True,
            note=request.note,
        )
        if isinstance(write_result, Failure):
            return write_result
        return self.lifecycle_port.approve(request.instance_id)

    def reject(self, request: ApprovalRequest) -> Result[None, AnimaError]:
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        state = state_result.unwrap()
        if state.state != AgentState.awaiting_approval:
            return Failure(
                InvalidInputError(
                    f"Agent is not awaiting approval (state: {state.state.value})",
                    {
                        "instance_id": state.instance_id,
                        "state": state.state.value,
                    },
                )
            )
        write_result = self.approval_port.write_approval_response(
            request.instance_id,
            approved=False,
            note=request.note,
        )
        if isinstance(write_result, Failure):
            return write_result
        return self.lifecycle_port.reject(request.instance_id, reason=request.note)

    def resume(self, request: ResumeRequest) -> Result[None, AnimaError]:
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        state = state_result.unwrap()
        if state.state != AgentState.suspended:
            return Failure(
                InvalidInputError(
                    f"Agent is not suspended (state: {state.state.value})",
                    {
                        "instance_id": state.instance_id,
                        "state": state.state.value,
                    },
                )
            )
        return self.lifecycle_port.resume(request.instance_id)

    def kill(self, request: KillRequest) -> Result[None, AnimaError]:
        state_result = self.contract_state_port.read_state(request.instance_id)
        if isinstance(state_result, Failure):
            return state_result
        state = state_result.unwrap()
        if state.state in _TERMINAL_STATES:
            return Failure(
                AgentTerminatedError(
                    f"Already terminal: {state.state.value}",
                    details={
                        "instance_id": state.instance_id,
                        "current_state": state.state.value,
                    },
                )
            )
        return self.lifecycle_port.kill(request.instance_id, reason=request.reason)

    def spawn_subagent(
        self, request: SpawnSubagentRequest
    ) -> Result[SpawnSubagentOutcome, AnimaError]:
        if self.launch_port is None:
            return Failure(
                InternalControlError("Subagent launch port is not configured")
            )
        if self.spawn_persona_port is None:
            return Failure(InternalControlError("Spawn persona port is not configured"))
        if self.generate_instance_id is None:
            return Failure(
                InternalControlError("Instance ID generator is not configured")
            )

        status_result = self.status(
            StatusRequest(instance_id=request.parent_instance_id)
        )
        if isinstance(status_result, Failure):
            return status_result
        parent_state, parent_contract = status_result.unwrap()

        overrides_result = _validate_spawn_overrides(
            parent_max_turns=parent_state.max_turns,
            parent_budget_total=parent_state.budget_total,
            requested_max_turns=request.max_turns,
            requested_max_tokens=request.max_tokens,
        )
        if isinstance(overrides_result, Failure):
            return overrides_result

        persona_resolution_result = self._resolve_child_persona(
            request=request,
            parent_state=parent_state,
            parent_contract=parent_contract,
        )
        if isinstance(persona_resolution_result, Failure):
            return persona_resolution_result
        if isinstance(persona_resolution_result.unwrap(), SpawnSubagentRejected):
            return Success(persona_resolution_result.unwrap())

        persona_resolution = persona_resolution_result.unwrap()
        child_persona_ref = _persona_ref_from_resolution(persona_resolution)

        # Parent persona_ref is only needed when child inherits from parent
        # (no explicit persona_ref). When child specifies its own, parent's ref is irrelevant.
        if child_persona_ref is None:
            parent_ref = parent_state.persona_ref
            if not isinstance(parent_ref, str) or len(parent_ref) == 0:
                return Failure(
                    InvalidInputError(
                        "Parent agent missing persona_ref (needed for child inheritance)",
                        {"instance_id": parent_state.instance_id},
                    )
                )

        child_persona_result = self.spawn_persona_port.load_persona(child_persona_ref)
        if isinstance(child_persona_result, Failure):
            return child_persona_result
        child_persona = child_persona_result.unwrap()

        # V0 guard: warn if child has approval_required policy.
        # Subagent approval requests are invisible to the chat session
        # (it only monitors the parent). Until V1 child polling is
        # implemented, approval_required on a child causes silent deadlock.
        if child_persona.side_effect_policy == SideEffectPolicy.approval_required:
            import logging

            logging.getLogger(__name__).warning(
                f"Subagent '{child_persona.id}' has side_effect_policy=approval_required. "
                "Approval requests from subagents are not yet surfaced to the "
                "interactive session and may cause the subagent to hang. "
                "Consider using side_effect_policy=allow for subagents."
            )

        parent_persona_ref = parent_state.persona_ref
        if isinstance(parent_persona_ref, str) and len(parent_persona_ref) > 0:
            parent_persona_result = self.spawn_persona_port.load_persona(
                parent_persona_ref
            )
            if isinstance(parent_persona_result, Success):
                permission_result = (
                    self.spawn_persona_port.validate_parent_spawn_permission(
                        parent_persona=parent_persona_result.unwrap(),
                        child_persona_name=child_persona.id,
                    )
                )
                if isinstance(permission_result, Failure):
                    return permission_result

        next_depth = parent_state.depth + 1
        if next_depth >= parent_state.max_depth:
            return Failure(
                MaxDepthExceededError(
                    f"Max depth exceeded: would be {next_depth}, max is {parent_state.max_depth}",
                    {
                        "current_depth": parent_state.depth,
                        "max_depth": parent_state.max_depth,
                    },
                )
            )

        child_instance_id = self.generate_instance_id()
        child_job = JobSpec(
            persona_ref=child_persona_ref,
            instance_id=child_instance_id,
            lifecycle=request.lifecycle or parent_state.lifecycle,
            max_turns=(
                request.max_turns
                if request.max_turns is not None
                else parent_state.max_turns
            ),
            max_tokens=(
                request.max_tokens
                if request.max_tokens is not None
                else parent_state.budget_total
            ),
            max_depth=parent_state.max_depth,
            context_mode=request.context_mode,
            finalize_grace_budget=parent_state.grace_budget,
        )

        resolved_child_contract = parent_contract
        if self.contract_resolver is not None:
            resolved_child_contract = self.contract_resolver(
                ExecutionIntent(persona_spec=child_persona, job_spec=child_job)
            )

        launch_request = LaunchSubagentRequest(
            task=request.task,
            instance_id=child_instance_id,
            trace_id=parent_state.trace_id,
            parent_instance_id=parent_state.instance_id,
            child_persona_ref=child_persona_ref,
            child_persona=child_persona,
            child_job=child_job,
            resolved_contract=resolved_child_contract,
        )
        launch_result = self.launch_port.launch_subagent(launch_request)
        if isinstance(launch_result, Failure):
            return launch_result

        return Success(
            SpawnSubagentAccepted(
                kind="spawned",
                launch_request=launch_request,
                launch_result=launch_result.unwrap(),
                persona_resolution=persona_resolution,
            )
        )

    def _resolve_child_persona(
        self,
        *,
        request: SpawnSubagentRequest,
        parent_state: AgentMutableStateView,
        parent_contract: ExecutionControlView,
    ) -> Result[SpawnPersonaResolution | SpawnSubagentRejected, AnimaError]:
        if request.persona_ref is not None:
            return Success(
                ExplicitChildPersonaOverride(persona_ref=request.persona_ref)
            )

        spawnability = parent_contract.persona_provenance.spawnability
        if not spawnability.can_spawn:
            if spawnability.reason is not None:
                return Success(
                    SpawnSubagentRejected(
                        kind="spawnability_error",
                        reason=spawnability.reason,
                    )
                )
            return Success(
                SpawnSubagentRejected(
                    kind="spawnability_error",
                    reason=SpawnabilityReason(
                        code="contract_resolution_incomplete",
                        message="Parent persona is not spawnable by resolved contract truth.",
                        user_action="Provide explicit child persona or materialize reusable snapshot.",
                        retryable=True,
                    ),
                )
            )

        reusable_persona_ref = parent_contract.persona_provenance.reusable_persona_ref
        if reusable_persona_ref is None:
            return Success(
                SpawnSubagentRejected(
                    kind="spawnability_error",
                    reason=SpawnabilityReason(
                        code="missing_reusable_persona_ref",
                        message="Parent persona is spawnable but no reusable persona reference is available.",
                        user_action="Provide explicit child persona_ref or materialize parent persona snapshot.",
                        retryable=True,
                    ),
                )
            )

        source = parent_contract.persona_provenance.source
        if source in {"file", "snapshot"}:
            return Success(
                ExistingReusablePersonaForInheritance(
                    reusable_persona_ref=reusable_persona_ref,
                    source=source,
                )
            )

        return Success(
            MaterializedReusablePersonaForInheritance(
                reusable_persona_ref=reusable_persona_ref,
                source=source,
            )
        )


@dataclass(frozen=True)
class InternalControlError(AnimaError):
    """Internal misconfiguration error for control-service wiring."""

    def __init__(self, message: str) -> None:  # type: ignore[override] # justified: AnimaError custom constructor
        super().__init__(code=10, message=message)


_INTERACTIVE_STATES: frozenset[AgentState] = frozenset(
    {
        AgentState.running,
        AgentState.suspended,
        AgentState.awaiting_approval,
    }
)

_TERMINAL_STATES: frozenset[AgentState] = frozenset(
    {
        AgentState.completed,
        AgentState.failed,
        AgentState.killed,
    }
)


# @invar:allow shell_result: Pure helper returns resolved persona reference string.
def _persona_ref_from_resolution(resolution: SpawnPersonaResolution) -> str:
    if isinstance(resolution, ExplicitChildPersonaOverride):
        return resolution.persona_ref
    return resolution.reusable_persona_ref


def _validate_spawn_overrides(
    *,
    parent_max_turns: int | None,
    parent_budget_total: int | None,
    requested_max_turns: int | None,
    requested_max_tokens: int | None,
) -> Result[None, AnimaError]:
    if requested_max_turns is not None and requested_max_turns < 1:
        return Failure(
            InvalidInputError(
                f"max_turns must be >= 1 if set, got {requested_max_turns}",
                {"max_turns": requested_max_turns},
            )
        )
    if requested_max_tokens is not None and requested_max_tokens < 1:
        return Failure(
            InvalidInputError(
                f"max_tokens must be >= 1 if set, got {requested_max_tokens}",
                {"max_tokens": requested_max_tokens},
            )
        )
    if (
        parent_max_turns is not None
        and requested_max_turns is not None
        and requested_max_turns > parent_max_turns
    ):
        return Failure(
            InvalidInputError(
                "max_turns override exceeds parent max_turns",
                {
                    "requested": requested_max_turns,
                    "parent_max_turns": parent_max_turns,
                },
            )
        )
    if (
        parent_budget_total is not None
        and requested_max_tokens is not None
        and requested_max_tokens > parent_budget_total
    ):
        return Failure(
            InvalidInputError(
                "max_tokens override exceeds parent budget_total",
                {
                    "requested": requested_max_tokens,
                    "parent_max_tokens": parent_budget_total,
                },
            )
        )
    return Success(None)
