//! gRPC service implementation for HL7v2.

use crate::models::{
    EvidenceBundleSummary as ServerEvidenceBundleSummary,
    QuarantineOutputSummary as ServerQuarantineOutputSummary,
    QuarantineReason as ServerQuarantineReason, RedactionAction as ServerRedactionAction,
    RedactionActionReceipt as ServerRedactionActionReceipt,
    RedactionActionStatus as ServerRedactionActionStatus,
    RedactionReceipt as ServerRedactionReceipt,
};
use crate::redaction::redact_message;
use crate::server::AppState;
use hl7v2::{
    Comp as RustComp, Field as RustField, Message as RustMessage, Rep as RustRep,
    Segment as RustSegment, parse as rust_parse,
};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::sync::Arc;
use tonic::{Request, Response, Status};

// Include the generated gRPC code
/// Generated gRPC protocol code (protobuf messages and service traits).
#[expect(
    missing_docs,
    reason = "protobuf bindings are generated by tonic/prost"
)]
pub mod proto {
    tonic::include_proto!("hl7v2.v1");
}

use proto::hl7_service_server::Hl7Service;
use proto::*;

/// Implementation of the HL7Service gRPC trait
pub struct Hl7ServiceImpl {
    state: Arc<AppState>,
}

impl Hl7ServiceImpl {
    /// Create a new gRPC service instance
    pub fn new(state: Arc<AppState>) -> Self {
        Self { state }
    }
}

#[tonic::async_trait]
impl Hl7Service for Hl7ServiceImpl {
    async fn parse(
        &self,
        request: Request<ParseRequest>,
    ) -> Result<Response<ParseResponse>, Status> {
        let req = request.into_inner();
        Ok(Response::new(parse_grpc_message_response(
            &req.message,
            req.mllp_framed,
        )))
    }

    type ParseStreamStream =
        tokio_stream::wrappers::ReceiverStream<Result<ParseStreamResponse, Status>>;

    async fn parse_stream(
        &self,
        request: Request<tonic::Streaming<ParseStreamRequest>>,
    ) -> Result<Response<Self::ParseStreamStream>, Status> {
        let mut stream = request.into_inner();
        let (sender, receiver) = tokio::sync::mpsc::channel(8);

        tokio::spawn(async move {
            loop {
                match stream.message().await {
                    Ok(Some(request)) => {
                        let response = parse_stream_response_from_parse_response(
                            parse_grpc_message_response(&request.message, request.mllp_framed),
                        );
                        if sender.send(Ok(response)).await.is_err() {
                            break;
                        }
                    }
                    Ok(None) => break,
                    Err(status) => {
                        if sender.send(Err(status)).await.is_err() {
                            break;
                        }
                        break;
                    }
                }
            }
        });

        Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(
            receiver,
        )))
    }

    async fn validate(
        &self,
        request: Request<ValidateRequest>,
    ) -> Result<Response<ValidateResponse>, Status> {
        let req = request.into_inner();

        let message_bytes = if req.mllp_framed {
            hl7v2::unwrap_mllp(&req.message)
                .map_err(|e| Status::invalid_argument(format!("Failed to unwrap MLLP: {}", e)))?
        } else {
            req.message.as_slice()
        };

        let message = rust_parse(message_bytes)
            .map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {}", e)))?;

        let profile = hl7v2::load_profile(&req.profile)
            .map_err(|_error| Status::invalid_argument(crate::PROFILE_LOAD_SAFE_MESSAGE))?;

        let issues = hl7v2::validate(&message, &profile);
        let report = hl7v2::ValidationReport::from_issues(
            &message,
            Some(profile.message_structure.clone()),
            issues.clone(),
        );
        let valid = report.valid;

        let mut errors = Vec::new();
        let mut warnings = Vec::new();

        for issue in issues {
            let location = issue.path.map(|p| {
                let mut loc = Location::default();
                let parts: Vec<&str> = p.split('.').collect();
                if !parts.is_empty() {
                    loc.segment = parts[0].to_string();
                }
                if parts.len() > 1 {
                    loc.field = parts[1].parse().unwrap_or(0);
                }
                if parts.len() > 2 {
                    loc.component = parts[2].parse().unwrap_or(0);
                }
                loc
            });

            let proto_issue = ValidationIssue {
                code: issue.code,
                message: issue.detail,
                severity: match issue.severity {
                    hl7v2::Severity::Error => validation_issue::Severity::Error as i32,
                    hl7v2::Severity::Warning => validation_issue::Severity::Warning as i32,
                },
                location,
                advice: String::new(),
            };

            if issue.severity == hl7v2::Severity::Error {
                errors.push(proto_issue);
            } else {
                warnings.push(proto_issue);
            }
        }

        let summary = Some(ValidationSummary {
            error_count: errors.len() as i32,
            warning_count: warnings.len() as i32,
            info_count: 0,
        });

        Ok(Response::new(ValidateResponse {
            valid,
            errors,
            warnings,
            summary,
            validation_report: Some(proto_validation_report_from_rust(&report)),
            validation_report_v2: (req.report_schema_version == 2).then(|| {
                let profile_identity = Some(hl7v2::ValidationReportProfileIdentity {
                    label: profile.message_structure.clone(),
                    message_structure: Some(profile.message_structure.clone()),
                    version: Some(profile.version.clone()),
                    sha256: None,
                });
                proto_validation_report_v2_from_rust(&report.to_v2(
                    "hl7v2-server-grpc",
                    env!("CARGO_PKG_VERSION"),
                    profile_identity,
                ))
            }),
        }))
    }

    async fn profile_lint(
        &self,
        request: Request<ProfileLintRequest>,
    ) -> Result<Response<ProfileLintResponse>, Status> {
        let req = request.into_inner();
        let report_schema_version =
            grpc_requested_schema_version(req.report_schema_version, "profile lint report")
                .map_err(Status::invalid_argument)?;

        let report = hl7v2::lint_profile_yaml(&req.profile);
        let profile_lint_report_v2 = (report_schema_version == 2).then(|| {
            proto_profile_lint_report_v2_from_rust(
                &report.to_v2("hl7v2-server-grpc", env!("CARGO_PKG_VERSION")),
            )
        });

        Ok(Response::new(ProfileLintResponse {
            profile_lint_report: Some(proto_profile_lint_report_from_rust(&report)),
            profile_lint_report_v2,
        }))
    }

    async fn profile_explain(
        &self,
        request: Request<ProfileExplainRequest>,
    ) -> Result<Response<ProfileExplainResponse>, Status> {
        let req = request.into_inner();
        let report_schema_version =
            grpc_requested_schema_version(req.report_schema_version, "profile explain report")
                .map_err(Status::invalid_argument)?;

        let profile = hl7v2::load_profile_checked(&req.profile)
            .map_err(|_error| Status::invalid_argument(crate::PROFILE_LOAD_SAFE_MESSAGE))?;
        let lint_report = hl7v2::lint_profile_yaml(&req.profile);
        let report =
            hl7v2::explain_profile("<inline-profile>", &req.profile, &profile, &lint_report);
        let profile_explain_report_v2 = (report_schema_version == 2).then(|| {
            proto_profile_explain_report_v2_from_rust(
                &report.to_v2("hl7v2-server-grpc", env!("CARGO_PKG_VERSION")),
            )
        });

        Ok(Response::new(ProfileExplainResponse {
            profile_explain_report: Some(proto_profile_explain_report_from_rust(&report)),
            profile_explain_report_v2,
        }))
    }

    async fn profile_test(
        &self,
        request: Request<ProfileTestRequest>,
    ) -> Result<Response<ProfileTestResponse>, Status> {
        let req = request.into_inner();
        let report_schema_version =
            grpc_requested_schema_version(req.report_schema_version, "profile test report")
                .map_err(Status::invalid_argument)?;

        let profile = hl7v2::load_profile_checked(&req.profile)
            .map_err(|_error| Status::invalid_argument(crate::PROFILE_LOAD_SAFE_MESSAGE))?;
        let report = grpc_profile_test_report_from_inline_fixtures(&req.fixtures, &profile)
            .map_err(Status::invalid_argument)?;
        let profile_test_report_v2 = (report_schema_version == 2).then(|| {
            proto_profile_test_report_v2_from_rust(
                &report.to_v2("hl7v2-server-grpc", env!("CARGO_PKG_VERSION")),
            )
        });

        Ok(Response::new(ProfileTestResponse {
            profile_test_report: Some(proto_profile_test_report_from_rust(&report)),
            profile_test_report_v2,
        }))
    }

    async fn validate_redacted(
        &self,
        request: Request<ValidateRedactedRequest>,
    ) -> Result<Response<ValidateRedactedResponse>, Status> {
        let req = request.into_inner();
        let report_schema_version =
            grpc_requested_schema_version(req.report_schema_version, "validation report")
                .map_err(Status::invalid_argument)?;
        let redaction_receipt_schema_version = grpc_requested_schema_version(
            req.redaction_receipt_schema_version,
            "redaction receipt",
        )
        .map_err(Status::invalid_argument)?;
        let quarantine_schema_version =
            grpc_requested_schema_version(req.quarantine_schema_version, "quarantine output")
                .map_err(Status::invalid_argument)?;

        let raw_input = req.message;
        let message_bytes = if req.mllp_framed {
            hl7v2::unwrap_mllp(&raw_input)
                .map_err(|e| Status::invalid_argument(format!("Failed to unwrap MLLP: {e}")))?
        } else {
            raw_input.as_slice()
        };

        let mut message = rust_parse(message_bytes)
            .map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {e}")))?;

        let receipt = redact_message(&mut message, &req.redaction_policy)
            .map_err(|e| Status::invalid_argument(format!("Failed to redact HL7: {e}")))?;
        let redacted_hl7 = String::from_utf8(hl7v2::write(&message)).map_err(|error| {
            Status::internal(format!(
                "redacted message could not be encoded as UTF-8: {error}"
            ))
        })?;

        let profile = hl7v2::load_profile_checked(&req.profile)
            .map_err(|_error| Status::invalid_argument(crate::PROFILE_LOAD_SAFE_MESSAGE))?;

        let issues = hl7v2::validate(&message, &profile);
        let report = hl7v2::ValidationReport::from_issues(
            &message,
            Some(profile.message_structure.clone()),
            issues,
        );

        let validation_report_v2 = (report_schema_version == 2).then(|| {
            let profile_identity = Some(hl7v2::ValidationReportProfileIdentity {
                label: profile.message_structure.clone(),
                message_structure: Some(profile.message_structure.clone()),
                version: Some(profile.version.clone()),
                sha256: Some(compute_sha256(&req.profile)),
            });
            proto_validation_report_v2_from_rust(&report.to_v2(
                "hl7v2-server-grpc",
                env!("CARGO_PKG_VERSION"),
                profile_identity,
            ))
        });

        let redaction_receipt_v2 = (redaction_receipt_schema_version == 2)
            .then(|| proto_redaction_receipt_v2_from_server(&receipt, "hl7v2-server-grpc"));
        let quarantine = maybe_write_grpc_redacted_quarantine(GrpcRedactedQuarantineContext {
            state: &self.state,
            raw_input: &raw_input,
            profile_yaml: &req.profile,
            policy_text: &req.redaction_policy,
            redacted_message: &message,
            redacted_hl7: &redacted_hl7,
            redaction_receipt: &receipt,
            validation_report: &report,
        })
        .map_err(grpc_quarantine_output_status)?;
        let quarantine_v2 = if quarantine_schema_version == 2 {
            quarantine.as_ref().map(|summary| {
                proto_quarantine_output_summary_v2_from_server(
                    &summary.to_v2("hl7v2-server-grpc", env!("CARGO_PKG_VERSION")),
                )
            })
        } else {
            None
        };

        Ok(Response::new(ValidateRedactedResponse {
            validation_report: Some(proto_validation_report_from_rust(&report)),
            validation_report_v2,
            redaction_receipt: Some(proto_redaction_receipt_from_server(&receipt)),
            redaction_receipt_v2,
            redacted_hl7: req.include_redacted_hl7.then(|| redacted_hl7.into_bytes()),
            quarantine: quarantine
                .as_ref()
                .map(proto_quarantine_output_summary_from_server),
            quarantine_v2,
        }))
    }

    async fn create_evidence_bundle(
        &self,
        request: Request<CreateEvidenceBundleRequest>,
    ) -> Result<Response<CreateEvidenceBundleResponse>, Status> {
        let req = request.into_inner();
        let artifact_schema_version: u8 =
            if grpc_requested_schema_version(req.bundle_artifact_schema_version, "bundle artifact")
                .map_err(Status::invalid_argument)?
                == 2
            {
                2
            } else {
                1
            };
        let bundle_output_root =
            self.state.bundle_output_root.as_deref().ok_or_else(|| {
                Status::failed_precondition("bundle output root is not configured")
            })?;

        let raw_input = req.message;
        let message_bytes = if req.mllp_framed {
            hl7v2::unwrap_mllp(&raw_input)
                .map_err(|e| Status::invalid_argument(format!("Failed to unwrap MLLP: {e}")))?
        } else {
            raw_input.as_slice()
        };
        let mut message = rust_parse(message_bytes)
            .map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {e}")))?;

        let receipt = redact_message(&mut message, &req.redaction_policy)
            .map_err(|e| Status::invalid_argument(format!("Failed to redact HL7: {e}")))?;
        let redacted_hl7 = String::from_utf8(hl7v2::write(&message)).map_err(|error| {
            Status::internal(format!(
                "redacted message could not be encoded as UTF-8: {error}"
            ))
        })?;

        let profile = hl7v2::load_profile_checked(&req.profile)
            .map_err(|_error| Status::invalid_argument(crate::PROFILE_LOAD_SAFE_MESSAGE))?;
        let issues = hl7v2::validate(&message, &profile);
        let validation_report = hl7v2::ValidationReport::from_issues(
            &message,
            Some("profile.yaml".to_string()),
            issues,
        );

        let summary =
            crate::evidence::write_evidence_bundle(crate::evidence::EvidenceBundleWriteRequest {
                root: bundle_output_root,
                bundle_id: &req.bundle_id,
                public_output_dir: Some(&crate::audit::hash_identifier(&req.bundle_id)),
                raw_input: &raw_input,
                profile_yaml: &req.profile,
                policy_text: &req.redaction_policy,
                redacted_message: &message,
                redacted_hl7: &redacted_hl7,
                redaction_receipt: &receipt,
                validation_report: &validation_report,
                artifact_schema_version,
            })
            .map_err(grpc_evidence_bundle_error)?;

        Ok(Response::new(CreateEvidenceBundleResponse {
            summary: Some(proto_evidence_bundle_summary_from_server(&summary)),
        }))
    }

    async fn replay_evidence_bundle(
        &self,
        request: Request<ReplayEvidenceBundleRequest>,
    ) -> Result<Response<ReplayEvidenceBundleResponse>, Status> {
        let req = request.into_inner();
        let report_schema_version =
            grpc_requested_schema_version(req.replay_report_schema_version, "replay report")
                .map_err(Status::invalid_argument)?;
        let bundle_output_root =
            self.state.bundle_output_root.as_deref().ok_or_else(|| {
                Status::failed_precondition("bundle output root is not configured")
            })?;
        let bundle_dir = crate::evidence::bundle_path_for_id(bundle_output_root, &req.bundle_id)
            .map_err(grpc_evidence_bundle_error)?;

        if !bundle_dir.is_dir() {
            crate::metrics::record_replay_result(false);
            return Err(Status::not_found("bundle id was not found"));
        }

        let report = hl7v2::evidence::replay_evidence_bundle(&bundle_dir, "hl7v2-server-grpc");
        crate::metrics::record_replay_result(report.reproduced);
        let replay_report_v2 = (report_schema_version == 2)
            .then(|| proto_evidence_replay_report_v2_from_rust(&report.to_v2()));

        Ok(Response::new(ReplayEvidenceBundleResponse {
            replay_report: Some(proto_evidence_replay_report_from_rust(&report)),
            replay_report_v2,
        }))
    }

    async fn corpus_summarize(
        &self,
        request: Request<CorpusSummarizeRequest>,
    ) -> Result<Response<CorpusSummarizeResponse>, Status> {
        let req = request.into_inner();
        let summary_schema_version =
            grpc_requested_schema_version(req.summary_schema_version, "corpus summary")
                .map_err(Status::invalid_argument)?;
        let ids =
            validated_grpc_corpus_message_ids(&req.messages).map_err(Status::invalid_argument)?;
        let messages = grpc_corpus_message_refs(&req.messages, &ids);
        let summary =
            hl7v2::synthetic::corpus::summarize_corpus_messages("<inline-corpus>", &messages);
        let summary_v2 = (summary_schema_version == 2).then(|| {
            proto_corpus_summary_v2_from_rust(
                &summary.to_v2("hl7v2-server-grpc", env!("CARGO_PKG_VERSION")),
            )
        });

        Ok(Response::new(CorpusSummarizeResponse {
            summary: Some(proto_corpus_summary_from_rust(&summary)),
            summary_v2,
        }))
    }

    async fn corpus_fingerprint(
        &self,
        request: Request<CorpusFingerprintRequest>,
    ) -> Result<Response<CorpusFingerprintResponse>, Status> {
        let req = request.into_inner();
        let fingerprint_schema_version =
            grpc_requested_schema_version(req.fingerprint_schema_version, "corpus fingerprint")
                .map_err(Status::invalid_argument)?;
        let ids =
            validated_grpc_corpus_message_ids(&req.messages).map_err(Status::invalid_argument)?;
        let messages = grpc_corpus_message_refs(&req.messages, &ids);
        let mut fingerprint =
            hl7v2::synthetic::corpus::fingerprint_corpus_messages("<inline-corpus>", &messages);

        if let Some(profile_yaml) = req.profile.as_deref() {
            attach_grpc_profile_to_fingerprint(&mut fingerprint, profile_yaml, &req.messages)
                .map_err(Status::invalid_argument)?;
        }

        let fingerprint_v2 = (fingerprint_schema_version == 2).then(|| {
            proto_corpus_fingerprint_v2_from_rust(&fingerprint.to_v2("hl7v2-server-grpc"))
        });

        Ok(Response::new(CorpusFingerprintResponse {
            fingerprint: Some(proto_corpus_fingerprint_from_rust(&fingerprint)),
            fingerprint_v2,
        }))
    }

    async fn corpus_diff(
        &self,
        request: Request<CorpusDiffRequest>,
    ) -> Result<Response<CorpusDiffResponse>, Status> {
        let req = request.into_inner();
        let diff_schema_version =
            grpc_requested_schema_version(req.diff_schema_version, "corpus diff")
                .map_err(Status::invalid_argument)?;
        let before_ids =
            validated_grpc_corpus_message_ids(&req.before).map_err(Status::invalid_argument)?;
        let after_ids =
            validated_grpc_corpus_message_ids(&req.after).map_err(Status::invalid_argument)?;
        let before_messages = grpc_corpus_message_refs(&req.before, &before_ids);
        let after_messages = grpc_corpus_message_refs(&req.after, &after_ids);
        let mut before_fingerprint = hl7v2::synthetic::corpus::fingerprint_corpus_messages(
            "<inline-before>",
            &before_messages,
        );
        let mut after_fingerprint = hl7v2::synthetic::corpus::fingerprint_corpus_messages(
            "<inline-after>",
            &after_messages,
        );

        if let Some(profile_yaml) = req.profile.as_deref() {
            let (profile, profile_metadata) =
                load_grpc_fingerprint_profile(profile_yaml).map_err(Status::invalid_argument)?;
            before_fingerprint.profile = Some(profile_metadata.clone());
            after_fingerprint.profile = Some(profile_metadata);
            before_fingerprint.validation_issue_code_counts =
                grpc_validation_issue_counts_for_loaded_profile(&req.before, &profile);
            after_fingerprint.validation_issue_code_counts =
                grpc_validation_issue_counts_for_loaded_profile(&req.after, &profile);
        }

        let diff = hl7v2::synthetic::corpus::diff_corpus_fingerprints(
            &before_fingerprint,
            &after_fingerprint,
        );
        let diff_v2 = (diff_schema_version == 2)
            .then(|| proto_corpus_diff_report_v2_from_rust(&diff.to_v2("hl7v2-server-grpc")));

        Ok(Response::new(CorpusDiffResponse {
            diff: Some(proto_corpus_diff_report_from_rust(&diff)),
            diff_v2,
        }))
    }

    async fn generate_ack(
        &self,
        request: Request<GenerateAckRequest>,
    ) -> Result<Response<GenerateAckResponse>, Status> {
        let req = request.into_inner();

        let message = rust_parse(&req.message)
            .map_err(|e| Status::invalid_argument(format!("Failed to parse HL7: {}", e)))?;

        let ack_code = match req.code() {
            generate_ack_request::AckCode::Aa => hl7v2::AckCode::AA,
            generate_ack_request::AckCode::Ae => hl7v2::AckCode::AE,
            generate_ack_request::AckCode::Ar => hl7v2::AckCode::AR,
            _ => hl7v2::AckCode::AA,
        };

        let ack_msg = hl7v2::ack(&message, ack_code)
            .map_err(|e| Status::internal(format!("Failed to generate ACK: {}", e)))?;
        let ack_bytes = hl7v2::write(&ack_msg);
        let proto_ack = proto::Message::from(ack_msg);

        Ok(Response::new(GenerateAckResponse {
            ack_message: ack_bytes,
            parsed_ack: Some(proto_ack),
        }))
    }

    async fn normalize(
        &self,
        request: Request<NormalizeRequest>,
    ) -> Result<Response<NormalizeResponse>, Status> {
        let req = request.into_inner();
        let canonical = req
            .options
            .as_ref()
            .map(|o| o.canonical_delimiters)
            .unwrap_or(true);

        let normalized_bytes = hl7v2::normalize(&req.message, canonical)
            .map_err(|e| Status::invalid_argument(format!("Failed to normalize HL7: {}", e)))?;

        let mut final_bytes = normalized_bytes;
        if let Some(options) = req.options
            && options.mllp_frame
        {
            final_bytes = hl7v2::wrap_mllp(&final_bytes);
        }

        Ok(Response::new(NormalizeResponse {
            normalized: final_bytes,
        }))
    }

    async fn health_check(
        &self,
        _request: Request<HealthCheckRequest>,
    ) -> Result<Response<HealthCheckResponse>, Status> {
        let response = HealthCheckResponse {
            status: health_check_response::ServingStatus::Serving as i32,
            version: env!("CARGO_PKG_VERSION").to_string(),
            uptime_seconds: 0,
        };
        Ok(Response::new(response))
    }
}

// ============================================================================
// Conversions
// ============================================================================

fn parse_grpc_message_response(message: &[u8], mllp_framed: bool) -> ParseResponse {
    let parse_result = if mllp_framed {
        match hl7v2::unwrap_mllp(message) {
            Ok(hl7) => rust_parse(hl7),
            Err(e) => {
                return ParseResponse {
                    success: false,
                    message: None,
                    errors: vec![Error {
                        code: "MLLP_ERROR".to_string(),
                        message: format!("Failed to unwrap MLLP: {}", e),
                        details: std::collections::HashMap::new(),
                        trace_id: String::new(),
                    }],
                    metadata: None,
                };
            }
        }
    } else {
        rust_parse(message)
    };

    match parse_result {
        Ok(msg) => {
            let metadata = extract_grpc_metadata(&msg);
            let proto_msg = proto::Message::from(msg);

            ParseResponse {
                success: true,
                message: Some(proto_msg),
                errors: Vec::new(),
                metadata: Some(metadata),
            }
        }
        Err(e) => ParseResponse {
            success: false,
            message: None,
            errors: vec![Error {
                code: "PARSE_ERROR".to_string(),
                message: format!("Failed to parse HL7: {}", e),
                details: std::collections::HashMap::new(),
                trace_id: String::new(),
            }],
            metadata: None,
        },
    }
}

fn parse_stream_response_from_parse_response(response: ParseResponse) -> ParseStreamResponse {
    ParseStreamResponse {
        success: response.success,
        message: response.message,
        errors: response.errors,
        metadata: response.metadata,
    }
}

fn grpc_requested_schema_version(version: i32, artifact: &str) -> Result<i32, String> {
    match version {
        0 | 1 => Ok(1),
        2 => Ok(2),
        other => Err(format!(
            "unsupported {artifact} schema version {other}; expected 1 or 2"
        )),
    }
}

fn compute_sha256(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    format!("{:x}", hasher.finalize())
}

struct GrpcRedactedQuarantineContext<'a> {
    state: &'a AppState,
    raw_input: &'a [u8],
    profile_yaml: &'a str,
    policy_text: &'a str,
    redacted_message: &'a hl7v2::Message,
    redacted_hl7: &'a str,
    redaction_receipt: &'a ServerRedactionReceipt,
    validation_report: &'a hl7v2::ValidationReport,
}

enum GrpcQuarantineOutputError {
    MissingRoot,
    Write(crate::evidence::EvidenceBundleError),
}

fn maybe_write_grpc_redacted_quarantine(
    context: GrpcRedactedQuarantineContext<'_>,
) -> Result<Option<ServerQuarantineOutputSummary>, GrpcQuarantineOutputError> {
    let GrpcRedactedQuarantineContext {
        state,
        raw_input,
        profile_yaml,
        policy_text,
        redacted_message,
        redacted_hl7,
        redaction_receipt,
        validation_report,
    } = context;

    if validation_report.valid || !state.quarantine.enabled {
        return Ok(None);
    }

    let root = state
        .quarantine
        .path
        .as_deref()
        .ok_or(GrpcQuarantineOutputError::MissingRoot)?;
    let output_id = generated_quarantine_id();
    let summary =
        crate::evidence::write_quarantine_output(crate::evidence::QuarantineOutputWriteRequest {
            root,
            output_id: &output_id,
            config: &state.quarantine,
            raw_input,
            profile_yaml,
            policy_text,
            redacted_message,
            redacted_hl7,
            redaction_receipt,
            validation_report,
        })
        .map_err(GrpcQuarantineOutputError::Write)?;

    Ok(Some(summary))
}

fn generated_quarantine_id() -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos());
    format!("quarantine-{}-{nanos}", std::process::id())
}

fn validated_grpc_corpus_message_ids(
    messages: &[CorpusMessageInput],
) -> Result<Vec<String>, &'static str> {
    if messages.is_empty() {
        return Err("messages must contain at least one message");
    }

    messages
        .iter()
        .enumerate()
        .map(|(index, message)| {
            let label = message
                .id
                .clone()
                .unwrap_or_else(|| format!("message-{}", index.saturating_add(1)));
            validate_grpc_corpus_message_id(&label)?;
            Ok(label)
        })
        .collect()
}

fn validate_grpc_corpus_message_id(label: &str) -> Result<(), &'static str> {
    if label.is_empty() || label == "." || label == ".." || label.len() > 128 {
        return Err("corpus message id must be 1-128 characters and cannot be '.' or '..'");
    }

    if !label
        .chars()
        .all(|character| character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-'))
    {
        return Err("corpus message id must use only ASCII letters, numbers, '.', '_' or '-'");
    }

    Ok(())
}

fn grpc_corpus_message_refs<'a>(
    messages: &'a [CorpusMessageInput],
    ids: &'a [String],
) -> Vec<hl7v2::synthetic::corpus::CorpusMessageRef<'a>> {
    messages
        .iter()
        .zip(ids.iter())
        .map(|(message, id)| {
            hl7v2::synthetic::corpus::CorpusMessageRef::new(id.as_str(), message.message.as_slice())
        })
        .collect()
}

fn attach_grpc_profile_to_fingerprint(
    fingerprint: &mut hl7v2::synthetic::corpus::CorpusFingerprint,
    profile_yaml: &str,
    messages: &[CorpusMessageInput],
) -> Result<hl7v2::synthetic::corpus::CorpusFingerprintProfile, &'static str> {
    let (profile, metadata) = load_grpc_fingerprint_profile(profile_yaml)?;
    fingerprint.profile = Some(metadata.clone());
    fingerprint.validation_issue_code_counts =
        grpc_validation_issue_counts_for_loaded_profile(messages, &profile);
    Ok(metadata)
}

fn load_grpc_fingerprint_profile(
    profile_yaml: &str,
) -> Result<
    (
        hl7v2::Profile,
        hl7v2::synthetic::corpus::CorpusFingerprintProfile,
    ),
    &'static str,
> {
    let profile = hl7v2::load_profile_checked(profile_yaml)
        .map_err(|_error| crate::PROFILE_LOAD_SAFE_MESSAGE)?;
    let metadata = hl7v2::synthetic::corpus::CorpusFingerprintProfile {
        path: "<inline-profile>".to_string(),
        sha256: compute_sha256(profile_yaml),
        version: profile.version.clone(),
        message_structure: profile.message_structure.clone(),
    };
    Ok((profile, metadata))
}

fn grpc_validation_issue_counts_for_loaded_profile(
    messages: &[CorpusMessageInput],
    profile: &hl7v2::Profile,
) -> Vec<hl7v2::synthetic::corpus::CorpusCount> {
    let mut counts: BTreeMap<String, usize> = BTreeMap::new();

    for message in messages {
        let parsed = if hl7v2::is_mllp_framed(message.message.as_slice()) {
            hl7v2::parse_mllp(message.message.as_slice())
        } else {
            hl7v2::parse(message.message.as_slice())
        };
        let Ok(parsed) = parsed else {
            continue;
        };

        let issues = hl7v2::validate(&parsed, profile);
        let report = hl7v2::ValidationReport::from_issues(
            &parsed,
            Some(profile.message_structure.clone()),
            issues,
        );
        for issue in report.issues {
            let count = counts.entry(issue.code).or_insert(0);
            *count = count.saturating_add(1);
        }
    }

    counts
        .into_iter()
        .map(|(value, count)| hl7v2::synthetic::corpus::CorpusCount { value, count })
        .collect()
}

fn extract_grpc_metadata(msg: &RustMessage) -> MessageMetadata {
    MessageMetadata {
        message_type: joined_components(msg, "MSH.9").unwrap_or_else(|| "UNKNOWN".to_string()),
        version: hl7v2::get(msg, "MSH.12").unwrap_or("UNKNOWN").to_string(),
        control_id: hl7v2::get(msg, "MSH.10").unwrap_or("UNKNOWN").to_string(),
        sending_facility: hl7v2::get(msg, "MSH.4").unwrap_or("").to_string(),
        receiving_facility: hl7v2::get(msg, "MSH.6").unwrap_or("").to_string(),
    }
}

fn proto_corpus_summary_from_rust(
    summary: &hl7v2::synthetic::corpus::CorpusSummary,
) -> CorpusSummary {
    CorpusSummary {
        root: summary.root.clone(),
        file_count: usize_to_u32(summary.file_count),
        message_count: usize_to_u32(summary.message_count),
        parse_error_count: usize_to_u32(summary.parse_error_count),
        total_bytes: usize_to_u64(summary.total_bytes),
        message_types: summary
            .message_types
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        segments: summary
            .segments
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        field_presence: summary
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_from_rust)
            .collect(),
        parse_errors: summary
            .parse_errors
            .iter()
            .map(proto_corpus_parse_failure_from_rust)
            .collect(),
    }
}

fn proto_corpus_summary_v2_from_rust(
    summary: &hl7v2::synthetic::corpus::CorpusSummaryV2,
) -> CorpusSummaryV2 {
    CorpusSummaryV2 {
        schema_version: summary.schema_version.clone(),
        tool_name: summary.tool_name.clone(),
        tool_version: summary.tool_version.clone(),
        root: summary.summary.root.clone(),
        file_count: usize_to_u32(summary.summary.file_count),
        message_count: usize_to_u32(summary.summary.message_count),
        parse_error_count: usize_to_u32(summary.summary.parse_error_count),
        total_bytes: usize_to_u64(summary.summary.total_bytes),
        message_types: summary
            .summary
            .message_types
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        segments: summary
            .summary
            .segments
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        field_presence: summary
            .summary
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_from_rust)
            .collect(),
        parse_errors: summary
            .summary
            .parse_errors
            .iter()
            .map(proto_corpus_parse_failure_from_rust)
            .collect(),
    }
}

fn proto_corpus_count_from_rust(count: &hl7v2::synthetic::corpus::CorpusCount) -> CorpusCount {
    CorpusCount {
        value: count.value.clone(),
        count: usize_to_u32(count.count),
    }
}

fn proto_corpus_field_presence_from_rust(
    field: &hl7v2::synthetic::corpus::CorpusFieldPresence,
) -> CorpusFieldPresence {
    CorpusFieldPresence {
        path: field.path.clone(),
        message_count: usize_to_u32(field.message_count),
        occurrence_count: usize_to_u32(field.occurrence_count),
    }
}

fn proto_corpus_parse_failure_from_rust(
    failure: &hl7v2::synthetic::corpus::CorpusParseFailure,
) -> CorpusParseFailure {
    CorpusParseFailure {
        path: failure.path.clone(),
        error: failure.error.clone(),
    }
}

fn proto_corpus_fingerprint_from_rust(
    fingerprint: &hl7v2::synthetic::corpus::CorpusFingerprint,
) -> CorpusFingerprint {
    CorpusFingerprint {
        fingerprint_version: fingerprint.fingerprint_version.clone(),
        tool_version: fingerprint.tool_version.clone(),
        root: fingerprint.root.clone(),
        profile: fingerprint
            .profile
            .as_ref()
            .map(proto_corpus_fingerprint_profile_from_rust),
        file_count: usize_to_u32(fingerprint.file_count),
        message_count: usize_to_u32(fingerprint.message_count),
        parse_error_count: usize_to_u32(fingerprint.parse_error_count),
        message_type_counts: fingerprint
            .message_type_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        segment_counts: fingerprint
            .segment_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        field_presence: fingerprint
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_from_rust)
            .collect(),
        field_cardinality: fingerprint
            .field_cardinality
            .iter()
            .map(proto_corpus_field_cardinality_from_rust)
            .collect(),
        value_shape_stats: fingerprint
            .value_shape_stats
            .iter()
            .map(proto_corpus_value_shape_stats_from_rust)
            .collect(),
        validation_issue_code_counts: fingerprint
            .validation_issue_code_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
    }
}

fn proto_corpus_fingerprint_v2_from_rust(
    fingerprint: &hl7v2::synthetic::corpus::CorpusFingerprintV2,
) -> CorpusFingerprintV2 {
    let report = &fingerprint.fingerprint;
    CorpusFingerprintV2 {
        schema_version: fingerprint.schema_version.clone(),
        tool_name: fingerprint.tool_name.clone(),
        fingerprint_version: report.fingerprint_version.clone(),
        tool_version: report.tool_version.clone(),
        root: report.root.clone(),
        profile: report
            .profile
            .as_ref()
            .map(proto_corpus_fingerprint_profile_from_rust),
        file_count: usize_to_u32(report.file_count),
        message_count: usize_to_u32(report.message_count),
        parse_error_count: usize_to_u32(report.parse_error_count),
        message_type_counts: report
            .message_type_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        segment_counts: report
            .segment_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
        field_presence: report
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_from_rust)
            .collect(),
        field_cardinality: report
            .field_cardinality
            .iter()
            .map(proto_corpus_field_cardinality_from_rust)
            .collect(),
        value_shape_stats: report
            .value_shape_stats
            .iter()
            .map(proto_corpus_value_shape_stats_from_rust)
            .collect(),
        validation_issue_code_counts: report
            .validation_issue_code_counts
            .iter()
            .map(proto_corpus_count_from_rust)
            .collect(),
    }
}

fn proto_corpus_diff_report_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusDiffReport,
) -> CorpusDiffReport {
    CorpusDiffReport {
        diff_version: diff.diff_version.clone(),
        tool_version: diff.tool_version.clone(),
        before_root: diff.before_root.clone(),
        after_root: diff.after_root.clone(),
        profile: diff
            .profile
            .as_ref()
            .map(proto_corpus_fingerprint_profile_from_rust),
        file_count: Some(proto_corpus_total_diff_from_rust(&diff.file_count)),
        message_count: Some(proto_corpus_total_diff_from_rust(&diff.message_count)),
        parse_error_count: Some(proto_corpus_total_diff_from_rust(&diff.parse_error_count)),
        new_message_types: diff.new_message_types.clone(),
        removed_message_types: diff.removed_message_types.clone(),
        new_segments: diff.new_segments.clone(),
        removed_segments: diff.removed_segments.clone(),
        message_type_counts: diff
            .message_type_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
        segment_counts: diff
            .segment_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
        field_presence: diff
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_diff_from_rust)
            .collect(),
        field_cardinality: diff
            .field_cardinality
            .iter()
            .map(proto_corpus_field_cardinality_diff_from_rust)
            .collect(),
        value_shape_stats: diff
            .value_shape_stats
            .iter()
            .map(proto_corpus_value_shape_stats_diff_from_rust)
            .collect(),
        validation_issue_code_counts: diff
            .validation_issue_code_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
    }
}

fn proto_corpus_diff_report_v2_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusDiffReportV2,
) -> CorpusDiffReportV2 {
    let report = &diff.report;
    CorpusDiffReportV2 {
        schema_version: diff.schema_version.clone(),
        tool_name: diff.tool_name.clone(),
        diff_version: report.diff_version.clone(),
        tool_version: report.tool_version.clone(),
        before_root: report.before_root.clone(),
        after_root: report.after_root.clone(),
        profile: report
            .profile
            .as_ref()
            .map(proto_corpus_fingerprint_profile_from_rust),
        file_count: Some(proto_corpus_total_diff_from_rust(&report.file_count)),
        message_count: Some(proto_corpus_total_diff_from_rust(&report.message_count)),
        parse_error_count: Some(proto_corpus_total_diff_from_rust(&report.parse_error_count)),
        new_message_types: report.new_message_types.clone(),
        removed_message_types: report.removed_message_types.clone(),
        new_segments: report.new_segments.clone(),
        removed_segments: report.removed_segments.clone(),
        message_type_counts: report
            .message_type_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
        segment_counts: report
            .segment_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
        field_presence: report
            .field_presence
            .iter()
            .map(proto_corpus_field_presence_diff_from_rust)
            .collect(),
        field_cardinality: report
            .field_cardinality
            .iter()
            .map(proto_corpus_field_cardinality_diff_from_rust)
            .collect(),
        value_shape_stats: report
            .value_shape_stats
            .iter()
            .map(proto_corpus_value_shape_stats_diff_from_rust)
            .collect(),
        validation_issue_code_counts: report
            .validation_issue_code_counts
            .iter()
            .map(proto_corpus_count_diff_from_rust)
            .collect(),
    }
}

fn proto_corpus_fingerprint_profile_from_rust(
    profile: &hl7v2::synthetic::corpus::CorpusFingerprintProfile,
) -> CorpusFingerprintProfile {
    CorpusFingerprintProfile {
        path: profile.path.clone(),
        sha256: profile.sha256.clone(),
        version: profile.version.clone(),
        message_structure: profile.message_structure.clone(),
    }
}

fn proto_corpus_total_diff_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusTotalDiff,
) -> CorpusTotalDiff {
    CorpusTotalDiff {
        before: usize_to_u64(diff.before),
        after: usize_to_u64(diff.after),
        delta: i128_to_i64(diff.delta),
    }
}

fn proto_corpus_count_diff_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusCountDiff,
) -> CorpusCountDiff {
    CorpusCountDiff {
        value: diff.value.clone(),
        before: usize_to_u64(diff.before),
        after: usize_to_u64(diff.after),
        delta: i128_to_i64(diff.delta),
    }
}

fn proto_corpus_field_presence_diff_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusFieldPresenceDiff,
) -> CorpusFieldPresenceDiff {
    CorpusFieldPresenceDiff {
        path: diff.path.clone(),
        before_message_count: usize_to_u64(diff.before_message_count),
        after_message_count: usize_to_u64(diff.after_message_count),
        message_count_delta: i128_to_i64(diff.message_count_delta),
        before_occurrence_count: usize_to_u64(diff.before_occurrence_count),
        after_occurrence_count: usize_to_u64(diff.after_occurrence_count),
        occurrence_count_delta: i128_to_i64(diff.occurrence_count_delta),
    }
}

fn proto_corpus_field_cardinality_diff_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusFieldCardinalityDiff,
) -> CorpusFieldCardinalityDiff {
    CorpusFieldCardinalityDiff {
        path: diff.path.clone(),
        before_min_per_message: usize_to_u64(diff.before_min_per_message),
        after_min_per_message: usize_to_u64(diff.after_min_per_message),
        min_per_message_delta: i128_to_i64(diff.min_per_message_delta),
        before_max_per_message: usize_to_u64(diff.before_max_per_message),
        after_max_per_message: usize_to_u64(diff.after_max_per_message),
        max_per_message_delta: i128_to_i64(diff.max_per_message_delta),
        before_total_occurrences: usize_to_u64(diff.before_total_occurrences),
        after_total_occurrences: usize_to_u64(diff.after_total_occurrences),
        total_occurrences_delta: i128_to_i64(diff.total_occurrences_delta),
        before_message_count: usize_to_u64(diff.before_message_count),
        after_message_count: usize_to_u64(diff.after_message_count),
        message_count_delta: i128_to_i64(diff.message_count_delta),
    }
}

fn proto_corpus_value_shape_stats_diff_from_rust(
    diff: &hl7v2::synthetic::corpus::CorpusValueShapeStatsDiff,
) -> CorpusValueShapeStatsDiff {
    CorpusValueShapeStatsDiff {
        path: diff.path.clone(),
        coded_count: Some(proto_corpus_total_diff_from_rust(&diff.coded_count)),
        timestamp_count: Some(proto_corpus_total_diff_from_rust(&diff.timestamp_count)),
        numeric_count: Some(proto_corpus_total_diff_from_rust(&diff.numeric_count)),
        null_count: Some(proto_corpus_total_diff_from_rust(&diff.null_count)),
        text_count: Some(proto_corpus_total_diff_from_rust(&diff.text_count)),
    }
}

fn proto_corpus_field_cardinality_from_rust(
    field: &hl7v2::synthetic::corpus::CorpusFieldCardinality,
) -> CorpusFieldCardinality {
    CorpusFieldCardinality {
        path: field.path.clone(),
        min_per_message: usize_to_u32(field.min_per_message),
        max_per_message: usize_to_u32(field.max_per_message),
        total_occurrences: usize_to_u32(field.total_occurrences),
        message_count: usize_to_u32(field.message_count),
    }
}

fn proto_corpus_value_shape_stats_from_rust(
    stats: &hl7v2::synthetic::corpus::CorpusValueShapeStats,
) -> CorpusValueShapeStats {
    CorpusValueShapeStats {
        path: stats.path.clone(),
        coded_count: usize_to_u32(stats.coded_count),
        timestamp_count: usize_to_u32(stats.timestamp_count),
        numeric_count: usize_to_u32(stats.numeric_count),
        null_count: usize_to_u32(stats.null_count),
        text_count: usize_to_u32(stats.text_count),
    }
}

fn usize_to_u32(value: usize) -> u32 {
    u32::try_from(value).unwrap_or(u32::MAX)
}

fn usize_to_u64(value: usize) -> u64 {
    u64::try_from(value).unwrap_or(u64::MAX)
}

fn i128_to_i64(value: i128) -> i64 {
    i64::try_from(value).unwrap_or(if value.is_negative() {
        i64::MIN
    } else {
        i64::MAX
    })
}

fn proto_validation_report_from_rust(report: &hl7v2::ValidationReport) -> ValidationReport {
    ValidationReport {
        valid: report.valid,
        message_type: report.message_type.clone(),
        profile: report.profile.clone(),
        segment_count: report.segment_count as i32,
        issue_count: report.issue_count as i32,
        issues: report
            .issues
            .iter()
            .map(proto_validation_report_issue_from_rust)
            .collect(),
    }
}

fn proto_validation_report_v2_from_rust(report: &hl7v2::ValidationReportV2) -> ValidationReportV2 {
    ValidationReportV2 {
        schema_version: report.schema_version.clone(),
        tool_name: report.tool_name.clone(),
        tool_version: report.tool_version.clone(),
        valid: report.valid,
        message_type: report.message_type.clone(),
        profile: report.profile.clone(),
        profile_identity: report
            .profile_identity
            .as_ref()
            .map(proto_validation_report_profile_identity_from_rust),
        segment_count: report.segment_count as i32,
        issue_count: report.issue_count as i32,
        issues: report
            .issues
            .iter()
            .map(proto_validation_report_issue_from_rust)
            .collect(),
    }
}

fn proto_validation_report_profile_identity_from_rust(
    profile: &hl7v2::ValidationReportProfileIdentity,
) -> ValidationReportProfileIdentity {
    ValidationReportProfileIdentity {
        label: profile.label.clone(),
        message_structure: profile.message_structure.clone(),
        version: profile.version.clone(),
        sha256: profile.sha256.clone(),
    }
}

fn proto_validation_report_issue_from_rust(
    issue: &hl7v2::ValidationReportIssue,
) -> ValidationReportIssue {
    ValidationReportIssue {
        code: issue.code.clone(),
        severity: issue.severity.as_str().to_string(),
        path: issue.path.clone(),
        rule_id: issue.rule_id.clone(),
        message: issue.message.clone(),
        segment_index: issue.segment_index.map(|value| value as u32),
        field_index: issue.field_index.map(|value| value as u32),
    }
}

fn proto_profile_lint_report_from_rust(report: &hl7v2::ProfileLintReport) -> ProfileLintReport {
    ProfileLintReport {
        valid: report.valid,
        error_count: usize_to_u32(report.error_count),
        warning_count: usize_to_u32(report.warning_count),
        issue_count: usize_to_u32(report.issue_count),
        issues: report
            .issues
            .iter()
            .map(proto_profile_lint_issue_from_rust)
            .collect(),
    }
}

fn proto_profile_lint_report_v2_from_rust(
    report: &hl7v2::ProfileLintReportV2,
) -> ProfileLintReportV2 {
    ProfileLintReportV2 {
        schema_version: report.schema_version.clone(),
        tool_name: report.tool_name.clone(),
        tool_version: report.tool_version.clone(),
        valid: report.report.valid,
        error_count: usize_to_u32(report.report.error_count),
        warning_count: usize_to_u32(report.report.warning_count),
        issue_count: usize_to_u32(report.report.issue_count),
        issues: report
            .report
            .issues
            .iter()
            .map(proto_profile_lint_issue_from_rust)
            .collect(),
    }
}

fn proto_profile_lint_issue_from_rust(issue: &hl7v2::ProfileLintIssue) -> ProfileLintIssue {
    ProfileLintIssue {
        code: issue.code.clone(),
        severity: issue.severity.as_str().to_string(),
        path: issue.path.clone(),
        message: issue.message.clone(),
    }
}

fn proto_profile_explain_report_from_rust(
    report: &hl7v2::ProfileExplainReport,
) -> ProfileExplainReport {
    ProfileExplainReport {
        profile: report.profile.clone(),
        profile_sha256: report.profile_sha256.clone(),
        message_structure: report.message_structure.clone(),
        version: report.version.clone(),
        message_type: report.message_type.clone(),
        parent: report.parent.clone(),
        summary: Some(proto_profile_explain_summary_from_rust(&report.summary)),
        segments: report
            .segments
            .iter()
            .map(proto_profile_explain_segment_from_rust)
            .collect(),
        required_fields: report
            .required_fields
            .iter()
            .map(proto_profile_explain_required_field_from_rust)
            .collect(),
        field_constraints: report
            .field_constraints
            .iter()
            .map(proto_profile_explain_constraint_from_rust)
            .collect(),
        length_rules: report
            .length_rules
            .iter()
            .map(proto_profile_explain_length_rule_from_rust)
            .collect(),
        datatype_rules: report
            .datatype_rules
            .iter()
            .map(proto_profile_explain_datatype_rule_from_rust)
            .collect(),
        value_sets: report
            .value_sets
            .iter()
            .map(proto_profile_explain_value_set_from_rust)
            .collect(),
        rules: Some(proto_profile_explain_rules_from_rust(&report.rules)),
        hl7_tables: report
            .hl7_tables
            .iter()
            .map(proto_profile_explain_table_from_rust)
            .collect(),
        table_precedence: report.table_precedence.clone(),
        expression_guardrails: Some(proto_profile_explain_guardrails_from_rust(
            &report.expression_guardrails,
        )),
        lint: Some(proto_profile_explain_lint_summary_from_rust(&report.lint)),
    }
}

fn proto_profile_explain_report_v2_from_rust(
    report: &hl7v2::ProfileExplainReportV2,
) -> ProfileExplainReportV2 {
    ProfileExplainReportV2 {
        schema_version: report.schema_version.clone(),
        tool_name: report.tool_name.clone(),
        tool_version: report.tool_version.clone(),
        report: Some(proto_profile_explain_report_from_rust(&report.report)),
    }
}

fn proto_profile_explain_summary_from_rust(
    summary: &hl7v2::ProfileExplainSummary,
) -> ProfileExplainSummary {
    ProfileExplainSummary {
        segment_count: usize_to_u32(summary.segment_count),
        required_field_count: usize_to_u32(summary.required_field_count),
        field_constraint_count: usize_to_u32(summary.field_constraint_count),
        length_rule_count: usize_to_u32(summary.length_rule_count),
        datatype_rule_count: usize_to_u32(summary.datatype_rule_count),
        advanced_datatype_rule_count: usize_to_u32(summary.advanced_datatype_rule_count),
        value_set_count: usize_to_u32(summary.value_set_count),
        cross_field_rule_count: usize_to_u32(summary.cross_field_rule_count),
        temporal_rule_count: usize_to_u32(summary.temporal_rule_count),
        contextual_rule_count: usize_to_u32(summary.contextual_rule_count),
        custom_rule_count: usize_to_u32(summary.custom_rule_count),
        hl7_table_count: usize_to_u32(summary.hl7_table_count),
    }
}

fn proto_profile_explain_segment_from_rust(
    segment: &hl7v2::ProfileExplainSegment,
) -> ProfileExplainSegment {
    ProfileExplainSegment {
        id: segment.id.clone(),
    }
}

fn proto_profile_explain_required_field_from_rust(
    field: &hl7v2::ProfileExplainRequiredField,
) -> ProfileExplainRequiredField {
    ProfileExplainRequiredField {
        path: field.path.clone(),
        conditional: field.conditional,
    }
}

fn proto_profile_explain_constraint_from_rust(
    constraint: &hl7v2::ProfileExplainConstraint,
) -> ProfileExplainConstraint {
    ProfileExplainConstraint {
        path: constraint.path.clone(),
        required: constraint.required,
        conditional: constraint.conditional,
        component_min: constraint.component_min.map(usize_to_u32),
        component_max: constraint.component_max.map(usize_to_u32),
        allowed_value_count: usize_to_u32(constraint.allowed_value_count),
        allowed_values: constraint.allowed_values.clone(),
        pattern: constraint.pattern.clone(),
    }
}

fn proto_profile_explain_length_rule_from_rust(
    rule: &hl7v2::ProfileExplainLengthRule,
) -> ProfileExplainLengthRule {
    ProfileExplainLengthRule {
        path: rule.path.clone(),
        max: rule.max.map(usize_to_u32),
        policy: rule.policy.clone(),
    }
}

fn proto_profile_explain_datatype_rule_from_rust(
    rule: &hl7v2::ProfileExplainDatatypeRule,
) -> ProfileExplainDatatypeRule {
    ProfileExplainDatatypeRule {
        path: rule.path.clone(),
        datatype: rule.datatype.clone(),
        kind: rule.kind.clone(),
        pattern: rule.pattern.clone(),
        min_length: rule.min_length.map(usize_to_u32),
        max_length: rule.max_length.map(usize_to_u32),
        format: rule.format.clone(),
        checksum: rule.checksum.clone(),
    }
}

fn proto_profile_explain_value_set_from_rust(
    value_set: &hl7v2::ProfileExplainValueSet,
) -> ProfileExplainValueSet {
    ProfileExplainValueSet {
        name: value_set.name.clone(),
        path: value_set.path.clone(),
        source: value_set.source.clone(),
        inline_code_count: usize_to_u32(value_set.inline_code_count),
        table_code_count: usize_to_u32(value_set.table_code_count),
    }
}

fn proto_profile_explain_rules_from_rust(
    rules: &hl7v2::ProfileExplainRules,
) -> ProfileExplainRules {
    ProfileExplainRules {
        cross_field: rules
            .cross_field
            .iter()
            .map(proto_profile_explain_rule_from_rust)
            .collect(),
        temporal: rules
            .temporal
            .iter()
            .map(proto_profile_explain_rule_from_rust)
            .collect(),
        contextual: rules
            .contextual
            .iter()
            .map(proto_profile_explain_rule_from_rust)
            .collect(),
        custom: rules
            .custom
            .iter()
            .map(proto_profile_explain_rule_from_rust)
            .collect(),
    }
}

fn proto_profile_explain_rule_from_rust(rule: &hl7v2::ProfileExplainRule) -> ProfileExplainRule {
    ProfileExplainRule {
        id: rule.id.clone(),
        description: rule.description.clone(),
    }
}

fn proto_profile_explain_table_from_rust(
    table: &hl7v2::ProfileExplainTable,
) -> ProfileExplainTable {
    ProfileExplainTable {
        id: table.id.clone(),
        name: table.name.clone(),
        version: table.version.clone(),
        code_count: usize_to_u32(table.code_count),
    }
}

fn proto_profile_explain_guardrails_from_rust(
    guardrails: &hl7v2::ProfileExplainExpressionGuardrails,
) -> ProfileExplainExpressionGuardrails {
    ProfileExplainExpressionGuardrails {
        max_depth: guardrails.max_depth.map(usize_to_u32),
        max_length: guardrails.max_length.map(usize_to_u32),
        allow_custom_scripts: guardrails.allow_custom_scripts,
    }
}

fn proto_profile_explain_lint_summary_from_rust(
    lint: &hl7v2::ProfileExplainLintSummary,
) -> ProfileExplainLintSummary {
    ProfileExplainLintSummary {
        valid: lint.valid,
        error_count: usize_to_u32(lint.error_count),
        warning_count: usize_to_u32(lint.warning_count),
        issue_count: usize_to_u32(lint.issue_count),
        ignored_or_unsupported: lint
            .ignored_or_unsupported
            .iter()
            .map(proto_profile_lint_issue_from_rust)
            .collect(),
    }
}

fn grpc_profile_test_report_from_inline_fixtures(
    fixtures: &[ProfileTestFixture],
    profile: &hl7v2::Profile,
) -> Result<hl7v2::ProfileTestReport, String> {
    if fixtures.is_empty() {
        return Err("fixtures must contain at least one fixture".to_string());
    }

    let cases = fixtures
        .iter()
        .enumerate()
        .map(|(index, fixture)| grpc_profile_test_case_from_inline_fixture(index, fixture, profile))
        .collect::<Result<Vec<_>, _>>()?;
    let passed_count = cases.iter().filter(|case| case.passed).count();
    let case_count = cases.len();
    let failed_count = case_count.saturating_sub(passed_count);

    Ok(hl7v2::ProfileTestReport {
        profile: "<inline-profile>".to_string(),
        fixtures: "<inline-fixtures>".to_string(),
        valid: failed_count == 0,
        case_count,
        passed_count,
        failed_count,
        cases,
    })
}

fn grpc_profile_test_case_from_inline_fixture(
    index: usize,
    fixture: &ProfileTestFixture,
    profile: &hl7v2::Profile,
) -> Result<hl7v2::ProfileTestCaseReport, String> {
    let name = grpc_profile_test_fixture_label(index, fixture);
    let expectation = grpc_profile_test_fixture_expectation(fixture.expectation)?;
    let message_bytes = if fixture.mllp_framed {
        match hl7v2::unwrap_mllp(&fixture.message) {
            Ok(message) => message,
            Err(err) => {
                return Ok(hl7v2::ProfileTestCaseReport {
                    name: name.clone(),
                    path: name,
                    expectation,
                    passed: false,
                    message: format!("fixture did not unwrap as MLLP: {err}"),
                    validation_report: None,
                    expected_report: None,
                });
            }
        }
    } else {
        fixture.message.as_slice()
    };

    let message = match rust_parse(message_bytes) {
        Ok(message) => message,
        Err(err) => {
            return Ok(hl7v2::ProfileTestCaseReport {
                name: name.clone(),
                path: name,
                expectation,
                passed: false,
                message: format!("fixture did not parse as HL7: {err}"),
                validation_report: None,
                expected_report: None,
            });
        }
    };

    let issues = hl7v2::validate(&message, profile);
    let validation_report = hl7v2::ValidationReport::from_issues(
        &message,
        Some("<inline-profile>".to_string()),
        issues,
    );
    let expected_valid = expectation == hl7v2::ProfileFixtureExpectation::Valid;
    let mut passed = validation_report.valid == expected_valid;
    let mut case_message = if passed {
        format!(
            "expected {} and report was {}",
            expectation.as_str(),
            if validation_report.valid {
                "valid"
            } else {
                "invalid"
            }
        )
    } else {
        format!(
            "expected {} but report was {}",
            expectation.as_str(),
            if validation_report.valid {
                "valid"
            } else {
                "invalid"
            }
        )
    };

    let expected_report = fixture.expected_report_json.as_deref().map(|expected| {
        grpc_compare_inline_expected_report_json(&name, expected, &validation_report)
    });
    if let Some(comparison) = &expected_report {
        if comparison.matched {
            case_message.push_str("; expected report matched");
        } else {
            passed = false;
            let detail = comparison
                .message
                .as_deref()
                .unwrap_or("expected report did not match");
            case_message.push_str(&format!("; {detail}"));
        }
    }

    Ok(hl7v2::ProfileTestCaseReport {
        name: name.clone(),
        path: name,
        expectation,
        passed,
        message: case_message,
        validation_report: Some(validation_report),
        expected_report,
    })
}

fn grpc_profile_test_fixture_label(index: usize, fixture: &ProfileTestFixture) -> String {
    let trimmed = fixture.name.trim();
    if trimmed.is_empty() {
        format!("fixture-{}", index.saturating_add(1))
    } else {
        trimmed.to_string()
    }
}

fn grpc_profile_test_fixture_expectation(
    value: i32,
) -> Result<hl7v2::ProfileFixtureExpectation, String> {
    match profile_test_fixture::Expectation::try_from(value)
        .unwrap_or(profile_test_fixture::Expectation::Unspecified)
    {
        profile_test_fixture::Expectation::Valid => Ok(hl7v2::ProfileFixtureExpectation::Valid),
        profile_test_fixture::Expectation::Invalid => Ok(hl7v2::ProfileFixtureExpectation::Invalid),
        profile_test_fixture::Expectation::Unspecified => {
            Err("fixture expectation must be valid or invalid".to_string())
        }
    }
}

fn grpc_compare_inline_expected_report_json(
    fixture_name: &str,
    expected_json: &str,
    actual_report: &hl7v2::ValidationReport,
) -> hl7v2::ExpectedReportComparison {
    let path = format!("{fixture_name}.expected-report.json");
    let expected = match serde_json::from_str::<serde_json::Value>(expected_json) {
        Ok(expected) => expected,
        Err(err) => {
            return hl7v2::ExpectedReportComparison {
                path,
                matched: false,
                message: Some(format!("expected report is not valid JSON: {err}")),
            };
        }
    };
    let actual = match serde_json::to_value(actual_report) {
        Ok(actual) => actual,
        Err(err) => {
            return hl7v2::ExpectedReportComparison {
                path,
                matched: false,
                message: Some(format!("actual report could not be serialized: {err}")),
            };
        }
    };

    match grpc_json_subset_matches(&expected, &actual, "$") {
        Ok(()) => hl7v2::ExpectedReportComparison {
            path,
            matched: true,
            message: None,
        },
        Err(message) => hl7v2::ExpectedReportComparison {
            path,
            matched: false,
            message: Some(message),
        },
    }
}

fn grpc_json_subset_matches(
    expected: &serde_json::Value,
    actual: &serde_json::Value,
    path: &str,
) -> Result<(), String> {
    match (expected, actual) {
        (serde_json::Value::Object(expected), serde_json::Value::Object(actual)) => {
            for (key, expected_value) in expected {
                let actual_value = actual
                    .get(key)
                    .ok_or_else(|| format!("{path}.{key} was missing from actual report"))?;
                grpc_json_subset_matches(expected_value, actual_value, &format!("{path}.{key}"))?;
            }
            Ok(())
        }
        (serde_json::Value::Array(expected), serde_json::Value::Array(actual)) => {
            for (index, expected_value) in expected.iter().enumerate() {
                let matched = actual.iter().any(|actual_value| {
                    grpc_json_subset_matches(
                        expected_value,
                        actual_value,
                        &format!("{path}[{index}]"),
                    )
                    .is_ok()
                });
                if !matched {
                    return Err(format!(
                        "{path}[{index}] did not match any actual report item"
                    ));
                }
            }
            Ok(())
        }
        _ if expected == actual => Ok(()),
        _ => Err(format!(
            "{path} expected {expected} but actual report had {actual}"
        )),
    }
}

fn proto_profile_test_report_from_rust(report: &hl7v2::ProfileTestReport) -> ProfileTestReport {
    ProfileTestReport {
        profile: report.profile.clone(),
        fixtures: report.fixtures.clone(),
        valid: report.valid,
        case_count: usize_to_u32(report.case_count),
        passed_count: usize_to_u32(report.passed_count),
        failed_count: usize_to_u32(report.failed_count),
        cases: report
            .cases
            .iter()
            .map(proto_profile_test_case_report_from_rust)
            .collect(),
    }
}

fn proto_profile_test_report_v2_from_rust(
    report: &hl7v2::ProfileTestReportV2,
) -> ProfileTestReportV2 {
    ProfileTestReportV2 {
        schema_version: report.schema_version.clone(),
        tool_name: report.tool_name.clone(),
        tool_version: report.tool_version.clone(),
        report: Some(proto_profile_test_report_from_rust(&report.report)),
    }
}

fn proto_profile_test_case_report_from_rust(
    case: &hl7v2::ProfileTestCaseReport,
) -> ProfileTestCaseReport {
    ProfileTestCaseReport {
        name: case.name.clone(),
        path: case.path.clone(),
        expectation: case.expectation.as_str().to_string(),
        passed: case.passed,
        message: case.message.clone(),
        validation_report: case
            .validation_report
            .as_ref()
            .map(proto_validation_report_from_rust),
        expected_report: case
            .expected_report
            .as_ref()
            .map(proto_expected_report_comparison_from_rust),
    }
}

fn proto_expected_report_comparison_from_rust(
    comparison: &hl7v2::ExpectedReportComparison,
) -> ExpectedReportComparison {
    ExpectedReportComparison {
        path: comparison.path.clone(),
        matched: comparison.matched,
        message: comparison.message.clone(),
    }
}

fn proto_redaction_receipt_from_server(receipt: &ServerRedactionReceipt) -> RedactionReceipt {
    RedactionReceipt {
        phi_removed: receipt.phi_removed,
        hash_algorithm: receipt.hash_algorithm.clone(),
        actions: receipt
            .actions
            .iter()
            .map(proto_redaction_action_receipt_from_server)
            .collect(),
    }
}

fn proto_redaction_receipt_v2_from_server(
    receipt: &ServerRedactionReceipt,
    tool_name: &str,
) -> RedactionReceiptV2 {
    RedactionReceiptV2 {
        schema_version: "2".to_string(),
        tool_name: tool_name.to_string(),
        tool_version: env!("CARGO_PKG_VERSION").to_string(),
        phi_removed: receipt.phi_removed,
        hash_algorithm: receipt.hash_algorithm.clone(),
        actions: receipt
            .actions
            .iter()
            .map(proto_redaction_action_receipt_from_server)
            .collect(),
    }
}

fn proto_quarantine_output_summary_from_server(
    summary: &ServerQuarantineOutputSummary,
) -> QuarantineOutputSummary {
    QuarantineOutputSummary {
        quarantine_version: summary.quarantine_version.clone(),
        output_dir: summary.output_dir.clone(),
        reason: server_quarantine_reason_name(summary.reason).to_string(),
        validation_issue_count: usize_to_u32(summary.validation_issue_count),
        artifacts: summary.artifacts.clone(),
    }
}

fn proto_quarantine_output_summary_v2_from_server(
    summary: &crate::models::QuarantineOutputSummaryV2,
) -> QuarantineOutputSummaryV2 {
    QuarantineOutputSummaryV2 {
        schema_version: summary.schema_version.clone(),
        tool_name: summary.tool_name.clone(),
        tool_version: summary.tool_version.clone(),
        quarantine_version: summary.summary.quarantine_version.clone(),
        output_dir: summary.summary.output_dir.clone(),
        reason: server_quarantine_reason_name(summary.summary.reason).to_string(),
        validation_issue_count: usize_to_u32(summary.summary.validation_issue_count),
        artifacts: summary.summary.artifacts.clone(),
    }
}

fn proto_evidence_bundle_summary_from_server(
    summary: &ServerEvidenceBundleSummary,
) -> EvidenceBundleSummary {
    EvidenceBundleSummary {
        bundle_version: summary.bundle_version.clone(),
        output_dir: summary.output_dir.clone(),
        message_type: summary.message_type.clone(),
        validation_valid: summary.validation_valid,
        validation_issue_count: usize_to_u32(summary.validation_issue_count),
        redaction_phi_removed: summary.redaction_phi_removed,
        artifacts: summary.artifacts.clone(),
    }
}

fn proto_evidence_replay_report_from_rust(
    report: &hl7v2::evidence::EvidenceReplayReport,
) -> EvidenceReplayReport {
    EvidenceReplayReport {
        replay_version: report.replay_version.clone(),
        bundle_version: report.bundle_version.clone(),
        tool_name: report.tool_name.clone(),
        tool_version: report.tool_version.clone(),
        message_type: report.message_type.clone(),
        reproduced: report.reproduced,
        validation_valid: report.validation_valid,
        validation_issue_count: report.validation_issue_count.map(usize_to_u32),
        checks: report
            .checks
            .iter()
            .map(proto_evidence_replay_check_from_rust)
            .collect(),
        validation_report: report
            .validation_report
            .as_ref()
            .map(proto_validation_report_from_rust),
    }
}

fn proto_evidence_replay_report_v2_from_rust(
    report: &hl7v2::evidence::EvidenceReplayReportV2,
) -> EvidenceReplayReportV2 {
    EvidenceReplayReportV2 {
        schema_version: report.schema_version.clone(),
        report: Some(proto_evidence_replay_report_from_rust(&report.report)),
    }
}

fn proto_evidence_replay_check_from_rust(
    check: &hl7v2::evidence::EvidenceReplayCheck,
) -> EvidenceReplayCheck {
    EvidenceReplayCheck {
        name: check.name.clone(),
        status: match check.status {
            hl7v2::evidence::EvidenceReplayCheckStatus::Pass => {
                evidence_replay_check::Status::Pass as i32
            }
            hl7v2::evidence::EvidenceReplayCheckStatus::Fail => {
                evidence_replay_check::Status::Fail as i32
            }
        },
        message: check.message.clone(),
    }
}

fn grpc_evidence_bundle_error(error: crate::evidence::EvidenceBundleError) -> Status {
    match error {
        crate::evidence::EvidenceBundleError::InvalidRequest(message) => {
            Status::invalid_argument(message)
        }
        crate::evidence::EvidenceBundleError::Conflict(message) => Status::already_exists(message),
        crate::evidence::EvidenceBundleError::Io(message) => Status::unavailable(message),
    }
}

fn grpc_quarantine_output_status(error: GrpcQuarantineOutputError) -> Status {
    match error {
        GrpcQuarantineOutputError::MissingRoot => Status::failed_precondition(
            "server quarantine output is enabled but no path is configured",
        ),
        GrpcQuarantineOutputError::Write(error) => grpc_quarantine_evidence_error(error),
    }
}

fn grpc_quarantine_evidence_error(error: crate::evidence::EvidenceBundleError) -> Status {
    match error {
        crate::evidence::EvidenceBundleError::InvalidRequest(message) => {
            Status::invalid_argument(message)
        }
        crate::evidence::EvidenceBundleError::Conflict(message) => Status::already_exists(message),
        crate::evidence::EvidenceBundleError::Io(message) => Status::unavailable(message),
    }
}

fn proto_redaction_action_receipt_from_server(
    action: &ServerRedactionActionReceipt,
) -> RedactionActionReceipt {
    RedactionActionReceipt {
        path: action.path.clone(),
        action: server_redaction_action_name(action.action).to_string(),
        reason: action.reason.clone(),
        matched_count: u32::try_from(action.matched_count).unwrap_or(u32::MAX),
        optional: action.optional,
        status: server_redaction_action_status_name(action.status).to_string(),
    }
}

fn server_redaction_action_name(action: ServerRedactionAction) -> &'static str {
    match action {
        ServerRedactionAction::Hash => "hash",
        ServerRedactionAction::Drop => "drop",
        ServerRedactionAction::Retain => "retain",
    }
}

fn server_redaction_action_status_name(status: ServerRedactionActionStatus) -> &'static str {
    match status {
        ServerRedactionActionStatus::Applied => "applied",
        ServerRedactionActionStatus::Retained => "retained",
        ServerRedactionActionStatus::NotFound => "not_found",
    }
}

fn server_quarantine_reason_name(reason: ServerQuarantineReason) -> &'static str {
    match reason {
        ServerQuarantineReason::ValidationError => "validation_error",
    }
}

fn joined_components(msg: &RustMessage, field_path: &str) -> Option<String> {
    let mut components = Vec::new();

    for component in 1.. {
        let path = format!("{field_path}.{component}");
        match hl7v2::get(msg, &path) {
            Some(value) if !value.is_empty() => components.push(value.to_string()),
            _ if component == 1 => return None,
            _ => break,
        }
    }

    Some(components.join("^"))
}

impl From<RustMessage> for proto::Message {
    fn from(msg: RustMessage) -> Self {
        proto::Message {
            delimiters: Some(proto::Delimiters {
                field: msg.delims.field.to_string(),
                component: msg.delims.comp.to_string(),
                repetition: msg.delims.rep.to_string(),
                escape: msg.delims.esc.to_string(),
                subcomponent: msg.delims.sub.to_string(),
            }),
            segments: msg.segments.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<RustSegment> for proto::Segment {
    fn from(seg: RustSegment) -> Self {
        proto::Segment {
            id: String::from_utf8_lossy(&seg.id).to_string(),
            fields: seg.fields.into_iter().map(Into::into).collect(),
            sequence: 0,
        }
    }
}

impl From<RustField> for proto::Field {
    fn from(f: RustField) -> Self {
        let presence = if f.reps.is_empty() {
            proto::field::Presence::Missing as i32
        } else {
            proto::field::Presence::Value as i32
        };

        proto::Field {
            presence,
            value: f.first_text().map(String::from),
            repetitions: f.reps.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<RustRep> for proto::Repetition {
    fn from(r: RustRep) -> Self {
        proto::Repetition {
            value: r.first_text().map(String::from),
            components: r.comps.into_iter().map(Into::into).collect(),
        }
    }
}

impl From<RustComp> for proto::Component {
    fn from(c: RustComp) -> Self {
        proto::Component {
            value: c.first_text().map(String::from),
            subcomponents: c
                .subs
                .into_iter()
                .filter_map(|a| match a {
                    hl7v2::Atom::Text(t) => Some(t),
                    _ => None,
                })
                .collect(),
        }
    }
}
