///|
using @agent_message {type AgentNotification, type AgentRequest}

///|
using @client_protocol {
  type ClientProtocolError,
  type ClientProtocolState,
  type ClientProtocolStep,
  client_protocol_reduce,
}

///|
using @jsonrpc {
  type JsonRpcError,
  type JsonRpcId,
  type JsonRpcMessage,
  type JsonRpcResponse,
  type RequestId,
}

///|
using @message {
  type ClientMessageError,
  type ClientNotification,
  type ClientRequest,
  decode_client_notification,
  decode_client_request,
}

///|
using @protocol_data {
  type AuthenticateParams,
  type AuthenticateResult,
  type CapabilityMarker,
  type ClientCapabilities,
  type ClientElicitationCapabilities,
  type ClientFileSystemCapabilities,
  type ElicitationCompleteParams,
  type ElicitationCreateParams,
  type ElicitationCreateResult,
  type ElicitationFormParams,
  type ElicitationUrlParams,
  type Implementation,
  type InitializeParams,
  type InitializeResult,
  type LogoutParams,
  type LogoutResult,
  type ProtocolDecodeError,
  type ProtocolNullable,
  type ReadTextFileParams,
  type ReadTextFileResult,
  type RequestPermissionRequest,
  type RequestPermissionResponse,
  type TerminalCreateParams,
  type TerminalCreateResult,
  type TerminalKillParams,
  type TerminalKillResult,
  type TerminalOutputParams,
  type TerminalOutputResult,
  type TerminalReleaseParams,
  type TerminalReleaseResult,
  type TerminalWaitForExitParams,
  type TerminalWaitForExitResult,
  type WriteTextFileParams,
  type WriteTextFileResult,
  elicitation_create_result_to_json,
  read_text_file_result_to_json,
  request_permission_response_to_json,
  terminal_create_result_to_json,
  terminal_kill_result_to_json,
  terminal_output_result_to_json,
  terminal_release_result_to_json,
  terminal_wait_for_exit_result_to_json,
  write_text_file_result_to_json,
}

///|
using @session_protocol {
  type CancelParams,
  type CloseSessionParams,
  type CloseSessionResult,
  type DeleteSessionParams,
  type DeleteSessionResult,
  type ForkSessionParams,
  type ForkSessionResult,
  type ListSessionsParams,
  type ListSessionsResult,
  type LoadSessionParams,
  type LoadSessionResult,
  type NewSessionParams,
  type NewSessionResult,
  type PromptParams,
  type PromptResult,
  type ResumeSessionParams,
  type ResumeSessionResult,
  type SessionUpdateParams,
  type SetSessionConfigOptionParams,
  type SetSessionConfigOptionResult,
  type SetSessionModeParams,
  type SetSessionModeResult,
}

///|
/// Runtime intents emitted by the Client adapter.  These values are data
/// only; cancellation itself is owned by the connection/reducer runtime.
pub(all) enum ClientAdapterCommand {
  CancelRequest(request_id~ : RequestId)
  TraceError(method_name~ : String, error~ : JsonRpcError)
} derive(Eq, Debug)

///|
/// Wire responses and runtime trace intents produced by one adapter step.
pub(all) enum ClientAdapterOutput {
  Response(JsonRpcResponse)
  Command(ClientAdapterCommand)
} derive(Eq, Debug)

///|
/// Adapter state contains only the protocol lifecycle and negotiated
/// capabilities.  Request correlation and task tokens remain in the
/// connection/reducer owner; the adapter does not mirror them.
pub(all) struct ClientAdapterState {
  protocol : ClientProtocolState
} derive(Eq, Debug)

///|
pub fn client_adapter_state_new(
  protocol~ : ClientProtocolState,
) -> ClientAdapterState {
  { protocol, }
}

///|
pub fn ClientAdapterState::protocol(
  self : ClientAdapterState,
) -> ClientProtocolState {
  self.protocol
}

///|
/// The explicit state/output result of an admission or completion step.
pub(all) struct ClientAdapterStep {
  state : ClientAdapterState
  outputs : Array[ClientAdapterOutput]
} derive(Eq, Debug)

///|
/// Admission is synchronous and pure.  Only `Invoke` crosses the async
/// handler boundary; `Immediate` already contains its complete owner-loop
/// state transition and output.
pub(all) enum ClientAdapterAdmission {
  Immediate(ClientAdapterStep)
  Invoke(state~ : ClientAdapterState, invocation~ : ClientAdapterInvocation)
}

///|
/// Invalid adapter composition or a completion whose closed-union variant or
/// request id does not match the admitted operation.  Duplicate/late task
/// events are owned by the connection reducer and are not shadowed here.
pub(all) suberror ClientAdapterError {
  EndpointMismatch(reason~ : String)
  UnexpectedMessage(kind~ : String)
  CompletionMismatch(method_name~ : String)
} derive(Eq, Debug)

///|
fn client_adapter_response_id(id : RequestId) -> JsonRpcId {
  match id {
    String(value) => String(value)
    Number(value) => Number(value)
    Null => Null
  }
}

///|
fn client_adapter_response(
  state : ClientAdapterState,
  id : RequestId,
  error : JsonRpcError,
) -> ClientAdapterStep {
  {
    state,
    outputs: [
      Response(
        JsonRpcResponse::error(id=client_adapter_response_id(id), error~),
      ),
    ],
  }
}

///|
fn client_adapter_success(
  state : ClientAdapterState,
  id : RequestId,
  result : Json,
) -> ClientAdapterStep {
  { state, outputs: [Response(JsonRpcResponse::success(id~, result~))], }
}

///|
fn client_adapter_trace(
  state : ClientAdapterState,
  method_name : String,
  error : JsonRpcError,
) -> ClientAdapterStep {
  { state, outputs: [Command(TraceError(method_name~, error~))], }
}

///|
fn client_adapter_validate_endpoint(
  state : ClientAdapterState,
  endpoint : ClientEndpoint,
) -> Unit raise ClientAdapterError {
  let expected : ProtocolNullable[ClientCapabilities] = Value(
    endpoint.capabilities(),
  )
  if state.protocol.capabilities != expected {
    raise EndpointMismatch(reason="state capabilities do not match endpoint")
  }
}

///|
/// One wire error that carries its human-readable detail on both JSON-RPC
/// channels: `message` (what peers render by default) and `data` (the field
/// Zed-style error builders surface). Every caller passes an
/// already-sanitized, host-authored string; this helper only stops the
/// adapter from discarding what handlers chose to tell the peer.
fn client_adapter_detailed_error(
  code~ : @jsonrpc.JsonRpcErrorCode,
  message~ : String,
) -> JsonRpcError {
  JsonRpcError::new(code~, message~, data=Json::string(message))
}

///|
/// Render one decoder failure as a short, path-carrying string.
fn client_adapter_decode_error_message(error : ProtocolDecodeError) -> String {
  match error {
    JsonParse(message~) => message
    ExpectedObject(path~) => "expected object at " + path
    ExpectedArray(path~) => "expected array at " + path
    ExpectedString(path~) => "expected string at " + path
    ExpectedBoolean(path~) => "expected boolean at " + path
    ExpectedNumber(path~) => "expected number at " + path
    MissingField(path~) => "missing field at " + path
    UnknownField(path~) => "unknown field at " + path
    InvalidField(path~, reason~) => path + ": " + reason
    InvalidDiscriminator(path~, value~) => path + ": " + value
    InvalidPath(path~, value~) => path + ": " + value
    InvalidBase64(path~) => "invalid base64 at " + path
  }
}

///|
fn client_adapter_message_error(error : ClientMessageError) -> JsonRpcError {
  match error {
    InvalidParams(method_name~, cause~) =>
      client_adapter_detailed_error(
        code=InvalidParams,
        message=method_name + ": " + client_adapter_decode_error_message(cause),
      )
    UnknownMethod(method_name~) =>
      client_adapter_detailed_error(
        code=MethodNotFound,
        message="unknown method: " + method_name,
      )
    WrongDirection(method_name~) =>
      client_adapter_detailed_error(
        code=MethodNotFound,
        message="method is not a client request: " + method_name,
      )
    WrongKind(method_name~) =>
      client_adapter_detailed_error(
        code=InvalidRequest,
        message="wrong message kind for method: " + method_name,
      )
  }
}

///|
fn client_adapter_protocol_error(error : ClientProtocolError) -> JsonRpcError {
  match error {
    NotReady(method_name~) =>
      client_adapter_detailed_error(
        code=InvalidRequest,
        message="initialize required before " + method_name,
      )
    AlreadyReady =>
      client_adapter_detailed_error(
        code=InvalidRequest,
        message="client is already initialized",
      )
    CapabilityDenied(method_name~, capability~) =>
      client_adapter_detailed_error(
        code=ResourceNotFound,
        message="capability '" +
          capability +
          "' required by " +
          method_name +
          " is not enabled",
      )
  }
}

///|
fn client_adapter_handler_error(error : HandlerError) -> JsonRpcError {
  match error {
    Application(message~) =>
      client_adapter_detailed_error(code=InternalError, message~)
    InvalidParams(message~) =>
      client_adapter_detailed_error(code=InvalidParams, message~)
    Cancelled => JsonRpcError::request_cancelled()
    UnavailableOperation(method_name~) =>
      client_adapter_detailed_error(
        code=ResourceNotFound,
        message="client does not implement operation: " + method_name,
      )
  }
}

///|
fn client_adapter_reduce_request(
  state : ClientAdapterState,
  request : ClientRequest,
) -> Result[ClientProtocolStep, JsonRpcError] {
  try client_protocol_reduce(state.protocol, Request(request)) catch {
    error => Err(client_adapter_protocol_error(error))
  } noraise {
    step => Ok(step)
  }
}

///|
fn client_adapter_reduce_notification(
  state : ClientAdapterState,
  notification : ClientNotification,
) -> Result[ClientProtocolStep, JsonRpcError] {
  try client_protocol_reduce(state.protocol, Notification(notification)) catch {
    error => Err(client_adapter_protocol_error(error))
  } noraise {
    step => Ok(step)
  }
}

///|
/// Normalize handler failures without confusing an application-raised
/// `Cancelled` value with the async runtime's cancellation exception.
/// Handler-authored messages pass through verbatim (they are the
/// host-sanitized channel); an unexpected exception names itself with the
/// error's display text so the peer can report it to a developer.
async fn[T] client_adapter_call(
  handler : async () -> T,
) -> Result[T, HandlerError] noraise {
  try handler() catch {
    error =>
      if @async.is_cancellation_error(error) {
        Err(Cancelled)
      } else {
        match error {
          HandlerError::Application(message~) => Err(Application(message~))
          HandlerError::InvalidParams(message~) => Err(InvalidParams(message~))
          HandlerError::Cancelled => Err(Cancelled)
          HandlerError::UnavailableOperation(method_name~) =>
            Err(UnavailableOperation(method_name~))
          _ =>
            Err(
              Application(message="client handler failed: " + error.to_string()),
            )
        }
      }
  } noraise {
    value => Ok(value)
  }
}

///|
fn[T] client_adapter_encode(
  value : T,
  encode : (T) -> Json raise ProtocolDecodeError,
) -> Result[Json, JsonRpcError] {
  try encode(value) catch {
    _ => Err(JsonRpcError::internal_error())
  } noraise {
    result => Ok(result)
  }
}

///|
fn[T] client_adapter_complete_result(
  state : ClientAdapterState,
  id : RequestId,
  result : Result[T, HandlerError],
  encode : (T) -> Json raise ProtocolDecodeError,
) -> ClientAdapterStep {
  match result {
    Err(error) =>
      client_adapter_response(state, id, client_adapter_handler_error(error))
    Ok(value) =>
      match client_adapter_encode(value, encode) {
        Err(error) => client_adapter_response(state, id, error)
        Ok(result) => client_adapter_success(state, id, result)
      }
  }
}

///|
fn client_adapter_permission_option_offered(
  params : RequestPermissionRequest,
  option_id : String,
) -> Bool {
  for option in params.options {
    if option.option_id == option_id {
      return true
    }
  }
  false
}

///|
fn client_adapter_permission_response(
  state : ClientAdapterState,
  id : RequestId,
  response : RequestPermissionResponse,
) -> ClientAdapterStep {
  match client_adapter_encode(response, request_permission_response_to_json) {
    Err(error) => client_adapter_response(state, id, error)
    Ok(result) => client_adapter_success(state, id, result)
  }
}

///|
/// Permission cancellation is a normal ACP response, while an unknown
/// selected option is a local invalid-params failure.  The admitted request
/// remains the source of truth for the option membership check.
fn client_adapter_complete_permission(
  state : ClientAdapterState,
  id : RequestId,
  params : RequestPermissionRequest,
  result : Result[RequestPermissionResponse, HandlerError],
) -> ClientAdapterStep {
  match result {
    Err(Cancelled) =>
      client_adapter_permission_response(state, id, {
        outcome: Cancelled,
        meta: Omitted,
      })
    Err(error) =>
      client_adapter_response(state, id, client_adapter_handler_error(error))
    Ok(response) => {
      let valid = match response.outcome {
        Cancelled => true
        Selected(selected) =>
          client_adapter_permission_option_offered(params, selected.option_id)
      }
      if valid {
        client_adapter_permission_response(state, id, response)
      } else {
        client_adapter_response(
          state,
          id,
          JsonRpcError::new(code=InvalidParams, message="invalid params"),
        )
      }
    }
  }
}

///|
fn client_adapter_complete_notification(
  state : ClientAdapterState,
  method_name : String,
  result : Result[Unit, HandlerError],
) -> ClientAdapterStep {
  match result {
    Ok(_) => { state, outputs: [], }
    Err(error) =>
      client_adapter_trace(
        state,
        method_name,
        client_adapter_handler_error(error),
      )
  }
}

///|
fn client_adapter_request_invocation(
  state : ClientAdapterState,
  id : RequestId,
  request : ClientRequest,
) -> ClientAdapterAdmission {
  match request {
    SessionRequestPermission(params) =>
      Invoke(state~, invocation=RequestPermissionInvocation(id~, params~))
    FsReadTextFile(params) =>
      Invoke(state~, invocation=FsReadTextFileInvocation(id~, params~))
    FsWriteTextFile(params) =>
      Invoke(state~, invocation=FsWriteTextFileInvocation(id~, params~))
    TerminalCreate(params) =>
      Invoke(state~, invocation=TerminalCreateInvocation(id~, params~))
    TerminalOutput(params) =>
      Invoke(state~, invocation=TerminalOutputInvocation(id~, params~))
    TerminalWaitForExit(params) =>
      Invoke(state~, invocation=TerminalWaitForExitInvocation(id~, params~))
    TerminalKill(params) =>
      Invoke(state~, invocation=TerminalKillInvocation(id~, params~))
    TerminalRelease(params) =>
      Invoke(state~, invocation=TerminalReleaseInvocation(id~, params~))
    ElicitationCreate(params) =>
      Invoke(state~, invocation=ElicitationCreateInvocation(id~, params~))
  }
}

///|
fn client_adapter_admit_request(
  state : ClientAdapterState,
  id : RequestId,
  request : ClientRequest,
) -> ClientAdapterAdmission {
  match client_adapter_reduce_request(state, request) {
    Err(error) => Immediate(client_adapter_response(state, id, error))
    Ok(step) =>
      client_adapter_request_invocation({ protocol: step.state, }, id, request)
  }
}

///|
fn client_adapter_admit_notification(
  state : ClientAdapterState,
  notification : ClientNotification,
) -> ClientAdapterAdmission {
  match notification {
    SessionUpdate(params) =>
      match client_adapter_reduce_notification(state, notification) {
        Err(error) =>
          Immediate(client_adapter_trace(state, "session/update", error))
        Ok(step) =>
          Invoke(
            state={ protocol: step.state, },
            invocation=SessionUpdateInvocation(params~),
          )
      }
    ElicitationComplete(params) =>
      match client_adapter_reduce_notification(state, notification) {
        Err(error) =>
          Immediate(client_adapter_trace(state, "elicitation/complete", error))
        Ok(step) =>
          Invoke(
            state={ protocol: step.state, },
            invocation=ElicitationCompleteInvocation(params~),
          )
      }
    CancelRequest(request_id) =>
      match client_adapter_reduce_notification(state, notification) {
        Err(error) =>
          Immediate(client_adapter_trace(state, "$/cancel_request", error))
        Ok(step) =>
          Immediate({
            state: { protocol: step.state, },
            outputs: [Command(CancelRequest(request_id~))],
          })
      }
  }
}

///|
/// Decode and admit one Agent-to-Client JSON-RPC message.  No endpoint
/// handler is called by this function.
pub fn client_adapter_admit(
  state : ClientAdapterState,
  endpoint : ClientEndpoint,
  message : JsonRpcMessage,
) -> ClientAdapterAdmission raise ClientAdapterError {
  client_adapter_validate_endpoint(state, endpoint)
  match message {
    Request(request) => {
      let id = request.id
      let decoded = try
        decode_client_request(request.method_name, request.params)
      catch {
        error => Err(client_adapter_message_error(error))
      } noraise {
        value => Ok(value)
      }
      match decoded {
        Err(error) => Immediate(client_adapter_response(state, id, error))
        Ok(request) => client_adapter_admit_request(state, id, request)
      }
    }
    Notification(notification) => {
      let decoded = try
        decode_client_notification(
          notification.method_name,
          notification.params,
        )
      catch {
        error => Err(client_adapter_message_error(error))
      } noraise {
        value => Ok(value)
      }
      match decoded {
        Err(error) =>
          Immediate(
            client_adapter_trace(state, notification.method_name, error),
          )
        Ok(notification) =>
          client_adapter_admit_notification(state, notification)
      }
    }
    Response(_) => raise UnexpectedMessage(kind="response")
  }
}

///|
/// Execute exactly one already-admitted typed operation.  This phase has no
/// protocol state and cannot emit a response, trace, queue operation, or
/// cancellation intent.
pub async fn client_adapter_execute(
  invocation : ClientAdapterInvocation,
  endpoint : ClientEndpoint,
) -> ClientAdapterCompletion noraise {
  match invocation {
    RequestPermissionInvocation(id~, params~) =>
      RequestPermissionCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.request_permission(params)
        }),
      )
    FsReadTextFileInvocation(id~, params~) =>
      FsReadTextFileCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.read_text_file(params)
        }),
      )
    FsWriteTextFileInvocation(id~, params~) =>
      FsWriteTextFileCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.write_text_file(params)
        }),
      )
    TerminalCreateInvocation(id~, params~) =>
      TerminalCreateCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.terminal_create(params)
        }),
      )
    TerminalOutputInvocation(id~, params~) =>
      TerminalOutputCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.terminal_output(params)
        }),
      )
    TerminalWaitForExitInvocation(id~, params~) =>
      TerminalWaitForExitCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.terminal_wait_for_exit(params)
        }),
      )
    TerminalKillInvocation(id~, params~) =>
      TerminalKillCompleted(
        id~,
        result=client_adapter_call(async fn() { endpoint.terminal_kill(params) }),
      )
    TerminalReleaseInvocation(id~, params~) =>
      TerminalReleaseCompleted(
        id~,
        result=client_adapter_call(async fn() {
          endpoint.terminal_release(params)
        }),
      )
    ElicitationCreateInvocation(id~, params~) =>
      match params {
        Form(params) =>
          ElicitationCreateCompleted(
            id~,
            result=client_adapter_call(async fn() {
              endpoint.elicitation_form(params)
            }),
          )
        Url(params) =>
          ElicitationCreateCompleted(
            id~,
            result=client_adapter_call(async fn() {
              endpoint.elicitation_url(params)
            }),
          )
      }
    SessionUpdateInvocation(params~) =>
      SessionUpdateCompleted(
        result=client_adapter_call(async fn() {
          endpoint.session_update(params)
        }),
      )
    ElicitationCompleteInvocation(params~) =>
      ElicitationCompleteCompleted(
        result=client_adapter_call(async fn() {
          endpoint.elicitation_complete(params)
        }),
      )
  }
}

///|
/// Complete one admitted invocation in the owner loop.  The adapter checks
/// only the typed variant and request id; correlation lifetime belongs to the
/// connection reducer that owns the task table.
pub fn client_adapter_complete(
  state : ClientAdapterState,
  invocation : ClientAdapterInvocation,
  completion : ClientAdapterCompletion,
) -> ClientAdapterStep raise ClientAdapterError {
  match invocation {
    RequestPermissionInvocation(id~, params~) =>
      match completion {
        RequestPermissionCompleted(id=completion_id, result~) if completion_id ==
          id => client_adapter_complete_permission(state, id, params, result)
        RequestPermissionCompleted(..) | _ =>
          raise CompletionMismatch(method_name="session/request_permission")
      }
    FsReadTextFileInvocation(id~, params=_) =>
      match completion {
        FsReadTextFileCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, read_text_file_result_to_json,
          )
        FsReadTextFileCompleted(..) | _ =>
          raise CompletionMismatch(method_name="fs/read_text_file")
      }
    FsWriteTextFileInvocation(id~, params=_) =>
      match completion {
        FsWriteTextFileCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, write_text_file_result_to_json,
          )
        FsWriteTextFileCompleted(..) | _ =>
          raise CompletionMismatch(method_name="fs/write_text_file")
      }
    TerminalCreateInvocation(id~, params=_) =>
      match completion {
        TerminalCreateCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, terminal_create_result_to_json,
          )
        TerminalCreateCompleted(..) | _ =>
          raise CompletionMismatch(method_name="terminal/create")
      }
    TerminalOutputInvocation(id~, params=_) =>
      match completion {
        TerminalOutputCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, terminal_output_result_to_json,
          )
        TerminalOutputCompleted(..) | _ =>
          raise CompletionMismatch(method_name="terminal/output")
      }
    TerminalWaitForExitInvocation(id~, params=_) =>
      match completion {
        TerminalWaitForExitCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, terminal_wait_for_exit_result_to_json,
          )
        TerminalWaitForExitCompleted(..) | _ =>
          raise CompletionMismatch(method_name="terminal/wait_for_exit")
      }
    TerminalKillInvocation(id~, params=_) =>
      match completion {
        TerminalKillCompleted(id=completion_id, result~) if completion_id == id =>
          client_adapter_complete_result(
            state, id, result, terminal_kill_result_to_json,
          )
        TerminalKillCompleted(..) | _ =>
          raise CompletionMismatch(method_name="terminal/kill")
      }
    TerminalReleaseInvocation(id~, params=_) =>
      match completion {
        TerminalReleaseCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, terminal_release_result_to_json,
          )
        TerminalReleaseCompleted(..) | _ =>
          raise CompletionMismatch(method_name="terminal/release")
      }
    ElicitationCreateInvocation(id~, params=_) =>
      match completion {
        ElicitationCreateCompleted(id=completion_id, result~) if completion_id ==
          id =>
          client_adapter_complete_result(
            state, id, result, elicitation_create_result_to_json,
          )
        ElicitationCreateCompleted(..) | _ =>
          raise CompletionMismatch(method_name="elicitation/create")
      }
    SessionUpdateInvocation(params=_) =>
      match completion {
        SessionUpdateCompleted(result~) =>
          client_adapter_complete_notification(state, "session/update", result)
        _ => raise CompletionMismatch(method_name="session/update")
      }
    ElicitationCompleteInvocation(params=_) =>
      match completion {
        ElicitationCompleteCompleted(result~) =>
          client_adapter_complete_notification(
            state, "elicitation/complete", result,
          )
        _ => raise CompletionMismatch(method_name="elicitation/complete")
      }
  }
}