//! `pic apply [--file picloud.toml]` — reconcile the live app to the
//! manifest's desired state in one server-side transaction. Creates and
//! updates are applied; `--prune` additionally deletes live scripts/routes/
//! triggers absent from the manifest (secrets are never pruned).

use std::io::{IsTerminal, Write};
use std::path::Path;

use anyhow::{Context, Result};

use crate::client::{Client, NodeKind};
use crate::cmds::plan::build_bundle;
use crate::config;
use crate::manifest::Manifest;
use crate::output::{KvBlock, OutputMode};

pub async fn run(
    manifest_path: &Path,
    env: Option<&str>,
    prune: bool,
    yes: bool,
    force: bool,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;

    let manifest = Manifest::load_with_env(manifest_path, env)?;
    let base_dir = manifest_path.parent().unwrap_or_else(|| Path::new("."));
    let bundle = build_bundle(&manifest, base_dir)?;
    let kind = if manifest.is_group() {
        NodeKind::Group
    } else {
        NodeKind::App
    };
    let slug = manifest.slug().to_string();

    if prune && !yes {
        confirm_prune(&slug)?;
    }

    // Bound-plan check: replay the token from the last `pic plan` (for this
    // node) so the server refuses if it changed since it was reviewed.
    // `--force` skips it; no recorded plan means no check (apply still works
    // standalone). The token is single-use — cleared after a successful apply.
    let expected_token = if force {
        None
    } else {
        crate::linkstate::read_plan(base_dir)
            .filter(|l| l.app == slug)
            .map(|l| l.state_token)
    };

    let report = client
        .apply_node(kind, &slug, &bundle, prune, expected_token.as_deref())
        .await?;
    crate::linkstate::clear_plan(base_dir, &slug);

    let label = if manifest.is_group() { "group" } else { "app" };
    let mut block = KvBlock::new();
    block
        .field(label, slug.clone())
        .field(
            "scripts",
            format!(
                "+{} ~{} -{}",
                report.scripts_created, report.scripts_updated, report.scripts_deleted
            ),
        )
        .field(
            "routes",
            format!(
                "+{} ~{} -{}",
                report.routes_created, report.routes_updated, report.routes_deleted
            ),
        )
        .field(
            "triggers",
            format!("+{} -{}", report.triggers_created, report.triggers_deleted),
        )
        .field(
            "vars",
            format!(
                "+{} ~{} -{}",
                report.vars_created, report.vars_updated, report.vars_deleted
            ),
        );
    for w in &report.warnings {
        block.field("warning", w.clone());
    }
    block.print(mode);
    Ok(())
}

/// `pic apply --dir <root>` (Phase 5): reconcile a whole project tree — every
/// `picloud.toml` under `root` — in ONE atomic server transaction.
pub async fn run_tree(
    dir: &Path,
    prune: bool,
    yes: bool,
    force: bool,
    env: Option<&str>,
    mode: OutputMode,
) -> Result<()> {
    let creds = config::resolve()?;
    let client = Client::from_creds(&creds)?;
    let (bundle, node_count) = crate::discover::build_tree(dir, env)?;

    if prune && !yes {
        confirm_prune(&format!("{node_count} node(s) under {}", dir.display()))?;
    }

    let token_key = crate::cmds::plan::TREE_TOKEN_KEY;
    let expected_token = if force {
        None
    } else {
        crate::linkstate::read_plan(dir)
            .filter(|l| l.app == token_key)
            .map(|l| l.state_token)
    };

    let report = client
        .apply_tree(&bundle, prune, expected_token.as_deref())
        .await?;
    crate::linkstate::clear_plan(dir, token_key);

    let mut block = KvBlock::new();
    block
        .field("nodes", node_count.to_string())
        .field(
            "scripts",
            format!(
                "+{} ~{} -{}",
                report.scripts_created, report.scripts_updated, report.scripts_deleted
            ),
        )
        .field(
            "routes",
            format!(
                "+{} ~{} -{}",
                report.routes_created, report.routes_updated, report.routes_deleted
            ),
        )
        .field(
            "triggers",
            format!("+{} -{}", report.triggers_created, report.triggers_deleted),
        )
        .field(
            "vars",
            format!(
                "+{} ~{} -{}",
                report.vars_created, report.vars_updated, report.vars_deleted
            ),
        );
    for w in &report.warnings {
        block.field("warning", w.clone());
    }
    block.print(mode);
    Ok(())
}

/// `--prune` deletes live scripts/routes/triggers absent from the manifest —
/// irreversible. Require an explicit go-ahead: an interactive `y`, or `--yes`
/// for non-interactive/CI use. Refuse a non-interactive prune without `--yes`
/// rather than silently deleting (review the deletions first with `pic plan`).
fn confirm_prune(slug: &str) -> Result<()> {
    if !std::io::stdin().is_terminal() {
        anyhow::bail!(
            "refusing to `apply --prune` non-interactively without `--yes`: prune \
             deletes resources absent from the manifest and cannot be undone. \
             Review with `pic plan`, then re-run with `--yes`."
        );
    }
    eprint!(
        "apply --prune will DELETE live scripts/routes/triggers on `{slug}` that are \
         absent from the manifest. This cannot be undone. Continue? [y/N] "
    );
    std::io::stderr().flush().ok();
    let mut answer = String::new();
    std::io::stdin()
        .read_line(&mut answer)
        .context("read confirmation")?;
    if !matches!(answer.trim(), "y" | "Y" | "yes" | "Yes") {
        anyhow::bail!("aborted");
    }
    Ok(())
}
