#!/usr/bin/env python3
"""machines.yaml → env 行(sema-up.sh source 用)。
只解析 machines.example.yaml 的固定形状(2 级缩进+机器列表),标准库零依赖——
bootstrap 目标机不保证有 PyYAML,而形状是我们自己定的,mini-parser 覆盖即可;
遇到解析不了的结构 fail-loud 指出行号,绝不猜。
用法: parse-machines.py machines.yaml   # stdout 一行一个 KEY=VALUE
"""
import re, shlex, sys

# 结构字段严格校验:值会被 sema-up.sh eval/嵌入 ssh 与远端 root shell——不是防操作员(yaml 就是
# 操作员写的),是防粘贴事故/特殊字符静默注入(一次在 parser 层根治)
VALIDATORS = {
    "name":        (r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$", "RFC1123 节点名(小写字母数字与-)"),
    "host":        (r"^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$", "IPv4/主机名(首字符字母数字——防 ssh 把 -o… 当选项)"),
    "user":        (r"^[a-z_][a-z0-9_-]{0,31}$", "unix 用户名"),
    "role":        (r"^(server|agent)$", "server|agent"),
    "private_ip":  (r"^\d{1,3}(\.\d{1,3}){3}$", "IPv4"),
    "private_cidr":(r"^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$", "IPv4/prefix"),
    "public_iface":(r"^[A-Za-z0-9_.@-]{1,15}$", "接口名"),
}
def validate(field, value, ctx, ln=None):
    if field in VALIDATORS and value:
        pat, hint = VALIDATORS[field]
        if not re.match(pat, value):
            die(f"{ctx} 的 {field}={value!r} 不合法(期望 {hint})", ln)

def die(msg, ln=None):
    sys.stderr.write(f"machines.yaml 解析失败{f'(第 {ln} 行)' if ln else ''}: {msg}\n")
    sys.exit(1)

def strip_comment(v):
    # 值内不含引号字符串的场景(我们的模板如此);"# " 之后为注释
    return re.split(r"\s+#", v, maxsplit=1)[0].strip().strip('"').strip("'")

def main(path):
    top = {}            # 顶层标量
    sections = {}       # 顶层 section → {k: v}
    machines = []       # [{host,user,ssh_key,password,role}]
    cur_section = None
    cur_machine = None
    for ln, raw in enumerate(open(path, encoding="utf-8"), 1):
        line = raw.rstrip("\n")
        if not line.strip() or line.strip().startswith("#"):
            continue
        indent = len(line) - len(line.lstrip())
        body = line.strip()
        if indent == 0:
            cur_machine = None
            m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", body)
            if not m: die(f"看不懂的顶层行: {body!r}", ln)
            k, v = m.group(1), strip_comment(m.group(2))
            if v:
                top[k] = v; cur_section = None
            else:
                cur_section = k; sections.setdefault(k, {})
        elif body.startswith("- "):
            if cur_section != "machines": die(f"列表只允许出现在 machines: 下", ln)
            cur_machine = {}
            machines.append(cur_machine)
            m = re.match(r"^-\s+([\w-]+):\s*(.*)$", body)
            if not m: die(f"看不懂的列表项: {body!r}", ln)
            cur_machine[m.group(1)] = strip_comment(m.group(2))
        else:
            m = re.match(r"^([\w-]+):\s*(.*)$", body)
            if not m: die(f"看不懂的行: {body!r}", ln)
            k, v = m.group(1), strip_comment(m.group(2))
            if cur_section == "machines":
                if cur_machine is None: die("machines 下的键不在任何 '- host:' 项里", ln)
                cur_machine[k] = v
            elif cur_section:
                sections[cur_section][k] = v
            else:
                die(f"缩进了但没有所属 section: {body!r}", ln)

    out = []
    def emit(k, v):
        if v is None or v == "": return
        # shlex.quote:输出会被 shell eval,单引号量化让 $ ` 等永远是字面量(密码等自由字段也安全)
        out.append(f"{k}={shlex.quote(v)}")

    emit("PROFILE", top.get("profile"))
    emit("REGION", top.get("region", "auto"))
    emit("USERS", top.get("users", "single"))
    # server 镜像(registry 引用;海外=docker.io/<ns>/sema-server:<tag>)
    emit("SEMA_SERVER_IMAGE", top.get("server_image"))
    emit("MACHINE_COUNT", str(len(machines)))
    names = set()
    for i, mch in enumerate(machines):
        if "host" not in mch: die(f"machines[{i}] 缺 host")
        # name=k3s node-name(必须全局唯一;缺省 sema-0<i+1>)。教训:裸机批次系统 hostname
        # 常常整批相同,靠系统名注册 etcd 成员必撞;node-name 从第一次 join 就要唯一。
        name = mch.get("name") or f"sema-{i+1:02d}"
        if name in names: die(f"machines[{i}] name 重复: {name}")
        names.add(name)
        mch["name"] = name
        for k in ("host", "user", "ssh_key", "password", "role", "name", "private_ip", "private_cidr", "public_iface"):
            validate(k, mch.get(k), f"machines[{i}]")
            emit(f"MACHINE_{i}_{k.upper()}", mch.get(k))
    dp = sections.get("data_plane", {})
    emit("DP_PG", dp.get("pg", "deploy"))
    emit("DP_S3", dp.get("s3", "deploy"))
    emit("DP_BACKUP", dp.get("backup", "true"))
    sb = sections.get("sandbox", {})
    emit("SANDBOX_LANE", sb.get("lane"))
    emit("E2B_ENDPOINT", sb.get("e2b_endpoint"))
    emit("E2B_API_KEY", sb.get("e2b_api_key"))
    rg = sections.get("registry", {})
    emit("REGISTRY_ENABLED", rg.get("enabled"))
    emit("SEMA_REGISTRY_IMAGE", rg.get("image"))
    ir = sections.get("image_registry", {})
    emit("IMAGE_REGISTRY_MODE", ir.get("mode"))
    emit("IMAGE_REGISTRY_ENDPOINT", ir.get("endpoint"))
    print("\n".join(out))

if __name__ == "__main__":
    if len(sys.argv) != 2: die("用法: parse-machines.py <machines.yaml>")
    main(sys.argv[1])
