use super::errors::PackageError;
use super::*;

pub(crate) fn manifest_capabilities(
    manifest: &Manifest,
) -> Option<&harn_vm::llm::capabilities::CapabilitiesFile> {
    manifest.capabilities.as_ref()
}

pub(crate) fn is_empty_capabilities(file: &harn_vm::llm::capabilities::CapabilitiesFile) -> bool {
    file.provider.is_empty() && file.provider_family.is_empty()
}

pub fn validate_runtime_manifest_extensions(anchor: &Path) -> Result<(), PackageError> {
    let Some((manifest, _manifest_dir)) = find_nearest_manifest(anchor) else {
        return Ok(());
    };
    validate_handoff_routes(&manifest.handoff_routes, &manifest)?;
    validate_contributions(&manifest)
}

/// Load the nearest project manifest plus any installed package manifests and
/// merge the root project's runtime extensions.
pub fn try_load_runtime_extensions(anchor: &Path) -> Result<RuntimeExtensions, PackageError> {
    ensure_dependencies_materialized(anchor)?;
    let Some((root_manifest, manifest_dir)) = find_nearest_manifest(anchor) else {
        return Ok(RuntimeExtensions::default());
    };

    let mut llm = harn_vm::llm_config::ProvidersConfig::default();
    let mut capabilities = harn_vm::llm::capabilities::CapabilitiesFile::default();
    let mut hooks = Vec::new();
    let mut triggers = Vec::new();

    llm.merge_from(&root_manifest.llm);
    if let Some(file) = manifest_capabilities(&root_manifest) {
        merge_capability_overrides(&mut capabilities, file);
    }
    hooks.extend(resolved_hooks_from_manifest(&root_manifest, &manifest_dir));
    triggers.extend(resolved_triggers_from_manifest(
        &root_manifest,
        &manifest_dir,
    ));
    let handoff_routes = root_manifest.handoff_routes.clone();
    validate_handoff_routes(&handoff_routes, &root_manifest)?;
    let provider_connectors =
        resolved_provider_connectors_from_manifest(&root_manifest, &manifest_dir);

    Ok(RuntimeExtensions {
        root_manifest_path: Some(manifest_dir.join(MANIFEST)),
        root_manifest_dir: Some(manifest_dir),
        root_manifest: Some(root_manifest),
        llm: (!llm.is_empty()).then_some(llm),
        capabilities: (!is_empty_capabilities(&capabilities)).then_some(capabilities),
        hooks,
        triggers,
        handoff_routes,
        provider_connectors,
    })
}

pub fn load_runtime_extensions(anchor: &Path) -> RuntimeExtensions {
    match try_load_runtime_extensions(anchor) {
        Ok(extensions) => extensions,
        Err(error) => {
            eprintln!("error: {error}");
            process::exit(1);
        }
    }
}

/// Install merged runtime extensions on the current thread.
pub fn install_runtime_extensions(extensions: &RuntimeExtensions) {
    harn_vm::llm_config::set_user_overrides(extensions.llm.clone());
    harn_vm::llm::capabilities::set_user_overrides(extensions.capabilities.clone());
    install_manifest_handoff_routes(extensions);
    install_orchestrator_budget(extensions);
}

pub fn install_manifest_handoff_routes(extensions: &RuntimeExtensions) {
    harn_vm::install_handoff_routes(extensions.handoff_routes.clone());
}

pub fn install_orchestrator_budget(extensions: &RuntimeExtensions) {
    let budget = extensions
        .root_manifest
        .as_ref()
        .map(|manifest| harn_vm::OrchestratorBudgetConfig {
            daily_cost_usd: manifest.orchestrator.budget.daily_cost_usd,
            hourly_cost_usd: manifest.orchestrator.budget.hourly_cost_usd,
        })
        .unwrap_or_default();
    harn_vm::install_orchestrator_budget(budget);
}

pub async fn install_manifest_hooks(
    vm: &mut harn_vm::Vm,
    extensions: &RuntimeExtensions,
) -> Result<(), PackageError> {
    harn_vm::orchestration::clear_runtime_hooks();
    let mut loaded_exports: HashMap<ManifestModuleCacheKey, ManifestModuleExports> = HashMap::new();
    for hook in &extensions.hooks {
        let Some((module_name, function_name)) = hook.handler.rsplit_once("::") else {
            return Err(format!(
                "invalid hook handler '{}': expected <module>::<function>",
                hook.handler
            )
            .into());
        };
        let cache_key = (
            hook.manifest_dir.clone(),
            hook.package_name.clone(),
            Some(module_name.to_string()),
        );
        if !loaded_exports.contains_key(&cache_key) {
            let exports = resolve_manifest_exports(
                vm,
                &hook.manifest_dir,
                hook.package_name.as_deref(),
                &hook.exports,
                Some(module_name),
            )
            .await?;
            loaded_exports.insert(cache_key.clone(), exports);
        }
        let exports = loaded_exports
            .get(&cache_key)
            .expect("manifest hook exports cached");
        let Some(closure) = exports.get(function_name) else {
            return Err(format!(
                "hook handler '{function_name}' is not exported by module '{module_name}'"
            )
            .into());
        };
        harn_vm::orchestration::register_vm_hook(
            hook.event,
            hook.pattern.clone(),
            hook.handler.clone(),
            closure.clone(),
        );
    }
    Ok(())
}

pub async fn collect_manifest_triggers(
    vm: &mut harn_vm::Vm,
    extensions: &RuntimeExtensions,
) -> Result<Vec<CollectedManifestTrigger>, PackageError> {
    let _provider_schema_guard = lock_manifest_provider_schemas().await;
    let provider_catalog = build_manifest_provider_catalog(extensions).await?;
    validate_orchestrator_budget(extensions.root_manifest.as_ref())?;
    validate_static_trigger_configs(&extensions.triggers, &provider_catalog)?;
    let mut loaded_exports: HashMap<ManifestModuleCacheKey, ManifestModuleExports> = HashMap::new();
    let mut module_signatures: HashMap<PathBuf, BTreeMap<String, TriggerFunctionSignature>> =
        HashMap::new();
    let mut collected = Vec::new();

    for trigger in &extensions.triggers {
        let handler = parse_trigger_handler_uri(trigger)?;
        let collected_handler = match handler {
            TriggerHandlerUri::Local(reference) => {
                let cache_key = (
                    trigger.manifest_dir.clone(),
                    trigger.package_name.clone(),
                    reference.module_name.clone(),
                );
                if !loaded_exports.contains_key(&cache_key) {
                    let exports = resolve_manifest_exports(
                        vm,
                        &trigger.manifest_dir,
                        trigger.package_name.as_deref(),
                        &trigger.exports,
                        reference.module_name.as_deref(),
                    )
                    .await
                    .map_err(|error| trigger_error(trigger, error))?;
                    loaded_exports.insert(cache_key.clone(), exports);
                }
                let exports = loaded_exports
                    .get(&cache_key)
                    .expect("manifest trigger exports cached");
                let Some(closure) = exports.get(&reference.function_name) else {
                    return Err(trigger_error(
                        trigger,
                        format!(
                            "handler '{}' is not exported by the resolved module",
                            reference.raw
                        ),
                    ));
                };
                CollectedTriggerHandler::Local {
                    reference,
                    closure: closure.clone(),
                }
            }
            TriggerHandlerUri::A2a {
                target,
                allow_cleartext,
            } => CollectedTriggerHandler::A2a {
                target,
                allow_cleartext,
            },
            TriggerHandlerUri::Worker { queue } => CollectedTriggerHandler::Worker { queue },
            TriggerHandlerUri::Persona { name } => {
                let binding = persona_runtime_binding_for_handler(extensions, trigger, &name)?;
                CollectedTriggerHandler::Persona { binding }
            }
            TriggerHandlerUri::EvalPack { target } => {
                let manifest = eval_pack_manifest_for_handler(trigger, &target)?;
                let ledger_options = eval_pack_ledger_options_for_handler(trigger)?;
                CollectedTriggerHandler::EvalPack {
                    target,
                    manifest: Box::new(manifest),
                    ledger_options,
                }
            }
        };

        let collected_when = if let Some(when_raw) = &trigger.when {
            let reference = parse_local_trigger_ref(when_raw, "when", trigger)?;
            let cache_key = (
                trigger.manifest_dir.clone(),
                trigger.package_name.clone(),
                reference.module_name.clone(),
            );
            if !loaded_exports.contains_key(&cache_key) {
                let exports = resolve_manifest_exports(
                    vm,
                    &trigger.manifest_dir,
                    trigger.package_name.as_deref(),
                    &trigger.exports,
                    reference.module_name.as_deref(),
                )
                .await
                .map_err(|error| trigger_error(trigger, error))?;
                loaded_exports.insert(cache_key.clone(), exports);
            }
            let exports = loaded_exports
                .get(&cache_key)
                .expect("manifest trigger predicate exports cached");
            let Some(closure) = exports.get(&reference.function_name) else {
                return Err(trigger_error(
                    trigger,
                    format!(
                        "when predicate '{}' is not exported by the resolved module",
                        reference.raw
                    ),
                ));
            };

            let source_path = manifest_module_source_path(
                &trigger.manifest_dir,
                trigger.package_name.as_deref(),
                &trigger.exports,
                reference.module_name.as_deref(),
            )
            .map_err(|error| trigger_error(trigger, error))?;
            if !module_signatures.contains_key(&source_path) {
                let signatures = load_trigger_function_signatures(&source_path)
                    .map_err(|error| trigger_error(trigger, error))?;
                module_signatures.insert(source_path.clone(), signatures);
            }
            let signatures = module_signatures
                .get(&source_path)
                .expect("module signatures cached");
            let Some(signature) = signatures.get(&reference.function_name) else {
                return Err(trigger_error(
                    trigger,
                    format!(
                        "when predicate '{}' must resolve to a function declaration",
                        reference.raw
                    ),
                ));
            };
            if signature.params.len() != 1
                || signature.params[0]
                    .as_ref()
                    .is_none_or(|param| !is_trigger_event_type(param))
            {
                return Err(trigger_error(
                    trigger,
                    format!(
                        "when predicate '{}' must have signature fn(TriggerEvent) -> bool",
                        reference.raw
                    ),
                ));
            }
            if signature
                .return_type
                .as_ref()
                .is_none_or(|return_type| !is_predicate_return_type(return_type))
            {
                return Err(trigger_error(
                    trigger,
                    format!(
                        "when predicate '{}' must have signature fn(TriggerEvent) -> bool or Result<bool, _>",
                        reference.raw
                    ),
                ));
            }

            Some(CollectedTriggerPredicate {
                reference,
                closure: closure.clone(),
            })
        } else {
            None
        };

        let flow_control = collect_trigger_flow_control(vm, trigger).await?;

        collected.push(CollectedManifestTrigger {
            config: trigger.clone(),
            handler: collected_handler,
            when: collected_when,
            flow_control,
        });
    }

    harn_vm::install_provider_catalog(provider_catalog);
    Ok(collected)
}

pub(crate) async fn collect_trigger_flow_control(
    vm: &mut harn_vm::Vm,
    trigger: &ResolvedTriggerConfig,
) -> Result<harn_vm::TriggerFlowControlConfig, PackageError> {
    let mut flow = harn_vm::TriggerFlowControlConfig::default();

    let concurrency = if let Some(spec) = &trigger.concurrency {
        Some(spec.clone())
    } else if let Some(max) = trigger.budget.max_concurrent {
        eprintln!(
            "warning: {} uses deprecated budget.max_concurrent; prefer concurrency = {{ max = {} }}",
            manifest_trigger_location(trigger),
            max
        );
        Some(TriggerConcurrencyManifestSpec { key: None, max })
    } else {
        None
    };
    if let Some(spec) = concurrency {
        flow.concurrency = Some(harn_vm::TriggerConcurrencyConfig {
            key: compile_optional_trigger_expression(
                vm,
                trigger,
                "concurrency.key",
                spec.key.as_deref(),
            )
            .await?,
            max: spec.max,
        });
    }

    if let Some(spec) = &trigger.throttle {
        flow.throttle = Some(harn_vm::TriggerThrottleConfig {
            key: compile_optional_trigger_expression(
                vm,
                trigger,
                "throttle.key",
                spec.key.as_deref(),
            )
            .await?,
            period: harn_vm::parse_flow_control_duration(&spec.period)
                .map_err(|error| trigger_error(trigger, format!("throttle.period {error}")))?,
            max: spec.max,
        });
    }

    if let Some(spec) = &trigger.rate_limit {
        flow.rate_limit = Some(harn_vm::TriggerRateLimitConfig {
            key: compile_optional_trigger_expression(
                vm,
                trigger,
                "rate_limit.key",
                spec.key.as_deref(),
            )
            .await?,
            period: harn_vm::parse_flow_control_duration(&spec.period)
                .map_err(|error| trigger_error(trigger, format!("rate_limit.period {error}")))?,
            max: spec.max,
        });
    }

    if let Some(spec) = &trigger.debounce {
        flow.debounce = Some(harn_vm::TriggerDebounceConfig {
            key: compile_trigger_expression(vm, trigger, "debounce.key", &spec.key).await?,
            period: harn_vm::parse_flow_control_duration(&spec.period)
                .map_err(|error| trigger_error(trigger, format!("debounce.period {error}")))?,
        });
    }

    if let Some(spec) = &trigger.singleton {
        flow.singleton = Some(harn_vm::TriggerSingletonConfig {
            key: compile_optional_trigger_expression(
                vm,
                trigger,
                "singleton.key",
                spec.key.as_deref(),
            )
            .await?,
        });
    }

    if let Some(spec) = &trigger.batch {
        flow.batch = Some(harn_vm::TriggerBatchConfig {
            key: compile_optional_trigger_expression(vm, trigger, "batch.key", spec.key.as_deref())
                .await?,
            size: spec.size,
            timeout: harn_vm::parse_flow_control_duration(&spec.timeout)
                .map_err(|error| trigger_error(trigger, format!("batch.timeout {error}")))?,
        });
    }

    if let Some(spec) = &trigger.priority_flow {
        flow.priority = Some(harn_vm::TriggerPriorityOrderConfig {
            key: compile_trigger_expression(vm, trigger, "priority.key", &spec.key).await?,
            order: spec.order.clone(),
        });
    }

    Ok(flow)
}

fn persona_runtime_binding_for_handler(
    extensions: &RuntimeExtensions,
    trigger: &ResolvedTriggerConfig,
    name: &str,
) -> Result<harn_vm::PersonaRuntimeBinding, PackageError> {
    let Some(manifest) = extensions.root_manifest.as_ref() else {
        return Err(trigger_error(
            trigger,
            format!("handler persona://{name} requires a root manifest"),
        ));
    };
    let Some(persona) = manifest
        .personas
        .iter()
        .find(|persona| persona.name.as_deref() == Some(name))
    else {
        return Err(trigger_error(
            trigger,
            format!("handler persona://{name} does not match a declared persona"),
        ));
    };
    Ok(persona_runtime_binding(name, persona))
}

fn eval_pack_manifest_for_handler(
    trigger: &ResolvedTriggerConfig,
    target: &str,
) -> Result<harn_vm::orchestration::EvalPackManifest, PackageError> {
    if eval_pack_target_is_path(target) {
        let path = resolve_eval_pack_target_path(&trigger.manifest_dir, target);
        return harn_vm::orchestration::load_eval_pack_manifest(&path).map_err(|error| {
            trigger_error(
                trigger,
                format!(
                    "handler eval_pack://{target} failed to load eval pack {}: {error}",
                    path.display()
                ),
            )
        });
    }

    let paths = load_package_eval_pack_paths(Some(&trigger.manifest_path))
        .map_err(|error| trigger_error(trigger, error))?;
    let mut matches = Vec::new();
    for path in paths {
        let manifest = harn_vm::orchestration::load_eval_pack_manifest(&path).map_err(|error| {
            trigger_error(
                trigger,
                format!(
                    "failed to load package eval pack {}: {error}",
                    path.display()
                ),
            )
        })?;
        let file_stem = path.file_stem().and_then(|stem| stem.to_str());
        if manifest.id == target
            || manifest.name.as_deref() == Some(target)
            || file_stem == Some(target)
        {
            matches.push((path, manifest));
        }
    }

    match matches.len() {
        0 => Err(trigger_error(
            trigger,
            format!(
                "handler eval_pack://{target} did not match any package eval pack by id, name, or file stem",
            ),
        )),
        1 => Ok(matches.remove(0).1),
        _ => Err(trigger_error(
            trigger,
            format!("handler eval_pack://{target} matched multiple package eval packs"),
        )),
    }
}

fn eval_pack_target_is_path(target: &str) -> bool {
    target.contains('/')
        || target.contains('\\')
        || target.ends_with(".toml")
        || target.ends_with(".json")
}

fn resolve_eval_pack_target_path(manifest_dir: &Path, target: &str) -> PathBuf {
    let path = PathBuf::from(target);
    if path.is_absolute() {
        path
    } else {
        manifest_dir.join(path)
    }
}

fn eval_pack_ledger_options_for_handler(
    trigger: &ResolvedTriggerConfig,
) -> Result<Option<serde_json::Value>, PackageError> {
    let value = trigger
        .kind_specific
        .get("eval_options")
        .or_else(|| trigger.kind_specific.get("ledger"));
    value
        .map(|value| {
            serde_json::to_value(value).map_err(|error| {
                trigger_error(trigger, format!("invalid eval ledger options: {error}"))
            })
        })
        .transpose()
}

/// Lower a manifest persona entry into the runtime binding. Centralised so
/// every call site (`harn persona` CLI, trigger registration, etc.) carries
/// the same shape — stages included.
pub(crate) fn persona_runtime_binding(
    name: &str,
    persona: &PersonaManifestEntry,
) -> harn_vm::PersonaRuntimeBinding {
    harn_vm::PersonaRuntimeBinding {
        name: name.to_string(),
        template_ref: persona_template_ref(persona),
        entry_workflow: persona.entry_workflow.clone().unwrap_or_default(),
        schedules: persona.schedules.clone(),
        triggers: persona.triggers.clone(),
        budget: harn_vm::PersonaBudgetPolicy {
            daily_usd: persona.budget.daily_usd,
            hourly_usd: persona.budget.hourly_usd,
            run_usd: persona.budget.run_usd,
            max_tokens: persona.budget.max_tokens,
        },
        stages: persona
            .stages
            .iter()
            .map(persona_stage_decl_to_runtime)
            .collect(),
    }
}

fn persona_stage_decl_to_runtime(stage: &PersonaStageDecl) -> harn_vm::StageDecl {
    harn_vm::StageDecl {
        name: stage.name.clone(),
        allowed_tools: stage.allowed_tools.clone(),
        side_effect_level: stage.side_effect_level.clone(),
        max_iterations: stage.max_iterations,
        on_exit: stage.on_exit.as_ref().map(|exit| harn_vm::StageExit {
            on_complete: exit.on_complete.clone(),
            on_failure: exit.on_failure.clone(),
            policy_override: None,
        }),
    }
}

pub(crate) async fn compile_optional_trigger_expression(
    vm: &mut harn_vm::Vm,
    trigger: &ResolvedTriggerConfig,
    field_name: &str,
    expr: Option<&str>,
) -> Result<Option<harn_vm::TriggerExpressionSpec>, PackageError> {
    match expr {
        Some(expr) => compile_trigger_expression(vm, trigger, field_name, expr)
            .await
            .map(Some),
        None => Ok(None),
    }
}

pub(crate) async fn compile_trigger_expression(
    vm: &mut harn_vm::Vm,
    trigger: &ResolvedTriggerConfig,
    field_name: &str,
    expr: &str,
) -> Result<harn_vm::TriggerExpressionSpec, PackageError> {
    let synthetic = PathBuf::from(format!(
        "<trigger-expr>/{}/{:04}-{}.harn",
        harn_vm::event_log::sanitize_topic_component(&trigger.id),
        trigger.table_index,
        harn_vm::event_log::sanitize_topic_component(field_name),
    ));
    let source = format!(
        "import \"std/triggers\"\n\npub fn __trigger_expr(event: TriggerEvent) -> any {{\n  return {expr}\n}}\n"
    );
    let exports = vm
        .load_module_exports_from_source(synthetic, &source)
        .await
        .map_err(|error| {
            trigger_error(
                trigger,
                format!("{field_name} '{expr}' is invalid Harn expression: {error}"),
            )
        })?;
    let closure = exports.get("__trigger_expr").ok_or_else(|| {
        trigger_error(
            trigger,
            format!("{field_name} '{expr}' did not compile into an exported closure"),
        )
    })?;
    Ok(harn_vm::TriggerExpressionSpec {
        raw: expr.to_string(),
        closure: closure.clone(),
    })
}

pub(crate) fn trigger_kind_label(kind: TriggerKind) -> &'static str {
    match kind {
        TriggerKind::Webhook => "webhook",
        TriggerKind::Cron => "cron",
        TriggerKind::Poll => "poll",
        TriggerKind::Stream => "stream",
        TriggerKind::Predicate => "predicate",
        TriggerKind::A2aPush => "a2a-push",
    }
}

pub(crate) fn worker_queue_priority(
    priority: TriggerDispatchPriority,
) -> harn_vm::WorkerQueuePriority {
    match priority {
        TriggerDispatchPriority::High => harn_vm::WorkerQueuePriority::High,
        TriggerDispatchPriority::Normal => harn_vm::WorkerQueuePriority::Normal,
        TriggerDispatchPriority::Low => harn_vm::WorkerQueuePriority::Low,
    }
}

pub fn manifest_trigger_binding_spec(
    trigger: CollectedManifestTrigger,
) -> harn_vm::TriggerBindingSpec {
    let flow_control = trigger.flow_control.clone();
    let config = trigger.config;
    let (handler, handler_descriptor) = match trigger.handler {
        CollectedTriggerHandler::Local { reference, closure } => (
            harn_vm::TriggerHandlerSpec::Local {
                raw: reference.raw.clone(),
                closure,
            },
            serde_json::json!({
                "kind": "local",
                "raw": reference.raw,
            }),
        ),
        CollectedTriggerHandler::A2a {
            target,
            allow_cleartext,
        } => (
            harn_vm::TriggerHandlerSpec::A2a {
                target: target.clone(),
                allow_cleartext,
            },
            serde_json::json!({
                "kind": "a2a",
                "target": target,
                "allow_cleartext": allow_cleartext,
            }),
        ),
        CollectedTriggerHandler::Worker { queue } => (
            harn_vm::TriggerHandlerSpec::Worker {
                queue: queue.clone(),
            },
            serde_json::json!({
                "kind": "worker",
                "queue": queue,
            }),
        ),
        CollectedTriggerHandler::Persona { binding } => (
            harn_vm::TriggerHandlerSpec::Persona {
                binding: binding.clone(),
            },
            serde_json::json!({
                "kind": "persona",
                "name": binding.name,
                "entry_workflow": binding.entry_workflow,
            }),
        ),
        CollectedTriggerHandler::EvalPack {
            target,
            manifest,
            ledger_options,
        } => {
            let pack_id = manifest.id.clone();
            let harness_config_fingerprint =
                harn_vm::orchestration::eval_pack_harness_config_fingerprint(manifest.as_ref())
                    .ok();
            (
                harn_vm::TriggerHandlerSpec::EvalPack {
                    target: target.clone(),
                    manifest,
                    ledger_options: ledger_options.clone(),
                },
                serde_json::json!({
                    "kind": "eval_pack",
                    "target": target,
                    "pack_id": pack_id,
                    "harness_config_fingerprint": harness_config_fingerprint,
                    "ledger_options": ledger_options,
                }),
            )
        }
    };

    let when_raw = trigger
        .when
        .as_ref()
        .map(|predicate| predicate.reference.raw.clone());
    let when = trigger.when.map(|predicate| harn_vm::TriggerPredicateSpec {
        raw: predicate.reference.raw,
        closure: predicate.closure,
    });
    let mut when_budget = config
        .when_budget
        .as_ref()
        .map(|budget| {
            Ok::<harn_vm::TriggerPredicateBudget, String>(harn_vm::TriggerPredicateBudget {
                max_cost_usd: budget.max_cost_usd,
                tokens_max: budget.tokens_max,
                timeout_ms: budget
                    .timeout
                    .as_deref()
                    .map(parse_duration_millis)
                    .transpose()?,
            })
        })
        .transpose()
        .unwrap_or_default();
    if config.budget.max_cost_usd.is_some() || config.budget.max_tokens.is_some() {
        let budget = when_budget.get_or_insert_with(harn_vm::TriggerPredicateBudget::default);
        if budget.max_cost_usd.is_none() {
            budget.max_cost_usd = config.budget.max_cost_usd;
        }
        if budget.tokens_max.is_none() {
            budget.tokens_max = config.budget.max_tokens;
        }
    }
    let id = config.id.clone();
    let kind = trigger_kind_label(config.kind).to_string();
    let provider = config.provider.clone();
    let autonomy_tier = config.autonomy_tier;
    let match_events = config.match_.events.clone();
    let dedupe_key = config.dedupe_key.clone();
    let retry = harn_vm::TriggerRetryConfig::new(
        config.retry.max,
        match config.retry.backoff {
            TriggerRetryBackoff::Immediate => harn_vm::RetryPolicy::Linear { delay_ms: 0 },
            TriggerRetryBackoff::Svix => harn_vm::RetryPolicy::Svix,
        },
    );
    let filter = config.filter.clone();
    let dedupe_retention_days = config.retry.retention_days;
    let daily_cost_usd = config.budget.daily_cost_usd;
    let hourly_cost_usd = config.budget.hourly_cost_usd;
    let max_autonomous_decisions_per_hour = config.budget.max_autonomous_decisions_per_hour;
    let max_autonomous_decisions_per_day = config.budget.max_autonomous_decisions_per_day;
    let on_budget_exhausted = config.budget.on_budget_exhausted;
    let max_concurrent = flow_control.concurrency.as_ref().map(|config| config.max);
    let manifest_path = Some(config.manifest_path.clone());
    let package_name = config.package_name.clone();

    let fingerprint = serde_json::to_string(&serde_json::json!({
        "id": &id,
        "kind": &kind,
        "provider": provider.as_str(),
        "autonomy_tier": autonomy_tier,
        "match": config.match_,
        "when": when_raw,
        "when_budget": config.when_budget,
        "handler": handler_descriptor,
        "dedupe_key": &dedupe_key,
        "retry": config.retry,
        "dispatch_priority": config.dispatch_priority,
        "budget": config.budget,
        "flow_control": {
            "concurrency": config.concurrency,
            "throttle": config.throttle,
            "rate_limit": config.rate_limit,
            "debounce": config.debounce,
            "singleton": config.singleton,
            "batch": config.batch,
            "priority": config.priority_flow,
        },
        "window": config.window,
        "secrets": config.secrets,
        "filter": &filter,
        "kind_specific": config.kind_specific,
        "manifest_path": &manifest_path,
        "package_name": &package_name,
    }))
    .unwrap_or_else(|_| format!("{}:{}:{}", id, kind, provider.as_str()));

    harn_vm::TriggerBindingSpec {
        id,
        source: harn_vm::TriggerBindingSource::Manifest,
        kind,
        provider,
        autonomy_tier,
        handler,
        dispatch_priority: worker_queue_priority(config.dispatch_priority),
        when,
        when_budget,
        retry,
        match_events,
        dedupe_key,
        filter,
        dedupe_retention_days,
        daily_cost_usd,
        hourly_cost_usd,
        max_autonomous_decisions_per_hour,
        max_autonomous_decisions_per_day,
        on_budget_exhausted,
        max_concurrent,
        flow_control,
        aggregation: None,
        manifest_path,
        package_name,
        definition_fingerprint: fingerprint,
    }
}

pub async fn install_manifest_triggers(
    vm: &mut harn_vm::Vm,
    extensions: &RuntimeExtensions,
) -> Result<(), PackageError> {
    install_orchestrator_budget(extensions);
    let collected = collect_manifest_triggers(vm, extensions).await?;
    let mut bindings: Vec<_> = collected
        .iter()
        .cloned()
        .map(manifest_trigger_binding_spec)
        .collect();
    bindings.extend(collect_persona_trigger_binding_specs(extensions)?);
    harn_vm::install_manifest_triggers(bindings)
        .await
        .map_err(|error| PackageError::Extensions(error.to_string()))
}

pub async fn install_collected_manifest_triggers(
    collected: &[CollectedManifestTrigger],
) -> Result<(), PackageError> {
    let bindings = collected
        .iter()
        .cloned()
        .map(manifest_trigger_binding_spec)
        .collect();
    harn_vm::install_manifest_triggers(bindings)
        .await
        .map_err(|error| PackageError::Extensions(error.to_string()))
}

pub fn collect_persona_trigger_binding_specs(
    extensions: &RuntimeExtensions,
) -> Result<Vec<harn_vm::TriggerBindingSpec>, PackageError> {
    let Some(manifest) = extensions.root_manifest.clone() else {
        return Ok(Vec::new());
    };
    let manifest_path = extensions
        .root_manifest_path
        .clone()
        .unwrap_or_else(|| PathBuf::from(MANIFEST));
    let manifest_dir = extensions
        .root_manifest_dir
        .clone()
        .or_else(|| manifest_path.parent().map(Path::to_path_buf))
        .unwrap_or_else(|| PathBuf::from("."));
    let resolved =
        validate_and_resolve_personas(manifest, manifest_path, manifest_dir).map_err(|errors| {
            errors
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("\n")
        })?;
    let mut bindings = Vec::new();
    for persona in resolved.personas {
        let Some(name) = persona.name.clone() else {
            continue;
        };
        for trigger in &persona.triggers {
            let Some((provider, kind)) = trigger.split_once('.') else {
                continue;
            };
            let provider = provider.trim();
            let kind = kind.trim();
            if provider.is_empty() || kind.is_empty() {
                continue;
            }
            bindings.push(persona_trigger_binding_spec(
                &resolved.manifest_path,
                &name,
                provider,
                kind,
                &persona,
            ));
        }
    }
    Ok(bindings)
}

fn persona_trigger_binding_spec(
    manifest_path: &Path,
    name: &str,
    provider: &str,
    kind: &str,
    persona: &PersonaManifestEntry,
) -> harn_vm::TriggerBindingSpec {
    let runtime_binding = persona_runtime_binding(name, persona);
    let id = format!("persona.{name}.{provider}.{kind}");
    let handler = harn_vm::TriggerHandlerSpec::Persona {
        binding: runtime_binding.clone(),
    };
    let fingerprint = serde_json::to_string(&serde_json::json!({
        "id": &id,
        "kind": kind,
        "provider": provider,
        "handler": {
            "kind": "persona",
            "name": name,
            "entry_workflow": runtime_binding.entry_workflow,
        },
        "budget": runtime_binding.budget,
        "manifest_path": manifest_path,
    }))
    .unwrap_or_else(|_| format!("{id}:{provider}:{kind}:{name}"));

    harn_vm::TriggerBindingSpec {
        id,
        source: harn_vm::TriggerBindingSource::Manifest,
        kind: kind.to_string(),
        provider: harn_vm::ProviderId::from(provider.to_string()),
        autonomy_tier: persona
            .autonomy_tier
            .map(persona_autonomy_to_vm)
            .unwrap_or(harn_vm::AutonomyTier::Suggest),
        handler,
        dispatch_priority: harn_vm::WorkerQueuePriority::Normal,
        when: None,
        when_budget: None,
        retry: harn_vm::TriggerRetryConfig::default(),
        match_events: vec![kind.to_string()],
        dedupe_key: None,
        filter: None,
        dedupe_retention_days: 7,
        daily_cost_usd: persona.budget.daily_usd,
        hourly_cost_usd: persona.budget.hourly_usd,
        max_autonomous_decisions_per_hour: None,
        max_autonomous_decisions_per_day: None,
        on_budget_exhausted: harn_vm::TriggerBudgetExhaustionStrategy::RetryLater,
        max_concurrent: None,
        flow_control: harn_vm::TriggerFlowControlConfig::default(),
        aggregation: None,
        manifest_path: Some(manifest_path.to_path_buf()),
        package_name: None,
        definition_fingerprint: fingerprint,
    }
}

fn persona_autonomy_to_vm(value: PersonaAutonomyTier) -> harn_vm::AutonomyTier {
    match value {
        PersonaAutonomyTier::Shadow => harn_vm::AutonomyTier::Shadow,
        PersonaAutonomyTier::Suggest => harn_vm::AutonomyTier::Suggest,
        PersonaAutonomyTier::ActWithApproval => harn_vm::AutonomyTier::ActWithApproval,
        PersonaAutonomyTier::ActAuto => harn_vm::AutonomyTier::ActAuto,
    }
}

fn persona_template_ref(persona: &PersonaManifestEntry) -> Option<String> {
    persona
        .package_source
        .package
        .as_ref()
        .zip(persona.version.as_ref())
        .map(|(package, version)| format!("{package}@{version}"))
        .or_else(|| persona.package_source.package.clone())
        .or_else(|| {
            persona
                .name
                .as_ref()
                .zip(persona.version.as_ref())
                .map(|(name, version)| format!("{name}@{version}"))
        })
}

pub fn load_personas_from_manifest_path(
    manifest_path: &Path,
) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
    let manifest_path = if manifest_path.is_dir() {
        manifest_path.join(MANIFEST)
    } else {
        manifest_path.to_path_buf()
    };
    let manifest_dir = manifest_path
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| PathBuf::from("."));
    if manifest_path.extension().and_then(|ext| ext.to_str()) == Some("harn") {
        return match harn_modules::personas::parse_persona_source_file(&manifest_path) {
            Ok(document) if !document.personas.is_empty() => {
                validate_and_resolve_standalone_personas(
                    document.personas,
                    manifest_path,
                    manifest_dir,
                )
            }
            Ok(_) => Err(vec![PersonaValidationError {
                manifest_path: manifest_path.clone(),
                field_path: "persona".to_string(),
                message: "no @persona declarations found".to_string(),
            }]),
            Err(message) => Err(vec![PersonaValidationError {
                manifest_path: manifest_path.clone(),
                field_path: "persona".to_string(),
                message,
            }]),
        };
    }
    let manifest = match read_manifest_from_path(&manifest_path) {
        Ok(manifest) => manifest,
        Err(message) => {
            if let Ok(document) =
                harn_modules::personas::parse_persona_manifest_file(&manifest_path)
            {
                if !document.personas.is_empty() {
                    return validate_and_resolve_standalone_personas(
                        document.personas,
                        manifest_path,
                        manifest_dir,
                    );
                }
            }
            return Err(vec![PersonaValidationError {
                manifest_path: manifest_path.clone(),
                field_path: "harn.toml".to_string(),
                message: message.to_string(),
            }]);
        }
    };
    if manifest.personas.is_empty() {
        if let Ok(document) = harn_modules::personas::parse_persona_manifest_file(&manifest_path) {
            if !document.personas.is_empty() {
                return validate_and_resolve_standalone_personas(
                    document.personas,
                    manifest_path,
                    manifest_dir,
                );
            }
        }
    }
    validate_and_resolve_personas(manifest, manifest_path, manifest_dir)
}

fn validate_and_resolve_standalone_personas(
    personas: Vec<PersonaManifestEntry>,
    manifest_path: PathBuf,
    manifest_dir: PathBuf,
) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
    let known_names = personas
        .iter()
        .filter_map(|persona| persona.name.as_ref())
        .filter(|name| !name.trim().is_empty())
        .cloned()
        .collect();
    let context = harn_modules::personas::PersonaValidationContext {
        known_capabilities: harn_modules::personas::default_persona_capabilities(),
        known_tools: BTreeSet::new(),
        known_names,
    };
    harn_modules::personas::validate_persona_manifests(&manifest_path, &personas, &context)?;
    Ok(ResolvedPersonaManifest {
        manifest_path,
        manifest_dir,
        personas,
    })
}

pub fn load_personas_config(
    anchor: Option<&Path>,
) -> Result<Option<ResolvedPersonaManifest>, Vec<PersonaValidationError>> {
    let anchor = anchor
        .map(Path::to_path_buf)
        .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
    let Some((manifest, dir)) = find_nearest_manifest(&anchor) else {
        return Ok(None);
    };
    let manifest_path = dir.join(MANIFEST);
    validate_and_resolve_personas(manifest, manifest_path, dir).map(Some)
}

pub(crate) fn validate_and_resolve_personas(
    manifest: Manifest,
    manifest_path: PathBuf,
    manifest_dir: PathBuf,
) -> Result<ResolvedPersonaManifest, Vec<PersonaValidationError>> {
    let known_capabilities = known_persona_capabilities(&manifest, &manifest_dir);
    let known_tools = known_persona_tools(&manifest);
    let known_names: BTreeSet<String> = manifest
        .personas
        .iter()
        .filter_map(|persona| persona.name.as_ref())
        .filter(|name| !name.trim().is_empty())
        .cloned()
        .collect();
    let context = harn_modules::personas::PersonaValidationContext {
        known_capabilities,
        known_tools,
        known_names,
    };
    if let Err(errors) = harn_modules::personas::validate_persona_manifests(
        &manifest_path,
        &manifest.personas,
        &context,
    ) {
        Err(errors)
    } else {
        let mut personas = manifest.personas;
        attach_entry_workflow_steps(&mut personas, &manifest_dir);
        Ok(ResolvedPersonaManifest {
            manifest_path,
            manifest_dir,
            personas,
        })
    }
}

fn attach_entry_workflow_steps(personas: &mut [PersonaManifestEntry], manifest_dir: &Path) {
    for persona in personas {
        if !persona.steps.is_empty() {
            continue;
        }
        let Some(entry_workflow) = persona.entry_workflow.as_deref() else {
            continue;
        };
        let Some((path, entry_name)) = entry_workflow.split_once('#') else {
            continue;
        };
        if !path.ends_with(".harn") {
            continue;
        }
        let source_path = manifest_dir.join(path);
        let Ok(document) = harn_modules::personas::parse_persona_source_file(&source_path) else {
            continue;
        };
        let entry_name = entry_name.trim();
        if let Some(source_persona) = document.personas.iter().find(|candidate| {
            candidate.entry_workflow.as_deref() == Some(entry_name)
                || candidate.name.as_deref() == persona.name.as_deref()
        }) {
            persona.steps.clone_from(&source_persona.steps);
        }
    }
}

pub(crate) fn known_persona_capabilities(
    manifest: &Manifest,
    manifest_dir: &Path,
) -> BTreeSet<String> {
    let mut capabilities = BTreeSet::new();
    for (capability, operations) in default_persona_capability_map() {
        for operation in operations {
            capabilities.insert(format!("{capability}.{operation}"));
        }
    }
    for (capability, operations) in &manifest.check.host_capabilities {
        for operation in operations {
            capabilities.insert(format!("{capability}.{operation}"));
        }
    }
    if let Some(path) = manifest.check.host_capabilities_path.as_deref() {
        let path = PathBuf::from(path);
        let path = if path.is_absolute() {
            path
        } else {
            manifest_dir.join(path)
        };
        if let Ok(content) = fs::read_to_string(path) {
            let parsed_json = serde_json::from_str::<serde_json::Value>(&content).ok();
            let parsed_toml = toml::from_str::<toml::Value>(&content)
                .ok()
                .and_then(|value| serde_json::to_value(value).ok());
            if let Some(value) = parsed_json.or(parsed_toml) {
                collect_persona_capabilities_from_json(&value, &mut capabilities);
            }
        }
    }
    capabilities
}

pub(crate) fn collect_persona_capabilities_from_json(
    value: &serde_json::Value,
    out: &mut BTreeSet<String>,
) {
    let root = value.get("capabilities").unwrap_or(value);
    let Some(capabilities) = root.as_object() else {
        return;
    };
    for (capability, entry) in capabilities {
        if let Some(list) = entry.as_array() {
            for item in list {
                if let Some(operation) = item.as_str() {
                    out.insert(format!("{capability}.{operation}"));
                }
            }
        } else if let Some(obj) = entry.as_object() {
            if let Some(list) = obj
                .get("operations")
                .or_else(|| obj.get("ops"))
                .and_then(|v| v.as_array())
            {
                for item in list {
                    if let Some(operation) = item.as_str() {
                        out.insert(format!("{capability}.{operation}"));
                    }
                }
            } else {
                for (operation, enabled) in obj {
                    if enabled.as_bool().unwrap_or(true) {
                        out.insert(format!("{capability}.{operation}"));
                    }
                }
            }
        }
    }
}

pub(crate) fn default_persona_capability_map() -> BTreeMap<&'static str, Vec<&'static str>> {
    harn_modules::personas::default_persona_capability_map()
}

pub(crate) fn known_persona_tools(manifest: &Manifest) -> BTreeSet<String> {
    let mut tools = BTreeSet::from([
        "a2a".to_string(),
        "acp".to_string(),
        "ci".to_string(),
        "filesystem".to_string(),
        "github".to_string(),
        "linear".to_string(),
        "mcp".to_string(),
        "notion".to_string(),
        "pagerduty".to_string(),
        "shell".to_string(),
        "slack".to_string(),
    ]);
    for server in &manifest.mcp {
        tools.insert(server.name.clone());
    }
    for provider in &manifest.providers {
        tools.insert(provider.id.as_str().to_string());
    }
    for trigger in &manifest.triggers {
        if let Some(provider) = trigger.provider.as_ref() {
            tools.insert(provider.as_str().to_string());
        }
        for source in &trigger.sources {
            tools.insert(source.provider.as_str().to_string());
        }
    }
    tools
}

#[cfg(test)]
#[path = "extensions_tests.rs"]
mod tests;
