#!/usr/bin/env python3
"""生成 VMess 链接与 sing-box 配置。

用法:
    ./gen-config.py <域名> <路径> [选项]

示例:
    ./gen-config.py stargazer.org.cn /ray
    ./gen-config.py example.com /abc --uuid 已有的UUID --name HK-WS-TLS
    ./gen-config.py example.com /abc --port 8443 --outdir /tmp/cfg
"""

import argparse
import base64
import json
import os
import secrets
import subprocess
import sys
import uuid as uuidlib
from datetime import date

GEO_BASE = "https://cdn.jsdelivr.net/gh/SagerNet"


def gen_uuid():
    """优先用内核 CSPRNG，与文档中 cat /proc/sys/kernel/random/uuid 一致。"""
    try:
        with open("/proc/sys/kernel/random/uuid") as f:
            return f.read().strip()
    except OSError:
        return str(uuidlib.uuid4())


def vmess_link(p):
    """vmess:// 为 base64(JSON)，字段名是 v2rayN 约定的缩写。"""
    cfg = {
        "v": "2",
        "ps": p["name"],
        "add": p["domain"],
        "port": str(p["port"]),
        "id": p["uuid"],
        "aid": "0",
        "scy": "auto",
        "net": "ws",
        "type": "none",
        "host": p["domain"],  # 留空会导致请求不带 Host 头，Apache vhost 匹配失败
        "path": p["path"],
        "tls": "tls",
        "sni": p["domain"],
        "alpn": "",
        "fp": "",
    }
    s = json.dumps(cfg, separators=(",", ":"))
    return "vmess://" + base64.b64encode(s.encode()).decode()


def rule_sets():
    return [
        {
            "type": "remote",
            "tag": tag,
            "format": "binary",
            "url": f"{GEO_BASE}/sing-{repo}@rule-set/{tag}.srs",
            "http_client": {"detour": "proxy"},
        }
        for tag, repo in (("geosite-cn", "geosite"), ("geoip-cn", "geoip"))
    ]


def singbox(p, split):
    """split=True 国内直连分流版，False 全局代理版。语法对应 sing-box 1.14+。"""
    resolver = {"server": "dns-direct", "strategy": "ipv4_only"}
    selfdom = [p["domain"], f"www.{p['domain']}"]

    if split:
        dns = {
            "servers": [
                {"type": "https", "tag": "dns-remote",
                 "server": "1.1.1.1", "detour": "proxy"},
                {"type": "udp", "tag": "dns-direct", "server": "223.5.5.5"},
            ],
            # 自身域名必须直连解析，否则 DNS 查询要经代理才能连上代理
            "rules": [
                {"rule_set": "geosite-cn", "server": "dns-direct"},
                {"domain": selfdom, "server": "dns-direct"},
            ],
            "final": "dns-remote",
            "strategy": "ipv4_only",
        }
        rules = [
            {"action": "sniff"},
            {"protocol": "dns", "action": "hijack-dns"},
            {"ip_is_private": True, "outbound": "direct"},
            {"domain": selfdom, "outbound": "direct"},
            {"rule_set": ["geosite-cn", "geoip-cn"], "outbound": "direct"},
        ]
    else:
        dns = {
            "servers": [{"type": "udp", "tag": "dns-direct",
                         "server": "223.5.5.5"}],
            "final": "dns-direct",
            "strategy": "ipv4_only",
        }
        rules = [
            {"action": "sniff"},
            {"ip_is_private": True, "outbound": "direct"},
            {"domain": selfdom, "outbound": "direct"},
        ]

    route = {
        "rules": rules,
        "final": "proxy",
        "default_domain_resolver": resolver,
        "auto_detect_interface": True,
    }
    if split:
        route["rule_set"] = rule_sets()

    return {
        "log": {"level": "info", "timestamp": True},
        "dns": dns,
        "inbounds": [{
            "type": "tun",
            "tag": "tun-in",
            "address": ["172.19.0.1/30"],
            "mtu": 1400,
            "auto_route": True,
            "strict_route": False,
            "stack": "system",
        }],
        "outbounds": [
            {
                "type": "vmess",
                "tag": "proxy",
                "server": p["domain"],
                "server_port": p["port"],
                "uuid": p["uuid"],
                "security": "auto",
                "alter_id": 0,
                "domain_resolver": resolver,
                "tls": {"enabled": True, "server_name": p["domain"]},
                "transport": {"type": "ws", "path": p["path"]},
            },
            {"type": "direct", "tag": "direct"},
        ],
        "route": route,
    }


def server_inbound(p):
    """服务端 v2ray ws inbound 片段，追加到 /etc/v2ray/config.json 的 inbounds。"""
    return {
        "port": p["inbound_port"],
        # 容器内必须 0.0.0.0：docker-proxy 转发进来的源地址不是 127.0.0.1。
        # 对外安全性靠宿主侧 -p 127.0.0.1:PORT:PORT 保证。
        "listen": "0.0.0.0",
        "protocol": "vmess",
        "settings": {
            "clients": [{"id": p["uuid"], "alterId": 0, "security": "auto"}]
        },
        "streamSettings": {
            "network": "ws",
            "wsSettings": {"path": p["path"]},
        },
        "tag": "vmess-ws-in",
    }


def markdown(p, link, slug):
    d = p["domain"]
    ip = p["inbound_port"]
    return f"""# VMess 节点配置

更新时间：{date.today().isoformat()}

## 导入链接

v2rayN 中复制下面整行，按 Ctrl+V 从剪贴板导入：

```
{link}
```

## 参数明细

| 字段 | 值 |
|---|---|
| 别名 | {p['name']} |
| 地址 | {d} |
| 端口 | {p['port']} |
| UUID | {p['uuid']} |
| 额外ID (alterId) | 0 |
| 加密方式 | auto |
| 传输协议 | ws |
| 伪装类型 | none |
| 伪装域名 (host) | {d} |
| 路径 | {p['path']} |
| 传输层安全 | tls |
| SNI | {d} |
| 跳过证书验证 | 不勾选 |

## 注意事项

- 伪装域名 (host) 必须填 `{d}`。留空会导致请求不带 Host 头，Apache vhost 匹配失败，表现为连接成功但无法上网。
- 路径 `{p['path']}` 带斜杠，区分大小写。
- 不要勾选"跳过证书验证"，证书为正式 Let's Encrypt 证书。

## 服务端架构

```
客户端 --{p['port']}/TLS--> Apache ({d})
                      |- {p['path']}  --ws--> 127.0.0.1:{ip}  v2ray vmess+ws
                      |- 其他         /var/www/html 网站内容
```

## sing-box 配置

- 分流版（国内直连，推荐）：`{slug}-split.json`
- 全局版：`{slug}-global.json`

导入方式：sing-box → Profiles → `+` → Remote → 填 URL。

## 服务端部署片段

`server-inbound.json` 追加到 `/etc/v2ray/config.json` 的 `inbounds` 数组，然后：

```bash
docker restart v2ray
```

若 `{ip}` 为新端口，需重建容器加映射（Docker 不支持热加端口）：

```bash
docker stop v2ray && docker rm v2ray
docker run -d --name v2ray --restart unless-stopped \\
  -v /etc/v2ray/config.json:/etc/v2ray/config.json \\
  -p 127.0.0.1:{ip}:{ip} \\
  v2fly/v2fly-core:latest run -config /etc/v2ray/config.json
```

Apache 443 vhost 需有：

```apache
RewriteEngine on
RewriteCond %{{HTTP:Upgrade}} =websocket [NC]
RewriteRule ^{p['path']}$ ws://127.0.0.1:{ip}{p['path']} [P,L]
ProxyPreserveHost On
```
"""


def main():
    ap = argparse.ArgumentParser(
        description="生成 VMess 链接与 sing-box 配置",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="示例:\n  %(prog)s stargazer.org.cn /ray\n"
               "  %(prog)s example.com /abc --uuid 已有UUID --name HK-WS-TLS",
    )
    ap.add_argument("domain", help="域名，如 stargazer.org.cn")
    ap.add_argument("path", help="WebSocket 路径，如 /ray")
    ap.add_argument("--uuid", help="指定 UUID，默认新生成")
    ap.add_argument("--name", help="节点别名，默认 <域名首段>-WS-TLS")
    ap.add_argument("--port", type=int, default=443, help="对外端口，默认 443")
    ap.add_argument("--inbound-port", type=int, default=10087,
                    help="v2ray 本机 ws 监听端口，默认 10087")
    ap.add_argument("--outdir", default=os.path.dirname(os.path.abspath(__file__)),
                    help="输出目录，默认脚本所在目录")
    ap.add_argument("--slug", help="输出文件名前缀，默认 sb-<随机hex>")
    ap.add_argument("--check", metavar="SING_BOX",
                    help="用指定的 sing-box 二进制校验生成的配置")
    args = ap.parse_args()

    path = args.path if args.path.startswith("/") else "/" + args.path
    p = {
        "domain": args.domain,
        "path": path,
        "port": args.port,
        "inbound_port": args.inbound_port,
        "uuid": args.uuid or gen_uuid(),
        "name": args.name or f"{args.domain.split('.')[0].upper()}-WS-TLS",
    }
    # 随机 slug：配置放在 Web 目录时避免被扫到 UUID
    slug = args.slug or f"sb-{secrets.token_hex(6)}"
    out = args.outdir
    os.makedirs(out, exist_ok=True)

    link = vmess_link(p)
    files = {
        f"{slug}-split.json": singbox(p, True),
        f"{slug}-global.json": singbox(p, False),
        "server-inbound.json": server_inbound(p),
    }
    written = []
    for name, data in files.items():
        fp = os.path.join(out, name)
        with open(fp, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
            f.write("\n")
        written.append(fp)

    md = os.path.join(out, "vmess.md")
    with open(md, "w", encoding="utf-8") as f:
        f.write(markdown(p, link, slug))
    written.append(md)

    print(f"域名   : {p['domain']}:{p['port']}{p['path']}")
    print(f"UUID   : {p['uuid']}")
    print(f"别名   : {p['name']}")
    print(f"\n{link}\n")
    for fp in written:
        print(f"  {fp}")

    if args.check:
        print()
        ok = True
        for name in files:
            if not name.startswith(slug):
                continue
            fp = os.path.join(out, name)
            r = subprocess.run([args.check, "check", "-c", fp],
                               capture_output=True, text=True)
            tag = "OK" if r.returncode == 0 else "FAIL"
            print(f"  check {name}: {tag}")
            if r.returncode != 0:
                ok = False
                print((r.stderr or r.stdout).strip())
        if not ok:
            return 1
        # check 只做静态语法检查，弃用警告需实跑才能发现（见 v2ray.md 第五节）
        print("  注意: check 不覆盖弃用警告，建议按 v2ray.md 第五节实跑验证")
    return 0


if __name__ == "__main__":
    sys.exit(main())
