//! `lean-ctx gateway init` (enterprise#47) — plug-and-play gateway setup.
//!
//! One command produces a complete, immediately runnable instance directory:
//!
//! ```text
//! <dir>/config.toml         engine config (bind, tokens required, org, baseline)
//! <dir>/gateway-keys.toml   per-person keys (only if --person given)
//! <dir>/.env                generated secrets (tokens, Postgres password, DATABASE_URL)
//! <dir>/docker-compose.yml  gateway + Postgres 17, healthchecks, restart policies
//! <dir>/README.md           the 3-step quickstart for this instance
//! ```
//!
//! Security posture: secrets live **only** in `.env` (0600, gitignored by the
//! generated `.gitignore`); `config.toml` and the compose file are clean and
//! committable. Existing files are never overwritten — rerunning on a
//! non-empty directory fails loudly instead of rotating live credentials.

use std::fmt::Write as _;
use std::path::Path;

/// Options for `gateway init` (parsed by the CLI layer).
#[derive(Debug, Clone)]
pub struct InitOptions {
    pub org_label: String,
    pub seats: Option<u32>,
    pub reference_model: Option<String>,
    /// Persons to create keys for right away (`--person a@x --person b@y`).
    pub persons: Vec<String>,
    pub proxy_port: u16,
    pub admin_port: u16,
}

impl Default for InitOptions {
    fn default() -> Self {
        Self {
            org_label: String::new(),
            seats: None,
            reference_model: None,
            persons: Vec::new(),
            proxy_port: 8484,
            admin_port: 8485,
        }
    }
}

/// Result summary: what was created, which plaintext keys to hand out.
#[derive(Debug)]
pub struct InitOutcome {
    pub files: Vec<String>,
    /// `(person, plaintext_key)` — print once, never persisted.
    pub person_keys: Vec<(String, String)>,
}

/// Runs the init: creates the directory and all files. See module docs.
///
/// # Errors
/// Fails if any target file already exists, on CSPRNG failure, or on I/O.
pub fn run(dir: &Path, opts: &InitOptions) -> anyhow::Result<InitOutcome> {
    std::fs::create_dir_all(dir)?;
    for name in [
        "config.toml",
        ".env",
        "docker-compose.yml",
        "README.md",
        "gateway-keys.toml",
    ] {
        anyhow::ensure!(
            !dir.join(name).exists(),
            "{} already exists — `gateway init` never overwrites an instance \
             (delete the file or choose another directory)",
            dir.join(name).display()
        );
    }

    let proxy_token = random_token()?;
    let admin_token = random_token()?;
    let pg_password = random_token()?;

    let mut files = Vec::new();
    write_file(dir, &mut files, "config.toml", &render_config(opts), false)?;
    write_file(
        dir,
        &mut files,
        ".env",
        &render_env(&proxy_token, &admin_token, &pg_password),
        true,
    )?;
    write_file(
        dir,
        &mut files,
        "docker-compose.yml",
        &render_compose(opts),
        false,
    )?;
    write_file(dir, &mut files, ".gitignore", ".env\n", false)?;

    // Person keys through the real key manager (same file format as auth).
    let mut person_keys = Vec::new();
    let keys_path = dir.join("gateway-keys.toml");
    for person in &opts.persons {
        let key = super::keys_cli::add_key(&keys_path, person, None, None, false)?;
        person_keys.push((person.clone(), key));
    }
    if person_keys.is_empty() {
        // The compose file mounts the key file — it must exist even when empty.
        super::keys_cli::write_empty(&keys_path)?;
    }
    files.push("gateway-keys.toml".to_string());

    write_file(dir, &mut files, "README.md", &render_readme(opts), false)?;

    Ok(InitOutcome { files, person_keys })
}

fn write_file(
    dir: &Path,
    files: &mut Vec<String>,
    name: &str,
    contents: &str,
    secret: bool,
) -> anyhow::Result<()> {
    let path = dir.join(name);
    std::fs::write(&path, contents)?;
    #[cfg(unix)]
    if secret {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
    }
    let _ = secret; // non-unix: mode bits not applicable
    files.push(name.to_string());
    Ok(())
}

/// 32 random bytes, hex — the same entropy class as `openssl rand -hex 32`.
fn random_token() -> anyhow::Result<String> {
    let mut buf = [0u8; 32];
    getrandom::fill(&mut buf).map_err(|e| anyhow::anyhow!("CSPRNG unavailable: {e}"))?;
    Ok(buf.iter().fold(String::new(), |mut acc, b| {
        let _ = write!(acc, "{b:02x}");
        acc
    }))
}

fn render_config(opts: &InitOptions) -> String {
    let mut out = String::from(
        "# lean-ctx gateway configuration — generated by `lean-ctx gateway init`.\n\
         # Secrets never live here: tokens and DATABASE_URL come from .env.\n\n\
         # Bind beyond loopback (the container/K8s case) and require Bearer auth.\n\
         proxy_bind_host = \"0.0.0.0\"\n\
         proxy_require_token = true\n",
    );
    let _ = writeln!(out, "\n[gateway_server]");
    // In-container the admin listener must bind all interfaces so the compose
    // port mapping reaches it; exposure stays host-local via the mapping
    // ("127.0.0.1:<port>:8485"). Outside containers the default is loopback.
    let _ = writeln!(out, "admin_bind_host = \"0.0.0.0\"");
    if let Some(seats) = opts.seats {
        let _ = writeln!(out, "seats = {seats}");
    }
    if !opts.org_label.is_empty() {
        let _ = writeln!(
            out,
            "org_label = \"{}\"",
            opts.org_label.replace('"', "\\\"")
        );
    }
    if let Some(reference) = opts
        .reference_model
        .as_deref()
        .map(str::trim)
        .filter(|m| !m.is_empty())
    {
        let _ = writeln!(
            out,
            "\n# Counterfactual baseline: what the org would have paid without lean-ctx.\n\
             [proxy.baseline]\nreference_model = \"{reference}\""
        );
    }
    out.push_str(
        "\n# Registry providers (optional). Built-in routes work without any entry:\n\
         #   /anthropic/…  /openai/…  /gemini/…\n\
         # Add self-hosted or Foundry endpoints like this:\n\
         # [[proxy.providers]]\n\
         # id = \"local\"\n\
         # shape = \"openai\"\n\
         # base_url = \"http://host.docker.internal:11434\"\n\
         # local = true   # billed at the shadow rate, not cloud list prices\n\
         # # plain-HTTP non-loopback upstream additionally needs:\n\
         # # [proxy] allow_insecure_http_upstream = true\n\
         #\n\
         # [[proxy.providers]]\n\
         # id = \"foundry\"\n\
         # shape = \"openai\"\n\
         # base_url = \"https://<resource>.services.ai.azure.com/models\"\n\
         # api_key_env = \"FOUNDRY_API_KEY\"\n\
         \n\
         # Active routing (optional): aliases + tier targets, see docs/reference/05-advanced.md.\n\
         # Aliases are your org's model namespace — clients discover them via\n\
         # GET /v1/models on the proxy port and select them by name in the IDE:\n\
         # [proxy.routing]\n\
         # enabled = true\n\
         # [proxy.routing.aliases]\n\
         # \"acme/fast\" = \"foundry:gpt-4o-mini\"      # org name -> provider:model\n\
         # \"acme/local\" = \"local:llama3.3\"          # local target (shadow rate)\n",
    );
    out
}

fn render_env(proxy_token: &str, admin_token: &str, pg_password: &str) -> String {
    format!(
        "# Generated secrets — keep out of git (the generated .gitignore covers this file).\n\
         # Rotate by editing here and `docker compose up -d` (containers restart with new values).\n\
         LEAN_CTX_PROXY_TOKEN={proxy_token}\n\
         LEAN_CTX_GATEWAY_ADMIN_TOKEN={admin_token}\n\
         POSTGRES_PASSWORD={pg_password}\n\
         DATABASE_URL=postgres://leanctx:{pg_password}@postgres:5432/leanctx\n"
    )
}

fn render_compose(opts: &InitOptions) -> String {
    format!(
        r#"# lean-ctx gateway — pilot deployment (single host, docker compose).
# Production path: the lean-ctx-gateway Helm chart (see deploy template repo).
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: leanctx
      POSTGRES_PASSWORD: ${{POSTGRES_PASSWORD}}
      POSTGRES_DB: leanctx
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U leanctx -d leanctx"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  gateway:
    image: ${{LEANCTX_IMAGE:-lean-ctx-gateway:latest}}
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      LEAN_CTX_PROXY_TOKEN: ${{LEAN_CTX_PROXY_TOKEN}}
      LEAN_CTX_GATEWAY_ADMIN_TOKEN: ${{LEAN_CTX_GATEWAY_ADMIN_TOKEN}}
      DATABASE_URL: ${{DATABASE_URL}}
    volumes:
      - ./config.toml:/etc/lean-ctx/config.toml:ro
      - ./gateway-keys.toml:/etc/lean-ctx/gateway-keys.toml:ro
    ports:
      - "{proxy_port}:8484"          # proxy — the surface clients use
      - "127.0.0.1:{admin_port}:8485" # admin console — host-local only
    restart: unless-stopped

volumes:
  pgdata:
"#,
        proxy_port = opts.proxy_port,
        admin_port = opts.admin_port,
    )
}

fn render_readme(opts: &InitOptions) -> String {
    let org = if opts.org_label.is_empty() {
        "your org"
    } else {
        &opts.org_label
    };
    format!(
        r"# lean-ctx gateway — {org}

Generated by `lean-ctx gateway init`. Three steps to a running gateway:

## 1. Start

```bash
docker compose up -d
```

(The image comes from `LEANCTX_IMAGE`, default `lean-ctx-gateway:latest` — build it
with `docker build -f docker/Dockerfile.gateway -t lean-ctx-gateway:latest .` from
the engine repo, or point `LEANCTX_IMAGE` at your registry.)

## 2. Verify

```bash
lean-ctx gateway doctor --dir .          # preflight: config, secrets, DB, ports
curl -s http://127.0.0.1:{proxy_port}/health   # proxy liveness
open http://127.0.0.1:{admin_port}/            # admin console (token: LEAN_CTX_GATEWAY_ADMIN_TOKEN in .env)
```

## 3. Hand out keys

```bash
lean-ctx gateway keys add --person alice@example.com --team platform --project checkout --file gateway-keys.toml
lean-ctx gateway keys rotate --person alice@example.com --file gateway-keys.toml   # compromised/expiring key: one atomic step
docker compose restart gateway   # reload the key set
```

Point clients at the proxy:

```bash
export ANTHROPIC_BASE_URL=http://<host>:{proxy_port}/anthropic
export ANTHROPIC_AUTH_TOKEN=<the person's gk-… key>
```

Model catalog and personal view (each person uses their own key):

```bash
curl -s -H 'Authorization: Bearer <gk-… key>' http://<host>:{proxy_port}/v1/models   # org model aliases
open http://<host>:{proxy_port}/me                                                   # personal usage dashboard
```

## Files

| File | Purpose | Committable |
|---|---|---|
| `config.toml` | engine config (org, baseline, providers, routing) | yes |
| `docker-compose.yml` | pilot deployment | yes |
| `gateway-keys.toml` | SHA-256 key hashes (no plaintext) | yes |
| `.env` | generated secrets | **no** (gitignored) |
",
        proxy_port = opts.proxy_port,
        admin_port = opts.admin_port,
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn opts() -> InitOptions {
        InitOptions {
            org_label: "Zühlke Engineering AG".into(),
            seats: Some(800),
            reference_model: Some("claude-opus-4.5".into()),
            persons: vec!["alice@zuehlke.com".into(), "bob@zuehlke.com".into()],
            proxy_port: 8484,
            admin_port: 8485,
        }
    }

    #[test]
    fn init_creates_complete_runnable_instance() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("gw");
        let outcome = run(&dir, &opts()).unwrap();

        for f in [
            "config.toml",
            ".env",
            "docker-compose.yml",
            "README.md",
            "gateway-keys.toml",
            ".gitignore",
        ] {
            assert!(dir.join(f).exists(), "missing {f}");
        }

        // config.toml parses and carries the org parameters.
        let cfg = std::fs::read_to_string(dir.join("config.toml")).unwrap();
        let parsed: toml::Value = toml::from_str(&cfg).expect("generated config must parse");
        assert_eq!(
            parsed["gateway_server"]["org_label"].as_str(),
            Some("Zühlke Engineering AG")
        );
        assert_eq!(parsed["gateway_server"]["seats"].as_integer(), Some(800));
        assert_eq!(
            parsed["proxy"]["baseline"]["reference_model"].as_str(),
            Some("claude-opus-4.5")
        );
        assert_eq!(parsed["proxy_bind_host"].as_str(), Some("0.0.0.0"));
        assert_eq!(parsed["proxy_require_token"].as_bool(), Some(true));

        // Secrets: only in .env, wired into DATABASE_URL, never in config/compose.
        let env = std::fs::read_to_string(dir.join(".env")).unwrap();
        let token_of = |name: &str| {
            env.lines()
                .find_map(|l| l.strip_prefix(&format!("{name}=")))
                .map(str::to_string)
                .unwrap_or_default()
        };
        let proxy_token = token_of("LEAN_CTX_PROXY_TOKEN");
        assert_eq!(proxy_token.len(), 64);
        assert!(env.contains(&format!(
            "DATABASE_URL=postgres://leanctx:{}@postgres:5432/leanctx",
            token_of("POSTGRES_PASSWORD")
        )));
        assert!(!cfg.contains(&proxy_token));
        let compose = std::fs::read_to_string(dir.join("docker-compose.yml")).unwrap();
        assert!(!compose.contains(&proxy_token));
        assert!(compose.contains("service_healthy"));

        // Both persons got working keys resolvable via the real auth loader.
        assert_eq!(outcome.person_keys.len(), 2);
        let keys =
            crate::proxy::gateway_identity::GatewayKeys::load(&dir.join("gateway-keys.toml"))
                .unwrap();
        for (person, key) in &outcome.person_keys {
            let tags = keys.lookup(key).expect("generated key resolves");
            assert_eq!(tags.person.as_deref(), Some(person.as_str()));
        }

        // .env is owner-only.
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(dir.join(".env"))
                .unwrap()
                .permissions()
                .mode();
            assert_eq!(mode & 0o777, 0o600);
        }
    }

    #[test]
    fn init_refuses_to_overwrite_existing_instance() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("gw");
        run(&dir, &InitOptions::default()).unwrap();
        let err = run(&dir, &InitOptions::default()).unwrap_err();
        assert!(
            err.to_string().contains("never overwrites"),
            "second init must refuse: {err}"
        );
    }

    #[test]
    fn tokens_are_distinct_per_init() {
        let tmp = tempfile::tempdir().unwrap();
        run(&tmp.path().join("a"), &InitOptions::default()).unwrap();
        run(&tmp.path().join("b"), &InitOptions::default()).unwrap();
        let env_a = std::fs::read_to_string(tmp.path().join("a/.env")).unwrap();
        let env_b = std::fs::read_to_string(tmp.path().join("b/.env")).unwrap();
        let first_line = |s: &str| {
            s.lines()
                .find(|l| l.starts_with("LEAN_CTX_PROXY_TOKEN"))
                .unwrap()
                .to_string()
        };
        assert_ne!(first_line(&env_a), first_line(&env_b));
    }
}
