//! `.picloud/` link state — gitignored, per-project metadata the project tool
//! carries between CLI invocations. Today it holds just the bound-plan token:
//! `pic plan` records the fingerprint of the live state it diffed against, and
//! `pic apply` replays it so the server can refuse if the app moved underneath.
//!
//! All paths are relative to the manifest's directory (the project root).

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

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

const DIR: &str = ".picloud";
const PLAN_FILE: &str = "plan.json";

/// The recorded result of the last `pic plan`, scoped to the app it was for.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanLink {
    /// App slug the token belongs to — guards against replaying a token from a
    /// different app if the manifest's `slug` changed.
    pub app: String,
    pub state_token: String,
}

fn plan_path(base: &Path) -> PathBuf {
    base.join(DIR).join(PLAN_FILE)
}

/// Record the bound-plan token for `app` under `base/.picloud/`.
pub fn write_plan(base: &Path, app: &str, state_token: &str) -> Result<()> {
    let dir = base.join(DIR);
    fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
    // Self-ignore: a `.gitignore` of `*` inside `.picloud/` keeps the whole
    // dir out of git regardless of the project root's `.gitignore` — so this
    // is safe even when reached via `pic plan`/`pull` (which, unlike `init`,
    // don't touch the root `.gitignore`).
    let ignore = dir.join(".gitignore");
    if !ignore.exists() {
        fs::write(&ignore, "*\n").context("writing .picloud/.gitignore")?;
    }
    let link = PlanLink {
        app: app.to_string(),
        state_token: state_token.to_string(),
    };
    let body = serde_json::to_vec_pretty(&link).context("encoding .picloud/plan.json")?;
    fs::write(plan_path(base), body).context("writing .picloud/plan.json")?;
    Ok(())
}

/// Read the recorded plan token, if any. Returns `None` when absent or
/// unreadable (treated as "no prior plan" — never an error).
#[must_use]
pub fn read_plan(base: &Path) -> Option<PlanLink> {
    let body = fs::read(plan_path(base)).ok()?;
    serde_json::from_slice(&body).ok()
}

/// Remove the recorded plan token (best-effort) **iff it belongs to `app`**.
/// Called after a successful apply consumes it, so the next apply requires a
/// fresh plan — without clobbering a token recorded for a different app that
/// happens to share the directory.
pub fn clear_plan(base: &Path, app: &str) {
    if read_plan(base).is_some_and(|l| l.app == app) {
        let _ = fs::remove_file(plan_path(base));
    }
}
