#!/usr/bin/env raku
# Rakuish 的 **shell 引擎** —— 独立进程，跑一条 shell 命令。
#
# 为什么要有这个进程（详见 lib/Rakuish/Shell/Engine.rakumod 的文件头）：
#   · **崩溃隔离** —— 原生 shell 层做的是裸 libc 调用（pipe/dup2/posix_spawnp…），
#     一个野指针就是段错误。放在独立进程里，引擎崩了只是这条命令失败，
#     REPL 活着。
#   · **语义单一** —— REPL 进程里只有 Raku 语义，shell 语义全在这里。
#
# 用法：
#   rakuish-sh [--backend bash|native] [--last-status N]
#              [--env-out PATH] [--env-in PATH] [--in PATH] [--stream]
#
#   --backend       bash（默认，真 bash）| native（本项目自己实现的 Raku shell 层）
#   --last-status  `$?` 的初值（上一条命令的退出码）
#   --env-out      把**跑完之后的环境**写到这个文件（NUL 分隔的 KEY=VALUE）
#   --env-in       宿主**下传**的会话变量（NUL 分隔；`K=V` 设置，`-K` 删除）
#   --in           协议文件（见下）
#   --stream       直接把输出透传（不捕获）—— 由调用方（REPL）决定
#
# 协议文件的内容：
#   <命令文本> \0 [<heredoc 分隔符> \0 <正文> \0]*
#
# ⚠ **stdin 原样继承给命令**（`cat`、`grep` 要读终端输入）——
#   所以协议走文件，不占 stdin。

use lib $?FILE.IO.dirname.IO.parent.add('lib');
use Rakuish::Shell::Engine;    # 协议（编解码）
use Rakuish::Shell::Session;   # last-status / 会话表
use Rakuish::Shell::Heredoc;   # heredoc 识别（纯逻辑）
use Rakuish::Shell::Exec;      # 原生层（只有 native 后端用得到）
use Rakuish::Slow;             # SlowResult

# 临时脚本文件的序号
our $SCRIPT-SEQ = 0;

# ── 终端方言兜底 ────────────────────────────────────────────────────
#
# 有些实现（本机 WSL 的 uutils `ls` 实测）在 `TERM=dumb` 时**即使**
# 显式 `--color=always` 也不上色；`TERM` 为空同理。Rakuish 自己的 UI
# 已经假定 ANSI 终端（颜色由 `%RC<color>` / `NO_COLOR` 决定），
# 所以子命令也统一给一个能上色的 `TERM`。
#
# ⚠ 只改**引擎进程**的环境；REPL 侧的 `%*ENV<TERM>` 不被回传覆盖
#   （`TERM` 在 Engine 的 @ENV-NOISE 里）。
my $term = %*ENV<TERM> // q{};
%*ENV<TERM> = 'xterm-256color' if $term eq q{} or $term eq 'dumb';

# ── 参数 ────────────────────────────────────────────────────────────
my $backend   = 'bash';
my $last     = 0;
my $envout   = q{};
my $envin    = q{};
my $inpath   = q{};
my $stream   = False;

my @argv = @*ARGS;
while @argv.elems {
    my $a = @argv.shift;
    if $a eq '--backend'       { $backend = @argv.shift // 'bash'; next }
    if $a eq '--last-status'  { $last = (@argv.shift // '0').Int; next }
    if $a eq '--env-out'      { $envout = @argv.shift // q{}; next }
    if $a eq '--env-in'       { $envin  = @argv.shift // q{}; next }
    if $a eq '--in'           { $inpath = @argv.shift // q{}; next }
    if $a eq '--stream'       { $stream = True; next }
    if $a eq '--help' {
        print "用法: rakuish-sh [--backend bash|native] [--last-status N] "
            ~ "[--env-out PATH] [--env-in PATH] [--in PATH] [--stream]\n";
        exit 0;
    }
    # 不是选项 ⇒ 当作命令文本（不推荐：会泄漏到 ps）
    @argv.unshift: $a;
    last;
}

# ── 读协议 ──────────────────────────────────────────────────────────
my $text = q{};
if $inpath ne q{} and $inpath.IO.e {
    try { $text = $inpath.IO.slurp };
}
my $in = decode-input($text);
my $cmd = $in<cmd>;
my %bodies = $in<bodies>;

# ── 先应用宿主下传的会话变量（P2，内建归宿主之后必需）──────────────
#
# 必须在**跑命令之前**应用：bash 后端靠"改本进程的 %*ENV ⇒ bash 继承"，
# native 后端靠 `%*ENV` 兜底查找（Session::lookup）。
# `-K` 是"宿主已经 unset 了"——引擎从父进程继承到的还是旧值，必须删。
apply-env-in($envin);

# ── 跑 ──────────────────────────────────────────────────────────────
my $code = 0;

if $cmd.trim eq q{} {
    $code = 0;
}
elsif $backend eq 'native' {
    # ── native 后端：本项目自己实现的 shell 层 ──
    set-last-status($last);
    my $r = run-shell-line($cmd, %bodies, :capture(not $stream));
    $code = $r.code;
    if not $stream {
        # 捕获模式 ⇒ 转给引擎自己的 stdout/stderr（调用方在收）
        print $r.stdout;
        $*ERR.print($r.stderr);
    }
}
else {
    # ── bash 后端：真 bash ──
    $code = run-bash($cmd, %bodies, $last, $envout);
}

# ── 回传环境（会话变量跨格存活靠它）────────────────────────────────
#
# ⚠ **谁写这个文件，取决于引擎**：
#   · bash 后端 —— **bash 自己写**（脚本里的 `trap 'env -0 > …' EXIT`）：
#     bash 的 export 只影响它自己的进程，父进程拿不到 ⇒ 只能让 bash
#     在退出时把自己的环境吐出来。
#   · native 后端 —— **本进程写**（原生层的会话表就在本进程里）。
#
# ⚠ 千万别两边都写：CLI 写的那份是**引擎进程的环境**（没有用户 export 的
#   变量），会把 bash trap 写的那份**覆盖掉** ⇒ 会话变量全丢
#   （实测踩过：`export FOO=bar` 之后 env-out 里没有 FOO）。
if $envout ne q{} and $backend eq 'native' {
    write-env-out($envout);
}

exit $code;

# ══════════════════════════════════════════════════════════════════════
# 会话变量下行（宿主 → 引擎）
# ══════════════════════════════════════════════════════════════════════

sub apply-env-in(Str $path --> Nil) {
    return unless $path ne q{};
    return unless $path.IO.e;
    my $t = q{};
    try { $t = $path.IO.slurp };
    return if $t eq q{};
    for $t.split($SEP) -> $rec {
        next if $rec eq q{};
        if $rec.starts-with('-') {
            my $k = $rec.substr(1);
            next if $k eq q{};
            try { %*ENV{$k}:delete };
            next;
        }
        my $i = $rec.index('=');
        next unless $i.defined;
        next if $i < 1;
        my $k = $rec.substr(0, $i);
        my $v = $rec.substr($i + 1);
        %*ENV{$k} = $v;
    }
    Nil
}

# ══════════════════════════════════════════════════════════════════════
# bash 后端
# ══════════════════════════════════════════════════════════════════════

# 跑一条命令（bash）。
#
# ⚠ **脚本走文件不走 argv**：`bash -c '<脚本>'` 会让整条命令出现在 `ps` 里，
#   而且有长度上限 ⇒ 写成临时文件再 `bash <文件>`。
# ⚠ **stdin 原样继承**：命令要读终端输入时（`cat`、`grep`）才正常。
sub run-bash(Str $cmd, Hash $bodies, Int $last, Str $envout --> Int) {
    my $script = build-script($cmd, %bodies);

    # 前置的两件事：
    #   ① `trap … EXIT` —— 退出时把环境写出去（会话变量回传）
    #   ② `(exit N);`    —— 让脚本里的 `$?` 是**上一条命令的退出码**
    #      （bash 的 `$?` 是特殊变量，不能直接赋值；
    #        `(exit N)` 这个**子 shell** 的退出码会成为父 shell 的 `$?` ✓）
    #
    # ⚠ 两者都**接在同一行**上（不单独占行）—— 单独占行会让 bash 报错的
    #   行号整体偏移 1（用户看到 "line 3" 但脚本只有 2 行 ⇒ 迷惑）。
    #   接在同一行 ⇒ 行号不变，只是第一行的**列号**偏移。
    my $pre = q{};
    $pre ~= "trap 'env -0 > " ~ $envout ~ "' EXIT; " if $envout ne q{};
    $pre ~= "(exit " ~ $last ~ "); ";

    my $path = '/tmp/rakuish-sh-script-' ~ $*PID ~ '-' ~ ($SCRIPT-SEQ++);
    try { $path.IO.spurt: $pre ~ $script };

    my $p = run('bash', $path);
    my $code = $p.exitcode;
    try { unlink $path };
    $code
}

# 命令 + heredoc 正文 ⇒ 一整份脚本
#
# heredoc 的正文在 bash 里是**后续的输入行**，不是命令行的一部分 ——
# 所以要么把正文内联进脚本，要么用临时文件当 stdin。
# 这里选内联（更简单，且不占 stdin）。
sub build-script(Str $cmd, Hash $bodies --> Str) {
    my $script = $cmd ~ "\n";
    for heredoc-specs($cmd) -> $spec {
        my $body = $bodies{$spec.delim};
        $body = q{} unless $body.defined;
        $script ~= $body ~ $spec.delim ~ "\n";
    }
    $script
}

# 把当前环境写成 NUL 分隔的 KEY=VALUE（与 `env -0` 同格式）
sub write-env-out(Str $path) {
    my @pairs;
    for %*ENV.keys -> $k {
        my $v = %*ENV{$k};
        next unless $v.defined;
        @pairs.push: $k ~ '=' ~ $v;
    }
    try { $path.IO.spurt: @pairs.join("\0") };
}
