//! PRODUCT-INSTALL LIFECYCLE — install vs update vs uninstall, as data.
//!
//! AIRGAP6 treated install as a binary-swap (`SystemdInstaller`: verify → extract
//! ONE binary → render a systemd unit → open ports). This module makes install a
//! first-class **product** lifecycle over the init-NEUTRAL service tree
//! ([`crate::service`]) and the AIRGAP5 [`Layout`] (now with a per-product LOG dir
//! beside its DATA dir):
//!
//! - [`install_product`] — first-time bring-up: provision `software/<ver>/<name>`
//!   + `data/<name>` + `log/<name>`, **unfold the whole product** from its znippy
//!   archive ([`crate::archive::Archive::extract_all`]), mint an install identity
//!   under the data dir ([`crate::identity::ensure`]), render the service for a
//!   chosen [`Backend`] (systemd · OpenRC · …), write its unit/script/env files,
//!   atomically `activate()` the `current` generation, and return the
//!   enable+start **verbs as data**.
//! - [`update_product`] — a new version generation: unfold the new software into a
//!   NEW `software/<newver>/<name>`, **preserve** the existing `data/<name>` +
//!   `log/<name>` (never re-created/clobbered — `identity::ensure` is idempotent so
//!   the install id is kept), atomic `activate()` swap, and a restart verb.
//! - [`uninstall_product`] — delete the service: stop+disable **verbs as data**,
//!   remove the unit/script/env files, and — gated on `purge` — either remove or
//!   retain `data/<name>` + `log/<name>`.
//!
//! Every op returns an [`InstallOutcome`]: dirs created, files written, dirs
//! removed, identity, the [`RenderedService`], and the ordered privileged argv to
//! run. Mirrors [`crate::service`]'s "verbs as data" law — the lifecycle fns do the
//! unprivileged filesystem work (dirs, unpack, unit files under a fs `root`) but
//! NEVER invoke the service manager; a thin [`apply`] runs the verbs. That split is
//! what makes the whole lifecycle unit-testable offline into a tempdir.

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

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::archive::Archive;
use crate::layout::Layout;
use crate::product::ComponentSpec;
use crate::service::{renderer_for, Backend, RenderedService};
use crate::systemd::Scope;

/// Which lifecycle operation produced an [`InstallOutcome`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum InstallOp {
    /// First-time bring-up: create the service + all per-product dirs.
    Install,
    /// New version generation: swap software, preserve data + log.
    Update,
    /// Remove the service; optionally purge data + log.
    Uninstall,
}

/// The fully-inspectable result of a product-install lifecycle op. Every field is
/// plain data (serde), so a test asserts EXACTLY what happened (dirs, files, verbs)
/// without running a single privileged command, and a driver replays it uniformly.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstallOutcome {
    pub op: InstallOp,
    pub product: String,
    pub backend: Backend,
    /// The airgap generation this op targeted (`layout.version`).
    pub version: String,
    /// Per-product dirs created/ensured this op — `software/<ver>/<name>`,
    /// `data/<name>`, `log/<name>` (install/update). Empty for uninstall.
    pub dirs: Vec<PathBuf>,
    /// Paths removed this op — purged `data/<name>` + `log/<name>` and the
    /// deleted unit/script/env files (uninstall). Empty for install/update.
    pub removed: Vec<PathBuf>,
    /// Unit/script/env files written (under the driver's fs `root`).
    pub files_written: Vec<PathBuf>,
    /// The install identity wire string minted (install) or preserved (update);
    /// `None` for uninstall.
    pub identity: Option<String>,
    /// The rendered service — file bodies + enable/start/status/stop verbs as data.
    pub rendered: RenderedService,
    /// The privileged service-manager argv to run for THIS op, in order (verbs as
    /// data — a thin [`apply`] executes them; the lifecycle fn runs none of them):
    /// install ⇒ `[enable, start]`, update ⇒ `[stop, start]` (restart), uninstall
    /// ⇒ `[stop, disable]`.
    pub verbs: Vec<Vec<String>>,
}

/// INSTALL — first-time product bring-up over the neutral service tree. See the
/// module docs for the full sequence. `root` is the filesystem root the unit files
/// are written under (`/` in production; a tempdir in tests), so the absolute unit
/// paths (`/etc/systemd/system/…`, `/etc/init.d/…`) land somewhere writable and
/// assertable. Idempotent by construction (every step fills-the-gap).
pub fn install_product(
    spec: &ComponentSpec,
    layout: &Layout,
    bundle: &Path,
    archive: &dyn Archive,
    backend: Backend,
    root: &Path,
) -> Result<InstallOutcome> {
    // 1. Provision the per-product tree: software/<ver>/<name> + data/<name> + log/<name>.
    let (software, data, log) = layout.prepare_product(&spec.name)?;

    // 2. Unfold the WHOLE product out of its znippy archive into its software dir
    //    (the airgapped product, not just a swapped binary). Tree preserved.
    archive
        .extract_all(bundle, &software)
        .with_context(|| format!("unfold {} into {}", bundle.display(), software.display()))?;
    mark_binary_executable(&software, &spec.bundle_binary);

    // 3. Activate: point `current` at this generation (atomic swap) so the exec /
    //    working dir (which resolve THROUGH `current`) become live.
    layout.activate()?;

    // 4. Mint the install identity under the product's DATA dir (idempotent).
    let scope = scope_for(backend);
    let identity = crate::identity::ensure(&data, &spec.name, scope, &spec.user)
        .with_context(|| format!("mint identity for {} under {}", spec.name, data.display()))?;

    // 5. Render the service via the init-neutral tree + write its files.
    let rendered = render(spec, layout, backend)?;
    let files_written = write_rendered_files(root, &rendered)?;

    Ok(InstallOutcome {
        op: InstallOp::Install,
        product: spec.name.clone(),
        backend,
        version: layout.version.clone(),
        dirs: vec![software, data, log],
        removed: vec![],
        files_written,
        identity: Some(identity.wire()),
        rendered: rendered.clone(),
        // Create the service, then start it — both privileged, both as data.
        verbs: vec![rendered.enable, rendered.start],
    })
}

/// UPDATE — swap to a new version generation while PRESERVING data + log. `layout`
/// carries the NEW `version`; the software is unfolded into the new
/// `software/<newver>/<name>`, the existing `data/<name>` + `log/<name>` are kept
/// (never re-created/clobbered — [`crate::identity::ensure`] is idempotent so the
/// install id survives), `current` is atomically swapped, and the op returns a
/// restart verb. This is AIRGAP6's swap path, made explicit and data/log-safe.
pub fn update_product(
    spec: &ComponentSpec,
    layout: &Layout,
    bundle: &Path,
    archive: &dyn Archive,
    backend: Backend,
    root: &Path,
) -> Result<InstallOutcome> {
    // prepare_product is idempotent: it creates the NEW software/<newver>/<name>
    // and TOUCHES data/<name>+log/<name> only if absent — an existing data/log dir
    // (and its contents) is left exactly as-is. That is the "preserve" contract.
    let (software, data, log) = layout.prepare_product(&spec.name)?;

    archive
        .extract_all(bundle, &software)
        .with_context(|| format!("unfold new version {} into {}", bundle.display(), software.display()))?;
    mark_binary_executable(&software, &spec.bundle_binary);

    // Atomic swap to the new generation (the old version dir stays for rollback).
    layout.activate()?;

    // Identity is loaded (preserved), not re-minted — the reinstall-stable id.
    let scope = scope_for(backend);
    let identity = crate::identity::ensure(&data, &spec.name, scope, &spec.user)
        .with_context(|| format!("load identity for {} under {}", spec.name, data.display()))?;

    let rendered = render(spec, layout, backend)?;
    // The unit points through `current`, so it is version-independent — re-writing
    // it is a byte-identical no-op, kept so a changed spec still lands.
    let files_written = write_rendered_files(root, &rendered)?;

    Ok(InstallOutcome {
        op: InstallOp::Update,
        product: spec.name.clone(),
        backend,
        version: layout.version.clone(),
        dirs: vec![software, data, log],
        removed: vec![],
        files_written,
        identity: Some(identity.wire()),
        // Restart across the swap = stop then start (backend-neutral; the two verbs
        // every renderer emits). No daemon-reload needed — the unit never churns.
        verbs: vec![rendered.stop.clone(), rendered.start.clone()],
        rendered,
    })
}

/// UNINSTALL — delete the service. Returns the stop+disable **verbs as data**,
/// removes the unit/script/env files, and — gated on `purge` — either removes the
/// product's `data/<name>` + `log/<name>` (a true teardown) or retains them (the
/// default: a later reinstall keeps its identity + state). Never touches the
/// software tree (it is part of the shared versioned generation). Idempotent:
/// removing an already-absent file/dir is a no-op.
pub fn uninstall_product(
    spec: &ComponentSpec,
    layout: &Layout,
    backend: Backend,
    root: &Path,
    purge: bool,
) -> Result<InstallOutcome> {
    let rendered = render(spec, layout, backend)?;

    // Remove the unit/script/env files this backend installed (under `root`).
    let mut removed = Vec::new();
    for f in &rendered.files {
        let p = under_root(root, &f.path);
        if p.exists() {
            std::fs::remove_file(&p).with_context(|| format!("remove unit file {}", p.display()))?;
            removed.push(p);
        }
    }

    // Purge decides the fate of the product's fast-moving state.
    if purge {
        for dir in [layout.product_data(&spec.name), layout.product_log(&spec.name)] {
            if dir.exists() {
                std::fs::remove_dir_all(&dir)
                    .with_context(|| format!("purge {}", dir.display()))?;
                removed.push(dir);
            }
        }
    }

    Ok(InstallOutcome {
        op: InstallOp::Uninstall,
        product: spec.name.clone(),
        backend,
        version: layout.version.clone(),
        dirs: vec![],
        removed,
        files_written: vec![],
        identity: None,
        // Stop it, then disable start-at-boot — both privileged, both as data.
        verbs: vec![rendered.stop.clone(), rendered.disable_verb()],
        rendered,
    })
}

/// Run an outcome's privileged verbs, in order (the thin privileged driver). This
/// is the ONLY place the lifecycle actually invokes the service manager; keeping it
/// separate is what lets every lifecycle fn stay offline-testable.
pub fn apply(outcome: &InstallOutcome) -> Result<()> {
    for argv in &outcome.verbs {
        run_verb(argv)?;
    }
    Ok(())
}

// ───────────────────────────── internals ─────────────────────────────

/// Render `spec` for `backend` through the neutral tree; errors (not a fake) if the
/// backend has no renderer yet.
fn render(spec: &ComponentSpec, layout: &Layout, backend: Backend) -> Result<RenderedService> {
    let svc = spec.to_service_spec(layout, backend);
    let renderer = renderer_for(backend)
        .with_context(|| format!("backend {} has no renderer yet", backend.slug()))?;
    Ok(renderer.render(&svc))
}

/// systemd `--user` scope maps to a per-user identity; everything else is system.
fn scope_for(backend: Backend) -> Scope {
    match backend {
        Backend::SystemdUser => Scope::User,
        _ => Scope::System,
    }
}

/// Join a rendered file's install path under a filesystem `root`. Absolute paths
/// (`/etc/systemd/system/…`) are re-rooted; relative ones (user-scope
/// `.config/systemd/user/…`) join directly.
fn under_root(root: &Path, path: &str) -> PathBuf {
    root.join(path.strip_prefix('/').unwrap_or(path))
}

/// Write each rendered file under `root`, creating parents + applying its mode.
fn write_rendered_files(root: &Path, rendered: &RenderedService) -> Result<Vec<PathBuf>> {
    let mut written = Vec::new();
    for f in &rendered.files {
        let dest = under_root(root, &f.path);
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("create unit dir {}", parent.display()))?;
        }
        std::fs::write(&dest, &f.contents)
            .with_context(|| format!("write unit file {}", dest.display()))?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let _ = std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(f.mode));
        }
        written.push(dest);
    }
    Ok(written)
}

/// Best-effort `0o755` on the product's entrypoint binary after unfold (it landed
/// at `software/<bundle_binary>` since `extract_all` preserves the tree).
fn mark_binary_executable(software: &Path, bundle_binary: &str) {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let bin = software.join(bundle_binary);
        if bin.is_file() {
            let _ = std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755));
        }
    }
    #[cfg(not(unix))]
    {
        let _ = (software, bundle_binary);
    }
}

fn run_verb(argv: &[String]) -> Result<()> {
    let (cmd, args) = argv.split_first().context("empty verb")?;
    let status = std::process::Command::new(cmd)
        .args(args)
        .status()
        .with_context(|| format!("run `{}`", argv.join(" ")))?;
    if !status.success() {
        anyhow::bail!("verb failed (exit {:?}): {}", status.code(), argv.join(" "));
    }
    Ok(())
}

impl RenderedService {
    /// The disable/remove-from-boot verb for this backend — the inverse of
    /// [`RenderedService::enable`], derived from it so a new backend gets it for
    /// free (systemd `enable`→`disable`; OpenRC `rc-update add`→`rc-update del`).
    fn disable_verb(&self) -> Vec<String> {
        let mut v = self.enable.clone();
        for tok in v.iter_mut() {
            match tok.as_str() {
                "enable" => *tok = "disable".into(),
                "add" => *tok = "del".into(),
                _ => {}
            }
        }
        v
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::archive::{Archive, TarZstd};

    /// A tiny znippy-style product archive fixture: a staging tree with the product
    /// binary at its `bundle_binary` path (+ a payload file) packed via the default
    /// [`TarZstd`] engine. Returns the bundle path (its TempDirs are kept alive by
    /// the returned guard). Mirrors `archive.rs`'s `staging_fixture` style.
    fn product_bundle(spec: &ComponentSpec) -> (tempfile::TempDir, PathBuf) {
        let dir = tempfile::tempdir().unwrap();
        let staging = dir.path().join("staging");
        let bin = staging.join(&spec.bundle_binary);
        std::fs::create_dir_all(bin.parent().unwrap()).unwrap();
        std::fs::write(&bin, b"#!/fake product binary bytes\n").unwrap();
        // a second file so "unfold the WHOLE product" is a real multi-file unpack.
        std::fs::write(staging.join("README"), b"holger product tree\n").unwrap();

        let bundle = dir.path().join("holger.tar.zst");
        TarZstd::new().pack(&staging, "20250505", &bundle).unwrap();
        (dir, bundle)
    }

    /// A layout rooted in `tmp` at `version`.
    fn layout_in(tmp: &Path, version: &str) -> Layout {
        Layout {
            data_dir: tmp.join("data"),
            log_dir: tmp.join("log"),
            software_dir: tmp.join("software"),
            version: version.into(),
        }
    }

    /// LAW 1 (inject-assert): `install_product` for a given backend provisions ALL
    /// three per-product dirs, unfolds the whole product, mints identity under the
    /// data dir, writes the service files pointing at the right paths, activates
    /// `current`, and returns create-then-start verbs — asserted end-to-end into a
    /// tempdir with NO privilege.
    fn assert_install_for_backend(backend: Backend) {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("fsroot"); // where unit files get written
        let layout = layout_in(tmp.path(), "20250505");
        let spec = ComponentSpec::holger();
        let (_d, bundle) = product_bundle(&spec);

        let out = install_product(&spec, &layout, &bundle, &TarZstd::new(), backend, &root)
            .expect("install_product");

        // ── dirs: software/<ver>/<name> + data/<name> + log/<name> all exist ──
        assert_eq!(out.op, InstallOp::Install);
        assert!(layout.product_software("holger").is_dir(), "software dir");
        assert!(layout.product_data("holger").is_dir(), "data/<name> dir");
        assert!(layout.product_log("holger").is_dir(), "log/<name> dir");
        assert_eq!(
            out.dirs,
            vec![
                layout.product_software("holger"),
                layout.product_data("holger"),
                layout.product_log("holger"),
            ]
        );

        // ── the WHOLE product was unfolded (binary + payload), binary is +x ──
        let installed_bin = layout.product_software("holger").join(&spec.bundle_binary);
        assert!(installed_bin.is_file(), "product binary unfolded");
        assert!(layout.product_software("holger").join("README").is_file(), "payload unfolded");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(&installed_bin).unwrap().permissions().mode();
            assert_eq!(mode & 0o111, 0o111, "binary is executable");
        }

        // ── identity minted under the DATA dir ──
        assert!(layout.product_data("holger").join("identity.toml").is_file(), "identity file");
        assert!(out.identity.as_deref().unwrap().starts_with("nid1:"), "identity wire");

        // ── activate() made `current` resolve to this generation ──
        assert_eq!(layout.active_version().as_deref(), Some("20250505"));
        // the exec path resolves THROUGH current to the real unfolded binary.
        let via_current = layout.software_dir.join("current").join("holger").join(&spec.bundle_binary);
        assert!(std::fs::metadata(&via_current).is_ok(), "current/<name>/<bin> resolves");

        // ── service files written under root, referencing data + log dirs ──
        assert!(!out.files_written.is_empty(), "at least one unit/script file");
        for w in &out.files_written {
            assert!(w.starts_with(&root), "file written under fs root: {}", w.display());
            assert!(w.is_file(), "written: {}", w.display());
        }
        let data = layout.product_data("holger");
        let log = layout.product_log("holger");
        // DATA/LOG env may live in the unit itself (systemd) or a sibling env file
        // (OpenRC conf.d) — assert across ALL rendered files.
        let all_bodies: String = out.rendered.files.iter().map(|f| f.contents.as_str()).collect();
        assert!(all_bodies.contains(&*data.to_string_lossy()), "service references DATA dir");
        assert!(all_bodies.contains(&*log.to_string_lossy()), "service references LOG dir");
        let exec = layout
            .software_dir
            .join("current/holger")
            .join(&spec.bundle_binary);
        assert!(all_bodies.contains(&*exec.to_string_lossy()), "service execs the unfolded binary");

        // ── verbs: create-the-service (enable) THEN start, both as data ──
        assert_eq!(out.verbs.len(), 2, "enable then start");
        assert_eq!(out.verbs[0], out.rendered.enable);
        assert_eq!(out.verbs[1], out.rendered.start);
    }

    #[test]
    fn install_product_on_systemd_system() {
        assert_install_for_backend(Backend::SystemdSystem);
    }

    #[test]
    fn install_product_on_openrc() {
        assert_install_for_backend(Backend::OpenRc);

        // Extra OpenRC-specific shape: the init.d script + conf.d env, enable via
        // rc-update add, on the exact rendered paths.
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("fsroot");
        let layout = layout_in(tmp.path(), "20250505");
        let spec = ComponentSpec::holger();
        let (_d, bundle) = product_bundle(&spec);
        let out =
            install_product(&spec, &layout, &bundle, &TarZstd::new(), Backend::OpenRc, &root).unwrap();

        assert!(root.join("etc/init.d/holger").is_file(), "openrc init.d script written");
        assert!(root.join("etc/conf.d/holger").is_file(), "openrc conf.d env written");
        assert_eq!(out.verbs[0], vec!["rc-update", "add", "holger", "default"]);
        assert_eq!(out.verbs[1], vec!["rc-service", "holger", "start"]);
    }

    /// LAW 1: `update_product` swaps to a NEW version generation but PRESERVES the
    /// product's data + log dirs (and its install identity), retaining the old
    /// version for rollback and returning a restart verb.
    #[test]
    fn update_product_preserves_data_and_log_and_swaps_version() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("fsroot");
        let spec = ComponentSpec::holger();
        let (_d, bundle) = product_bundle(&spec);

        // v1 install.
        let v1 = layout_in(tmp.path(), "20250505");
        let first = install_product(&spec, &v1, &bundle, &TarZstd::new(), Backend::SystemdSystem, &root)
            .unwrap();
        let id_v1 = first.identity.clone().unwrap();

        // Write real state into data + log — the thing an update must NOT clobber.
        std::fs::write(v1.product_data("holger").join("state.db"), b"rows").unwrap();
        std::fs::write(v1.product_log("holger").join("app.log"), b"log line").unwrap();

        // v2 update (new generation).
        let v2 = layout_in(tmp.path(), "20250606");
        let up = update_product(&spec, &v2, &bundle, &TarZstd::new(), Backend::SystemdSystem, &root)
            .unwrap();
        assert_eq!(up.op, InstallOp::Update);

        // Data + log dirs (and their contents) survived the swap.
        assert_eq!(
            std::fs::read(v2.product_data("holger").join("state.db")).unwrap(),
            b"rows",
            "data preserved across update"
        );
        assert_eq!(
            std::fs::read(v2.product_log("holger").join("app.log")).unwrap(),
            b"log line",
            "log preserved across update"
        );
        // Identity is the SAME install (idempotent ensure — reinstall keeps id).
        assert_eq!(up.identity.as_deref().unwrap(), id_v1, "install identity preserved");

        // Version swapped atomically; the old generation is retained for rollback.
        assert_eq!(v2.active_version().as_deref(), Some("20250606"), "swapped to new gen");
        assert!(tmp.path().join("software/20250505/holger").is_dir(), "old gen retained");
        assert!(tmp.path().join("software/20250606/holger").is_dir(), "new gen unfolded");

        // Update restarts (stop then start), it does not re-enable.
        assert_eq!(up.verbs, vec![up.rendered.stop.clone(), up.rendered.start.clone()]);
    }

    /// LAW 1: `uninstall_product` removes the service unit + returns stop+disable
    /// verbs; `purge=false` RETAINS data+log, `purge=true` REMOVES them.
    #[test]
    fn uninstall_product_purge_flag_controls_data_retention() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("fsroot");
        let layout = layout_in(tmp.path(), "20250505");
        let spec = ComponentSpec::holger();
        let (_d, bundle) = product_bundle(&spec);

        install_product(&spec, &layout, &bundle, &TarZstd::new(), Backend::SystemdSystem, &root)
            .unwrap();
        let unit = root.join("etc/systemd/system/holger.service");
        assert!(unit.is_file(), "unit installed");

        // ── purge=false: remove the service, KEEP data + log ──
        let out = uninstall_product(&spec, &layout, Backend::SystemdSystem, &root, false).unwrap();
        assert_eq!(out.op, InstallOp::Uninstall);
        assert!(!unit.exists(), "unit file removed");
        assert!(out.removed.contains(&unit), "removed records the unit file");
        assert!(layout.product_data("holger").is_dir(), "data retained (no purge)");
        assert!(layout.product_log("holger").is_dir(), "log retained (no purge)");
        // stop then disable, as data.
        assert_eq!(out.verbs[0], out.rendered.stop);
        assert_eq!(out.verbs[1], vec!["systemctl", "disable", "holger.service"]);

        // ── purge=true: remove data + log too ──
        let purged = uninstall_product(&spec, &layout, Backend::SystemdSystem, &root, true).unwrap();
        assert!(!layout.product_data("holger").exists(), "data purged");
        assert!(!layout.product_log("holger").exists(), "log purged");
        assert!(purged.removed.contains(&layout.product_data("holger")), "records purged data");
        assert!(purged.removed.contains(&layout.product_log("holger")), "records purged log");
    }

    /// The whole outcome is plain serde data (a driver can ship it / a test can
    /// snapshot it) — round-trips through JSON.
    #[test]
    fn install_outcome_json_roundtrips() {
        let tmp = tempfile::tempdir().unwrap();
        let root = tmp.path().join("fsroot");
        let layout = layout_in(tmp.path(), "20250505");
        let spec = ComponentSpec::holger();
        let (_d, bundle) = product_bundle(&spec);
        let out =
            install_product(&spec, &layout, &bundle, &TarZstd::new(), Backend::OpenRc, &root).unwrap();

        let back: InstallOutcome =
            serde_json::from_str(&serde_json::to_string(&out).unwrap()).unwrap();
        assert_eq!(back, out);
    }
}
