use async_trait::async_trait;

// ─── Parameter types (transport-independent) ─────────────────────

/// A single query response in a batch feed.
#[derive(Debug)]
pub struct QueryResponse {
    /// Query ID (e.g. "q-0", "q-1").
    pub query_id: String,
    /// The host LLM's response for this query.
    pub response: String,
    /// Token usage reported by the host for this query.
    pub usage: Option<crate::TokenUsage>,
}

// ─── Engine API trait ────────────────────────────────────────────

/// Transport-independent API for the algocline engine.
///
/// Abstracts the full public surface of AppService so that callers
/// (MCP handler, future daemon client, etc.) can operate through
/// `Arc<dyn EngineApi>` without depending on the concrete implementation.
///
/// All methods are async to support both local (in-process) and remote
/// (socket/HTTP) implementations uniformly.
#[async_trait]
pub trait EngineApi: Send + Sync {
    // ─── Core execution ──────────────────────────────────────

    /// Execute Lua code with optional JSON context.
    ///
    /// When `host_mode: Some(true)` is passed, the call is proxied via
    /// `PoolClient` to a long-lived worker subprocess over a Unix domain socket.
    /// When `host_mode` is `None` or `Some(false)` the existing in-process
    /// `Executor::start_session` path is used unchanged.
    async fn run(
        &self,
        code: Option<String>,
        code_file: Option<String>,
        ctx: Option<serde_json::Value>,
        project_root: Option<String>,
        host_mode: Option<bool>,
    ) -> Result<String, String>;

    /// Apply an installed strategy package. Task is optional.
    async fn advice(
        &self,
        strategy: &str,
        task: Option<String>,
        opts: Option<serde_json::Value>,
        project_root: Option<String>,
    ) -> Result<String, String>;

    /// Continue a paused execution — single response (with optional query_id).
    async fn continue_single(
        &self,
        session_id: &str,
        response: String,
        query_id: Option<&str>,
        usage: Option<crate::TokenUsage>,
    ) -> Result<String, String>;

    /// Continue a paused execution — batch feed.
    async fn continue_batch(
        &self,
        session_id: &str,
        responses: Vec<QueryResponse>,
    ) -> Result<String, String>;

    // ─── Session status ──────────────────────────────────────

    /// Query active session status.
    ///
    /// `pending_filter` is a free-form JSON value forwarded from MCP
    /// callers, decoded inside the app layer into either a preset name
    /// (`"meta"` / `"preview"` / `"full"`) or a custom field-filter
    /// object. `None` keeps the legacy count-only snapshot.
    ///
    /// `include_history`: when `true`, each session snapshot includes
    /// `conversation_history` (capped at 10 entries). Default `false`
    /// preserves the lightweight snapshot contract for high-frequency pollers.
    async fn status(
        &self,
        session_id: Option<&str>,
        pending_filter: Option<serde_json::Value>,
        include_history: bool,
    ) -> Result<String, String>;

    // ─── Evaluation ──────────────────────────────────────────

    /// Run an evalframe evaluation suite.
    ///
    /// `auto_card`: when true, emit an immutable Card
    /// (`~/.algocline/cards/{strategy}/{card_id}.toml`) summarizing the run.
    async fn eval(
        &self,
        scenario: Option<String>,
        scenario_file: Option<String>,
        scenario_name: Option<String>,
        strategy: &str,
        strategy_opts: Option<serde_json::Value>,
        auto_card: bool,
    ) -> Result<String, String>;

    /// List eval history, optionally filtered by strategy.
    async fn eval_history(&self, strategy: Option<&str>, limit: usize) -> Result<String, String>;

    /// View a specific eval result by ID.
    async fn eval_detail(&self, eval_id: &str) -> Result<String, String>;

    /// Compare two eval results with statistical significance testing.
    async fn eval_compare(&self, eval_id_a: &str, eval_id_b: &str) -> Result<String, String>;

    // ─── Scenarios ───────────────────────────────────────────

    /// List available scenarios.
    async fn scenario_list(&self) -> Result<String, String>;

    /// Show the content of a named scenario.
    async fn scenario_show(&self, name: &str) -> Result<String, String>;

    /// Install scenarios from a Git URL or local path.
    async fn scenario_install(&self, url: String) -> Result<String, String>;

    // ─── Packages ────────────────────────────────────────────

    /// Link a local directory as a project-local package (symlink to cache).
    ///
    /// Scope selection:
    /// - `scope = None` or `Some("global")` — symlink into `~/.algocline/packages/`
    ///   (visible to all projects).
    /// - `scope = Some("variant")` — record the path in `alc.local.toml`
    ///   at the project root (worktree-scoped override, git-ignored). No
    ///   symlink is created.
    /// - Any other value → `Err("invalid scope: ...")`.
    ///
    /// `project_root` is only consulted when `scope = Some("variant")`.
    /// If `None`, falls back to `ALC_PROJECT_ROOT` env or ancestor walk
    /// from cwd.
    async fn pkg_link(
        &self,
        path: String,
        name: Option<String>,
        force: Option<bool>,
        scope: Option<String>,
        project_root: Option<String>,
    ) -> Result<String, String>;

    /// List installed packages with metadata.
    ///
    /// When `project_root` is provided, project-local packages from `alc.toml`/`alc.lock`
    /// are included with `scope: "project"`. Global packages carry `scope: "global"`.
    ///
    /// Mirrors the list-tool knob contract used by [`Self::hub_search`]
    /// (plan.md §4.1). Parameters are individual JSON-primitive
    /// `Option<T>` values so the `algocline-core` crate stays free of
    /// `algocline-app`-internal types; the impl folds them into its
    /// `pub(crate) ListOpts` struct.
    ///
    /// - `limit` is `Option<i32>` at this layer (MCP/JSON boundary).
    ///   The impl clamps negative values to 0 and casts to `usize`.
    ///   `Some(0)` (and thus clamped negatives) means **no limit**
    ///   (return all entries — empty-means-all idiom); `None` falls
    ///   back to the tool's default cap.
    /// - `filter` is a free-form JSON object; it is `Deserialize`d into
    ///   a `HashMap<String, Value>` inside the app layer. Non-object
    ///   values are logged via `tracing::warn` and treated as no filter.
    /// - `fields` / `verbose` drive projection on each entry of the
    ///   `packages` array; `fields` wins when both are supplied.
    /// - Top-level keys (`packages`, `search_paths`, `project_root`,
    ///   `lockfile_path`) are never projected away.
    #[allow(clippy::too_many_arguments)]
    async fn pkg_list(
        &self,
        project_root: Option<String>,
        limit: Option<i32>,
        sort: Option<String>,
        filter: Option<serde_json::Value>,
        fields: Option<Vec<String>>,
        verbose: Option<String>,
    ) -> Result<String, String>;

    /// Install a package from a Git URL or local path.
    ///
    /// `force` (optional, default `false`): Collection mode only — overwrite existing
    /// packages at dest. Single mode rejects pre-existing dest with an error regardless.
    async fn pkg_install(
        &self,
        url: String,
        name: Option<String>,
        force: Option<bool>,
    ) -> Result<String, String>;

    /// Remove a symlinked package from `~/.algocline/packages/`.
    ///
    /// Only removes symlinks; for installed (copied) packages, use `pkg_remove`.
    async fn pkg_unlink(&self, name: String) -> Result<String, String>;

    /// Remove a package entry, scoped by `scope` (`"project"` /
    /// `"global"` / `"all"`, default `"project"`).
    ///
    /// - `"project"`: remove from `alc.toml` + `alc.lock`. Requires an
    ///   `alc.toml` via `project_root` or ancestor walk.
    /// - `"global"`: remove from `~/.algocline/installed.json` only.
    ///   `project_root` is ignored.
    /// - `"all"`: remove from both; succeeds if either scope had the entry.
    ///
    /// Physical files in `~/.algocline/packages/{name}/` are never deleted.
    async fn pkg_remove(
        &self,
        name: &str,
        project_root: Option<String>,
        version: Option<String>,
        scope: Option<String>,
    ) -> Result<String, String>;

    /// Heal broken package state by reinstalling entries whose installed
    /// directory is missing. Other broken kinds (dangling symlink,
    /// declared-path missing) are surfaced as `unrepairable` with a
    /// suggested remediation.
    async fn pkg_repair(
        &self,
        name: Option<String>,
        project_root: Option<String>,
    ) -> Result<String, String>;

    /// Diagnose package state without side effects.
    ///
    /// Read-only counterpart of [`Self::pkg_repair`]. Classifies packages
    /// into four buckets — `healthy`, `installed_missing`, `symlink_dangling`,
    /// `path_missing` — and returns the result as a JSON string. No
    /// filesystem writes, no `pkg_install` calls.
    ///
    /// `name` restricts the report to a single package; `None` inspects
    /// every known package. `project_root` is used for the `alc.toml` /
    /// `alc.local.toml` pass (falls back to ancestor walk from cwd).
    async fn pkg_doctor(
        &self,
        name: Option<String>,
        project_root: Option<String>,
    ) -> Result<String, String>;

    /// Run mlua-lspec tests for a package, a single file, or inline code.
    ///
    /// Exactly one of `pkg`, `code_file`, or `code` must be provided; providing
    /// zero or more than one returns a typed `Err`.
    ///
    /// # Arguments
    ///
    /// * `pkg` — installed package name; spec files are discovered under
    ///   `<pkg_root>/<spec_dir>/*_spec.lua` (default `spec_dir = "spec"`).
    /// * `code_file` — absolute path to a single `.lua` test file.
    /// * `code` — inline Lua source code containing lspec tests.
    /// * `spec_dir` — subdirectory within the package root that holds spec files
    ///   (default `"spec"`). Only used when `pkg` is provided.
    /// * `filter` — substring filter applied to spec file stems (only when `pkg`
    ///   is provided).
    /// * `search_paths` — additional directories appended to `package.path`
    ///   inside the Lua VM, after auto-resolved paths.
    /// * `project_root` — optional project root for variant-scope package
    ///   resolution (`alc.local.toml`). Falls back to ancestor walk from cwd.
    /// * `auto_search_paths` — when `true` (default) or `None`, auto-prepends
    ///   parent dirs of all linked/installed packages (installed
    ///   `~/.algocline/packages/`, `alc.toml` path entries, `alc.local.toml`
    ///   path entries) to `package.path`. When `false`, no auto-resolve is
    ///   performed and zero paths are injected. Resolved mapping is returned in
    ///   the JSON response `resolved_search_paths` field.
    ///
    /// # Returns
    ///
    /// On success: JSON string `{passed, failed, pending, total, duration_ms,
    /// spec_files: [{path, passed, failed, total, duration_ms, tests: [{suite,
    /// name, passed, pending, error}]}], resolved_search_paths: [{name,
    /// search_dir, source}], search_path_warnings?: [...]}`.
    ///
    /// # Errors
    ///
    /// * Zero or multiple input sources provided → `"pkg_test: provide exactly
    ///   one of pkg, code_file, code"`.
    /// * `pkg` not found → `"pkg_test: package '<name>' not found …"`.
    /// * No spec files found → `"pkg_test: no spec files found in <path> …"`.
    /// * mlua VM init failure, I/O errors, or `spawn_blocking` panic → typed
    ///   `Err` string.
    // 9 parameters are justified by the MCP wire shape: 3 mutually exclusive
    // input sources (pkg / code_file / code) plus filtering/path/auto-resolve
    // options.
    #[allow(clippy::too_many_arguments)]
    async fn pkg_test(
        &self,
        pkg: Option<String>,
        code_file: Option<String>,
        code: Option<String>,
        spec_dir: Option<String>,
        filter: Option<String>,
        search_paths: Option<Vec<String>>,
        project_root: Option<String>,
        auto_search_paths: Option<bool>,
    ) -> Result<String, String>;

    // ─── Logging ─────────────────────────────────────────────

    /// Append a note to a session's log file.
    async fn add_note(
        &self,
        session_id: &str,
        content: &str,
        title: Option<&str>,
    ) -> Result<String, String>;

    /// View session logs.
    async fn log_view(
        &self,
        session_id: Option<&str>,
        limit: Option<usize>,
        max_chars: Option<usize>,
    ) -> Result<String, String>;

    /// Aggregate stats across all logged sessions.
    async fn stats(
        &self,
        strategy_filter: Option<&str>,
        days: Option<u64>,
    ) -> Result<String, String>;

    // ─── Project lifecycle ────────────────────────────────────

    /// Initialize `alc.toml` in the given project root.
    ///
    /// Creates a minimal `alc.toml` (`[packages]` section only).
    /// Fails if `alc.toml` already exists (no overwrite).
    async fn init(&self, project_root: Option<String>) -> Result<String, String>;

    /// Re-resolve all `alc.toml` entries and rewrite `alc.lock`.
    ///
    /// Requires an `alc.toml` to be present. Returns resolved count and errors.
    async fn update(&self, project_root: Option<String>) -> Result<String, String>;

    /// Migrate a legacy `alc.lock` to `alc.toml` + new `alc.lock` format.
    ///
    /// Detects legacy format via `linked_at` / `local_dir` fields.
    /// Backs up the old lock file as `alc.lock.bak`.
    async fn migrate(&self, project_root: Option<String>) -> Result<String, String>;

    // ─── Package narrative (issue #1778221491-39903) ─────────

    /// Render the narrative markdown for a package on-the-fly.
    ///
    /// Extracts the init.lua docstring H2/H3 sections via the embedded
    /// gendoc pipeline (`extract.split_sections` + `projections.narrative_md`)
    /// and returns the rendered markdown string.
    ///
    /// Returns `Ok(Some(markdown))` when the pkg is found and its init.lua
    /// is loadable. Returns `Ok(None)` when the pkg is not installed.
    /// Returns `Err(...)` when the pkg is found but the gendoc pipeline fails
    /// (e.g. malformed init.lua).
    async fn pkg_get_narrative_md(&self, name: &str) -> Result<Option<String>, String>;

    // ─── Session activation (issue #1776627475) ──────────────

    /// Activate a session pin for the current MCP connection.
    ///
    /// `project_root` is resolved at activation time using the
    /// existing fallback chain (P > E > W) and cached on the
    /// `AppService`. Subsequent tool calls without an explicit
    /// `project_root` argument resolve via P > **S** > E > W,
    /// where S is this pin (issue #1776627475 §6).
    ///
    /// `mode` accepts `"default"` (or `None`) and `"test"`. Unknown
    /// values return a typed error rather than silent fallback.
    /// Mode is exposed back to callers so downstream tools can
    /// adapt behaviour (e.g. scenario test isolation).
    ///
    /// Returns a JSON string with `session_id`, `project_root`
    /// (resolved or `null`), and `mode`.
    async fn session_new(
        &self,
        project_root: Option<String>,
        mode: Option<String>,
    ) -> Result<String, String>;

    // ─── Cards ───────────────────────────────────────────────

    /// List Card summaries, optionally filtered by pkg.
    async fn card_list(&self, pkg: Option<String>) -> Result<String, String>;

    /// Fetch a full Card by id.
    async fn card_get(&self, card_id: &str) -> Result<String, String>;

    /// Filter/sort Cards using the Prisma-style `where` DSL.
    ///
    /// - `pkg`: restricts filesystem scan to a single pkg subdir (I/O hint).
    /// - `where_`: nested-object predicate (see `card::parse_where`).
    /// - `order_by`: array of dotted-path sort keys; `-` prefix = desc.
    /// - `limit` / `offset`: pagination.
    async fn card_find(
        &self,
        pkg: Option<String>,
        where_: Option<serde_json::Value>,
        order_by: Option<serde_json::Value>,
        limit: Option<usize>,
        offset: Option<usize>,
    ) -> Result<String, String>;

    /// List aliases, optionally filtered by pkg.
    async fn card_alias_list(&self, pkg: Option<String>) -> Result<String, String>;

    /// Resolve an alias name to its bound Card and return the full Card JSON.
    async fn card_get_by_alias(&self, name: &str) -> Result<String, String>;

    /// Bind (or rebind) an alias to a Card.
    async fn card_alias_set(
        &self,
        name: &str,
        card_id: &str,
        pkg: Option<String>,
        note: Option<String>,
    ) -> Result<String, String>;

    /// Append new top-level fields to an existing Card (additive-only).
    async fn card_append(&self, card_id: &str, fields: serde_json::Value)
        -> Result<String, String>;

    /// Install Cards from a Card Collection repo (Git URL or local path).
    async fn card_install(&self, url: String) -> Result<String, String>;

    /// Read per-case samples from a Card's sidecar JSONL file.
    ///
    /// `where_` applies the same Prisma-style DSL used by `card_find`
    /// to each sample row; offset/limit page the post-filter stream.
    async fn card_samples(
        &self,
        card_id: &str,
        offset: Option<usize>,
        limit: Option<usize>,
        where_: Option<serde_json::Value>,
    ) -> Result<String, String>;

    /// Walk a Card's lineage tree via `metadata.prior_card_id`.
    ///
    /// - `direction`: `"up"` | `"down"` | `"both"` (default `"up"`).
    /// - `depth`: max traversal depth (default 10).
    /// - `include_stats`: include each node's `[stats]` section.
    /// - `relation_filter`: optional list of accepted `prior_relation` values.
    async fn card_lineage(
        &self,
        card_id: &str,
        direction: Option<String>,
        depth: Option<usize>,
        include_stats: Option<bool>,
        relation_filter: Option<Vec<String>>,
    ) -> Result<String, String>;

    /// Backfill one subscriber (`sink` URI) with all cards from the
    /// primary store. Drift-safe: cards already present on the sink are
    /// skipped (never overwritten). Returns a `SinkBackfillReport`
    /// serialized as a JSON string.
    async fn card_sink_backfill(&self, _sink: String, _dry_run: bool) -> Result<String, String> {
        Err("card_sink_backfill: not implemented by this EngineApi impl".into())
    }

    /// Run a Card analyzer package over a single Card.
    ///
    /// The host loads the Card body + samples sidecar, builds a Lua ctx
    /// (`{ card, samples, card_id }`), and dispatches to the named pkg
    /// via `require(pkg).run(ctx)`. The default pkg name is
    /// `"card_analysis"` — backed by the constant
    /// `DEFAULT_CARD_ANALYZE_PKG` defined in `algocline_app::service::card`.
    /// This is an IF promise, not a bundled hard dependency.
    /// If the pkg is missing the call returns an error.
    ///
    /// Sister tool to `advice`: `advice` runs a generic strategy over
    /// a free-form task, while `card_analyze` runs an analyzer over a
    /// Card and its samples. The Card domain is owned by the host
    /// (Card schema parsing + samples sidecar load), not the pkg.
    async fn card_analyze(&self, _card_id: &str, _pkg: Option<String>) -> Result<String, String> {
        Err("card_analyze: not implemented by this EngineApi impl".into())
    }

    // ─── Hub ─────────────────────────────────────────────────

    /// Rebuild hub index from a packages directory.
    ///
    /// When `source_dir` is provided, scans that directory directly
    /// (pure metadata, no manifest).  When omitted, scans `~/.algocline/packages/`.
    async fn hub_reindex(
        &self,
        output_path: Option<String>,
        source_dir: Option<String>,
    ) -> Result<String, String>;

    /// Generate human-readable documentation artifacts from a hub index.
    ///
    /// Runs the embedded Lua `gen_docs` pipeline (originally shipped
    /// with `algocline-bundled-packages`) against `source_dir`, which
    /// must contain a fresh `hub_index.json`. Emits
    /// `narrative/{pkg}.md`, `llms.txt`, `llms-full.txt` under
    /// `out_dir` (defaults to `{source_dir}/docs`), plus optional
    /// projections depending on `projections`:
    ///
    /// - `"hub"`       → `{out_dir}/hub/{pkg}.json`
    /// - `"context7"`  → `{source_dir}/context7.json`
    /// - `"devin"`     → `{source_dir}/.devin/wiki.json`
    /// - `"lint"`      → run V0 lint pass (warnings only)
    /// - `"lint_only"` → run lint, skip file generation
    ///
    /// `config_path` — optional path to a TOML config file. When omitted,
    /// the project root's `alc.toml` is auto-explored for `[hub.context7]`
    /// and `[hub.devin]` sections. Core defaults apply when neither a
    /// `config_path` nor `alc.toml` provides projection config. Passing a
    /// `.lua` path is a typed error (retired). See
    /// `docs/hub-gendoc-config.md` for the full schema.
    ///
    /// Projection names are validated strictly and unknown values are
    /// rejected with `Err("gendoc: unknown projection ...")`.
    ///
    /// `lint_strict = true` upgrades lint errors to a hard failure
    /// (equivalent to the `--strict` CLI flag).
    ///
    /// Returns a JSON string containing the collected stdout / stderr
    /// plus the resolved `source_dir` / `out_dir` for observability.
    async fn hub_gendoc(
        &self,
        source_dir: String,
        out_dir: Option<String>,
        projections: Option<Vec<String>>,
        config_path: Option<String>,
        lint_strict: Option<bool>,
    ) -> Result<String, String>;

    /// Run `hub_reindex` followed by `hub_gendoc` as a single facade.
    ///
    /// This is a convenience wrapper for downstream hub repositories that
    /// want to regenerate the index and the public docs in one call. The
    /// composed response is a JSON object:
    ///
    /// ```json
    /// {
    ///   "reindex": <hub_reindex response>,
    ///   "gendoc": <hub_gendoc response>,
    ///   "preset_catalog_version": "...",
    ///   "preset": { "name": ..., "catalog_version": ..., "resolved": { ... } }
    /// }
    /// ```
    ///
    /// Error propagation:
    ///
    /// - If `hub_reindex` fails, `hub_dist` returns immediately with
    ///   `Err("dist: reindex failed: {inner}")` and does not invoke
    ///   `hub_gendoc`.
    /// - If `hub_gendoc` fails, the error text includes the reindex JSON
    ///   that already succeeded:
    ///   `Err("dist: gendoc failed: {inner}\nreindex result (succeeded): {json}")`.
    ///   The reindex-side side effects (written `hub_index.json`) are not
    ///   rolled back.
    ///
    /// `output_path` is the `hub_index.json` destination (reindex arg).
    /// Callers typically pass `{source_dir}/hub_index.json` so the
    /// subsequent gendoc step can read it back.
    ///
    /// Presets (`preset`) are expanded inside `hub_dist` into primitive
    /// `hub_gendoc` arguments (`projections` / `config_path` /
    /// `lint_strict`). When `preset` is set, the successful JSON response
    /// includes a `preset` object with `catalog_version` plus the fully
    /// resolved knobs for observability.
    ///
    /// Merge order (strongest wins):
    /// 1) explicit MCP arguments (`projections` / `config_path` / `lint_strict`)
    /// 2) optional `alc.toml` overrides under `[hub.dist.presets.<name>]`
    ///    (keyed by `project_root`) — only fills **omitted** knobs
    /// 3) builtin `Current` defaults for the selected preset
    #[allow(clippy::too_many_arguments)]
    async fn hub_dist(
        &self,
        source_dir: String,
        output_path: Option<String>,
        out_dir: Option<String>,
        preset: Option<String>,
        project_root: Option<String>,
        projections: Option<Vec<String>>,
        config_path: Option<String>,
        lint_strict: Option<bool>,
    ) -> Result<String, String>;

    /// Show detailed information for a single package.
    async fn hub_info(&self, pkg: String) -> Result<String, String>;

    /// Search packages across remote index + local install state.
    ///
    /// This trait method mirrors the MCP `alc_hub_search` tool. Parameters
    /// are deliberately individual JSON-primitive `Option<T>` values
    /// (rather than an aggregate struct) so that the `algocline-core` crate
    /// stays free of `algocline-app`-internal types (see plan.md §4.1).
    /// The `algocline-app` side of the impl folds these into its
    /// `pub(crate) ListOpts` struct.
    ///
    /// - `limit` is `Option<i32>` at this layer (MCP/JSON boundary). The
    ///   impl casts to `usize` internally.
    /// - `filter` is a free-form JSON object; it is `Deserialize`d into
    ///   a `HashMap<String, Value>` inside the app layer.
    /// - `fields` / `verbose` drive projection; `fields` wins when both
    ///   are supplied.
    #[allow(clippy::too_many_arguments)]
    async fn hub_search(
        &self,
        query: Option<String>,
        category: Option<String>,
        installed_only: Option<bool>,
        limit: Option<i32>,
        sort: Option<String>,
        filter: Option<serde_json::Value>,
        fields: Option<Vec<String>>,
        verbose: Option<String>,
        local_indices: Option<Vec<String>>,
    ) -> Result<String, String>;

    // ─── Package scaffold ─────────────────────────────────────

    /// Generate a minimal package skeleton at `<target_dir>/<name>/init.lua`.
    ///
    /// Writes an `M.meta` / `M.spec.entries.run` / `M.run` template with a
    /// pre-filled `alc_shapes_compat` range derived from the embedded
    /// alc_shapes version.  Optional `category` / `description` are emitted
    /// as uncommented fields in `M.meta` when provided.
    ///
    /// Returns `{ "status": "ok", "path": "...", "bytes_written": N }` on
    /// success. Typed errors (`NameInvalid`, `AlreadyExists`, `IoError`) are
    /// propagated via `Err(String)` to the MCP wire response.
    async fn pkg_scaffold(
        &self,
        name: String,
        target_dir: Option<String>,
        category: Option<String>,
        description: Option<String>,
    ) -> Result<String, String>;

    /// Read the `init.lua` source of an installed package.
    ///
    /// Searches global (`~/.algocline/packages/`) and variant
    /// (`alc.local.toml`) scope in priority order (variant wins).
    /// Returns the raw Lua source on success, or an `Err(String)` describing
    /// why the package was not found or could not be read.
    async fn pkg_read_init_lua(&self, name: &str) -> Result<String, String>;

    /// Read metadata for a single installed package.
    ///
    /// Returns the JSON object string for one package entry (the same shape
    /// `pkg_list` returns inside `packages[*]`). `Err("pkg not found: ...")`
    /// when the package is unknown.
    async fn pkg_meta(&self, name: &str) -> Result<String, String>;

    // ─── Settings ────────────────────────────────────────────

    /// Resolve `[setting.<target>]` config across env, project (`alc.toml`/`alc.local.toml`),
    /// and global (`~/.algocline/config.toml`) layers.
    ///
    /// Field-level merge: each field independently selects the highest-priority layer
    /// that defines it (env > project > global). The returned JSON contains both the
    /// resolved values and a per-field `sources` map identifying the winning layer.
    ///
    /// When `target` is `None`, all `[setting.*]` tables across all layers are returned.
    /// When `target` is `Some(t)`, only the specified target (snake_case) is returned.
    ///
    /// Returns JSON string with shape:
    /// ```json
    /// {
    ///   "resolved": { "journal": { "path": "...", "pkg": true } },
    ///   "sources":  { "journal": { "path": "env",  "pkg": "global" } }
    /// }
    /// ```
    async fn setting_resolve(&self, target: Option<String>) -> Result<String, String>;

    // ─── Diagnostics ─────────────────────────────────────────

    /// Show server configuration and diagnostic info.
    async fn info(&self) -> String;

    // ─── Hub resources ───────────────────────────────────────

    /// Return the aggregated hub index across all registered sources as a JSON string.
    ///
    /// Merges the cached `hub_index.json` from every discovered source URL.
    /// Sources that fail to load produce warnings that are embedded in the
    /// returned JSON under a `"warnings"` field so the MCP caller can observe
    /// partial failures.
    ///
    /// Returns `Ok(json_string)` where the JSON has shape:
    /// ```json
    /// { "schema_version": "hub_index/v0", "packages": [...], "warnings": [...] }
    /// ```
    /// Returns `Err(message)` only when the hub registries file itself is
    /// corrupt (hard I/O failure), making further index discovery impossible.
    async fn hub_index_aggregate(&self) -> Result<String, String>;

    // ─── Pool management ─────────────────────────────────────────

    /// Ensure pool workers are alive; GC stale entries. Idempotent.
    ///
    /// Returns JSON `{"sessions": [...], "pool_version": "..."}`.
    async fn pool_ensure(&self) -> Result<String, String>;

    /// Return pool worker status (registry.json + live state).
    ///
    /// When `sid` is provided, restricts to a single worker.
    /// Returns JSON `{"sessions": [...], "pool_version": "..."}`.
    async fn pool_status(&self, sid: Option<String>) -> Result<String, String>;

    /// Send SIGTERM to all workers (`sid=None`) or a single worker.
    ///
    /// Returns JSON `{"stopped": [...], "errors": [...]}`.
    async fn pool_stop(&self, sid: Option<String>) -> Result<String, String>;
}
