//! App-lifecycle command nodes (ADR-0024 §migration Phase A).
//!
//! Eleven commands: quit, help, rebuild, save (+ save as), new,
//! load, export, import, mode, messages.
//!
//! Each block is one `CommandNode`: entry keyword, shape, AST
//! builder, help / usage references. The ast_builders match
//! against the `MatchedPath` items in declaration order.

use crate::dsl::command::{AppCommand, Command, CopyScope, MessagesValue, ModeValue};
use crate::dsl::grammar::{
    CommandNode, HintMode, IdentSource, IdentValidator, Node, ValidationError, Word,
};
use crate::dsl::walker::outcome::{MatchedKind, MatchedPath};

// --- Validators ----------------------------------------------------
//
// The catch-all `Ident` branches in `mode <value>` /
// `messages <value>` exist solely to convert any out-of-set
// identifier into a friendly `mode.unknown` / `messages.unknown`
// catalog wording. The known values are `Word` siblings in the
// same `Choice`, so they're never reached on the happy path —
// these validators always fail.

fn validate_unknown_mode(value: &str) -> Result<(), ValidationError> {
    Err(ValidationError {
        message_key: "mode.unknown",
        args: vec![("value", value.to_string())],
    })
}

fn validate_unknown_messages(value: &str) -> Result<(), ValidationError> {
    Err(ValidationError {
        message_key: "messages.unknown",
        args: vec![("value", value.to_string())],
    })
}

fn validate_unknown_copy(value: &str) -> Result<(), ValidationError> {
    Err(ValidationError {
        message_key: "copy.unknown",
        args: vec![("value", value.to_string())],
    })
}

const UNKNOWN_MODE_VALIDATOR: IdentValidator = validate_unknown_mode;
const UNKNOWN_MESSAGES_VALIDATOR: IdentValidator = validate_unknown_messages;
const UNKNOWN_COPY_VALIDATOR: IdentValidator = validate_unknown_copy;

// --- Shapes (constants are referenced by Optional/Choice slices) --

const SAVE_AS_WORD: Node = Node::Word(Word::keyword("as"));

const IMPORT_TARGET_IDENT: Node = Node::Ident {
    source: IdentSource::NewName,
    role: "target",
    validator: None,
    highlight_override: None,
    writes_table: false,
    writes_column: false,
    writes_user_listed_column: false,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: false,
};
const IMPORT_TARGET: Node = Node::Hinted {
    mode: HintMode::ForceProse("hint.ambient_typing_name"),
    inner: &IMPORT_TARGET_IDENT,
};

const IMPORT_AS_TARGET: Node = Node::Seq(&[Node::Word(Word::keyword("as")), IMPORT_TARGET]);
const IMPORT_AS_TARGET_OPT: Node = Node::Optional(&IMPORT_AS_TARGET);

const IMPORT_PATH_AND_TARGET: Node = Node::Seq(&[Node::BarePath, IMPORT_AS_TARGET_OPT]);

const EXPORT_PATH_OPT: Node = Node::Optional(&Node::BarePath);
const IMPORT_BODY_OPT: Node = Node::Optional(&IMPORT_PATH_AND_TARGET);

// `help [<topic>]` (H3): an optional single-word topic — a command
// entry word (`insert`, `create`, `show`, …) or `types`. Captured
// as a `BarePath` so any keyword-shaped word is accepted verbatim.
const HELP_TOPIC_OPT: Node = Node::Optional(&Node::BarePath);

// `mode <value>`: known keywords are surfaced as `Word` children
// so they appear in the walker's expected set (and feed the
// completion engine's keyword candidates). The trailing `Ident`
// child catches any other identifier shape and funnels it into
// the friendly `mode.unknown` validator.
const MODE_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("simple")),
    Node::Word(Word::keyword("advanced")),
    Node::Ident {
        source: IdentSource::Free,
        role: "mode_value",
        validator: Some(UNKNOWN_MODE_VALIDATOR),
        highlight_override: None,
        writes_table: false,
        writes_column: false,
        writes_user_listed_column: false,
        writes_table_alias: false,
        writes_cte_name: false,
        writes_projection_alias: false,
    },
];
const MODE_VALUE: Node = Node::Choice(MODE_CHOICES);

const MESSAGES_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("short")),
    Node::Word(Word::keyword("verbose")),
    Node::Ident {
        source: IdentSource::Free,
        role: "messages_value",
        validator: Some(UNKNOWN_MESSAGES_VALIDATOR),
        highlight_override: None,
        writes_table: false,
        writes_column: false,
        writes_user_listed_column: false,
        writes_table_alias: false,
        writes_cte_name: false,
        writes_projection_alias: false,
    },
];
const MESSAGES_VALUE: Node = Node::Choice(MESSAGES_CHOICES);
const MESSAGES_VALUE_OPT: Node = Node::Optional(&MESSAGES_VALUE);

// `copy [all|last]`: same shape as `messages` — known scope words are
// `Word` siblings (so they reach completion + the expected set); the
// trailing catch-all `Ident` funnels any other word into the friendly
// `copy.unknown` validator. Bare `copy` (no value) means `all`.
const COPY_CHOICES: &[Node] = &[
    Node::Word(Word::keyword("all")),
    Node::Word(Word::keyword("last")),
    Node::Ident {
        source: IdentSource::Free,
        role: "copy_value",
        validator: Some(UNKNOWN_COPY_VALIDATOR),
        highlight_override: None,
        writes_table: false,
        writes_column: false,
        writes_user_listed_column: false,
        writes_table_alias: false,
        writes_cte_name: false,
        writes_projection_alias: false,
    },
];
const COPY_VALUE: Node = Node::Choice(COPY_CHOICES);
const COPY_VALUE_OPT: Node = Node::Optional(&COPY_VALUE);

const EMPTY_SEQ: Node = Node::Seq(&[]);
const SAVE_AS_OPT: Node = Node::Optional(&SAVE_AS_WORD);

// --- AST builders --------------------------------------------------

const fn build_quit(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Quit))
}

fn build_help(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    // Optional single-word topic (a command entry word, or
    // `types`) captured as a `BarePath` — `help insert`,
    // `help create`, `help show`. Multi-word commands share an
    // entry word, so `help create` covers every create form.
    let topic = path
        .find(|i| matches!(i.kind, MatchedKind::BarePath))
        .map(|i| i.text.clone());
    Ok(Command::App(AppCommand::Help { topic }))
}

const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Rebuild))
}

const fn build_version(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Version))
}

const fn build_undo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Undo))
}
const fn build_hint(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Hint))
}

const fn build_redo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Redo))
}

fn build_save(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    if path.contains_word("as") {
        Ok(Command::App(AppCommand::SaveAs))
    } else {
        Ok(Command::App(AppCommand::Save))
    }
}

const fn build_new(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::New))
}

const fn build_load(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    Ok(Command::App(AppCommand::Load))
}

fn build_export(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    let bare = path
        .find(|i| matches!(i.kind, MatchedKind::BarePath))
        .map(|i| i.text.clone());
    Ok(Command::App(AppCommand::Export { path: bare }))
}

fn build_import(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    let bare_path = path
        .find(|i| matches!(i.kind, MatchedKind::BarePath))
        .map(|i| i.text.clone())
        .unwrap_or_default();
    let target = path
        .find(|i| matches!(&i.kind, MatchedKind::Ident { role, .. } if *role == "target"))
        .map(|i| i.text.clone());
    Ok(Command::App(AppCommand::Import {
        path: bare_path,
        target,
    }))
}

fn build_mode(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    // The Choice surfaces the matched value as either a `Word`
    // (known) or an `Ident` (unknown). The unknown branch's
    // validator always errors, so reaching the AST builder
    // implies one of the Word branches matched.
    let value = if path.contains_word("simple") {
        ModeValue::Simple
    } else if path.contains_word("advanced") {
        ModeValue::Advanced
    } else {
        ModeValue::Simple
    };
    Ok(Command::App(AppCommand::Mode { value }))
}

fn build_messages(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    let value = if path.contains_word("short") {
        Some(MessagesValue::Short)
    } else if path.contains_word("verbose") {
        Some(MessagesValue::Verbose)
    } else {
        None
    };
    Ok(Command::App(AppCommand::Messages { value }))
}

fn build_copy(path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
    // The unknown-value branch's validator always errors, so reaching
    // here means either a known scope word or a bare `copy` (= all).
    let scope = if path.contains_word("last") {
        CopyScope::Last
    } else {
        CopyScope::All
    };
    Ok(Command::App(AppCommand::Copy { scope }))
}

// --- Command nodes -------------------------------------------------

pub static QUIT: CommandNode = CommandNode {
    entry: Word::keyword("quit"),
    shape: EMPTY_SEQ,
    ast_builder: build_quit,
    help_id: Some("app.quit"),
    hint_ids: &["quit"],
    usage_ids: &["parse.usage.quit"],
};

pub static HELP: CommandNode = CommandNode {
    entry: Word::keyword("help"),
    shape: HELP_TOPIC_OPT,
    ast_builder: build_help,
    help_id: Some("app.help"),
    hint_ids: &["help"],
    usage_ids: &["parse.usage.help"],
};

pub static HINT: CommandNode = CommandNode {
    entry: Word::keyword("hint"),
    shape: EMPTY_SEQ,
    ast_builder: build_hint,
    help_id: Some("app.hint"),
    // hint_id assigned in Phase C with the tier-3 corpus (ADR-0053).
    hint_ids: &["hint"],
    usage_ids: &["parse.usage.hint"],
};

pub static REBUILD: CommandNode = CommandNode {
    entry: Word::keyword("rebuild"),
    shape: EMPTY_SEQ,
    ast_builder: build_rebuild,
    help_id: Some("app.rebuild"),
    hint_ids: &["rebuild"],
    usage_ids: &["parse.usage.rebuild"],
};

pub static VERSION: CommandNode = CommandNode {
    entry: Word::keyword("version"),
    shape: EMPTY_SEQ,
    ast_builder: build_version,
    help_id: Some("app.version"),
    hint_ids: &["version"],
    usage_ids: &["parse.usage.version"],
};

pub static SAVE: CommandNode = CommandNode {
    entry: Word::keyword("save"),
    shape: SAVE_AS_OPT,
    ast_builder: build_save,
    help_id: Some("app.save"),
    hint_ids: &["save"],
    usage_ids: &["parse.usage.save"],
};

pub static NEW: CommandNode = CommandNode {
    entry: Word::keyword("new"),
    shape: EMPTY_SEQ,
    ast_builder: build_new,
    help_id: Some("app.new"),
    hint_ids: &["new"],
    usage_ids: &["parse.usage.new"],
};

pub static LOAD: CommandNode = CommandNode {
    entry: Word::keyword("load"),
    shape: EMPTY_SEQ,
    ast_builder: build_load,
    help_id: Some("app.load"),
    hint_ids: &["load"],
    usage_ids: &["parse.usage.load"],
};

pub static EXPORT: CommandNode = CommandNode {
    entry: Word::keyword("export"),
    shape: EXPORT_PATH_OPT,
    ast_builder: build_export,
    help_id: Some("app.export"),
    hint_ids: &["export"],
    usage_ids: &["parse.usage.export"],
};

pub static IMPORT: CommandNode = CommandNode {
    entry: Word::keyword("import"),
    shape: IMPORT_BODY_OPT,
    ast_builder: build_import,
    help_id: Some("app.import"),
    hint_ids: &["import"],
    usage_ids: &["parse.usage.import"],
};

pub static MODE: CommandNode = CommandNode {
    entry: Word::keyword("mode"),
    shape: MODE_VALUE,
    ast_builder: build_mode,
    help_id: Some("app.mode"),
    hint_ids: &["mode"],
    usage_ids: &["parse.usage.mode"],
};

pub static MESSAGES: CommandNode = CommandNode {
    entry: Word::keyword("messages"),
    shape: MESSAGES_VALUE_OPT,
    ast_builder: build_messages,
    help_id: Some("app.messages"),
    hint_ids: &["messages"],
    usage_ids: &["parse.usage.messages"],
};

pub static UNDO: CommandNode = CommandNode {
    entry: Word::keyword("undo"),
    shape: EMPTY_SEQ,
    ast_builder: build_undo,
    help_id: Some("app.undo"),
    hint_ids: &["undo"],
    usage_ids: &["parse.usage.undo"],
};

pub static REDO: CommandNode = CommandNode {
    entry: Word::keyword("redo"),
    shape: EMPTY_SEQ,
    ast_builder: build_redo,
    help_id: Some("app.redo"),
    hint_ids: &["redo"],
    usage_ids: &["parse.usage.redo"],
};

pub static COPY: CommandNode = CommandNode {
    entry: Word::keyword("copy"),
    shape: COPY_VALUE_OPT,
    ast_builder: build_copy,
    help_id: Some("app.copy"),
    hint_ids: &["copy"],
    usage_ids: &["parse.usage.copy"],
};
