"""
一键部署节点引擎 — 主控 SSH 到新服务器自动安装节点全套环境
================================================================
流程:
  1. SSH 连接目标机, 检测 OS/架构
  2. 安装 Python3 + pip + 依赖 (requests/pysocks/dnspython/dkimpy/cryptography)
  3. 上传节点代码 (node_server/web_send/print_disguise/smart_profile/unsub_server + core/)
  4. 上传 jputls 二进制 (从主控 deploy_assets 取, x86_64)
  5. 生成 node_config.json (随机token) + systemd 服务 (jpnode/jputls)
  6. 启动并验证 /ping → 返回节点信息 (主控侧自动加入节点列表)
"""
import os
import json
import time
import random
import string
import threading

_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
_DEPLOY_ASSETS = os.path.join(_THIS_DIR, 'deploy_assets')

NODE_PY_FILES = ('node_server.py', 'web_send.py', 'print_disguise.py',
                 'smart_profile.py', 'unsub_server.py')
CORE_FILES = ('__init__.py', 'config.py', 'dkim_manager.py', 'dns_manager.py',
              'models.py', 'sender.py', 'text_engine.py')
PIP_PACKAGES = ['requests', 'pysocks', 'dnspython', 'dkimpy', 'cryptography']

JP_NODE_SERVICE = """[Unit]
Description=JP Mail Direct Send Node
After=network.target

[Service]
Type=simple
WorkingDirectory=/root/jpmail
Environment=PYTHONIOENCODING=utf-8
ExecStart=/usr/bin/python3 /root/jpmail/node_server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
"""

JP_UTLS_SERVICE = """[Unit]
Description=JP uTLS JA3 Forwarder
After=network.target

[Service]
Type=simple
WorkingDirectory=/root/jpmail
ExecStart=/root/jpmail/jputls 2526
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
"""

_deploy_state = {
    'running': False,
    'logs': [],
    'done': False,
    'ok': False,
    'msg': '',
    'result': None,
    'results': [],
    'started_at': '',
}
_deploy_lock = threading.Lock()


def _log(msg):
    line = '[' + time.strftime('%H:%M:%S') + '] ' + str(msg)
    _deploy_state['logs'].append(line)
    if len(_deploy_state['logs']) > 500:
        _deploy_state['logs'] = _deploy_state['logs'][-500:]
    print('[DEPLOY] ' + line)


def _gen_token():
    return 'nd_' + ''.join(random.choices(string.hexdigits.lower(), k=32))


def deploy_status():
    return dict(_deploy_state)


def start_deploy(name, ip, ssh_port, ssh_user, ssh_pwd, node_port):
    """启动单台部署线程 (一次只允许一个部署任务)"""
    return _start_deploy_items([(name, ip)], ssh_port, ssh_user, ssh_pwd, node_port)


def start_deploy_batch(ips, ssh_port, ssh_user, ssh_pwd, node_port, prefix='节点'):
    """批量部署: ips=[...] 串行逐台部署, 名称自动编号 (prefix+序号)"""
    items = [(prefix + str(i + 1), ip) for i, ip in enumerate(ips)]
    return _start_deploy_items(items, ssh_port, ssh_user, ssh_pwd, node_port)


def _start_deploy_items(items, ssh_port, ssh_user, ssh_pwd, node_port):
    global _deploy_state
    if _deploy_state['running']:
        return False, '已有部署任务进行中, 请等它完成'
    _deploy_state = {
        'running': True, 'logs': [], 'done': False, 'ok': False,
        'msg': '', 'result': None, 'results': [],
        'started_at': time.strftime('%Y-%m-%d %H:%M:%S'),
    }
    t = threading.Thread(target=_deploy_worker, daemon=True,
                         args=(items, ssh_port, ssh_user, ssh_pwd, node_port))
    t.start()
    return True, '部署已启动 (共 ' + str(len(items)) + ' 台, 串行执行)'


def _ssh_connect(ip, port, user, pwd):
    import paramiko
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(ip, port=int(port or 22), username=user or 'root', password=pwd,
                timeout=30, banner_timeout=30)
    return ssh


def _ssh_run(ssh, cmd, timeout=600):
    stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout)
    out = stdout.read().decode('utf-8', 'replace')
    err = stderr.read().decode('utf-8', 'replace')
    code = stdout.channel.recv_exit_status()
    return code, out, err


def _deploy_one(name, ip, ssh_port, ssh_user, ssh_pwd, node_port):
    """部署单台节点, 成功返回 {name,ip,port,token}, 失败抛异常"""
    ssh = None
    token = _gen_token()
    try:
        _log('开始部署节点 [' + (name or ip) + '] ' + ip + ' ...')

        # 1. SSH 连接
        _log('连接 ' + str(ssh_user or 'root') + '@' + ip + ':' + str(ssh_port or 22) + ' ...')
        ssh = _ssh_connect(ip, ssh_port, ssh_user, ssh_pwd)

        # 2. 检测 OS/架构
        code, out, _ = _ssh_run(ssh, '. /etc/os-release; echo ID=$ID VERSION_ID=$VERSION_ID; uname -m; whoami')
        _log('目标机信息: ' + out.strip().replace(chr(10), ' | '))
        low = out.lower()
        if 'x86_64' not in low:
            raise Exception('目标机不是 x86_64 架构 (当前仅支持 x86_64)')
        if 'ubuntu' in low or 'debian' in low:
            pass
        else:
            raise Exception('仅支持 Ubuntu/Debian 系统')

        # 3. 安装 Python3 + pip (幂等)
        _log('检查 Python3/pip (如缺失自动安装, 可能需1-3分钟)...')
        code, out, err = _ssh_run(ssh, 'command -v python3; command -v pip3; python3 -m pip --version 2>&1 | head -1',
                                  timeout=60)
        _log('Python 状态: ' + (out.strip() or err.strip()).replace(chr(10), ' | '))
        if 'No module named pip' in (out + err) or 'pip' not in (out + err):
            _log('安装 python3-pip ...')
            code, _, err = _ssh_run(ssh, 'apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-pip',
                                    timeout=900)
            if code != 0:
                raise Exception('安装 python3-pip 失败: ' + (err or '')[-300:])

        # 4. pip 依赖 (PEP 668 兼容: 失败重试 --break-system-packages)
        #    注意: 不能接管道(| tail), 否则拿到的退出码是 tail 的, pip 失败会被误判成功
        pkgs = ' '.join(PIP_PACKAGES)
        code, out, err = _ssh_run(ssh, 'python3 -m pip install -q --no-input ' + pkgs,
                                  timeout=900)
        if code != 0:
            _log('普通 pip 安装失败 (退出码' + str(code) + '), 使用 --break-system-packages 重试...')
            _log('错误: ' + (out + err)[-200:])
            code, out, err = _ssh_run(ssh, 'python3 -m pip install -q --no-input --break-system-packages '
                                           + pkgs, timeout=900)
            if code != 0:
                raise Exception('pip 依赖安装失败: ' + (out + err)[-400:])
        # 双重校验: 真能 import 才算装好
        vcode, vout, verr = _ssh_run(ssh, 'python3 -c "import dns.resolver, socks, dkim, cryptography, requests; print(\'OK\')"',
                                     timeout=60)
        if vcode != 0 or 'OK' not in vout:
            raise Exception('依赖校验失败 (pip装完仍无法import): ' + (vout + verr)[-300:])
        _log('Python 依赖安装并校验完成')

        # 5. 上传代码
        sftp = ssh.open_sftp()
        _log('上传节点代码 (以主控代码为模板, 版本一致)...')
        try:
            sftp.mkdir('/root/jpmail')
        except Exception:
            pass
        for f in NODE_PY_FILES:
            src = os.path.join(_THIS_DIR, f)
            if not os.path.exists(src):
                continue
            sftp.put(src, '/root/jpmail/' + f)
        _log('上传 core 模块...')
        try:
            sftp.mkdir('/root/core')
        except Exception:
            pass
        core_dir = os.path.join(_THIS_DIR, '..', 'core')
        for f in CORE_FILES:
            src = os.path.join(core_dir, f)
            if not os.path.exists(src):
                continue
            sftp.put(src, '/root/core/' + f)
        _log('代码上传完成')

        # 6. jputls 二进制 (先传临时名再mv, 避免覆盖运行中文件失败)
        jputls_src = os.path.join(_DEPLOY_ASSETS, 'jputls')
        if os.path.exists(jputls_src):
            sftp.put(jputls_src, '/root/jpmail/jputls.new')
            _ssh_run(ssh, 'chmod +x /root/jpmail/jputls.new; mv -f /root/jpmail/jputls.new /root/jpmail/jputls')
            _log('jputls 已部署')
        else:
            _log('⚠ 主控无 jputls 资产 (deploy_assets/jputls), 跳过 uTLS 隧道 (发信仍可用但JA3无伪装)')

        # 7. node_config.json
        cfg = {'port': int(node_port or 8898), 'token': token}
        _log('写入 node_config.json (端口 ' + str(cfg['port']) + ', token 自动生成)...')
        with sftp.open('/root/jpmail/node_config.json', 'w') as f:
            f.write(json.dumps(cfg, ensure_ascii=False, indent=2))

        # 8. systemd 服务
        _log('创建 systemd 服务 (jpnode + jputls)...')
        with sftp.open('/etc/systemd/system/jpnode.service', 'w') as f:
            f.write(JP_NODE_SERVICE)
        if os.path.exists(jputls_src):
            with sftp.open('/etc/systemd/system/jputls.service', 'w') as f:
                f.write(JP_UTLS_SERVICE)
        sftp.close()

        # 9. 启动
        _log('重载 systemd 并启动服务...')
        code, out, err = _ssh_run(ssh, 'systemctl daemon-reload; systemctl enable jpnode 2>&1 | tail -1; '
                                       'systemctl restart jpnode; sleep 3; systemctl is-active jpnode', timeout=120)
        state = out.strip().splitlines()[-1] if out.strip() else ''
        _log('jpnode 状态: ' + state)
        if os.path.exists(jputls_src):
            _ssh_run(ssh, 'systemctl enable jputls 2>&1 | tail -1; systemctl restart jputls; '
                          'sleep 1; systemctl is-active jputls', timeout=60)
        if state != 'active':
            code, out, err = _ssh_run(ssh, 'journalctl -u jpnode -n 20 --no-pager', timeout=60)
            _log('jpnode 日志尾部: ' + (out or err)[-500:])
            raise Exception('节点服务启动失败 (jpnode=' + state + ')')

        # 10. 验证 /ping
        _log('验证节点 /ping ...')
        import requests as _req
        for _ in range(5):
            try:
                r = _req.get('http://' + ip + ':' + str(node_port or 8898) + '/ping',
                             headers={'X-Token': token}, timeout=10)
                if r.status_code == 200 and r.json().get('ok'):
                    _log('✅ 节点在线验证通过 (uptime=' + str(r.json().get('uptime')) + 's)')
                    break
            except Exception:
                pass
            time.sleep(2)
        else:
            raise Exception('节点 /ping 验证失败 (服务已启动但无法访问 ' + str(node_port or 8898) + ' 端口, 检查安全组/防火墙)')

        _deploy_state['result'] = {
            'name': name or ip, 'ip': ip, 'port': int(node_port or 8898), 'token': token,
        }
        _log('🎉 部署完成: ' + ip)
        return {'name': name or ip, 'ip': ip, 'port': int(node_port or 8898), 'token': token}
    except Exception as e:
        _log('❌ ' + str(e)[:200])
        raise
    finally:
        if ssh:
            try:
                ssh.close()
            except Exception:
                pass


def _deploy_worker(items, ssh_port, ssh_user, ssh_pwd, node_port):
    results = []
    ok_n = [0]
    fail_n = [0]
    total = len(items)
    for idx, (name, ip) in enumerate(items):
        if total > 1:
            _log('==== [' + str(idx + 1) + '/' + str(total) + '] ' + (name or ip) + ' / ' + ip + ' ====')
        try:
            r = _deploy_one(name, ip, ssh_port, ssh_user, ssh_pwd, node_port)
            results.append(r)
            ok_n[0] += 1
        except Exception:
            fail_n[0] += 1
            if total > 1:
                _log('[' + (name or ip) + '] 部署失败, 继续下一台 ...')
    _deploy_state['results'] = results
    _deploy_state['result'] = results[0] if len(results) == 1 else (results[-1] if results else None)
    _deploy_state['ok'] = fail_n[0] == 0
    _deploy_state['msg'] = '批量部署完成: 成功 ' + str(ok_n[0]) + ' 台, 失败 ' + str(fail_n[0]) + ' 台'
    _log('🏁 ' + _deploy_state['msg'])
    _deploy_state['running'] = False
    _deploy_state['done'] = True
