mod commands;
mod dashboard;
mod graph_cmd;
mod graph_viz;
mod mcp;
mod report;

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(
    name = "mimir",
    version,
    about = "Unified local-first memory for AI coding agents"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,

    /// Emit JSONL instead of the human/agent text format.
    #[arg(long, global = true)]
    json: bool,
}

#[derive(Subcommand)]
enum Command {
    /// Create config + database and print setup instructions.
    Init {
        /// Skip the embedding-model download (search stays BM25-only).
        #[arg(long)]
        no_model: bool,
    },
    /// Store overview: counts by kind, scope, database size.
    Status,
    /// Activity table: day / week / month / year / all-time.
    Report,
    /// Health checks: database integrity, FTS availability, model presence.
    Doctor,
    /// Capture a memory (refuses near-duplicates unless --force).
    Remember {
        /// Text to remember.
        #[arg(required = true)]
        text: Vec<String>,
        /// insight|decision|gotcha|idea|note|person
        #[arg(short = 't', long = "type", default_value = "note")]
        mtype: String,
        /// Comma-separated tags.
        #[arg(long, value_delimiter = ',')]
        tags: Vec<String>,
        /// Store cross-project (global) even when inside a project.
        #[arg(short, long)]
        global: bool,
        /// Store even if a near-duplicate exists.
        #[arg(long)]
        force: bool,
        /// Link the new memory to an existing node.
        #[arg(long, value_name = "REF")]
        link: Option<String>,
    },
    /// Search memories (and later docs/code) with hybrid ranking.
    Recall {
        #[arg(required = true)]
        query: Vec<String>,
        /// all|memory|doc|code
        #[arg(long, default_value = "all")]
        kind: String,
        /// Only cross-project (global) results.
        #[arg(short, long)]
        global: bool,
        /// Search every project (no scope filter).
        #[arg(long)]
        all: bool,
        /// Only results newer than e.g. 12h, 7d, 2w, 3m, 1y.
        #[arg(long)]
        since: Option<String>,
        #[arg(short = 'n', long)]
        limit: Option<usize>,
        /// Print full bodies instead of one-line summaries.
        #[arg(long)]
        full: bool,
        /// Rescore the top candidates with a cross-encoder (needs the
        /// reranker model: `mimir embed --fetch --rerank`).
        #[arg(long)]
        rerank: bool,
        /// Show nodes linked to each hit (memories on code, code on memories).
        #[arg(long)]
        linked: bool,
    },
    /// Show full records by reference (logs the access).
    Get {
        #[arg(required = true)]
        refs: Vec<String>,
    },
    /// Recent memories, newest first.
    List {
        /// Filter by memory type.
        #[arg(short = 't', long = "type")]
        mtype: Option<String>,
        /// Filter by tag.
        #[arg(long)]
        tag: Option<String>,
        #[arg(short, long)]
        global: bool,
        #[arg(long)]
        all: bool,
        #[arg(short = 'n', long, default_value_t = 20)]
        limit: usize,
    },
    /// Soft-delete a node (--hard to remove permanently).
    Forget {
        reference: String,
        #[arg(long)]
        hard: bool,
    },
    /// Update a memory's text, title, type, or tags.
    Edit {
        reference: String,
        /// Replacement text (optional when only changing metadata).
        text: Vec<String>,
        #[arg(long)]
        title: Option<String>,
        #[arg(short = 't', long = "type")]
        mtype: Option<String>,
        #[arg(long, value_delimiter = ',')]
        tags: Option<Vec<String>>,
        /// Exempt from decay and consolidation merging.
        #[arg(long, conflicts_with = "unpin")]
        pin: bool,
        #[arg(long)]
        unpin: bool,
    },
    /// Create an edge between two nodes.
    Link {
        a: Option<String>,
        b: Option<String>,
        /// links|about|relates|mentions|describes|supersedes…
        #[arg(long, default_value = "relates")]
        rel: String,
        /// Auto-link memories to code symbols their text mentions
        /// (current project; precision-first heuristic, idempotent).
        #[arg(long)]
        scan: bool,
        /// With --scan: show what would be linked without writing.
        #[arg(long)]
        dry_run: bool,
    },
    /// Manage docs collections.
    Docs {
        #[command(subcommand)]
        cmd: DocsCmd,
    },
    /// (Re)index docs collections incrementally.
    Index {
        /// Collection name or path; omit to index all.
        name: Option<String>,
    },
    /// Embed new/changed content for semantic recall.
    Embed {
        /// Allow downloading the model if it isn't cached yet.
        #[arg(long)]
        fetch: bool,
        /// With --fetch: also download the reranker model.
        #[arg(long)]
        rerank: bool,
    },
    /// Strengthen or weaken a memory's ranking (explicit feedback).
    Mark {
        reference: String,
        /// This was useful (+1 strength).
        #[arg(long, conflicts_with = "noise")]
        useful: bool,
        /// This was noise (-1 strength).
        #[arg(long)]
        noise: bool,
    },
    /// Run the four consolidation passes (dedup, contradictions,
    /// distillation, decay archival).
    Consolidate {
        /// Report without changing anything.
        #[arg(long)]
        dry_run: bool,
    },
    /// Import memories from the tools Mimir replaces.
    Import {
        #[command(subcommand)]
        cmd: ImportCmd,
    },
    /// Dump all live nodes and edges as JSONL (backup / migration).
    Export,
    /// Generate a self-contained HTML stats dashboard.
    Dashboard {
        /// Output path (default: temp dir).
        #[arg(short, long)]
        out: Option<String>,
        /// Open in the default browser.
        #[arg(long)]
        open: bool,
    },
    /// Run the MCP stdio server (what Claude Code launches).
    Mcp,
    /// Build and query the code graph of the current project.
    Graph {
        #[command(subcommand)]
        cmd: GraphCmd,
    },
}

#[derive(Subcommand)]
enum ImportCmd {
    /// OpenBrain `list_thoughts` text export (file or - for stdin).
    Openbrain { file: String },
    /// A Claude Code auto-memory directory (the per-project memory/ dir).
    ClaudeMemory { dir: String },
    /// Register the collections from a qmd index.yml, then `mimir index`.
    Qmd {
        /// Path to index.yml (default: ~/.config/qmd/index.yml).
        file: Option<String>,
    },
}

#[derive(Subcommand)]
enum GraphCmd {
    /// Extract/refresh symbols and call edges (incremental).
    Build,
    /// Alias of build (always incremental).
    Update,
    /// Who calls this symbol (transitively).
    Callers {
        symbol: String,
        #[arg(long, default_value_t = 3)]
        depth: usize,
    },
    /// What this symbol calls (transitively).
    Calls {
        symbol: String,
        #[arg(long, default_value_t = 3)]
        depth: usize,
    },
    /// Blast radius of changing these files (try `$(git diff --name-only)`).
    Impact {
        #[arg(required = true)]
        files: Vec<String>,
        #[arg(long, default_value_t = 3)]
        depth: usize,
    },
    /// Full record for a symbol: signature, doc, edges, linked memories.
    Node { symbol: String },
    /// Shortest call path between two symbols.
    Path { from: String, to: String },
    /// Most-called symbols.
    Hubs {
        #[arg(short = 'n', long, default_value_t = 10)]
        limit: usize,
    },
    /// Call-graph communities with heuristic names.
    Communities {
        /// Store them as community nodes (member_of edges).
        #[arg(long)]
        persist: bool,
        #[arg(long, default_value_t = 4)]
        min_size: usize,
    },
    /// Interactive HTML visualization of the project graph (memories,
    /// symbols, files, docs and the edges between them).
    Viz {
        /// Output path (default: temp dir).
        #[arg(short, long)]
        out: Option<String>,
        /// Open in the default browser.
        #[arg(long)]
        open: bool,
        /// Node budget: all memories + best-connected code up to this many.
        #[arg(long, default_value_t = 1500)]
        max_nodes: usize,
    },
}

#[derive(Subcommand)]
enum DocsCmd {
    /// Register a folder of markdown docs.
    Add {
        path: String,
        /// Display name (default: folder name).
        #[arg(long)]
        name: Option<String>,
        /// Register cross-project (global) even when inside a project.
        #[arg(short, long)]
        global: bool,
    },
    /// List collections with file/chunk counts.
    List,
    /// Soft-delete a collection and everything indexed under it.
    Remove { name: String },
    /// Attach a context note to a collection or any node.
    Note {
        /// Collection name/path or node reference.
        target: String,
        #[arg(required = true)]
        text: Vec<String>,
    },
}

fn main() {
    // Behave like a normal Unix tool when piped into head/grep: die on
    // SIGPIPE instead of panicking on a failed stdout write.
    #[cfg(unix)]
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }

    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()),
        )
        .with_writer(std::io::stderr)
        .init();

    let cli = Cli::parse();
    let code = match run(cli) {
        Ok(()) => 0,
        Err(err) => {
            eprintln!("Error: {err:#}");
            1
        }
    };

    // GPU builds: Dawn/onnxruntime process-teardown destructors are known
    // to segfault (ort 2.0-rc). All work is flushed and SQLite-committed
    // by now, so skip libc teardown entirely instead of crashing on exit.
    #[cfg(any(feature = "gpu-webgpu", feature = "gpu-cuda"))]
    {
        use std::io::Write as _;
        let _ = std::io::stdout().flush();
        let _ = std::io::stderr().flush();
        // SAFETY: bypasses atexit/dso destructors on purpose; no state
        // depends on them.
        unsafe { libc::_exit(code) }
    }
    #[cfg(not(any(feature = "gpu-webgpu", feature = "gpu-cuda")))]
    std::process::exit(code)
}

fn run(cli: Cli) -> anyhow::Result<()> {
    match cli.command {
        Command::Init { no_model } => commands::init(no_model),
        Command::Status => commands::status(cli.json),
        Command::Doctor => commands::doctor(),
        Command::Remember {
            text,
            mtype,
            tags,
            global,
            force,
            link,
        } => commands::remember(cli.json, text.join(" "), &mtype, tags, global, force, link),
        Command::Recall {
            query,
            kind,
            global,
            all,
            since,
            limit,
            full,
            rerank,
            linked,
        } => commands::recall(
            cli.json,
            query.join(" "),
            &kind,
            global,
            all,
            since,
            limit,
            full,
            rerank,
            linked,
        ),
        Command::Get { refs } => commands::get(cli.json, refs),
        Command::List {
            mtype,
            tag,
            global,
            all,
            limit,
        } => commands::list(cli.json, mtype, tag, global, all, limit),
        Command::Forget { reference, hard } => commands::forget(&reference, hard),
        Command::Edit {
            reference,
            text,
            title,
            mtype,
            tags,
            pin,
            unpin,
        } => commands::edit(
            cli.json,
            &reference,
            text.join(" "),
            title,
            mtype,
            tags,
            if pin {
                Some(true)
            } else if unpin {
                Some(false)
            } else {
                None
            },
        ),
        Command::Link {
            a,
            b,
            rel,
            scan,
            dry_run,
        } => match (scan, a, b) {
            (true, _, _) => commands::link_scan(dry_run),
            (false, Some(a), Some(b)) => commands::link(&a, &b, &rel),
            _ => anyhow::bail!(
                "usage: mimir link <A> <B> [--rel REL]  |  mimir link --scan [--dry-run]"
            ),
        },
        Command::Docs { cmd } => match cmd {
            DocsCmd::Add { path, name, global } => commands::docs_add(&path, name, global),
            DocsCmd::List => commands::docs_list(cli.json),
            DocsCmd::Remove { name } => commands::docs_remove(&name),
            DocsCmd::Note { target, text } => commands::docs_note(&target, text.join(" ")),
        },
        Command::Index { name } => commands::index(name),
        Command::Embed { fetch, rerank } => commands::embed(fetch, rerank),
        Command::Mark {
            reference,
            useful,
            noise,
        } => commands::mark(&reference, useful || !noise),
        Command::Consolidate { dry_run } => commands::consolidate(dry_run),
        Command::Import { cmd } => match cmd {
            ImportCmd::Openbrain { file } => commands::import_openbrain(&file),
            ImportCmd::ClaudeMemory { dir } => commands::import_claude_memory(&dir),
            ImportCmd::Qmd { file } => commands::import_qmd(file),
        },
        Command::Export => commands::export(),
        Command::Dashboard { out, open } => dashboard::dashboard(out, open),
        Command::Report => report::report(cli.json),
        Command::Mcp => mcp::run(),
        Command::Graph { cmd } => match cmd {
            GraphCmd::Build | GraphCmd::Update => graph_cmd::build(),
            GraphCmd::Callers { symbol, depth } => graph_cmd::callers(&symbol, depth),
            GraphCmd::Calls { symbol, depth } => graph_cmd::calls(&symbol, depth),
            GraphCmd::Impact { files, depth } => graph_cmd::impact(files, depth),
            GraphCmd::Node { symbol } => graph_cmd::node_info(&symbol),
            GraphCmd::Path { from, to } => graph_cmd::path(&from, &to),
            GraphCmd::Hubs { limit } => graph_cmd::hubs(limit),
            GraphCmd::Communities { persist, min_size } => {
                graph_cmd::communities(persist, min_size)
            }
            GraphCmd::Viz {
                out,
                open,
                max_nodes,
            } => graph_viz::viz(out, open, max_nodes),
        },
    }
}
