//! `service-kicad` CLI — every Adom tool shells to this binary instead of
//! calling the HTTP API directly. Base URL is baked at build time from
//! `$SERVICE_KICAD_URL` or `../service.json::url`; at runtime
//! `$SERVICE_KICAD_URL` (or the legacy `$KICAD_SERVICE_API`) overrides it,
//! which is how a self-hosted instance is addressed.
//!
//! See PLAN Part 29.4 for the canonical command surface.

use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use reqwest::blocking::Client;
use std::{io::Write, path::PathBuf, time::Duration};

const PROD_URL: &str = env!("SERVICE_KICAD_URL");

#[derive(Parser)]
#[command(name = "service-kicad", version, about = "Shared headless KiCad CLI — see PLAN Part 29")]
struct Cli {
    #[command(subcommand)]
    command: Cmd,
}

#[derive(Subcommand)]
enum Cmd {
    /// GET /health.
    Health,
    /// GET /version.
    Version,
    /// Print the effective service URL (verify env override).
    Config,

    /// PCB operations (body = `.kicad_pcb` file on disk).
    #[command(subcommand)]
    Pcb(PcbCmd),

    /// Schematic operations (body = `.kicad_sch` file on disk).
    #[command(subcommand)]
    Sch(SchCmd),

    /// Symbol library lookup.
    #[command(subcommand)]
    Sym(SymCmd),

    /// Footprint library lookup.
    #[command(subcommand)]
    Fp(FpCmd),

    /// Download a static model file from kicad-packages3d.
    Model {
        #[command(subcommand)]
        cmd: ModelCmd,
    },

    /// Altium library upgrades (body = `.SchLib` / `.PcbLib` on disk).
    #[command(subcommand)]
    Altium(AltiumCmd),

    /// Drop SKILL.md + bash/zsh completions into ~/.claude/skills/ +
    /// ~/.bash_completion.d/ (tool-publisher pattern).
    Install {
        #[arg(long)] skill_only: bool,
        #[arg(long)] completions_only: bool,
    },
}

#[derive(Subcommand)]
enum PcbCmd {
    /// POST /kicad/pcb/drc.
    Drc {
        file: PathBuf,
        #[arg(long, default_value = "text")] format: String,
        #[arg(long)] out: Option<PathBuf>,
    },
    /// POST /kicad/pcb/export/{svg,gerbers,step}.
    #[command(subcommand)]
    Export(PcbExportCmd),
}

#[derive(Subcommand)]
enum PcbExportCmd {
    Svg {
        file: PathBuf,
        #[arg(long, default_value = "top")] side: String,
        /// Comma-separated KiCad layer names (e.g. F.Cu,F.Mask,Edge.Cuts).
        /// Overrides the side's default set; drops the drawing sheet.
        #[arg(long)] layers: Option<String>,
        #[arg(long)] out: Option<PathBuf>,
    },
    Gerbers { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
    Step   { file: PathBuf, #[arg(long)] out: Option<PathBuf>, #[arg(short = 'F', long = "flag", allow_hyphen_values = true)] flags: Vec<String> },
    // Gated per page issue #106: KiCad-native GLB bypasses the molecule
    // pipeline (no anchoring, gold pins, normalization, footprint emit).
    // Hidden from help and refused without the acknowledgment flag, so
    // agents that reach for the old verb get routed to STEP + step2glb.
    // Output GLBs are provenance-stamped server-side (asset.extras.adom).
    #[command(hide = true)]
    Glb    { file: PathBuf, #[arg(long)] out: Option<PathBuf>, #[arg(short = 'F', long = "flag", allow_hyphen_values = true)] flags: Vec<String>, #[arg(long)] i_know_this_skips_the_pipeline: bool },
    #[command(hide = true)]
    GlbZup { file: PathBuf, #[arg(long)] out: Option<PathBuf>, #[arg(short = 'F', long = "flag", allow_hyphen_values = true)] flags: Vec<String>, #[arg(long)] i_know_this_skips_the_pipeline: bool },
}

#[derive(Subcommand)]
enum SchCmd {
    /// POST /kicad/sch/erc.
    Erc {
        file: PathBuf,
        #[arg(long, default_value = "text")] format: String,
        #[arg(long)] out: Option<PathBuf>,
    },
    #[command(subcommand)]
    Export(SchExportCmd),
}

#[derive(Subcommand)]
enum SchExportCmd {
    Svg { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
    /// Hierarchical project → one SVG per sheet. DIR is the project
    /// directory (.kicad_pro + all .kicad_sch); output is a directory.
    SvgProject { dir: PathBuf, #[arg(long)] out: Option<PathBuf> },
    Pdf { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
    Bom { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
}

#[derive(Subcommand)]
enum SymCmd {
    /// GET /sym/export/svg/:library/:name.
    Svg { library: String, name: String, #[arg(long)] out: Option<PathBuf> },
    /// GET /symbols/<library>.kicad_sym (raw fetch).
    Fetch { library: String, #[arg(long)] out: Option<PathBuf> },
}

#[derive(Subcommand)]
enum FpCmd {
    /// GET /fp/export/svg/:library/:name.
    Svg { library: String, name: String, #[arg(long)] out: Option<PathBuf> },
    /// GET /footprints/<library>.pretty/<name>.kicad_mod.
    Fetch { library: String, name: String, #[arg(long)] out: Option<PathBuf> },
}

#[derive(Subcommand)]
enum ModelCmd {
    /// GET /models/<path>.
    Fetch { path: String, #[arg(long)] out: Option<PathBuf> },
}

#[derive(Subcommand)]
enum AltiumCmd {
    /// POST /kicad/altium/sym — Altium `.SchLib` → KiCad `.kicad_sym`.
    Sym { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
    /// POST /kicad/altium/fp — Altium `.PcbLib` → KiCad `.kicad_mod`
    /// (single-part libraries; multi-part returns the first footprint).
    Fp { file: PathBuf, #[arg(long)] out: Option<PathBuf> },
}

// ─── Transport ────────────────────────────────────────────────────────

/// Resolve the service URL. Order: $SERVICE_KICAD_URL env →
/// $KICAD_SERVICE_API env (legacy alias) → baked-in production URL.
/// Trailing slash trimmed. The env override is how a self-hosted
/// instance is addressed — same commands, your container.
fn base_url() -> String {
    std::env::var("SERVICE_KICAD_URL")
        .or_else(|_| std::env::var("KICAD_SERVICE_API"))
        .unwrap_or_else(|_| PROD_URL.to_string())
        .trim_end_matches('/')
        .to_string()
}

fn http_client() -> Client {
    Client::builder()
        .timeout(Duration::from_secs(120))
        .build()
        .expect("reqwest client")
}

/// Exit 2 on unreachable service, 3 on auth failure, 4 on any other
/// service-side error. 1 is reserved for input validation.
fn http_error_exit_code(status: u16) -> i32 {
    match status {
        401 | 403 => 3,
        _ => 4,
    }
}

fn get_bytes(path: &str) -> Result<Vec<u8>> {
    let url = format!("{}{}", base_url(), path);
    let resp = http_client().get(&url).send().unwrap_or_else(|e| {
        eprintln!("service-kicad: unreachable ({e})");
        std::process::exit(2);
    });
    let status = resp.status();
    if !status.is_success() {
        eprintln!("service-kicad: {} — {}", status, resp.text().unwrap_or_default());
        std::process::exit(http_error_exit_code(status.as_u16()));
    }
    Ok(resp.bytes().map(|b| b.to_vec()).unwrap_or_default())
}

fn flags_to_query_string(flags: &[String]) -> String {
    if flags.is_empty() { return String::new(); }
    let params: Vec<String> = flags.iter().map(|f| {
        let encoded = f.replace('%', "%25").replace('&', "%26").replace('=', "%3D").replace(' ', "%20");
        format!("F={encoded}")
    }).collect();
    format!("?{}", params.join("&"))
}

fn post_file_bytes(path: &str, file: &PathBuf) -> Result<Vec<u8>> {
    let body = std::fs::read(file)
        .with_context(|| format!("read {}", file.display()))?;
    let url = format!("{}{}", base_url(), path);
    let resp = http_client().post(&url).body(body).send().unwrap_or_else(|e| {
        eprintln!("service-kicad: unreachable ({e})");
        std::process::exit(2);
    });
    let status = resp.status();
    if !status.is_success() {
        eprintln!("service-kicad: {} — {}", status, resp.text().unwrap_or_default());
        std::process::exit(http_error_exit_code(status.as_u16()));
    }
    Ok(resp.bytes().map(|b| b.to_vec()).unwrap_or_default())
}

#[allow(dead_code)]
fn _unused() -> Result<()> {
    Ok(())
}

fn write_or_stdout(bytes: &[u8], out: Option<&PathBuf>) -> Result<()> {
    match out {
        Some(p) => {
            std::fs::write(p, bytes).with_context(|| format!("write {}", p.display()))?;
            eprintln!("→ {} ({} bytes)", p.display(), bytes.len());
        }
        None => {
            std::io::stdout().write_all(bytes).context("stdout")?;
        }
    }
    Ok(())
}

fn extract_tarball(bytes: &[u8], dest: &PathBuf) -> Result<()> {
    std::fs::create_dir_all(dest).with_context(|| format!("create {}", dest.display()))?;
    let tmp = tempfile::Builder::new().suffix(".tar").tempfile()?;
    std::fs::write(tmp.path(), bytes)?;
    let status = std::process::Command::new("tar")
        .args(["-xf", tmp.path().to_str().unwrap_or_default(),
               "-C",  dest.to_str().unwrap_or_default()])
        .status()
        .context("tar")?;
    if !status.success() { anyhow::bail!("tar exit {}", status.code().unwrap_or(-1)); }
    Ok(())
}

fn print_json_pretty_or_write(bytes: &[u8], format: &str, out: Option<&PathBuf>) -> Result<()> {
    if format == "json" || out.is_some() {
        return write_or_stdout(bytes, out);
    }
    // Pretty text for humans. Fall back to raw JSON if it doesn't parse.
    let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) else {
        return write_or_stdout(bytes, None);
    };
    let errs = v.get("errors").and_then(|x| x.as_u64()).unwrap_or(0);
    let warns = v.get("warnings").and_then(|x| x.as_u64())
        .or_else(|| v.get("warning").and_then(|x| x.as_u64()))
        .unwrap_or(0);
    let total = v.get("count").and_then(|x| x.as_u64()).unwrap_or(errs + warns);
    println!("errors: {errs}  warnings: {warns}  total: {total}");
    if let Some(violations) = v.get("violations").and_then(|x| x.as_array()) {
        for violation in violations.iter().take(20) {
            let sev = violation.get("severity").and_then(|s| s.as_str()).unwrap_or("?");
            let typ = violation.get("type").and_then(|s| s.as_str()).unwrap_or("?");
            let msg = violation.get("description").or_else(|| violation.get("message")).and_then(|s| s.as_str()).unwrap_or("");
            println!("  [{sev}] {typ}: {msg}");
        }
        if violations.len() > 20 { println!("  … {} more", violations.len() - 20); }
    }
    Ok(())
}

// ─── Commands ─────────────────────────────────────────────────────────

fn main() -> Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Cmd::Health => {
            let bytes = get_bytes("/health")?;
            std::io::stdout().write_all(&bytes)?;
            println!();
            Ok(())
        }
        Cmd::Version => {
            let bytes = get_bytes("/version")?;
            std::io::stdout().write_all(&bytes)?;
            println!();
            Ok(())
        }
        Cmd::Config => {
            let effective = base_url();
            let prod = PROD_URL;
            let source = if std::env::var("SERVICE_KICAD_URL").is_ok() {
                "SERVICE_KICAD_URL env"
            } else if std::env::var("KICAD_SERVICE_API").is_ok() {
                "KICAD_SERVICE_API env (legacy alias)"
            } else {
                "compile-time baked-in URL"
            };
            println!("url       : {effective}");
            println!("source    : {source}");
            println!("prod bake : {prod}");
            if source.starts_with("compile") {
                println!("hint      : export SERVICE_KICAD_URL=<url> to target a self-hosted instance");
            }
            Ok(())
        }
        Cmd::Pcb(PcbCmd::Drc { file, format, out }) => {
            let bytes = post_file_bytes("/kicad/pcb/drc", &file)?;
            print_json_pretty_or_write(&bytes, &format, out.as_ref())
        }
        Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Svg { file, side, layers, out })) => {
            let mut path = format!("/kicad/pcb/export/svg?side={}", urlencode(&side));
            if let Some(l) = layers {
                path.push_str(&format!("&layers={}", urlencode(&l)));
            }
            let bytes = post_file_bytes(&path, &file)?;
            write_or_stdout(&bytes, out.as_ref())
        }
        Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Gerbers { file, out })) => {
            let bytes = post_file_bytes("/kicad/pcb/export/gerbers", &file)?;
            match out {
                Some(ref d) => extract_tarball(&bytes, d),
                None => {
                    let d = PathBuf::from("gerbers");
                    extract_tarball(&bytes, &d)
                }
            }
        }
        Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Step { file, out, flags })) => {
            let qs = flags_to_query_string(&flags);
            let path = format!("/kicad/pcb/export/step{qs}");
            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())
        }
        Cmd::Pcb(PcbCmd::Export(PcbExportCmd::Glb { file, out, flags, i_know_this_skips_the_pipeline })) => {
            glb_gate(i_know_this_skips_the_pipeline);
            let qs = flags_to_query_string(&flags);
            let path = format!("/kicad/pcb/export/glb{qs}");
            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())
        }
        Cmd::Pcb(PcbCmd::Export(PcbExportCmd::GlbZup { file, out, flags, i_know_this_skips_the_pipeline })) => {
            glb_gate(i_know_this_skips_the_pipeline);
            let qs = flags_to_query_string(&flags);
            let path = format!("/kicad/pcb/export/glb-zup{qs}");
            write_or_stdout(&post_file_bytes(&path, &file)?, out.as_ref())
        }

        Cmd::Sch(SchCmd::Erc { file, format, out }) => {
            let bytes = post_file_bytes("/kicad/sch/erc", &file)?;
            print_json_pretty_or_write(&bytes, &format, out.as_ref())
        }
        Cmd::Sch(SchCmd::Export(SchExportCmd::Svg { file, out })) =>
            write_or_stdout(&post_file_bytes("/kicad/sch/export/svg", &file)?, out.as_ref()),
        Cmd::Sch(SchCmd::Export(SchExportCmd::SvgProject { dir, out })) => {
            if !dir.is_dir() {
                anyhow::bail!("{} is not a directory — pass the project dir (.kicad_pro + sheets)", dir.display());
            }
            // Tar the project dir client-side; the service picks the root
            // sheet (stem matching the .kicad_pro) and returns a tar of SVGs.
            let tmp = tempfile::Builder::new().suffix(".tar").tempfile()?;
            let status = std::process::Command::new("tar")
                .args(["-cf", tmp.path().to_str().unwrap_or_default(),
                       "-C",  dir.to_str().unwrap_or_default(), "."])
                .status().context("tar")?;
            if !status.success() { anyhow::bail!("tar exit {}", status.code().unwrap_or(-1)); }
            let bytes = post_file_bytes("/kicad/sch/export/svg-project", &tmp.path().to_path_buf())?;
            let dest = out.unwrap_or_else(|| PathBuf::from("sch-svg"));
            extract_tarball(&bytes, &dest)?;
            eprintln!("→ {}/ (per-sheet SVGs)", dest.display());
            Ok(())
        }
        Cmd::Sch(SchCmd::Export(SchExportCmd::Pdf { file, out })) =>
            write_or_stdout(&post_file_bytes("/kicad/sch/export/pdf", &file)?, out.as_ref()),
        Cmd::Sch(SchCmd::Export(SchExportCmd::Bom { file, out })) =>
            write_or_stdout(&post_file_bytes("/kicad/sch/export/bom", &file)?, out.as_ref()),

        Cmd::Sym(SymCmd::Svg { library, name, out }) => {
            let bytes = get_bytes(&format!("/sym/export/svg/{}/{}", urlencode(&library), urlencode(&name)))?;
            write_or_stdout(&bytes, out.as_ref())
        }
        Cmd::Sym(SymCmd::Fetch { library, out }) => {
            let bytes = get_bytes(&format!("/symbols/{}.kicad_sym", urlencode(&library)))?;
            write_or_stdout(&bytes, out.as_ref())
        }
        Cmd::Fp(FpCmd::Svg { library, name, out }) => {
            let bytes = get_bytes(&format!("/fp/export/svg/{}/{}", urlencode(&library), urlencode(&name)))?;
            write_or_stdout(&bytes, out.as_ref())
        }
        Cmd::Fp(FpCmd::Fetch { library, name, out }) => {
            let bytes = get_bytes(&format!("/footprints/{}.pretty/{}.kicad_mod", urlencode(&library), urlencode(&name)))?;
            write_or_stdout(&bytes, out.as_ref())
        }
        Cmd::Model { cmd: ModelCmd::Fetch { path, out } } => {
            let bytes = get_bytes(&format!("/models/{}", path.trim_start_matches('/')))?;
            write_or_stdout(&bytes, out.as_ref())
        }

        Cmd::Altium(AltiumCmd::Sym { file, out }) => {
            let bytes = post_file_bytes("/kicad/altium/sym", &file)?;
            let dest = out.unwrap_or_else(|| file.with_extension("kicad_sym"));
            write_or_stdout(&bytes, Some(&dest))
        }
        Cmd::Altium(AltiumCmd::Fp { file, out }) => {
            let bytes = post_file_bytes("/kicad/altium/fp", &file)?;
            let dest = out.unwrap_or_else(|| file.with_extension("kicad_mod"));
            write_or_stdout(&bytes, Some(&dest))
        }

        Cmd::Install { skill_only, completions_only } => do_install(skill_only, completions_only),
    }
}

/// Refuse KiCad-native GLB export unless explicitly acknowledged (page
/// issue #106). Acknowledged exports still come back provenance-stamped
/// by the service (`asset.extras.adom.provenance = "kicad-native-glb"`).
fn glb_gate(acknowledged: bool) {
    if acknowledged {
        eprintln!("WARNING: KiCad-native GLB skips the molecule pipeline; output is");
        eprintln!("provenance-stamped (asset.extras.adom.provenance = \"kicad-native-glb\").");
        return;
    }
    eprintln!("ERROR: `pcb export glb` is gated (page issue #106).");
    eprintln!("KiCad-native GLB bypasses the molecule pipeline — no MP-marker");
    eprintln!("anchoring, gold machine-pin material, meter normalization, or");
    eprintln!("footprint emit. Export STEP here, then convert with step2glb:");
    eprintln!("Hint: service-kicad pcb export step board.kicad_pcb --out board.step");
    eprintln!("Hint: adom-step2glb board.step   # molecule mode when the board has machine pins");
    eprintln!("Hint: for a quick non-molecule preview only, re-run with --i-know-this-skips-the-pipeline");
    std::process::exit(1);
}

/// Absolute-minimum URL encoder — just the three characters we actually
/// see in library/name path segments.
fn urlencode(s: &str) -> String {
    s.replace('%', "%25")
     .replace(' ', "%20")
     .replace('/', "%2F")
}

const WIKI_BASE: &str = "https://wiki.adom.inc";

/// Fetch the canonical SKILL.md from the wiki page repo's raw-file
/// endpoint. No fallback bundled via include_str! — we never bake skill
/// prose into the binary because prose is edited far more often than the
/// binary ships (per adom-cli-design rule).
fn fetch_skill_from_wiki() -> Result<String> {
    let url = format!("{}/api/pages/adom/service-kicad/files/SKILL.md", WIKI_BASE);
    let text = http_client()
        .get(&url)
        .send()?
        .error_for_status()?
        .text()?;
    if text.trim().is_empty() || !text.starts_with("---") {
        return Err(anyhow!("wiki returned unexpected SKILL.md content from {url}"));
    }
    Ok(text)
}

fn do_install(skill_only: bool, completions_only: bool) -> Result<()> {
    let home = std::env::var("HOME").map(PathBuf::from)
        .map_err(|_| anyhow!("no $HOME"))?;
    if !completions_only {
        let skill_dir = home.join(".claude/skills/service-kicad");
        std::fs::create_dir_all(&skill_dir)?;
        let dst = skill_dir.join("SKILL.md");
        match fetch_skill_from_wiki() {
            Ok(skill) => {
                std::fs::write(&dst, skill)?;
                eprintln!("✓ skill fetched from wiki → {}", dst.display());
            }
            Err(e) if dst.exists() => {
                // Gallia (Tier A path) or a prior install already wrote
                // SKILL.md. A transient wiki outage isn't a reason to
                // overwrite or blow up.
                eprintln!("! wiki fetch failed ({e}) — keeping existing {}", dst.display());
            }
            Err(e) => {
                return Err(anyhow!("could not fetch SKILL.md from wiki and no existing skill to keep: {e}"));
            }
        }
    }
    if !skill_only {
        let comp_dir = home.join(".bash_completion.d");
        std::fs::create_dir_all(&comp_dir)?;
        let bash_comp = comp_dir.join("service-kicad");
        std::fs::write(&bash_comp,
            "# service-kicad bash completions\n\
             _service_kicad() {\n\
             \tlocal cur=\"${COMP_WORDS[COMP_CWORD]}\"\n\
             \tCOMPREPLY=($(compgen -W \"health version config pcb sch sym fp model altium install\" -- \"$cur\"))\n\
             }\n\
             complete -F _service_kicad service-kicad\n")?;
        eprintln!("✓ completions at {}", bash_comp.display());
    }
    Ok(())
}