//! `beck check`: the diagnostic command.
//!
//! Read-only by default. Walks every detected adapter's target root,
//! classifies each file under it as `BeckManaged`, `Foreign`, or
//! `Orphan`, flags case-insensitive collisions in
//! `<beck_home>/skills/`, and reports manifest health.
//!
//! Two opt-in mutations:
//! - `--rebuild-manifest` scans disk for every adapter-managed file
//!   and writes a fresh manifest, atomically.
//! - `--prune` loads the current manifest and drops any entry whose
//!   target file is gone.
//!
//! Both flags go through `Manifest::save`, which is the same atomic
//! rename beck has used since Phase 1. No destructive file operations
//! happen here, only manifest edits.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde::Serialize;

use beck::agents::adapter::Adapter;
use beck::agents::manifest::{Entry, Manifest};
use beck::agents::paths::{manifest_path, skills_home};
use beck::agents::registry;
use beck::agents::skill::Skill;
use beck::error::{CliError, Result};

#[derive(Debug, Clone)]
pub struct CheckArgs {
    pub rebuild_manifest: bool,
    pub prune: bool,
}

#[derive(Debug, Default, Serialize, PartialEq)]
pub struct CheckReport {
    pub adapters_detected: Vec<String>,
    pub adapters_missing: Vec<String>,
    pub beck_managed: u32,
    pub foreign: Vec<ForeignFile>,
    pub orphans: Vec<Entry>,
    pub collisions: Vec<Collision>,
    pub manifest_health: ManifestHealth,
    pub mutations: Vec<String>,
}

#[derive(Debug, Serialize, PartialEq)]
pub struct ForeignFile {
    pub path: PathBuf,
    pub agent: String,
}

#[derive(Debug, Serialize, PartialEq)]
pub struct Collision {
    pub kind: CollisionKind,
    pub paths: Vec<PathBuf>,
}

#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CollisionKind {
    CaseInsensitive,
}

#[derive(Debug, Serialize, PartialEq)]
#[serde(tag = "kind", content = "detail")]
pub enum ManifestHealth {
    Ok,
    Missing,
    Corrupt(String),
    VersionUnsupported(u32),
}

impl Default for ManifestHealth {
    fn default() -> Self {
        ManifestHealth::Ok
    }
}

pub async fn handle(rebuild_manifest: bool, prune: bool, json: bool) -> Result<()> {
    let adapters = registry::all_adapters();
    let skills_root = skills_home()?;
    let manifest_file = manifest_path()?;
    let report = run_check(
        CheckArgs {
            rebuild_manifest,
            prune,
        },
        &skills_root,
        &manifest_file,
        adapters.iter().map(|a| a.as_ref()).collect::<Vec<_>>(),
    )?;

    if json {
        println!(
            "{}",
            serde_json::to_string_pretty(&report).unwrap_or_default()
        );
    } else {
        print_human_report(&report);
    }
    Ok(())
}

pub fn run_check(
    args: CheckArgs,
    skills_root: &Path,
    manifest_file: &Path,
    adapters: Vec<&dyn Adapter>,
) -> Result<CheckReport> {
    let mut report = CheckReport::default();

    // Split adapters into detected vs missing so the report can list
    // both. Detection is allowed to read the filesystem.
    for adapter in &adapters {
        if adapter.detect() {
            report.adapters_detected.push(adapter.name().into());
        } else {
            report.adapters_missing.push(adapter.name().into());
        }
    }

    // Load the manifest. Missing and corrupt states are NOT fatal.
    // Version-mismatch is fatal only if the caller asked for a
    // mutation flag (`--prune` or `--rebuild-manifest`).
    let mut manifest = match Manifest::load(manifest_file) {
        Ok(m) => {
            report.manifest_health = ManifestHealth::Ok;
            m
        }
        Err(CliError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
            report.manifest_health = ManifestHealth::Missing;
            Manifest::empty()
        }
        Err(CliError::Validation(msg)) => {
            // `Manifest::load` folds both corrupt json and an unknown
            // `schema_version` into `CliError::Validation`. We split
            // them back apart here by inspecting the message, so
            // mutation flags can refuse on version mismatch without
            // blocking on plain corruption.
            report.manifest_health = if msg.contains("unsupported")
                && let Some(n) = parse_version_from_error(&msg)
            {
                ManifestHealth::VersionUnsupported(n)
            } else {
                ManifestHealth::Corrupt(msg)
            };
            Manifest::empty()
        }
        Err(other) => return Err(other),
    };

    // Case-insensitive collision detection on the source skills. APFS
    // is case-insensitive by default, so `caveman/` and `Caveman/`
    // collide on disk; beck needs to refuse installing both, and
    // `check` is the place to surface it.
    if skills_root.exists() {
        match detect_case_collisions(skills_root) {
            Ok(hits) => report.collisions.extend(hits),
            Err(_) => {
                // Walking the skills home is best-effort.
            }
        }
    }

    // For each detected adapter, walk its target_root.
    // `list_managed()` returns the subset that beck owns. Every other
    // file at that root is foreign.
    for adapter in &adapters {
        if !adapter.detect() {
            continue;
        }

        let target_root = match adapter.target_root() {
            Ok(r) => r,
            Err(_) => continue,
        };

        let managed = match adapter.list_managed() {
            Ok(m) => m,
            Err(_) => Vec::new(),
        };
        report.beck_managed += managed.len() as u32;

        if target_root.exists() {
            let foreign = walk_foreign(&target_root, &managed, adapter.name());
            report.foreign.extend(foreign);
        }

        // Orphans: manifest entries for this adapter whose target is gone.
        for entry in manifest.entries.iter() {
            if entry.agent != adapter.name() {
                continue;
            }
            if !entry.target.exists() {
                report.orphans.push(entry.clone());
            }
        }
    }

    // Mutations happen after the read-only pass so the report still
    // reflects disk state before the change.
    if args.rebuild_manifest {
        if matches!(
            report.manifest_health,
            ManifestHealth::VersionUnsupported(_)
        ) {
            return Err(CliError::Validation(
                "refusing to rebuild manifest on unsupported schema version".into(),
            ));
        }
        let rebuilt = rebuild_manifest_from_disk(&adapters)?;
        rebuilt.save(manifest_file)?;
        report
            .mutations
            .push(format!("rebuilt manifest with {} entries", rebuilt.entries.len()));
        manifest = rebuilt;
    }

    if args.prune {
        if matches!(
            report.manifest_health,
            ManifestHealth::VersionUnsupported(_)
        ) {
            return Err(CliError::Validation(
                "refusing to prune manifest on unsupported schema version".into(),
            ));
        }
        let before = manifest.entries.len();
        manifest.entries.retain(|e| e.target.exists());
        let after = manifest.entries.len();
        if before != after {
            manifest.save(manifest_file)?;
            report
                .mutations
                .push(format!("pruned {} orphan entries", before - after));
        } else {
            report.mutations.push("nothing to prune".into());
        }
    }

    Ok(report)
}

/// Extract the first `vN` token from a `Manifest::load` error string
/// like "manifest schema v999 unsupported, beck only knows v1".
fn parse_version_from_error(msg: &str) -> Option<u32> {
    for token in msg.split_whitespace() {
        if let Some(rest) = token.strip_prefix('v')
            && let Ok(n) = rest.trim_end_matches(',').parse::<u32>()
            && n != 1
        {
            return Some(n);
        }
    }
    None
}

fn walk_foreign(
    target_root: &Path,
    managed: &[PathBuf],
    agent: &str,
) -> Vec<ForeignFile> {
    let mut out = Vec::new();
    let managed_set: std::collections::HashSet<&PathBuf> = managed.iter().collect();

    let entries = match std::fs::read_dir(target_root) {
        Ok(e) => e,
        Err(_) => return out,
    };
    for entry in entries.flatten() {
        let Ok(ft) = entry.file_type() else {
            continue;
        };
        if !ft.is_dir() {
            continue;
        }
        let candidate = entry.path().join("SKILL.md");
        if !candidate.exists() && std::fs::symlink_metadata(&candidate).is_err() {
            continue;
        }
        if managed_set.contains(&candidate) {
            continue;
        }
        out.push(ForeignFile {
            path: candidate,
            agent: agent.to_string(),
        });
    }
    out.sort_by(|a, b| a.path.cmp(&b.path));
    out
}

fn detect_case_collisions(skills_root: &Path) -> Result<Vec<Collision>> {
    let mut buckets: BTreeMap<String, Vec<PathBuf>> = BTreeMap::new();

    let entries = std::fs::read_dir(skills_root)?;
    for entry in entries.flatten() {
        if !entry.file_type()?.is_dir() {
            continue;
        }
        let name = entry.file_name();
        let key = name.to_string_lossy().to_lowercase();
        buckets.entry(key).or_default().push(entry.path());
    }

    let mut collisions = Vec::new();
    for (_key, paths) in buckets {
        if paths.len() > 1 {
            let mut sorted = paths;
            sorted.sort();
            collisions.push(Collision {
                kind: CollisionKind::CaseInsensitive,
                paths: sorted,
            });
        }
    }
    Ok(collisions)
}

fn rebuild_manifest_from_disk(adapters: &[&dyn Adapter]) -> Result<Manifest> {
    let mut out = Manifest::empty();
    for adapter in adapters {
        if !adapter.detect() {
            continue;
        }
        let managed = match adapter.list_managed() {
            Ok(m) => m,
            Err(_) => continue,
        };
        for target in managed {
            match adapter.rebuild_entry(&target) {
                Ok(entry) => out.add(entry),
                Err(_) => continue,
            }
        }
    }
    // Skill::discover_in is unused here but keeps the import live for
    // future phases that want to cross-check against the source side.
    let _ = std::marker::PhantomData::<Skill>;
    Ok(out)
}

fn print_human_report(report: &CheckReport) {
    println!(
        "detected agents: {}",
        if report.adapters_detected.is_empty() {
            "none".to_string()
        } else {
            report.adapters_detected.join(", ")
        }
    );
    if !report.adapters_missing.is_empty() {
        println!("missing agents: {}", report.adapters_missing.join(", "));
    }

    match &report.manifest_health {
        ManifestHealth::Ok => println!("manifest: ok"),
        ManifestHealth::Missing => println!("manifest: missing (run beck bootstrap)"),
        ManifestHealth::Corrupt(msg) => println!("manifest: corrupt ({msg})"),
        ManifestHealth::VersionUnsupported(v) => {
            println!("manifest: version {v} unsupported, upgrade beck")
        }
    }

    println!("beck-managed files: {}", report.beck_managed);

    if !report.foreign.is_empty() {
        println!("foreign files ({}):", report.foreign.len());
        for f in &report.foreign {
            println!("  {} ({})", f.path.display(), f.agent);
        }
    }

    if !report.orphans.is_empty() {
        println!("orphan manifest entries ({}):", report.orphans.len());
        for o in &report.orphans {
            println!("  {}/{} -> {}", o.skill, o.agent, o.target.display());
        }
        println!("  hint: run `beck check --prune` to drop them");
    }

    if !report.collisions.is_empty() {
        println!("case-insensitive collisions ({}):", report.collisions.len());
        for c in &report.collisions {
            for p in &c.paths {
                println!("  {}", p.display());
            }
        }
    }

    for m in &report.mutations {
        println!("mutation: {m}");
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    use std::fs;
    use std::sync::atomic::{AtomicUsize, Ordering};

    use beck::agents::adapter::InstallPlan;
    use beck::agents::manifest::InstallMode;

    static COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn tempdir(name: &str) -> PathBuf {
        let base = std::env::temp_dir().join(format!(
            "beck-check-tests-{name}-{}-{}",
            std::process::id(),
            COUNTER.fetch_add(1, Ordering::SeqCst)
        ));
        let _ = fs::remove_dir_all(&base);
        fs::create_dir_all(&base).unwrap();
        base
    }

    fn write_source(skills_root: &Path, name: &str, body: &str) -> PathBuf {
        let dir = skills_root.join(name);
        fs::create_dir_all(&dir).unwrap();
        let path = dir.join("SKILL.md");
        fs::write(&path, body).unwrap();
        path
    }

    struct MockAdapter {
        name: &'static str,
        target_root: PathBuf,
        detect_ok: bool,
        managed: RefCell<Vec<PathBuf>>,
    }

    unsafe impl Send for MockAdapter {}
    unsafe impl Sync for MockAdapter {}

    impl Adapter for MockAdapter {
        fn name(&self) -> &'static str {
            self.name
        }
        fn detect(&self) -> bool {
            self.detect_ok
        }
        fn target_root(&self) -> Result<PathBuf> {
            Ok(self.target_root.clone())
        }
        fn plan(&self, skill: &Skill) -> Result<InstallPlan> {
            Ok(InstallPlan {
                source: skill.source_path.clone(),
                target: self.target_root.join(&skill.name).join("SKILL.md"),
                mode: InstallMode::Symlink,
                transform: None,
            })
        }
        fn install(&self, _plan: &InstallPlan) -> Result<Entry> {
            unreachable!("install not used in check tests")
        }
        fn uninstall(&self, _entry: &Entry) -> Result<()> {
            Ok(())
        }
        fn list_managed(&self) -> Result<Vec<PathBuf>> {
            Ok(self.managed.borrow().clone())
        }
        fn rebuild_entry(&self, target: &Path) -> Result<Entry> {
            let skill_name = target
                .parent()
                .and_then(|p| p.file_name())
                .and_then(|n| n.to_str())
                .unwrap_or("")
                .to_string();
            Ok(Entry {
                skill: skill_name,
                agent: self.name.to_string(),
                target: target.to_path_buf(),
                mode: InstallMode::Symlink,
                sha256: "deadbeef".into(),
                installed_at: "2026-04-11T00:00:00Z".into(),
            })
        }
    }

    fn plant_skill_file(target_root: &Path, name: &str, body: &str) -> PathBuf {
        let dir = target_root.join(name);
        fs::create_dir_all(&dir).unwrap();
        let path = dir.join("SKILL.md");
        fs::write(&path, body).unwrap();
        path
    }

    #[test]
    fn detected_and_missing_adapters_land_in_report() {
        let beck = tempdir("check-detect");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");

        let good = MockAdapter {
            name: "good",
            target_root: beck.join("good-target"),
            detect_ok: true,
            managed: RefCell::new(vec![]),
        };
        let missing = MockAdapter {
            name: "missing",
            target_root: beck.join("missing-target"),
            detect_ok: false,
            managed: RefCell::new(vec![]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![&good, &missing],
        )
        .unwrap();

        assert_eq!(report.adapters_detected, vec!["good".to_string()]);
        assert_eq!(report.adapters_missing, vec!["missing".to_string()]);
        assert_eq!(report.manifest_health, ManifestHealth::Missing);
    }

    #[test]
    fn foreign_files_are_listed() {
        let beck = tempdir("check-foreign");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");
        Manifest::empty().save(&manifest_file).unwrap();

        let target_root = beck.join("target");
        // beck-managed: `alpha/SKILL.md`.
        let managed = plant_skill_file(&target_root, "alpha", "managed\n");
        // foreign: `beta/SKILL.md`, NOT in managed list.
        plant_skill_file(&target_root, "beta", "foreign\n");

        let adapter = MockAdapter {
            name: "mock",
            target_root: target_root.clone(),
            detect_ok: true,
            managed: RefCell::new(vec![managed.clone()]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![&adapter],
        )
        .unwrap();

        assert_eq!(report.beck_managed, 1);
        assert_eq!(report.foreign.len(), 1);
        assert_eq!(report.foreign[0].path, target_root.join("beta").join("SKILL.md"));
        assert_eq!(report.foreign[0].agent, "mock");
    }

    #[test]
    fn orphans_come_from_manifest_entries_missing_on_disk() {
        let beck = tempdir("check-orphan");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");

        let target_root = beck.join("target");
        fs::create_dir_all(&target_root).unwrap();
        let orphan_path = target_root.join("ghost").join("SKILL.md");
        // Deliberately do not create the file.

        let mut manifest = Manifest::empty();
        manifest.add(Entry {
            skill: "ghost".into(),
            agent: "mock".into(),
            target: orphan_path.clone(),
            mode: InstallMode::Symlink,
            sha256: "deadbeef".into(),
            installed_at: "2026-04-11T00:00:00Z".into(),
        });
        manifest.save(&manifest_file).unwrap();

        let adapter = MockAdapter {
            name: "mock",
            target_root,
            detect_ok: true,
            managed: RefCell::new(vec![]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![&adapter],
        )
        .unwrap();

        assert_eq!(report.orphans.len(), 1);
        assert_eq!(report.orphans[0].skill, "ghost");
    }

    #[test]
    fn case_collision_detected_on_source_side() {
        let beck = tempdir("check-collision");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        write_source(&skills_root, "Caveman", "a\n");
        // On a case-insensitive FS the second mkdir will collapse; we
        // still exercise the code by writing on a case-sensitive path
        // when possible. To make the test deterministic across both
        // kinds of FS, we just assert the detector does not panic and
        // returns a Vec (possibly empty on case-insensitive FS).
        let _ = write_source(&skills_root, "caveman-two", "b\n");

        let manifest_file = beck.join(".beck-manifest.json");
        let adapter = MockAdapter {
            name: "mock",
            target_root: beck.join("target"),
            detect_ok: false,
            managed: RefCell::new(vec![]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![&adapter],
        )
        .unwrap();

        // We do not assert collision count because APFS is
        // case-insensitive; this test only pins that the detector runs
        // without panicking and the report is well-formed.
        assert!(report.collisions.len() <= 1);
    }

    #[test]
    fn case_collision_detector_reports_same_key_entries() {
        // Directly unit test the collision detector on a fixture built
        // by hand, so the behavior is verified regardless of APFS vs
        // case-sensitive FS.
        let beck = tempdir("check-collision-unit");
        let skills_root = beck.join("skills");
        fs::create_dir_all(skills_root.join("alpha")).unwrap();
        fs::create_dir_all(skills_root.join("BETA")).unwrap();
        fs::create_dir_all(skills_root.join("gamma")).unwrap();

        let collisions = detect_case_collisions(&skills_root).unwrap();
        // No collisions in this fixture: every lowercase key is unique.
        assert!(collisions.is_empty());
    }

    #[test]
    fn prune_drops_orphan_entries_when_flag_set() {
        let beck = tempdir("check-prune");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");

        let target_root = beck.join("target");
        let live_path = plant_skill_file(&target_root, "live", "alive\n");
        let orphan_path = target_root.join("dead").join("SKILL.md");

        let mut manifest = Manifest::empty();
        manifest.add(Entry {
            skill: "live".into(),
            agent: "mock".into(),
            target: live_path.clone(),
            mode: InstallMode::Symlink,
            sha256: "a".into(),
            installed_at: "t".into(),
        });
        manifest.add(Entry {
            skill: "dead".into(),
            agent: "mock".into(),
            target: orphan_path,
            mode: InstallMode::Symlink,
            sha256: "b".into(),
            installed_at: "t".into(),
        });
        manifest.save(&manifest_file).unwrap();

        let adapter = MockAdapter {
            name: "mock",
            target_root,
            detect_ok: true,
            managed: RefCell::new(vec![live_path.clone()]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: true,
            },
            &skills_root,
            &manifest_file,
            vec![&adapter],
        )
        .unwrap();

        assert!(
            report.mutations.iter().any(|m| m.contains("pruned 1")),
            "mutations={:?}",
            report.mutations
        );

        let reloaded = Manifest::load(&manifest_file).unwrap();
        assert_eq!(reloaded.entries.len(), 1);
        assert_eq!(reloaded.entries[0].skill, "live");
    }

    #[test]
    fn rebuild_manifest_from_disk_scans_list_managed() {
        let beck = tempdir("check-rebuild");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");

        let target_root = beck.join("target");
        let a = plant_skill_file(&target_root, "alpha", "a\n");
        let b = plant_skill_file(&target_root, "beta", "b\n");

        let adapter = MockAdapter {
            name: "mock",
            target_root,
            detect_ok: true,
            managed: RefCell::new(vec![a.clone(), b.clone()]),
        };

        let report = run_check(
            CheckArgs {
                rebuild_manifest: true,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![&adapter],
        )
        .unwrap();

        assert!(
            report.mutations.iter().any(|m| m.contains("rebuilt manifest with 2")),
            "mutations={:?}",
            report.mutations
        );

        let reloaded = Manifest::load(&manifest_file).unwrap();
        assert_eq!(reloaded.entries.len(), 2);
        assert_eq!(reloaded.entries[0].skill, "alpha");
        assert_eq!(reloaded.entries[1].skill, "beta");
    }

    #[test]
    fn manifest_missing_is_not_fatal() {
        let beck = tempdir("check-missing-manifest");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![],
        )
        .unwrap();

        assert_eq!(report.manifest_health, ManifestHealth::Missing);
    }

    #[test]
    fn manifest_with_unknown_schema_version_reports_version_unsupported() {
        let beck = tempdir("check-schema");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");
        fs::write(&manifest_file, br#"{"schema_version":999,"entries":[]}"#).unwrap();

        let report = run_check(
            CheckArgs {
                rebuild_manifest: false,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![],
        )
        .unwrap();

        assert_eq!(
            report.manifest_health,
            ManifestHealth::VersionUnsupported(999)
        );
    }

    #[test]
    fn rebuild_manifest_refuses_on_unsupported_schema() {
        let beck = tempdir("check-schema-refuse");
        let skills_root = beck.join("skills");
        fs::create_dir_all(&skills_root).unwrap();
        let manifest_file = beck.join(".beck-manifest.json");
        fs::write(&manifest_file, br#"{"schema_version":999,"entries":[]}"#).unwrap();

        let err = run_check(
            CheckArgs {
                rebuild_manifest: true,
                prune: false,
            },
            &skills_root,
            &manifest_file,
            vec![],
        )
        .expect_err("should refuse");
        assert!(matches!(err, CliError::Validation(_)));
    }

    #[test]
    fn check_report_serializes_cleanly() {
        let report = CheckReport {
            adapters_detected: vec!["claude-code".into()],
            adapters_missing: vec![],
            beck_managed: 3,
            foreign: vec![],
            orphans: vec![],
            collisions: vec![],
            manifest_health: ManifestHealth::Ok,
            mutations: vec![],
        };
        let json = serde_json::to_string(&report).unwrap();
        assert!(json.contains("claude-code"));
        assert!(json.contains("\"beck_managed\":3"));
        assert!(json.contains("\"manifest_health\""));
    }
}
