//! `obs init` — scaffold a new schema crate. Spec 50 § 3.1.
//!
//! Idempotent: re-running on an existing crate only adds missing pieces
//! and skips files that already exist (printing a "skipped" line for
//! each so the user can spot conflicts).

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

use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;

#[derive(clap::Args, Debug)]
pub struct InitArgs {
    /// Authoring mode.
    #[arg(long, value_enum, default_value_t = Mode::Rust)]
    pub mode: Mode,

    /// Crate name (defaults to the directory's basename).
    #[arg(long, value_name = "NAME")]
    pub name: Option<String>,

    /// Proto package name (defaults to `<name>.v1`).
    #[arg(long, value_name = "PACKAGE")]
    pub package: Option<String>,

    /// Path to scaffold under (created if absent).
    pub path: PathBuf,
}

#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum Mode {
    /// Rust-first authoring (`#[derive(Event)]`).
    Rust,
    /// Proto-first authoring (`obs-build` over a `.proto` file).
    Proto,
}

pub fn run(args: InitArgs) -> Result<()> {
    fs::create_dir_all(&args.path)
        .with_context(|| format!("create scaffold dir `{}`", args.path.display()))?;
    let name = match args.name {
        Some(n) => n,
        None => args
            .path
            .file_name()
            .and_then(|s| s.to_str())
            .map(str::to_string)
            .ok_or_else(|| anyhow!("path has no basename — pass --name"))?,
    };
    let package = args.package.unwrap_or_else(|| format!("{name}.v1"));

    write_unless_exists(&args.path.join("Cargo.toml"), &cargo_toml(&name, args.mode))?;
    fs::create_dir_all(args.path.join("src"))?;
    write_unless_exists(&args.path.join("obs.yaml"), DEFAULT_OBS_YAML)?;

    match args.mode {
        Mode::Rust => {
            write_unless_exists(
                &args.path.join("src").join("main.rs"),
                &rust_first_main_rs(&name),
            )?;
            write_unless_exists(
                &args.path.join("src").join("events.rs"),
                &rust_first_events_rs(&package),
            )?;
        }
        Mode::Proto => {
            let proto_dir = args.path.join("proto").join(package.replace('.', "/"));
            fs::create_dir_all(&proto_dir)?;
            write_unless_exists(
                &proto_dir.join("events.proto"),
                &proto_first_events(&package),
            )?;
            write_unless_exists(&args.path.join("build.rs"), &proto_first_build_rs(&package))?;
            write_unless_exists(
                &args.path.join("src").join("main.rs"),
                &proto_first_main_rs(&name, &package),
            )?;
        }
    }
    println!(
        "Scaffolded `{name}` ({package}) at `{}`",
        args.path.display()
    );
    Ok(())
}

fn write_unless_exists(path: &Path, body: &str) -> Result<()> {
    if path.exists() {
        println!("  skipped {} (already exists)", path.display());
        return Ok(());
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, body).with_context(|| format!("write `{}`", path.display()))?;
    println!("  created {}", path.display());
    Ok(())
}

fn cargo_toml(name: &str, mode: Mode) -> String {
    // Proto-first generated code references absolute `::buffa::` and
    // `::obs_core::` paths (codegen.rs / include_schemas!), so the
    // user crate must depend on both directly. Without these the
    // `cargo build` after scaffolding fails with E0432/E0433.
    let (extra_deps, build_deps) = match mode {
        Mode::Rust => ("", String::new()),
        Mode::Proto => (
            "buffa = \"0.4\"\nobs-core = \"0.1\"\n",
            "[build-dependencies]\nanyhow = \"1\"\nobs-build = \"0.1\"\n\n".to_string(),
        ),
    };
    format!(
        r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"

[package.metadata.obs]
schema-source = "{src}"
forensic_max  = 5

[dependencies]
anyhow = "1"
{extra_deps}obs-kit = "0.1"
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}

{build_deps}"#,
        name = name,
        src = match mode {
            Mode::Rust => "rust",
            Mode::Proto => "proto",
        },
    )
}

fn rust_first_main_rs(name: &str) -> String {
    format!(
        r#"//! Demo binary for `{name}` (rust-first authoring). Generated by `obs init`.

mod events;

use obs_kit::{{install_observer, observer, StandardObserver}};

#[tokio::main]
async fn main() -> anyhow::Result<()> {{
    install_observer(StandardObserver::dev()?);
    events::ObsHelloEmitted::builder().who("world").emit();
    observer().shutdown().await;
    Ok(())
}}
"#,
        name = name
    )
}

fn proto_first_main_rs(name: &str, package: &str) -> String {
    let pkg_path = package.replace('.', "::");
    format!(
        r#"//! Demo binary for `{name}` (proto-first authoring). Generated by `obs init`.

use obs_kit::{{install_observer, observer, StandardObserver}};

obs_kit::include_schemas!("{package}");

#[tokio::main]
async fn main() -> anyhow::Result<()> {{
    install_observer(StandardObserver::dev()?);
    {pkg_path}::ObsHelloEmitted::builder().who("world").emit();
    observer().shutdown().await;
    Ok(())
}}
"#,
        name = name,
        package = package,
        pkg_path = pkg_path,
    )
}

fn rust_first_events_rs(package: &str) -> String {
    format!(
        r#"//! Event schemas for this crate. Generated by `obs init --mode rust`.

use obs_kit::Event;

#[derive(Debug, Default, Event)]
#[event(tier = "log", default_sev = "info", full_name = "{package}.ObsHelloEmitted")]
pub struct ObsHelloEmitted {{
    #[obs(label, cardinality = "low")]
    pub who: String,
}}
"#,
        package = package
    )
}

fn proto_first_events(package: &str) -> String {
    format!(
        r#"syntax = "proto3";
package {package};

import "obs/v1/options.proto";

message ObsHelloEmitted {{
  option (obs.v1.event) = {{
    tier: TIER_LOG,
    default_sev: SEVERITY_INFO,
  }};

  string who = 1 [(obs.v1.field) = {{ kind: LABEL, cardinality: LOW }}];
}}
"#,
        package = package
    )
}

fn proto_first_build_rs(package: &str) -> String {
    let path = format!("proto/{}/events.proto", package.replace('.', "/"));
    format!(
        r#"//! Build-time codegen for the obs SDK; see spec 12 § 4.

fn main() -> anyhow::Result<()> {{
    obs_build::Config::new()
        .files(&["{path}"])
        .include("proto")
        .include_obs_options()
        .compile()?;
    Ok(())
}}
"#,
        path = path
    )
}

const DEFAULT_OBS_YAML: &str = r#"# Default observer configuration. Edit to taste.

filter: info
sampling:
  default_rate: 1.0
  always_log_at_or_above: warn
limits:
  max_payload_bytes: 262144
  max_label_value_bytes: 1024
"#;
