//! `codemmet expand`: abbreviation in, rendered code out.

use std::collections::BTreeMap;
use std::io::Read as _;
use std::process::ExitCode;

use clap::Args;
use codemmet_core::Engine;
use codemmet_core::context::{Collection, ExpansionContext};
use codemmet_core::engine::ExpandError;
use codemmet_core::snippet::render::Expansion;

use super::{
    EXIT_PARSE, EXIT_RENDER, Sources, Tabstops, config_failure, load_registry, resolve_indent_unit,
};

/// Arguments for `codemmet expand`.
#[derive(Debug, Args)]
pub struct ExpandArgs {
    /// Language id, file extension, or LSP language id.
    #[arg(long, value_name = "ID")]
    lang: String,

    /// Tabstop handling.
    #[arg(long, value_enum, default_value = "strip")]
    tabstops: Tabstops,

    /// Literal indent unit: a single tab or a run of spaces
    /// (default: the language profile's indent).
    #[arg(long, value_name = "STR")]
    indent: Option<String>,

    /// Prefix prepended to every continuation line.
    #[arg(long, value_name = "STR", default_value = "")]
    base_indent: String,

    /// Print one JSON object (`{"text", "cursor", "is_snippet"}`) instead of
    /// plain text.
    #[arg(long)]
    json: bool,

    /// Supply a collection for a data-driven repeat, as `NAME=v1,v2,…` (each
    /// value may carry a member as `value:type`). Repeatable. Example:
    /// `--repeat Msg=Quit,Move,Write` with `match:Msg>arm*`.
    #[arg(long, value_name = "NAME=v1,v2,…")]
    repeat: Vec<String>,

    /// Attach a parallel value list as a named member of a `--repeat`
    /// collection ("zip" two lists), as `NAME.key=v1,v2,…` — one value per
    /// item, in order; bodies read it as `${value.key}`. An empty slot skips
    /// that item. Repeatable. Example: `--repeat P=host,port` with
    /// `--member P.type=String,u16`.
    #[arg(long, value_name = "NAME.key=v1,v2,…")]
    member: Vec<String>,

    /// The abbreviation; omitted or `-` reads it from stdin.
    #[arg(value_name = "ABBR")]
    abbr: Option<String>,
}

/// Run `codemmet expand` with `user_dir` as the snippet override directory.
pub fn run(sources: &Sources, args: &ExpandArgs) -> ExitCode {
    let registry = match load_registry(sources) {
        Ok(registry) => registry,
        Err(err) => return config_failure(&err),
    };

    let indent_unit = match resolve_indent_unit(&registry, &args.lang, args.indent.as_deref()) {
        Ok(unit) => unit,
        Err(msg) => return config_failure(&msg),
    };

    let abbr = match read_abbr(args.abbr.as_deref()) {
        Ok(abbr) => abbr,
        Err(msg) => return config_failure(&msg),
    };

    let collections = match parse_repeat(&args.repeat) {
        Ok(mut collections) => match attach_members(&mut collections, &args.member) {
            Ok(()) => collections,
            Err(msg) => return config_failure(&msg),
        },
        Err(msg) => return config_failure(&msg),
    };

    let engine = Engine::with_registry(registry);
    let mut ctx = ExpansionContext::plain(args.base_indent.clone(), indent_unit);
    ctx.collections = collections;
    match engine.expand(&abbr, &args.lang, &ctx, args.tabstops.into()) {
        Ok(expansion) => emit(&expansion, &args.lang, args.json),
        Err(err) => {
            eprintln!("error: cannot expand `{abbr}`: {err}");
            let code = match err {
                ExpandError::Parse(_) => EXIT_PARSE,
                ExpandError::Render(_) => EXIT_RENDER,
                ExpandError::UnknownLanguage(_) => super::EXIT_CONFIG,
            };
            ExitCode::from(code)
        }
    }
}

/// Parse `--repeat NAME=v1,v2,…` specs into named collections. Each value may
/// carry a single member as `value:type`. Shared with `codemmet render`.
pub(super) fn parse_repeat(specs: &[String]) -> Result<BTreeMap<String, Collection>, String> {
    let mut collections = BTreeMap::new();
    for spec in specs {
        let (name, collection) =
            Collection::parse_spec(spec).map_err(|err| format!("--repeat {err}"))?;
        collections.insert(name, collection);
    }
    Ok(collections)
}

/// Apply `--member NAME.key=v1,v2,…` specs to the parsed collections: each
/// attaches a parallel value list as member `key` of collection `NAME`, one
/// slot per item. Runs after every `--repeat` is parsed, so flag order does
/// not matter. The slot count must match the item count exactly (an empty
/// slot skips that item); referencing an unknown collection is an error.
fn attach_members(
    collections: &mut BTreeMap<String, Collection>,
    specs: &[String],
) -> Result<(), String> {
    for spec in specs {
        let (name, key, slots) =
            Collection::parse_member_spec(spec).map_err(|err| format!("--member {err}"))?;
        let collection = collections.get_mut(&name).ok_or_else(|| {
            format!("--member {name}.{key}: no `--repeat {name}=…` collection to attach to")
        })?;
        collection
            .attach_member(&key, slots)
            .map_err(|err| format!("--member {name}.{key}: {err}"))?;
    }
    Ok(())
}

/// Resolve the abbreviation argument; `None` or `-` reads stdin, trimming
/// trailing newlines.
fn read_abbr(arg: Option<&str>) -> Result<String, String> {
    match arg {
        Some(abbr) if abbr != "-" => Ok(abbr.to_owned()),
        _ => {
            let mut buf = String::new();
            std::io::stdin()
                .read_to_string(&mut buf)
                .map_err(|err| format!("cannot read abbreviation from stdin: {err}"))?;
            Ok(buf.trim_end_matches(['\r', '\n']).to_owned())
        }
    }
}

/// Print the expansion to stdout, as plain text or a one-line JSON object.
///
/// Plain-text mode ignores `requires` (stdout stays pipeable code); JSON
/// mode reports each spec with its formatted import line (`null` when the
/// language has no import formatter).
fn emit(expansion: &Expansion, language: &str, json: bool) -> ExitCode {
    if json {
        let cursor = expansion.cursor.map_or(
            serde_json::Value::Null,
            |pos| serde_json::json!({ "line": pos.line, "character": pos.character }),
        );
        // `--lang` accepts extensions and LSP ids; specs format against the
        // canonical profile id.
        let profile_id = codemmet_core::lang::profile::find(language).map(|p| p.id);
        let requires: Vec<serde_json::Value> = expansion
            .requires
            .iter()
            .map(|spec| {
                let line = profile_id
                    .and_then(|id| codemmet_core::lang::imports::parse_spec(id, spec).ok())
                    .map(|parsed| parsed.line());
                serde_json::json!({ "spec": spec, "line": line })
            })
            .collect();
        let value = serde_json::json!({
            "text": expansion.body,
            "cursor": cursor,
            "is_snippet": expansion.is_snippet,
            "requires": requires,
        });
        println!("{value}");
    } else {
        println!("{}", expansion.body);
    }
    ExitCode::SUCCESS
}
