impl RemoteLlmRequest {
    pub fn from_core(request_id: impl Into<String>, value: core_llm::LlmRequest) -> Self {
        let core_llm::LlmRequest {
            model,
            messages,
            attachments,
            tools,
            tool_choice,
            model_variant,
            generation,
            session_id,
            output_spec,
            stream_events: _,
            provider_trace: _,
        } = value;
        Self {
            protocol_version: REMOTE_PROTOCOL_VERSION,
            request_id: request_id.into(),
            model_intent: RemoteModelIntent {
                model,
                variant: model_variant,
                provider: None,
                metadata: HashMap::new(),
            },
            messages: messages.into_iter().map(Into::into).collect(),
            attachments: attachments.into_iter().map(Into::into).collect(),
            tools: tools.iter().cloned().map(Into::into).collect(),
            tool_choice: tool_choice.into(),
            output_spec: output_spec.map(Into::into),
            generation: generation.into(),
            request_metadata: RemoteLlmRequestMetadata {
                session_id,
                idempotency_key: None,
                trace_id: None,
            },
            metadata: HashMap::new(),
        }
    }
}

impl TryFrom<RemoteLlmRequest> for core_llm::LlmRequest {
    type Error = RemoteProtocolError;

    fn try_from(value: RemoteLlmRequest) -> Result<Self, Self::Error> {
        value.validate()?;
        let RemoteLlmRequest {
            protocol_version: _,
            request_id: _,
            model_intent,
            messages,
            attachments,
            tools,
            tool_choice,
            output_spec,
            generation,
            request_metadata,
            metadata: _,
        } = value;
        let RemoteModelIntent {
            model,
            variant,
            provider: _,
            metadata: _,
        } = model_intent;
        let RemoteLlmRequestMetadata {
            session_id,
            idempotency_key: _,
            trace_id: _,
        } = request_metadata;
        Ok(Self {
            model,
            messages: messages.into_iter().map(Into::into).collect(),
            attachments: attachments
                .into_iter()
                .map(TryInto::try_into)
                .collect::<Result<Vec<_>, _>>()?,
            tools: Arc::new(tools.into_iter().map(Into::into).collect()),
            tool_choice: tool_choice.into(),
            model_variant: variant,
            generation: generation.try_into()?,
            session_id,
            output_spec: output_spec.map(Into::into),
            stream_events: None,
            provider_trace: None,
        })
    }
}

impl RemoteLlmResponse {
    pub fn from_core(request_id: impl Into<String>, value: core_llm::LlmResponse) -> Self {
        let core_llm::LlmResponse {
            full_text,
            parts,
            usage,
            terminal_reason,
            terminal_diagnostic,
            provider_usage,
            request_body,
            http_summary,
        } = value;
        let mut diagnostics = Vec::new();
        if let Some(message) = terminal_diagnostic {
            diagnostics.push(RemoteDiagnostic {
                kind: "terminal".to_string(),
                code: None,
                message,
                data: None,
            });
        }
        Self {
            protocol_version: REMOTE_PROTOCOL_VERSION,
            request_id: request_id.into(),
            full_text,
            output_parts: parts.into_iter().map(Into::into).collect(),
            usage: usage.into(),
            terminal_reason: terminal_reason.into(),
            diagnostics,
            provider_metadata: RemoteProviderMetadata {
                usage: provider_usage,
                request_body,
                http_summary,
                data: HashMap::new(),
            },
        }
    }
}

impl From<RemoteLlmResponse> for core_llm::LlmResponse {
    fn from(value: RemoteLlmResponse) -> Self {
        let RemoteLlmResponse {
            protocol_version: _,
            request_id: _,
            full_text,
            output_parts,
            usage,
            terminal_reason,
            diagnostics,
            provider_metadata,
        } = value;
        let RemoteProviderMetadata {
            usage: provider_usage,
            request_body,
            http_summary,
            data: _,
        } = provider_metadata;
        Self {
            full_text,
            parts: output_parts.into_iter().map(Into::into).collect(),
            usage: usage.into(),
            terminal_reason: terminal_reason.into(),
            terminal_diagnostic: diagnostics.first().map(|diag| diag.message.clone()),
            provider_usage,
            request_body,
            http_summary,
        }
    }
}

impl From<core_llm::GenerationOptions> for RemoteGenerationOptions {
    fn from(value: core_llm::GenerationOptions) -> Self {
        let core_llm::GenerationOptions { output_token_cap } = value;
        Self {
            output_token_cap: output_token_cap.map(|cap| cap.get() as u64),
            temperature: None,
            top_p: None,
            stop: Vec::new(),
            provider_options: HashMap::new(),
        }
    }
}

impl TryFrom<RemoteGenerationOptions> for core_llm::GenerationOptions {
    type Error = RemoteProtocolError;

    fn try_from(value: RemoteGenerationOptions) -> Result<Self, Self::Error> {
        value.validate("RemoteGenerationOptions")?;
        let RemoteGenerationOptions {
            output_token_cap,
            temperature: _,
            top_p: _,
            stop: _,
            provider_options: _,
        } = value;
        Ok(Self {
            output_token_cap: output_token_cap
                .and_then(|cap| usize::try_from(cap).ok())
                .and_then(NonZeroUsize::new),
        })
    }
}

impl From<core_llm::LlmMessage> for RemoteLlmMessage {
    fn from(value: core_llm::LlmMessage) -> Self {
        let core_llm::LlmMessage { role, blocks } = value;
        Self {
            role: role.into(),
            content: blocks.iter().cloned().map(Into::into).collect(),
        }
    }
}

impl From<RemoteLlmMessage> for core_llm::LlmMessage {
    fn from(value: RemoteLlmMessage) -> Self {
        let RemoteLlmMessage { role, content } = value;
        Self::new(role.into(), content.into_iter().map(Into::into).collect())
    }
}

impl From<core_llm::LlmRole> for RemoteLlmRole {
    fn from(value: core_llm::LlmRole) -> Self {
        match value {
            core_llm::LlmRole::User => Self::User,
            core_llm::LlmRole::Assistant => Self::Assistant,
            core_llm::LlmRole::System => Self::System,
        }
    }
}

impl From<RemoteLlmRole> for core_llm::LlmRole {
    fn from(value: RemoteLlmRole) -> Self {
        match value {
            RemoteLlmRole::User => Self::User,
            RemoteLlmRole::Assistant => Self::Assistant,
            RemoteLlmRole::System => Self::System,
        }
    }
}

impl From<core_llm::LlmContentBlock> for RemoteLlmContentBlock {
    fn from(value: core_llm::LlmContentBlock) -> Self {
        match value {
            core_llm::LlmContentBlock::Text {
                text,
                response_meta,
                cache_breakpoint,
            } => Self::Text {
                text: text.to_string(),
                response_meta: response_meta.map(Into::into),
                cache_breakpoint,
            },
            core_llm::LlmContentBlock::Image { attachment_idx } => Self::ImageAttachment {
                attachment_index: attachment_idx,
            },
            core_llm::LlmContentBlock::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay,
            } => Self::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay: replay.map(Into::into),
            },
            core_llm::LlmContentBlock::ToolResult {
                call_id,
                content,
                tool_name,
            } => Self::ToolResult {
                call_id,
                content,
                tool_name,
            },
            core_llm::LlmContentBlock::Reasoning { text, replay } => Self::Reasoning {
                text,
                replay: replay.map(Into::into),
            },
        }
    }
}

impl From<RemoteLlmContentBlock> for core_llm::LlmContentBlock {
    fn from(value: RemoteLlmContentBlock) -> Self {
        match value {
            RemoteLlmContentBlock::Text {
                text,
                response_meta,
                cache_breakpoint,
            } => Self::Text {
                text: Arc::<str>::from(text),
                response_meta: response_meta.map(Into::into),
                cache_breakpoint,
            },
            RemoteLlmContentBlock::ImageAttachment { attachment_index } => Self::Image {
                attachment_idx: attachment_index,
            },
            RemoteLlmContentBlock::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay,
            } => Self::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay: replay.map(Into::into),
            },
            RemoteLlmContentBlock::ToolResult {
                call_id,
                content,
                tool_name,
            } => Self::ToolResult {
                call_id,
                content,
                tool_name,
            },
            RemoteLlmContentBlock::Reasoning { text, replay } => Self::Reasoning {
                text,
                replay: replay.map(Into::into),
            },
        }
    }
}

impl From<core_llm::ResponseTextMeta> for RemoteResponseTextMeta {
    fn from(value: core_llm::ResponseTextMeta) -> Self {
        let core_llm::ResponseTextMeta { id, status, phase } = value;
        Self { id, status, phase }
    }
}

impl From<RemoteResponseTextMeta> for core_llm::ResponseTextMeta {
    fn from(value: RemoteResponseTextMeta) -> Self {
        let RemoteResponseTextMeta { id, status, phase } = value;
        Self { id, status, phase }
    }
}

impl From<core_llm::ProviderReplayMeta> for RemoteProviderReplayMeta {
    fn from(value: core_llm::ProviderReplayMeta) -> Self {
        let core_llm::ProviderReplayMeta { item_id, opaque } = value;
        Self { item_id, opaque }
    }
}

impl From<RemoteProviderReplayMeta> for core_llm::ProviderReplayMeta {
    fn from(value: RemoteProviderReplayMeta) -> Self {
        let RemoteProviderReplayMeta { item_id, opaque } = value;
        Self { item_id, opaque }
    }
}

impl From<core_llm::ProviderReasoningReplay> for RemoteProviderReasoningReplay {
    fn from(value: core_llm::ProviderReasoningReplay) -> Self {
        let core_llm::ProviderReasoningReplay {
            item_id,
            encrypted_content,
            signature,
            redacted,
            summary,
        } = value;
        Self {
            item_id,
            encrypted_content,
            signature,
            redacted,
            summary,
        }
    }
}

impl From<RemoteProviderReasoningReplay> for core_llm::ProviderReasoningReplay {
    fn from(value: RemoteProviderReasoningReplay) -> Self {
        let RemoteProviderReasoningReplay {
            item_id,
            encrypted_content,
            signature,
            redacted,
            summary,
        } = value;
        Self {
            item_id,
            encrypted_content,
            signature,
            redacted,
            summary,
        }
    }
}

impl From<core_llm::LlmAttachment> for RemoteLlmAttachment {
    fn from(value: core_llm::LlmAttachment) -> Self {
        let core_llm::LlmAttachment {
            mime,
            data,
            reference,
        } = value;
        Self {
            id: reference.as_ref().map(|reference| reference.id.to_string()),
            mime,
            data_base64: (!data.is_empty())
                .then(|| base64::engine::general_purpose::STANDARD.encode(data)),
            reference: reference.map(Into::into),
            metadata: HashMap::new(),
        }
    }
}

impl TryFrom<RemoteLlmAttachment> for core_llm::LlmAttachment {
    type Error = RemoteProtocolError;

    fn try_from(value: RemoteLlmAttachment) -> Result<Self, Self::Error> {
        let RemoteLlmAttachment {
            id,
            mime,
            data_base64,
            reference,
            metadata: _,
        } = value;
        let data = match data_base64 {
            Some(encoded) => base64::engine::general_purpose::STANDARD
                .decode(encoded.as_bytes())
                .map_err(|err| RemoteProtocolError::InvalidImageBlob {
                    id: id.unwrap_or_else(|| "<inline>".to_string()),
                    message: err.to_string(),
                })?,
            None => Vec::new(),
        };
        Ok(Self {
            mime,
            data,
            reference: reference.map(TryInto::try_into).transpose()?,
        })
    }
}

impl From<lash_core::AttachmentRef> for RemoteAttachmentRef {
    fn from(value: lash_core::AttachmentRef) -> Self {
        let mime = value.canonical_mime().to_string();
        let lash_core::AttachmentRef {
            id,
            media_type: _,
            byte_len,
            width,
            height,
            label,
        } = value;
        Self {
            id: id.to_string(),
            mime,
            byte_len,
            width,
            height,
            label,
            metadata: HashMap::new(),
        }
    }
}

impl TryFrom<RemoteAttachmentRef> for lash_core::AttachmentRef {
    type Error = RemoteProtocolError;

    fn try_from(value: RemoteAttachmentRef) -> Result<Self, Self::Error> {
        value.validate()?;
        let RemoteAttachmentRef {
            id,
            mime,
            byte_len,
            width,
            height,
            label,
            metadata: _,
        } = value;
        let media_type = lash_core::MediaType::from_mime(&mime).ok_or_else(|| {
            RemoteProtocolError::InvalidAttachmentRef {
                id: id.clone(),
                message: format!("unsupported attachment mime `{mime}`"),
            }
        })?;
        Ok(Self {
            id: lash_core::AttachmentId::new(id),
            media_type,
            byte_len,
            width,
            height,
            label,
        })
    }
}

impl From<core_llm::LlmToolSpec> for RemoteLlmToolSpec {
    fn from(value: core_llm::LlmToolSpec) -> Self {
        let core_llm::LlmToolSpec {
            name,
            description,
            input_schema,
            output_schema,
            input_schema_projections,
            output_schema_projections,
        } = value;
        Self {
            name,
            description,
            input_schema,
            output_schema,
            input_schema_projections: input_schema_projections
                .into_iter()
                .map(Into::into)
                .collect(),
            output_schema_projections: output_schema_projections
                .into_iter()
                .map(Into::into)
                .collect(),
        }
    }
}

impl From<RemoteLlmToolSpec> for core_llm::LlmToolSpec {
    fn from(value: RemoteLlmToolSpec) -> Self {
        let RemoteLlmToolSpec {
            name,
            description,
            input_schema,
            output_schema,
            input_schema_projections,
            output_schema_projections,
        } = value;
        Self {
            name,
            description,
            input_schema,
            output_schema,
            input_schema_projections: input_schema_projections
                .into_iter()
                .map(Into::into)
                .collect(),
            output_schema_projections: output_schema_projections
                .into_iter()
                .map(Into::into)
                .collect(),
        }
    }
}

impl From<core_llm::LlmToolChoice> for RemoteLlmToolChoice {
    fn from(value: core_llm::LlmToolChoice) -> Self {
        match value {
            core_llm::LlmToolChoice::Auto => Self::Auto,
            core_llm::LlmToolChoice::None => Self::None,
            core_llm::LlmToolChoice::Required => Self::Required,
        }
    }
}

impl From<RemoteLlmToolChoice> for core_llm::LlmToolChoice {
    fn from(value: RemoteLlmToolChoice) -> Self {
        match value {
            RemoteLlmToolChoice::Auto => Self::Auto,
            RemoteLlmToolChoice::None => Self::None,
            RemoteLlmToolChoice::Required => Self::Required,
        }
    }
}

impl From<core_llm::LlmOutputSpec> for RemoteLlmOutputSpec {
    fn from(value: core_llm::LlmOutputSpec) -> Self {
        match value {
            core_llm::LlmOutputSpec::JsonObject => Self::JsonObject,
            core_llm::LlmOutputSpec::JsonSchema(core_llm::LlmJsonSchema {
                name,
                schema,
                strict,
            }) => Self::JsonSchema {
                name,
                schema,
                strict,
            },
        }
    }
}

impl From<RemoteLlmOutputSpec> for core_llm::LlmOutputSpec {
    fn from(value: RemoteLlmOutputSpec) -> Self {
        match value {
            RemoteLlmOutputSpec::JsonObject => Self::JsonObject,
            RemoteLlmOutputSpec::JsonSchema {
                name,
                schema,
                strict,
            } => Self::JsonSchema(core_llm::LlmJsonSchema {
                name,
                schema,
                strict,
            }),
        }
    }
}

impl From<core_llm::LlmOutputPart> for RemoteLlmOutputPart {
    fn from(value: core_llm::LlmOutputPart) -> Self {
        match value {
            core_llm::LlmOutputPart::Text {
                text,
                response_meta,
            } => Self::Text {
                text,
                response_meta: response_meta.map(Into::into),
            },
            core_llm::LlmOutputPart::Reasoning { text, replay } => Self::Reasoning {
                text,
                replay: replay.map(Into::into),
            },
            core_llm::LlmOutputPart::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay,
            } => Self::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay: replay.map(Into::into),
            },
        }
    }
}

impl From<RemoteLlmOutputPart> for core_llm::LlmOutputPart {
    fn from(value: RemoteLlmOutputPart) -> Self {
        match value {
            RemoteLlmOutputPart::Text {
                text,
                response_meta,
            } => Self::Text {
                text,
                response_meta: response_meta.map(Into::into),
            },
            RemoteLlmOutputPart::Reasoning { text, replay } => Self::Reasoning {
                text,
                replay: replay.map(Into::into),
            },
            RemoteLlmOutputPart::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay,
            } => Self::ToolCall {
                call_id,
                tool_name,
                input_json,
                replay: replay.map(Into::into),
            },
        }
    }
}

impl From<core_llm::LlmTerminalReason> for RemoteLlmTerminalReason {
    fn from(value: core_llm::LlmTerminalReason) -> Self {
        match value {
            core_llm::LlmTerminalReason::Stop => Self::Stop,
            core_llm::LlmTerminalReason::ToolUse => Self::ToolUse,
            core_llm::LlmTerminalReason::OutputLimit => Self::OutputLimit,
            core_llm::LlmTerminalReason::ContextOverflow => Self::ContextOverflow,
            core_llm::LlmTerminalReason::ContentFilter => Self::ContentFilter,
            core_llm::LlmTerminalReason::ProviderError => Self::ProviderError,
            core_llm::LlmTerminalReason::Cancelled => Self::Cancelled,
            core_llm::LlmTerminalReason::Unknown => Self::Unknown,
        }
    }
}

impl From<RemoteLlmTerminalReason> for core_llm::LlmTerminalReason {
    fn from(value: RemoteLlmTerminalReason) -> Self {
        match value {
            RemoteLlmTerminalReason::Stop => Self::Stop,
            RemoteLlmTerminalReason::ToolUse => Self::ToolUse,
            RemoteLlmTerminalReason::OutputLimit => Self::OutputLimit,
            RemoteLlmTerminalReason::ContextOverflow => Self::ContextOverflow,
            RemoteLlmTerminalReason::ContentFilter => Self::ContentFilter,
            RemoteLlmTerminalReason::ProviderError => Self::ProviderError,
            RemoteLlmTerminalReason::Cancelled => Self::Cancelled,
            RemoteLlmTerminalReason::Unknown => Self::Unknown,
        }
    }
}

impl From<core_llm::LlmUsage> for RemoteUsage {
    fn from(value: core_llm::LlmUsage) -> Self {
        let core_llm::LlmUsage {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        } = value;
        Self {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        }
    }
}

impl From<RemoteUsage> for core_llm::LlmUsage {
    fn from(value: RemoteUsage) -> Self {
        let RemoteUsage {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        } = value;
        Self {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        }
    }
}

impl From<lash_core::TokenUsage> for RemoteUsage {
    fn from(value: lash_core::TokenUsage) -> Self {
        let lash_core::TokenUsage {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        } = value;
        Self {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        }
    }
}

impl From<RemoteUsage> for lash_core::TokenUsage {
    fn from(value: RemoteUsage) -> Self {
        let RemoteUsage {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        } = value;
        Self {
            input_tokens,
            output_tokens,
            cached_input_tokens,
            reasoning_tokens,
        }
    }
}
