use crate::ingest::hex_hash;
use crate::models::*;
use crate::source_meta;
use anyhow::{Context, Result, bail};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Component, Path, PathBuf};

pub fn write_bundle(bundle: &SkillBundle, out: &Path) -> Result<()> {
    fs::create_dir_all(out.join("skills"))?;
    fs::create_dir_all(out.join("sources"))?;
    fs::create_dir_all(out.join("graph"))?;
    fs::create_dir_all(out.join("audit"))?;
    write_yaml(&out.join("package.yaml"), &bundle.package)?;
    write_yaml(&out.join("sources/index.yaml"), &bundle.sources)?;
    write_yaml(&out.join("sources/sections.yaml"), &bundle.sections)?;
    write_yaml(&out.join("candidates.yaml"), &bundle.capability_candidates)?;
    write_yaml(&out.join("graph/concepts.yaml"), &bundle.graph.concepts)?;
    write_yaml(
        &out.join("graph/dependencies.yaml"),
        &bundle.graph.dependencies,
    )?;
    write_yaml(&out.join("audit/events.yaml"), &bundle.audit_events)?;
    write_yaml(&out.join("forge_requests.yaml"), &bundle.forge_requests)?;
    write_yaml(&out.join("forge_responses.yaml"), &bundle.forge_responses)?;
    if !bundle.forge_requests.is_empty() || !bundle.forge_responses.is_empty() {
        let summary = crate::forge::summarize_forge_history(bundle);
        write_yaml(&out.join("forge_summary.yaml"), &summary)?;
        fs::write(
            out.join("forge_summary.md"),
            crate::forge::forge_summary_markdown(&summary),
        )?;
    }
    for skill in &bundle.skills {
        write_yaml(
            &out.join("skills").join(format!("{}.yaml", skill.id)),
            skill,
        )?;
    }
    fs::write(
        out.join("README.md"),
        format!("# {}\n\nGenerated by Skiller Rust.\n", bundle.package.name),
    )?;
    write_manifest(out)?;
    Ok(())
}

pub fn read_bundle(path: &Path) -> Result<SkillBundle> {
    let package: SkillPackage = read_yaml(&path.join("package.yaml"))?;
    let sources: Vec<SourceDocument> =
        read_yaml(&path.join("sources/index.yaml")).unwrap_or_default();
    let sections: Vec<DocumentSection> =
        read_yaml(&path.join("sources/sections.yaml")).unwrap_or_default();
    let concepts: Vec<ConceptNode> =
        read_yaml(&path.join("graph/concepts.yaml")).unwrap_or_default();
    let dependencies: Vec<SkillDependency> =
        read_yaml(&path.join("graph/dependencies.yaml")).unwrap_or_default();
    let audit_events: Vec<AuditEvent> =
        read_yaml(&path.join("audit/events.yaml")).unwrap_or_default();
    let capability_candidates: Vec<CapabilityCandidate> =
        read_yaml(&path.join("candidates.yaml")).unwrap_or_default();
    let forge_requests: Vec<ForgeRequestEnvelope> =
        read_yaml(&path.join("forge_requests.yaml")).unwrap_or_default();
    let forge_responses: Vec<ForgeResponseEnvelope> =
        read_yaml(&path.join("forge_responses.yaml")).unwrap_or_default();
    let mut skills = Vec::new();
    for entry in fs::read_dir(path.join("skills")).with_context(|| "read skills directory")? {
        let entry = entry?;
        if entry.path().extension().and_then(|s| s.to_str()) == Some("yaml") {
            skills.push(read_yaml(&entry.path())?);
        }
    }
    Ok(SkillBundle {
        package,
        sources,
        sections,
        capability_candidates,
        skills,
        graph: SkillGraph {
            dependencies,
            related: vec![],
            concepts,
        },
        audit_events,
        forge_requests,
        forge_responses,
    })
}

#[derive(Debug, Serialize)]
pub struct ValidationReport {
    pub valid: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
}
pub fn validate_bundle(bundle: &SkillBundle) -> ValidationReport {
    let mut errors = Vec::new();
    let mut warnings = Vec::new();
    if bundle.skills.is_empty() {
        errors.push("bundle contains no skills".into());
    }
    let section_ids: std::collections::BTreeSet<_> = bundle
        .sections
        .iter()
        .map(|s| s.section_id.as_str())
        .collect();
    let section_sources: std::collections::BTreeMap<_, _> = bundle
        .sections
        .iter()
        .map(|s| (s.section_id.as_str(), s.source_id.as_str()))
        .collect();
    let source_ids: std::collections::BTreeSet<_> = bundle
        .sources
        .iter()
        .map(|s| s.source_id.as_str())
        .collect();
    for source in &bundle.sources {
        if let PermissionStatus::Blocked(reason) = &source.permission_status {
            errors.push(format!(
                "source {} permission blocked: {}",
                source.source_id, reason
            ));
        }
        if let ScanStatus::Findings(findings) = &source.secret_scan_status {
            errors.push(format!(
                "source {} has unresolved secret-scan findings: {}",
                source.source_id,
                findings.join(", ")
            ));
        }
        if matches!(source.retention_policy, RetentionPolicy::DeleteAfterCompile)
            && bundle.sections.iter().any(|section| {
                section.source_id == source.source_id && !section.text_excerpt.trim().is_empty()
            })
        {
            errors.push(format!(
                "source {} requires delete-after-compile but retained section excerpts",
                source.source_id
            ));
        }
    }
    let mut seen_skill_ids = std::collections::BTreeSet::new();
    if let Err(err) = crate::forge::validate_stored_forge(bundle) {
        errors.push(err.to_string());
    }
    for skill in &bundle.skills {
        if !seen_skill_ids.insert(skill.id.as_str()) {
            errors.push(format!("duplicate skill id {}", skill.id));
        }
        if skill.citations.is_empty() {
            errors.push(format!("{} has no citations", skill.id));
        }
        if crate::security::contains_secret_like_content(
            &serde_yaml::to_string(skill).unwrap_or_default(),
        ) {
            errors.push(format!("{} contains secret-like content", skill.id));
        }
        for sid in &skill.source_section_ids {
            if !section_ids.contains(sid.as_str()) {
                errors.push(format!("{} references missing section {}", skill.id, sid));
            }
        }
        for citation in &skill.citations {
            if !source_ids.contains(citation.source_id.as_str()) {
                errors.push(format!(
                    "{} citation {} references missing source {}",
                    skill.id, citation.citation_id, citation.source_id
                ));
            }
            match section_sources.get(citation.section_id.as_str()) {
                Some(section_source) if *section_source == citation.source_id.as_str() => {}
                Some(section_source) => errors.push(format!(
                    "{} citation {} source {} does not match section {} source {}",
                    skill.id,
                    citation.citation_id,
                    citation.source_id,
                    citation.section_id,
                    section_source
                )),
                None => errors.push(format!(
                    "{} citation {} references missing section {}",
                    skill.id, citation.citation_id, citation.section_id
                )),
            }
        }
        if let Err(err) = crate::forge::validate_confidence_breakdown(
            &format!("{} confidence", skill.id),
            &skill.confidence,
        ) {
            errors.push(err.to_string());
        }
        if let Err(err) = crate::forge::validate_evidence_breakdown(
            &format!("{} evidence_breakdown", skill.id),
            &skill.evidence_breakdown,
        ) {
            errors.push(err.to_string());
        }
        for role in &skill.role_suitability {
            if let Err(err) = crate::forge::validate_probability(
                &format!("{} role_suitability {}", skill.id, role.role),
                role.suitability,
            ) {
                errors.push(err.to_string());
            }
        }
        if let Err(err) = crate::forge::validate_probability(
            &format!("{} version_confidence", skill.id),
            skill.version_applicability.version_confidence,
        ) {
            errors.push(err.to_string());
        }
        for version_ref in &skill.version_applicability.version_source_refs {
            if !section_ids.contains(version_ref.as_str()) {
                errors.push(format!(
                    "{} version applicability references missing section {}",
                    skill.id, version_ref
                ));
            }
        }
        for record in &skill.inference_records {
            if let Err(err) = crate::forge::validate_probability(
                &format!(
                    "{} inference record {} confidence",
                    skill.id, record.inference_id
                ),
                record.confidence,
            ) {
                errors.push(err.to_string());
            }
            for sid in &record.source_refs_used {
                if !section_ids.contains(sid.as_str()) {
                    errors.push(format!(
                        "{} inference record {} references missing section {}",
                        skill.id, record.inference_id, sid
                    ));
                }
            }
            for candidate_id in &record.candidate_ids_used {
                if bundle
                    .capability_candidates
                    .iter()
                    .all(|candidate| candidate.candidate_id != *candidate_id)
                    && bundle.skills.iter().all(|skill| skill.id != *candidate_id)
                {
                    errors.push(format!(
                        "{} inference record {} references unknown candidate {}",
                        skill.id, record.inference_id, candidate_id
                    ));
                }
            }
        }
        if skill.evidence_breakdown.speculative_candidate > 0.4
            && skill.status == SkillStatus::Published
        {
            errors.push(format!(
                "{} is published with high speculative evidence",
                skill.id
            ));
        }
        if skill.runtime_policy.modify_external_systems
            && !skill.runtime_policy.requires_user_approval
        {
            errors.push(format!(
                "{} can modify external systems without approval",
                skill.id
            ));
        }
        if skill.confidence.raw < 0.35 {
            warnings.push(format!("{} has low confidence", skill.id));
        }
    }
    ValidationReport {
        valid: errors.is_empty(),
        errors,
        warnings,
    }
}

pub fn validate_bundle_path(path: &Path) -> Result<ValidationReport> {
    let bundle = read_bundle(path)?;
    let mut report = validate_bundle(&bundle);
    if let Err(err) = crate::forge::validate_forge_summary_artifacts(path, &bundle) {
        report.errors.push(err.to_string());
        report.valid = false;
    }
    Ok(report)
}

#[derive(Debug, Serialize)]
pub struct EvalReport {
    pub passed: bool,
    pub checks: Vec<String>,
    pub failures: Vec<String>,
    pub warnings: Vec<String>,
    pub skill_reports: Vec<SkillEvalReport>,
    pub total_eval_cases: usize,
    pub skills_without_evals: usize,
    pub safety_eval_count: usize,
    pub routing_eval_count: usize,
    pub source_grounding_eval_count: usize,
    pub tool_use_planning_eval_count: usize,
}

#[derive(Debug, Serialize)]
pub struct SkillEvalReport {
    pub skill_id: String,
    pub title: String,
    pub status: SkillStatus,
    pub maturity: SkillMaturity,
    pub eval_count: usize,
    pub eval_types: Vec<EvalType>,
    pub coverage: EvalCoverage,
    pub failures: Vec<String>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct EvalCoverage {
    pub has_positive: bool,
    pub has_negative: bool,
    pub has_safety: bool,
    pub has_routing: bool,
    pub has_tool_use_planning: bool,
    pub has_source_grounding: bool,
}

pub fn eval_bundle(bundle: &SkillBundle) -> EvalReport {
    let validation = validate_bundle(bundle);
    let mut failures = validation.errors;
    let mut warnings = validation.warnings;
    let mut skill_reports = Vec::new();
    let mut total_eval_cases = 0usize;
    let mut skills_without_evals = 0usize;
    let mut safety_eval_count = 0usize;
    let mut routing_eval_count = 0usize;
    let mut source_grounding_eval_count = 0usize;
    let mut tool_use_planning_eval_count = 0usize;

    for skill in &bundle.skills {
        let mut skill_failures = Vec::new();
        let mut skill_warnings = Vec::new();
        let mut seen_eval_ids = std::collections::BTreeSet::new();
        let mut eval_types = Vec::new();
        let mut coverage = EvalCoverage {
            has_positive: false,
            has_negative: false,
            has_safety: false,
            has_routing: false,
            has_tool_use_planning: false,
            has_source_grounding: false,
        };

        if skill.evals.is_empty() {
            skills_without_evals += 1;
            skill_failures.push("skill has no eval cases".to_string());
        }

        for eval in &skill.evals {
            total_eval_cases += 1;
            if !seen_eval_ids.insert(eval.id.as_str()) {
                skill_failures.push(format!("duplicate eval id {}", eval.id));
            }
            if eval.prompt.trim().is_empty() {
                skill_failures.push(format!("eval {} has empty prompt", eval.id));
            }
            if eval.expected_behavior.trim().is_empty() {
                skill_failures.push(format!("eval {} has empty expected_behavior", eval.id));
            }
            match eval.eval_type {
                EvalType::Positive => coverage.has_positive = true,
                EvalType::Negative => coverage.has_negative = true,
                EvalType::EdgeCase => {}
                EvalType::Safety => {
                    coverage.has_safety = true;
                    safety_eval_count += 1;
                }
                EvalType::Routing => {
                    coverage.has_routing = true;
                    routing_eval_count += 1;
                }
                EvalType::ToolUsePlanning => {
                    coverage.has_tool_use_planning = true;
                    tool_use_planning_eval_count += 1;
                }
                EvalType::SourceGrounding => {
                    coverage.has_source_grounding = true;
                    source_grounding_eval_count += 1;
                }
            }
            if !eval_types.iter().any(|kind| kind == &eval.eval_type) {
                eval_types.push(eval.eval_type.clone());
            }
        }

        let has_tool_requirements = !skill.tool_requirements.is_empty();
        let high_risk_tool = skill.tool_requirements.iter().any(|tool| {
            matches!(
                tool.permission_level,
                PermissionLevel::ExternalMutation | PermissionLevel::Dangerous
            ) || matches!(tool.requirement_type, ToolRequirementType::Dangerous)
        });
        let operational = skill.runtime_policy.modify_files
            || skill.runtime_policy.modify_external_systems
            || has_tool_requirements;

        if !coverage.has_routing {
            skill_warnings.push("missing routing eval".into());
        }
        if !coverage.has_source_grounding {
            skill_warnings.push("missing source-grounding eval".into());
        }
        if operational && !coverage.has_tool_use_planning {
            skill_warnings.push("operational skill missing tool-use-planning eval".into());
        }
        if (high_risk_tool || skill.runtime_policy.modify_external_systems) && !coverage.has_safety
        {
            skill_failures.push("high-risk skill missing safety eval".into());
        }
        if matches!(skill.status, SkillStatus::Approved | SkillStatus::Published)
            && (!coverage.has_source_grounding || !coverage.has_routing)
        {
            skill_failures.push(
                "approved/published skill must include routing and source-grounding evals".into(),
            );
        }
        if skill.confidence.eval >= 0.7 && skill.evals.len() < 2 {
            skill_warnings.push("high eval confidence with fewer than two eval cases".into());
        }

        for failure in &skill_failures {
            failures.push(format!("{}: {}", skill.id, failure));
        }
        for warning in &skill_warnings {
            warnings.push(format!("{}: {}", skill.id, warning));
        }
        skill_reports.push(SkillEvalReport {
            skill_id: skill.id.clone(),
            title: skill.title.clone(),
            status: skill.status.clone(),
            maturity: skill.maturity.clone(),
            eval_count: skill.evals.len(),
            eval_types,
            coverage,
            failures: skill_failures,
            warnings: skill_warnings,
        });
    }

    EvalReport {
        passed: failures.is_empty(),
        checks: vec![
            "schema/reference validation".into(),
            "citation presence".into(),
            "publication safety".into(),
            "eval case structural completeness".into(),
            "routing eval coverage".into(),
            "source-grounding eval coverage".into(),
            "high-risk safety eval coverage".into(),
            "tool-use planning eval coverage".into(),
        ],
        failures,
        warnings,
        skill_reports,
        total_eval_cases,
        skills_without_evals,
        safety_eval_count,
        routing_eval_count,
        source_grounding_eval_count,
        tool_use_planning_eval_count,
    }
}

#[derive(Debug, Serialize)]
pub struct ReadinessReport {
    pub ready: bool,
    pub blockers: Vec<String>,
    pub warnings: Vec<String>,
    pub skill_count: usize,
}
pub fn readiness_report(bundle: &SkillBundle) -> ReadinessReport {
    let validation = validate_bundle(bundle);
    let mut blockers = validation.errors;
    let mut warnings = validation.warnings;
    let public_publish_requested = bundle
        .package
        .compatibility
        .get("publish_visibility")
        .map(|visibility| visibility.eq_ignore_ascii_case("public"))
        .unwrap_or(false);
    for source in &bundle.sources {
        match &source.permission_status {
            PermissionStatus::Blocked(reason) => blockers.push(format!(
                "source {} permission blocked: {}",
                source.source_id, reason
            )),
            PermissionStatus::IndexOnly => warnings.push(format!(
                "source {} is index-only; publish should retain pointers and short excerpts only",
                source.source_id
            )),
            PermissionStatus::Allowed => {}
        }
        if let ScanStatus::Findings(findings) = &source.secret_scan_status {
            blockers.push(format!(
                "source {} has unresolved secret-scan findings: {}",
                source.source_id,
                findings.join(", ")
            ));
        }
        if public_publish_requested && !matches!(source.export_policy, ExportPolicy::PublicAllowed)
        {
            blockers.push(format!(
                "source {} export policy does not allow public publication",
                source.source_id
            ));
        }
        if matches!(source.export_policy, ExportPolicy::PrivateOnly) {
            warnings.push(format!(
                "source {} is private-only; keep registry/export scope private",
                source.source_id
            ));
        }
        if matches!(
            source.visibility,
            Visibility::Private | Visibility::Internal | Visibility::Restricted
        ) {
            warnings.push(format!(
                "source {} visibility is {:?}; avoid public export without explicit policy",
                source.source_id, source.visibility
            ));
        }
        if matches!(source.retention_policy, RetentionPolicy::DeleteAfterCompile) {
            blockers.push(format!(
                "source {} requires delete-after-compile retention handling before publication",
                source.source_id
            ));
        }
    }
    for s in &bundle.skills {
        match s.status {
            SkillStatus::Draft | SkillStatus::Candidate | SkillStatus::NeedsReview => {
                blockers.push(format!("{} is not reviewed/approved", s.id));
            }
            SkillStatus::Unsafe => {
                blockers.push(format!("{} is unsafe and cannot be published", s.id))
            }
            SkillStatus::Archived => {
                blockers.push(format!("{} is archived and cannot be published", s.id));
            }
            SkillStatus::Deprecated => {
                blockers.push(format!("{} is deprecated and cannot be published", s.id));
            }
            SkillStatus::Reviewed | SkillStatus::Approved | SkillStatus::Published => {}
        }

        let high_risk_tool = s.tool_requirements.iter().any(|tool| {
            matches!(
                tool.permission_level,
                PermissionLevel::ExternalMutation | PermissionLevel::Dangerous
            ) || matches!(tool.requirement_type, ToolRequirementType::Dangerous)
        });
        let high_risk_runtime = s.runtime_policy.modify_external_systems;
        if (high_risk_runtime || high_risk_tool)
            && (!matches!(s.status, SkillStatus::Approved | SkillStatus::Published)
                || s.maturity < SkillMaturity::Level4HumanApproved
                || s.confidence.human_review < 0.5)
        {
            blockers.push(format!(
                "{} is high-risk and requires human-approved Level4+ maturity before publication",
                s.id
            ));
        }

        if s.runtime_policy.modify_files && !s.runtime_policy.requires_backup_or_rollback {
            blockers.push(format!(
                "{} can modify files without backup or rollback requirement",
                s.id
            ));
        }

        if s.inference_records
            .iter()
            .any(|record| record.required_review)
            && !matches!(s.status, SkillStatus::Approved | SkillStatus::Published)
        {
            blockers.push(format!(
                "{} contains inference records requiring human review",
                s.id
            ));
        }
    }
    ReadinessReport {
        ready: blockers.is_empty(),
        blockers,
        warnings,
        skill_count: bundle.skills.len(),
    }
}

pub fn publish_bundle(bundle: &SkillBundle, registry: &Path, force: bool) -> Result<()> {
    let readiness = readiness_report(bundle);
    if !readiness.ready && !force {
        bail!("bundle is not publication-ready; rerun with --force only after review");
    }
    let dest = registry
        .join(&bundle.package.bundle_id)
        .join(&bundle.package.version);
    write_bundle(bundle, &dest)?;
    let content_manifest_hash = manifest_file_hash(&dest)?;
    let provenance =
        ProvenanceRecord::from_bundle(bundle, &readiness, force, content_manifest_hash);
    fs::write(
        dest.join("PROVENANCE.json"),
        serde_json::to_string_pretty(&provenance)?,
    )?;
    write_manifest(&dest)?;
    write_registry_index(registry)?;
    Ok(())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProvenanceRecord {
    pub bundle_id: String,
    pub bundle_name: String,
    pub version: String,
    pub domain: Option<String>,
    pub published_at: chrono::DateTime<Utc>,
    pub force: bool,
    pub readiness_ready: bool,
    pub readiness_blockers: Vec<String>,
    pub readiness_warnings: Vec<String>,
    pub content_manifest_hash: String,
    pub skill_count: usize,
    pub source_count: usize,
    pub reviewed_skill_count: usize,
    pub approved_skill_count: usize,
    pub published_skill_count: usize,
    pub high_risk_skill_count: usize,
    pub inference_record_count: usize,
    pub forge_request_count: usize,
    pub forge_response_count: usize,
    pub forge_provider_summary: std::collections::BTreeMap<String, usize>,
    pub forge_adapter_mode_summary: std::collections::BTreeMap<String, usize>,
    pub forge_live_reasoning_used: bool,
    pub source_trust_summary: std::collections::BTreeMap<String, usize>,
    pub source_rights_summary: std::collections::BTreeMap<String, usize>,
}

impl ProvenanceRecord {
    fn from_bundle(
        bundle: &SkillBundle,
        readiness: &ReadinessReport,
        force: bool,
        content_manifest_hash: String,
    ) -> Self {
        let reviewed_skill_count = bundle
            .skills
            .iter()
            .filter(|skill| matches!(skill.status, SkillStatus::Reviewed))
            .count();
        let approved_skill_count = bundle
            .skills
            .iter()
            .filter(|skill| matches!(skill.status, SkillStatus::Approved))
            .count();
        let published_skill_count = bundle
            .skills
            .iter()
            .filter(|skill| matches!(skill.status, SkillStatus::Published))
            .count();
        let high_risk_skill_count = bundle
            .skills
            .iter()
            .filter(|skill| {
                skill.runtime_policy.modify_external_systems
                    || skill.tool_requirements.iter().any(|tool| {
                        matches!(
                            tool.permission_level,
                            PermissionLevel::ExternalMutation | PermissionLevel::Dangerous
                        ) || matches!(tool.requirement_type, ToolRequirementType::Dangerous)
                    })
            })
            .count();
        let inference_record_count = bundle
            .skills
            .iter()
            .map(|skill| skill.inference_records.len())
            .sum();
        let mut forge_provider_summary = std::collections::BTreeMap::new();
        let mut forge_adapter_mode_summary = std::collections::BTreeMap::new();
        let mut forge_live_reasoning_used = false;
        for request in &bundle.forge_requests {
            if let Some(provenance) = &request.provider_provenance {
                *forge_provider_summary
                    .entry(provenance.provider.clone())
                    .or_insert(0) += 1;
                *forge_adapter_mode_summary
                    .entry(provenance.adapter_mode.clone())
                    .or_insert(0) += 1;
                forge_live_reasoning_used |= provenance.live_reasoning;
            } else {
                *forge_provider_summary
                    .entry(request.provider.clone())
                    .or_insert(0) += 1;
                *forge_adapter_mode_summary
                    .entry("unknown".to_string())
                    .or_insert(0) += 1;
            }
        }

        let mut source_trust_summary = std::collections::BTreeMap::new();
        let mut source_rights_summary = std::collections::BTreeMap::new();
        for source in &bundle.sources {
            *source_trust_summary
                .entry(format!("{:?}", source_meta::infer_source_trust(source)))
                .or_insert(0) += 1;
            *source_rights_summary
                .entry(format!("{:?}", source.export_policy))
                .or_insert(0) += 1;
        }
        Self {
            bundle_id: bundle.package.bundle_id.clone(),
            bundle_name: bundle.package.name.clone(),
            version: bundle.package.version.clone(),
            domain: bundle.package.domain.clone(),
            published_at: Utc::now(),
            force,
            readiness_ready: readiness.ready,
            readiness_blockers: readiness.blockers.clone(),
            readiness_warnings: readiness.warnings.clone(),
            content_manifest_hash,
            skill_count: bundle.skills.len(),
            source_count: bundle.sources.len(),
            reviewed_skill_count,
            approved_skill_count,
            published_skill_count,
            high_risk_skill_count,
            inference_record_count,
            forge_request_count: bundle.forge_requests.len(),
            forge_response_count: bundle.forge_responses.len(),
            forge_provider_summary,
            forge_adapter_mode_summary,
            forge_live_reasoning_used,
            source_trust_summary,
            source_rights_summary,
        }
    }
}

#[derive(Debug, Serialize)]
pub struct RegistryEntry {
    pub bundle_id: String,
    pub name: String,
    pub version: String,
    pub domain: Option<String>,
    pub path: PathBuf,
    pub skill_count: usize,
    pub source_count: Option<usize>,
    pub high_risk_skill_count: Option<usize>,
    pub inference_record_count: Option<usize>,
    pub forge_request_count: Option<usize>,
    pub forge_response_count: Option<usize>,
    pub forge_provider_summary: Option<std::collections::BTreeMap<String, usize>>,
    pub forge_adapter_mode_summary: Option<std::collections::BTreeMap<String, usize>>,
    pub forge_live_reasoning_used: Option<bool>,
    pub readiness_ready: Option<bool>,
    pub force_published: Option<bool>,
    pub content_manifest_hash: Option<String>,
    pub published_at: Option<String>,
    pub manifest_valid: bool,
    pub deprecated: bool,
    pub deprecation_reason: Option<String>,
    pub active_version: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct RegistryIndex {
    pub generated_at: chrono::DateTime<Utc>,
    pub registry_path: PathBuf,
    pub entries: Vec<RegistryEntry>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeprecationRecord {
    pub bundle_id: String,
    pub version: String,
    pub deprecated_at: chrono::DateTime<Utc>,
    pub reason: String,
    pub replacement_version: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RollbackRecord {
    pub bundle_id: String,
    pub active_version: String,
    pub rolled_back_at: chrono::DateTime<Utc>,
    pub reason: String,
}

#[derive(Debug, Serialize)]
pub struct ManifestVerificationReport {
    pub valid: bool,
    pub checked_files: usize,
    pub missing_files: Vec<String>,
    pub changed_files: Vec<String>,
    pub extra_files: Vec<String>,
    pub malformed_lines: Vec<String>,
}

pub fn list_registry(registry: &Path) -> Result<RegistryIndex> {
    let mut entries = Vec::new();
    if !registry.exists() {
        return Ok(RegistryIndex {
            generated_at: Utc::now(),
            registry_path: registry.to_path_buf(),
            entries,
        });
    }

    for bundle_dir in
        fs::read_dir(registry).with_context(|| format!("read {}", registry.display()))?
    {
        let bundle_dir = bundle_dir?;
        if !bundle_dir.file_type()?.is_dir() {
            continue;
        }
        for version_dir in fs::read_dir(bundle_dir.path())? {
            let version_dir = version_dir?;
            if !version_dir.file_type()?.is_dir() {
                continue;
            }
            let path = version_dir.path();
            let package_path = path.join("package.yaml");
            if !package_path.exists() {
                continue;
            }
            let package: SkillPackage = read_yaml(&package_path)?;
            let skill_count = fs::read_dir(path.join("skills"))
                .map(|entries| {
                    entries
                        .filter_map(Result::ok)
                        .filter(|entry| {
                            entry.path().extension().and_then(|s| s.to_str()) == Some("yaml")
                        })
                        .count()
                })
                .unwrap_or(0);
            let provenance_path = path.join("PROVENANCE.json");
            let provenance: Option<ProvenanceRecord> = fs::read_to_string(&provenance_path)
                .ok()
                .and_then(|text| serde_json::from_str::<ProvenanceRecord>(&text).ok());
            let published_at = provenance.as_ref().map(|record| {
                serde_json::Value::String(record.published_at.to_rfc3339()).to_string()
            });
            let manifest_valid = verify_manifest(&path)
                .map(|report| report.valid)
                .unwrap_or(false);
            let deprecation: Option<DeprecationRecord> =
                read_yaml(&path.join("DEPRECATION.yaml")).ok();
            let rollback: Option<RollbackRecord> =
                read_yaml(&bundle_dir.path().join("ROLLBACK.yaml")).ok();
            entries.push(RegistryEntry {
                bundle_id: package.bundle_id,
                name: package.name,
                version: package.version,
                domain: package.domain,
                path,
                skill_count,
                source_count: provenance.as_ref().map(|record| record.source_count),
                high_risk_skill_count: provenance
                    .as_ref()
                    .map(|record| record.high_risk_skill_count),
                inference_record_count: provenance
                    .as_ref()
                    .map(|record| record.inference_record_count),
                forge_request_count: provenance.as_ref().map(|record| record.forge_request_count),
                forge_response_count: provenance
                    .as_ref()
                    .map(|record| record.forge_response_count),
                forge_provider_summary: provenance
                    .as_ref()
                    .map(|record| record.forge_provider_summary.clone()),
                forge_adapter_mode_summary: provenance
                    .as_ref()
                    .map(|record| record.forge_adapter_mode_summary.clone()),
                forge_live_reasoning_used: provenance
                    .as_ref()
                    .map(|record| record.forge_live_reasoning_used),
                readiness_ready: provenance.as_ref().map(|record| record.readiness_ready),
                force_published: provenance.as_ref().map(|record| record.force),
                content_manifest_hash: provenance
                    .as_ref()
                    .map(|record| record.content_manifest_hash.clone()),
                published_at,
                manifest_valid,
                deprecated: deprecation.is_some(),
                deprecation_reason: deprecation.map(|d| d.reason),
                active_version: rollback.map(|r| r.active_version),
            });
        }
    }
    entries.sort_by(|a, b| {
        a.bundle_id
            .cmp(&b.bundle_id)
            .then(a.version.cmp(&b.version))
            .then(a.path.cmp(&b.path))
    });
    Ok(RegistryIndex {
        generated_at: Utc::now(),
        registry_path: registry.to_path_buf(),
        entries,
    })
}

pub fn write_registry_index(registry: &Path) -> Result<RegistryIndex> {
    fs::create_dir_all(registry)?;
    let index = list_registry(registry)?;
    write_yaml(&registry.join("index.yaml"), &index)?;
    Ok(index)
}

pub fn deprecate_registry_entry(
    registry: &Path,
    bundle_id: &str,
    version: &str,
    reason: &str,
    replacement_version: Option<&str>,
) -> Result<DeprecationRecord> {
    let entry = registry.join(bundle_id).join(version);
    if !entry.join("package.yaml").exists() {
        bail!("registry entry not found: {}/{}", bundle_id, version);
    }
    let record = DeprecationRecord {
        bundle_id: bundle_id.to_string(),
        version: version.to_string(),
        deprecated_at: Utc::now(),
        reason: reason.to_string(),
        replacement_version: replacement_version.map(str::to_string),
    };
    write_yaml(&entry.join("DEPRECATION.yaml"), &record)?;
    write_manifest(&entry)?;
    write_registry_index(registry)?;
    Ok(record)
}

pub fn rollback_registry_entry(
    registry: &Path,
    bundle_id: &str,
    to_version: &str,
    reason: &str,
) -> Result<RollbackRecord> {
    let bundle_root = registry.join(bundle_id);
    let target = bundle_root.join(to_version);
    if !target.join("package.yaml").exists() {
        bail!("rollback target not found: {}/{}", bundle_id, to_version);
    }
    let record = RollbackRecord {
        bundle_id: bundle_id.to_string(),
        active_version: to_version.to_string(),
        rolled_back_at: Utc::now(),
        reason: reason.to_string(),
    };
    write_yaml(&bundle_root.join("ROLLBACK.yaml"), &record)?;
    write_registry_index(registry)?;
    Ok(record)
}

pub fn verify_manifest(root: &Path) -> Result<ManifestVerificationReport> {
    let manifest_path = root.join("MANIFEST.sha256");
    let manifest = fs::read_to_string(&manifest_path)
        .with_context(|| format!("read {}", manifest_path.display()))?;
    let mut checked_files = 0usize;
    let mut missing_files = Vec::new();
    let mut changed_files = Vec::new();
    let mut malformed_lines = Vec::new();
    let mut expected_paths = std::collections::BTreeSet::new();

    for line in manifest.lines() {
        if line.trim().is_empty() {
            continue;
        }
        let Some((hash, rel)) = line.split_once("  ") else {
            malformed_lines.push(line.to_string());
            continue;
        };
        if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
            malformed_lines.push(line.to_string());
            continue;
        }
        let rel_path = PathBuf::from(rel);
        if !is_safe_manifest_relative_path(&rel_path) {
            malformed_lines.push(line.to_string());
            continue;
        }
        expected_paths.insert(rel_path.clone());
        let file_path = root.join(&rel_path);
        if !file_path.exists() {
            missing_files.push(rel.to_string());
            continue;
        }
        let bytes = fs::read(&file_path)?;
        checked_files += 1;
        if hex_hash(&bytes) != hash {
            changed_files.push(rel.to_string());
        }
    }

    let mut extra_files = Vec::new();
    for entry in walkdir::WalkDir::new(root)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|e| e.file_type().is_file())
    {
        let path = entry.path();
        let rel = path.strip_prefix(root).unwrap().to_path_buf();
        if rel.as_path() == std::path::Path::new("MANIFEST.sha256") {
            continue;
        }
        if !expected_paths.contains(&rel) {
            extra_files.push(rel.display().to_string());
        }
    }
    extra_files.sort();

    Ok(ManifestVerificationReport {
        valid: missing_files.is_empty()
            && changed_files.is_empty()
            && extra_files.is_empty()
            && malformed_lines.is_empty(),
        checked_files,
        missing_files,
        changed_files,
        extra_files,
        malformed_lines,
    })
}

fn is_safe_manifest_relative_path(path: &Path) -> bool {
    !path.as_os_str().is_empty()
        && !path.is_absolute()
        && path
            .components()
            .all(|component| matches!(component, Component::Normal(_)))
}

fn manifest_file_hash(root: &Path) -> Result<String> {
    let path = root.join("MANIFEST.sha256");
    let bytes = fs::read(&path).with_context(|| format!("read {}", path.display()))?;
    Ok(hex_hash(&bytes))
}

fn write_yaml<T: Serialize>(path: &Path, value: &T) -> Result<()> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, serde_yaml::to_string(value)?)?;
    Ok(())
}
fn read_yaml<T: for<'de> serde::Deserialize<'de>>(path: &Path) -> Result<T> {
    Ok(serde_yaml::from_str(
        &fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?,
    )?)
}
fn write_manifest(root: &Path) -> Result<()> {
    let mut lines = Vec::new();
    for entry in walkdir::WalkDir::new(root)
        .into_iter()
        .filter_map(Result::ok)
        .filter(|e| e.file_type().is_file())
    {
        let path = entry.path();
        if path.file_name().and_then(|s| s.to_str()) == Some("MANIFEST.sha256") {
            continue;
        }
        let rel = path.strip_prefix(root).unwrap();
        let bytes = fs::read(path)?;
        lines.push(format!("{}  {}", hex_hash(&bytes), rel.display()));
    }
    lines.sort();
    fs::write(root.join("MANIFEST.sha256"), lines.join("\n"))?;
    Ok(())
}
