//! Parsers for top-level DSL declarations and metadata blocks.

use super::*;
use crate::ast::{
    AgreementProfileDeclaration, ChildEffectAggregation, ChildEffectAggregationPolicy,
    EffectAuthorityClass, EffectInterfaceDeclaration, EffectOperationDeclaration,
    ExecutionProfileDeclaration, GuestRuntimeDeclaration, OperationAgreementAttachment,
    OperationDeclaration, OperationParameterDeclaration, ProgressAttachment, RegionDeclaration,
    RoleSetDeclaration, TheoremPackDeclaration, TopologyDeclaration, TypeConstructorDeclaration,
    TypeDeclaration,
};

pub(super) fn enforce_same_line_equals(
    src: &str,
    span: pest::Span<'_>,
    input: &str,
    kind: &str,
) -> std::result::Result<(), ParseError> {
    if let Some(eq_index) = src.find('=') {
        if src[..eq_index].contains('\n') {
            return Err(ParseError::Syntax {
                span: ErrorSpan::from_pest_span(span, input),
                message: format!("{kind} must keep `=` on the same line as the declaration head"),
            });
        }
    }
    Ok(())
}

pub(super) fn parse_protocol_uses(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
    parse_declared_name_list(pair)
}

pub(super) fn parse_protocol_profiles(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
    parse_declared_name_list(pair)
}

fn parse_declared_name_list(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
    pair.into_inner()
        .filter(|p| p.as_rule() == Rule::ident)
        .map(|p| p.as_str().to_string())
        .collect()
}

pub(super) fn parse_module_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<String, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    if let Some(ident) = inner.next() {
        if ident.as_rule() == Rule::ident {
            return Ok(ident.as_str().to_string());
        }
    }
    Err(ParseError::Syntax {
        span: ErrorSpan::from_pest_span(span, input),
        message: "Invalid module declaration".to_string(),
    })
}

/// Parse a proof-bundle declaration from the AST.
pub(super) fn parse_proof_bundle_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<TheoremPackDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let Some(name_pair) = inner.next() else {
        return Err(ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "Invalid proof_bundle declaration".to_string(),
        });
    };
    if name_pair.as_rule() != Rule::ident {
        return Err(ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "Invalid proof_bundle name".to_string(),
        });
    }

    let mut capabilities = Vec::new();
    let mut version = None;
    let mut issuer = None;
    let mut constraints = Vec::new();

    for item in inner {
        match item.as_rule() {
            Rule::proof_bundle_meta => {
                let Some(meta) = item.into_inner().next() else {
                    continue;
                };
                match meta.as_rule() {
                    Rule::proof_bundle_version => {
                        let value = meta.into_inner().next().ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "proof_bundle version is missing value".to_string(),
                        })?;
                        version = Some(parse_quoted_string(value.as_str()).map_err(|message| {
                            ParseError::Syntax {
                                span: ErrorSpan::from_pest_span(span, input),
                                message,
                            }
                        })?);
                    }
                    Rule::proof_bundle_issuer => {
                        let value = meta.into_inner().next().ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "proof_bundle issuer is missing value".to_string(),
                        })?;
                        issuer = Some(parse_quoted_string(value.as_str()).map_err(|message| {
                            ParseError::Syntax {
                                span: ErrorSpan::from_pest_span(span, input),
                                message,
                            }
                        })?);
                    }
                    Rule::proof_bundle_constraint => {
                        let value = meta.into_inner().next().ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "proof_bundle constraint is missing value".to_string(),
                        })?;
                        constraints.push(parse_quoted_string(value.as_str()).map_err(
                            |message| ParseError::Syntax {
                                span: ErrorSpan::from_pest_span(span, input),
                                message,
                            },
                        )?);
                    }
                    _ => {}
                }
            }
            Rule::proof_bundle_requires => {
                for requires_item in item.into_inner() {
                    if requires_item.as_rule() == Rule::capability_list {
                        for cap in requires_item.into_inner() {
                            if cap.as_rule() == Rule::ident {
                                capabilities.push(cap.as_str().to_string());
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }

    Ok(TheoremPackDeclaration {
        name: name_pair.as_str().to_string(),
        capabilities,
        version,
        issuer,
        constraints,
    })
}

pub(super) fn parse_profile_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<ExecutionProfileDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "profile declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let mut fairness = None;
    let mut admissibility = None;
    let mut escalation_window = None;

    for item in inner {
        if item.as_rule() != Rule::profile_meta {
            continue;
        }
        let Some(meta) = item.into_inner().next() else {
            continue;
        };
        let rule = meta.as_rule();
        let value = meta
            .into_inner()
            .next()
            .ok_or_else(|| ParseError::Syntax {
                span: ErrorSpan::from_pest_span(span, input),
                message: "profile metadata is missing value".to_string(),
            })?
            .as_str()
            .to_string();
        match rule {
            Rule::profile_fairness => fairness = Some(value),
            Rule::profile_admissibility => admissibility = Some(value),
            Rule::profile_escalation_window => escalation_window = Some(value),
            _ => {}
        }
    }

    Ok(ExecutionProfileDeclaration {
        name,
        fairness,
        admissibility,
        escalation_window,
    })
}

pub(super) fn parse_agreement_profile_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<AgreementProfileDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();

    let mut visibility = None;
    let mut rule_name = None;
    let mut usable_at = None;
    let mut finalized_at = None;
    let mut evidence = None;

    for item in inner {
        if item.as_rule() != Rule::agreement_profile_body {
            continue;
        }
        for meta_item in item.into_inner() {
            if meta_item.as_rule() != Rule::agreement_profile_meta {
                continue;
            }
            let Some(meta) = meta_item.into_inner().next() else {
                continue;
            };
            let rule = meta.as_rule();
            let value = meta
                .into_inner()
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "agreement_profile metadata is missing value".to_string(),
                })?
                .as_str()
                .to_string();
            match rule {
                Rule::agreement_profile_visibility => visibility = Some(value),
                Rule::agreement_profile_rule => rule_name = Some(value),
                Rule::agreement_profile_usable_at => usable_at = Some(value),
                Rule::agreement_profile_finalized_at => finalized_at = Some(value),
                Rule::agreement_profile_evidence => evidence = Some(value),
                _ => {}
            }
        }
    }

    Ok(AgreementProfileDeclaration {
        name,
        visibility: visibility.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing visibility".to_string(),
        })?,
        rule: rule_name.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing rule".to_string(),
        })?,
        usable_at: usable_at.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing usable_at".to_string(),
        })?,
        finalized_at: finalized_at.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing finalized_at".to_string(),
        })?,
        evidence: evidence.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "agreement_profile declaration is missing evidence".to_string(),
        })?,
    })
}

fn parse_quoted_string(value: &str) -> Result<String, String> {
    if value.starts_with('\"') && value.ends_with('\"') && value.len() >= 2 {
        Ok(value[1..value.len() - 1].to_string())
    } else {
        Err("expected quoted string literal".to_string())
    }
}

/// Parse protocol-level required proof bundles.
pub(super) fn parse_protocol_requires(pair: pest::iterators::Pair<Rule>) -> Vec<String> {
    pair.into_inner()
        .filter(|p| p.as_rule() == Rule::ident)
        .map(|p| p.as_str().to_string())
        .collect()
}

pub(super) fn parse_role_set_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<RoleSetDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "role_set is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let expr = inner.next().ok_or_else(|| ParseError::Syntax {
        span: ErrorSpan::from_pest_span(span, input),
        message: "role_set is missing expression".to_string(),
    })?;

    let mut decl = RoleSetDeclaration {
        name,
        members: Vec::new(),
        subset_of: None,
        subset_start: None,
        subset_end: None,
    };

    for expr_item in expr.into_inner() {
        match expr_item.as_rule() {
            Rule::role_set_members => {
                decl.members = expr_item
                    .into_inner()
                    .filter(|p| p.as_rule() == Rule::ident)
                    .map(|p| p.as_str().to_string())
                    .collect();
            }
            Rule::role_set_subset => {
                let mut subset_inner = expr_item.into_inner();
                let source = subset_inner.next().ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "role_set subset is missing source".to_string(),
                })?;
                let start = subset_inner.next().ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "role_set subset is missing start".to_string(),
                })?;
                let end = subset_inner.next().ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "role_set subset is missing end".to_string(),
                })?;
                decl.subset_of = Some(source.as_str().to_string());
                decl.subset_start =
                    Some(
                        start
                            .as_str()
                            .parse::<u32>()
                            .map_err(|_| ParseError::Syntax {
                                span: ErrorSpan::from_pest_span(span, input),
                                message: "role_set subset start must be an integer".to_string(),
                            })?,
                    );
                decl.subset_end =
                    Some(
                        end.as_str()
                            .parse::<u32>()
                            .map_err(|_| ParseError::Syntax {
                                span: ErrorSpan::from_pest_span(span, input),
                                message: "role_set subset end must be an integer".to_string(),
                            })?,
                    );
            }
            _ => {}
        }
    }

    Ok(decl)
}

pub(super) fn parse_topology_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<TopologyDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let kind = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "topology declaration is missing kind".to_string(),
        })?
        .as_str()
        .to_string();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "topology declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let members_pair = inner.next().ok_or_else(|| ParseError::Syntax {
        span: ErrorSpan::from_pest_span(span, input),
        message: "topology declaration is missing members".to_string(),
    })?;
    let members = members_pair
        .into_inner()
        .filter(|p| p.as_rule() == Rule::ident)
        .map(|p| p.as_str().to_string())
        .collect();
    Ok(TopologyDeclaration {
        kind,
        name,
        members,
    })
}

pub(super) fn parse_type_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<TypeDeclaration, ParseError> {
    let span = pair.as_span();
    enforce_same_line_equals(pair.as_str(), span, input, "type declaration")?;
    let inner = pair.into_inner().next().ok_or_else(|| ParseError::Syntax {
        span: ErrorSpan::from_pest_span(span, input),
        message: "type declaration is empty".to_string(),
    })?;
    match inner.as_rule() {
        Rule::type_alias_decl => {
            let mut alias_inner = inner.into_inner();
            let name = alias_inner
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "type alias is missing name".to_string(),
                })?
                .as_str()
                .to_string();
            let alias_of = alias_inner
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "type alias is missing right-hand side".to_string(),
                })?
                .as_str()
                .trim()
                .to_string();
            Ok(TypeDeclaration {
                name,
                is_alias: true,
                alias_of: Some(alias_of),
                constructors: Vec::new(),
            })
        }
        Rule::union_type_decl => {
            let mut union_inner = inner.into_inner();
            let name = union_inner
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "type declaration is missing name".to_string(),
                })?
                .as_str()
                .to_string();
            let constructors = union_inner
                .flat_map(|p| match p.as_rule() {
                    Rule::union_ctor_decl => vec![p],
                    Rule::union_ctor_block => p
                        .into_inner()
                        .filter(|inner| inner.as_rule() == Rule::union_ctor_decl)
                        .collect(),
                    _ => Vec::new(),
                })
                .map(|ctor| {
                    let mut ctor_inner = ctor.into_inner();
                    let name = ctor_inner
                        .next()
                        .map(|p| p.as_str().to_string())
                        .unwrap_or_default();
                    let payload_type = ctor_inner.next().map(|p| {
                        p.into_inner()
                            .next()
                            .map(|inner| inner.as_str().trim().to_string())
                            .unwrap_or_default()
                    });
                    TypeConstructorDeclaration { name, payload_type }
                })
                .collect();
            Ok(TypeDeclaration {
                name,
                is_alias: false,
                alias_of: None,
                constructors,
            })
        }
        _ => Err(ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "unsupported type declaration".to_string(),
        }),
    }
}

pub(super) fn parse_effect_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<EffectInterfaceDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "effect declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let mut operations = Vec::new();
    for op in inner {
        let op_pairs: Vec<_> = match op.as_rule() {
            Rule::effect_op_decl => vec![op],
            Rule::effect_body => op
                .into_inner()
                .filter(|p| p.as_rule() == Rule::effect_op_decl)
                .collect(),
            _ => Vec::new(),
        };
        for op in op_pairs {
            let mut op_inner = op.into_inner();
            let head = op_inner.next().ok_or_else(|| ParseError::Syntax {
                span: ErrorSpan::from_pest_span(span, input),
                message: "effect operation is missing signature".to_string(),
            })?;
            let mut head_inner = head.into_inner();
            let first = head_inner.next().ok_or_else(|| ParseError::Syntax {
                span: ErrorSpan::from_pest_span(span, input),
                message: "effect operation is missing name".to_string(),
            })?;
            let (authority_class, op_name_pair) =
                if first.as_rule() == Rule::effect_op_authority_class {
                    let authority_class = match first.as_str() {
                        "authoritative" => EffectAuthorityClass::Authoritative,
                        "observe" => EffectAuthorityClass::Observe,
                        _ => EffectAuthorityClass::Command,
                    };
                    let op_name = head_inner.next().ok_or_else(|| ParseError::Syntax {
                        span: ErrorSpan::from_pest_span(span, input),
                        message: "effect operation is missing name".to_string(),
                    })?;
                    (authority_class, op_name)
                } else {
                    (EffectAuthorityClass::Command, first)
                };
            let op_name = op_name_pair.as_str().to_string();
            let input_type = head_inner
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing input type".to_string(),
                })?
                .as_str()
                .trim()
                .to_string();
            let output_type = head_inner
                .next()
                .ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing output type".to_string(),
                })?
                .as_str()
                .trim()
                .to_string();
            let mut semantic_class = None;
            let mut progress = None;
            let mut region = None;
            let mut agreement_use = None;
            let mut reentrancy = None;
            if let Some(meta_body) = op_inner.next() {
                for meta in meta_body.into_inner() {
                    match meta.as_rule() {
                        Rule::effect_op_semantic_class => {
                            semantic_class = meta
                                .into_inner()
                                .find(|item| item.as_rule() == Rule::ident)
                                .map(|item| item.as_str().to_string());
                        }
                        Rule::effect_op_progress => {
                            progress = meta
                                .into_inner()
                                .find(|item| item.as_rule() == Rule::ident)
                                .map(|item| item.as_str().to_string());
                        }
                        Rule::effect_op_region => {
                            region = meta
                                .into_inner()
                                .find(|item| item.as_rule() == Rule::ident)
                                .map(|item| item.as_str().to_string());
                        }
                        Rule::effect_op_agreement_use => {
                            agreement_use = meta
                                .into_inner()
                                .find(|item| item.as_rule() == Rule::ident)
                                .map(|item| item.as_str().to_string());
                        }
                        Rule::effect_op_reentrancy => {
                            reentrancy = meta
                                .into_inner()
                                .find(|item| item.as_rule() == Rule::ident)
                                .map(|item| item.as_str().to_string());
                        }
                        _ => {}
                    }
                }
            }
            operations.push(EffectOperationDeclaration {
                name: op_name,
                authority_class,
                semantic_class: semantic_class.ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing `class` metadata".to_string(),
                })?,
                progress: progress.ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing `progress` metadata".to_string(),
                })?,
                region: region.ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing `region` metadata".to_string(),
                })?,
                agreement_use: agreement_use.ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing `agreement_use` metadata".to_string(),
                })?,
                reentrancy: reentrancy.ok_or_else(|| ParseError::Syntax {
                    span: ErrorSpan::from_pest_span(span, input),
                    message: "effect operation is missing `reentrancy` metadata".to_string(),
                })?,
                input_type,
                output_type,
            });
        }
    }
    Ok(EffectInterfaceDeclaration { name, operations })
}

pub(super) fn parse_fragment_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<RegionDeclaration, ParseError> {
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "fragment declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let params = inner
        .next()
        .map(|p| {
            p.into_inner()
                .filter(|item| item.as_rule() == Rule::ident)
                .map(|item| item.as_str().to_string())
                .collect()
        })
        .unwrap_or_default();
    Ok(RegionDeclaration { name, params })
}

pub(super) fn parse_progress_attachment(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<ProgressAttachment, ParseError> {
    let span = pair.as_span();
    let pair = if pair.as_rule() == Rule::operation_progress {
        pair.into_inner().next().ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "progress attachment is missing content".to_string(),
        })?
    } else {
        pair
    };
    let span = pair.as_span();
    let mut inner = pair.into_inner();
    let contract_name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "progress attachment is missing contract name".to_string(),
        })?
        .as_str()
        .to_string();
    let mut requires_profile = None;
    let mut within_window = None;
    let mut on_timeout = None;
    let mut on_stall = None;

    for item in inner {
        match item.as_rule() {
            Rule::progress_requires => {
                requires_profile = Some(
                    item.into_inner()
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "progress requires clause is missing profile name".to_string(),
                        })?
                        .as_str()
                        .to_string(),
                );
            }
            Rule::progress_within => {
                within_window = Some(
                    item.into_inner()
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "progress within clause is missing window name".to_string(),
                        })?
                        .as_str()
                        .to_string(),
                );
            }
            Rule::progress_on_timeout => {
                on_timeout = Some(
                    item.into_inner()
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "progress on timeout clause is missing branch name"
                                .to_string(),
                        })?
                        .as_str()
                        .to_string(),
                );
            }
            Rule::progress_on_stall => {
                on_stall = Some(
                    item.into_inner()
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "progress on stall clause is missing branch name".to_string(),
                        })?
                        .as_str()
                        .to_string(),
                );
            }
            _ => {}
        }
    }

    Ok(ProgressAttachment {
        contract_name,
        requires_profile,
        within_window,
        on_timeout,
        on_stall,
    })
}

pub(super) fn parse_operation_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<OperationDeclaration, ParseError> {
    let span = pair.as_span();
    enforce_same_line_equals(pair.as_str(), span, input, "operation declaration")?;
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "operation declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();

    let mut params = Vec::new();
    let mut owner_role = None;
    let mut within = None;
    let mut progress_contract = None;
    let mut agreement = None;
    let mut child_effect_aggregation = None;
    let mut body_source = None;

    for item in inner {
        match item.as_rule() {
            Rule::operation_params => {
                for param in item.into_inner() {
                    if param.as_rule() != Rule::operation_param_decl {
                        continue;
                    }
                    let mut param_inner = param.into_inner();
                    let param_name = param_inner
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "operation parameter is missing a name".to_string(),
                        })?
                        .as_str()
                        .to_string();
                    let type_name = param_inner
                        .next()
                        .ok_or_else(|| ParseError::Syntax {
                            span: ErrorSpan::from_pest_span(span, input),
                            message: "operation parameter is missing a type".to_string(),
                        })?
                        .as_str()
                        .trim()
                        .to_string();
                    params.push(OperationParameterDeclaration {
                        name: param_name,
                        type_name,
                    });
                }
            }
            Rule::role_ref => {
                owner_role = Some(item.as_str().trim().to_string());
            }
            Rule::operation_within => {
                let mut within_inner = item.into_inner();
                let fragment_name = within_inner
                    .next()
                    .ok_or_else(|| ParseError::Syntax {
                        span: ErrorSpan::from_pest_span(span, input),
                        message: "operation within clause is missing fragment name".to_string(),
                    })?
                    .as_str()
                    .to_string();
                let args: Vec<String> = within_inner
                    .next()
                    .map(|p| {
                        p.into_inner()
                            .filter(|arg| arg.as_rule() == Rule::ident)
                            .map(|arg| arg.as_str().to_string())
                            .collect()
                    })
                    .unwrap_or_default();
                within = Some(if args.is_empty() {
                    fragment_name
                } else {
                    format!("{}({})", fragment_name, args.join(", "))
                });
            }
            Rule::operation_progress => {
                progress_contract = Some(parse_progress_attachment(item, input)?);
            }
            Rule::operation_agreement => {
                let mut agreement_inner = item.into_inner();
                let profile_name = agreement_inner
                    .next()
                    .ok_or_else(|| ParseError::Syntax {
                        span: ErrorSpan::from_pest_span(span, input),
                        message: "operation agreement clause is missing profile name".to_string(),
                    })?
                    .as_str()
                    .to_string();
                let prestate = agreement_inner.next().map(|prestate| {
                    prestate
                        .into_inner()
                        .next()
                        .map(|ident| ident.as_str().to_string())
                        .unwrap_or_default()
                });
                agreement = Some(OperationAgreementAttachment {
                    profile_name,
                    prestate,
                });
            }
            Rule::operation_compose => {
                let compose_span = ErrorSpan::from_pest_span(item.as_span(), input);
                let mut compose_inner = item.into_inner();
                let policy_name = compose_inner
                    .next()
                    .ok_or_else(|| ParseError::Syntax {
                        span: compose_span.clone(),
                        message: "operation compose clause is missing policy".to_string(),
                    })?
                    .as_str()
                    .to_string();
                let threshold = compose_inner
                    .next()
                    .map(|value| {
                        value
                            .as_str()
                            .parse::<u64>()
                            .map_err(|err| ParseError::Syntax {
                                span: compose_span.clone(),
                                message: format!(
                                    "invalid threshold_success count `{}`: {err}",
                                    value.as_str()
                                ),
                            })
                    })
                    .transpose()?;
                child_effect_aggregation = Some(parse_operation_composition(
                    &policy_name,
                    threshold,
                    compose_span,
                )?);
            }
            Rule::protocol_body => {
                body_source = Some(item.as_str().trim().to_string());
            }
            _ => {}
        }
    }

    Ok(OperationDeclaration {
        name,
        params,
        owner_role: owner_role.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "operation declaration is missing owner role".to_string(),
        })?,
        within,
        progress_contract,
        agreement,
        child_effect_aggregation,
        body_source: body_source.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "operation declaration is missing body".to_string(),
        })?,
    })
}

fn parse_operation_composition(
    policy_name: &str,
    threshold: Option<u64>,
    span: ErrorSpan,
) -> std::result::Result<ChildEffectAggregation, ParseError> {
    let policy = match policy_name {
        "all" | "all_success" => {
            if threshold.is_some() {
                return Err(ParseError::Syntax {
                    span,
                    message: "`all_success` does not take a threshold argument".to_string(),
                });
            }
            ChildEffectAggregationPolicy::All
        }
        "first" | "first_success" => {
            if threshold.is_some() {
                return Err(ParseError::Syntax {
                    span,
                    message: "`first_success` does not take a threshold argument".to_string(),
                });
            }
            ChildEffectAggregationPolicy::First
        }
        "threshold_success" => {
            let required_successes = threshold.ok_or_else(|| ParseError::Syntax {
                span: span.clone(),
                message:
                    "`threshold_success` requires an explicit success count, for example `compose threshold_success(2)`"
                        .to_string(),
            })?;
            if required_successes == 0 {
                return Err(ParseError::Syntax {
                    span,
                    message: "`threshold_success` requires a positive success count".to_string(),
                });
            }
            ChildEffectAggregationPolicy::Threshold { required_successes }
        }
        _ => {
            return Err(ParseError::Syntax {
                span,
                message: format!(
                    "unsupported operation compose policy `{policy_name}`; use `all_success`, `first_success`, or `threshold_success(n)`"
                ),
            });
        }
    };

    Ok(ChildEffectAggregation { policy })
}

pub(super) fn parse_guest_runtime_decl(
    pair: pest::iterators::Pair<Rule>,
    input: &str,
) -> std::result::Result<GuestRuntimeDeclaration, ParseError> {
    let span = pair.as_span();
    enforce_same_line_equals(pair.as_str(), span, input, "guest runtime declaration")?;
    let mut inner = pair.into_inner();
    let name = inner
        .next()
        .ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "guest runtime declaration is missing name".to_string(),
        })?
        .as_str()
        .to_string();
    let body = inner.next().ok_or_else(|| ParseError::Syntax {
        span: ErrorSpan::from_pest_span(span, input),
        message: "guest runtime declaration is missing body".to_string(),
    })?;

    let mut uses = Vec::new();
    let mut entry = None;
    for stmt in body.into_inner() {
        match stmt.as_rule() {
            Rule::guest_runtime_stmt => {
                for inner_stmt in stmt.into_inner() {
                    match inner_stmt.as_rule() {
                        Rule::guest_runtime_uses => {
                            uses = inner_stmt
                                .into_inner()
                                .filter(|p| p.as_rule() == Rule::ident)
                                .map(|p| p.as_str().to_string())
                                .collect();
                        }
                        Rule::guest_runtime_entry => {
                            entry = inner_stmt
                                .into_inner()
                                .find(|p| p.as_rule() == Rule::ident)
                                .map(|p| p.as_str().to_string());
                        }
                        _ => {}
                    }
                }
            }
            Rule::guest_runtime_uses => {
                uses = stmt
                    .into_inner()
                    .filter(|p| p.as_rule() == Rule::ident)
                    .map(|p| p.as_str().to_string())
                    .collect();
            }
            Rule::guest_runtime_entry => {
                entry = stmt
                    .into_inner()
                    .find(|p| p.as_rule() == Rule::ident)
                    .map(|p| p.as_str().to_string());
            }
            _ => {}
        }
    }

    Ok(GuestRuntimeDeclaration {
        name,
        uses,
        entry: entry.ok_or_else(|| ParseError::Syntax {
            span: ErrorSpan::from_pest_span(span, input),
            message: "guest runtime declaration is missing entry".to_string(),
        })?,
    })
}
