# -*- coding: utf-8 -*-
"""
safety-eval-service 一键发布到开发环境 K8s 脚本
================================================
在本地 Windows 执行，自动完成:
  本地 Maven 编译 (可选) -> 上传 JAR -> 远程 Docker build -> 推送 ACR
  -> 更新 K8s Deployment -> 滚动重启 -> 校验 Pod 状态

与 docs/build-tools/build_push.py 的区别:
  build_push.py 只构建并推送镜像，最后只"打印" kubectl 命令；
  本脚本会真正执行 K8s 部署、滚动更新并校验结果。

前置条件:
  - Python 3 + paramiko  (pip install paramiko)
  - 本地已编译好 JAR，或加 --build 让脚本自动 mvn 编译

用法:
  # 最简单: 自动查找项目 JAR 并发布 (推荐)
  python push_to_dev.py

  # 指定 JAR 路径
  python push_to_dev.py --jar "D:\\workspace\\code\\safety-eval-service\\safety-eval-start\\target\\safety-eval-start-1.0-SNAPSHOT.jar"

  # 先本地编译再发布
  python push_to_dev.py --build

  # 自定义版本序号 (默认自动递增当天序号)
  python push_to_dev.py --version 3

  # 只更新 K8s (镜像已存在，只触发滚动重启)
  python push_to_dev.py --deploy-only --image <完整镜像地址>

  # 跳过 K8s 部署，只构建推送镜像
  python push_to_dev.py --no-deploy

示例输出镜像:
  jjb-registry-registry.cn-hangzhou.cr.aliyuncs.com/ali_img_ns/prod-aly-ota-dragon-jjb-saas-safety-eval:ota-20260707-1
"""

import argparse
import os
import subprocess
import sys
import time
from datetime import datetime

import paramiko

# ==================== 配置区 ====================
# K8s Master (SSH)
MASTER = "192.168.20.100"
SSH_USER = "root"
SSH_PASSWD = "Zcloud@zcloud100"

# 杭州 ACR (K8s image-pull-secret 指向此处，必须用这个仓库)
ACR_REGISTRY = "jjb-registry-registry.cn-hangzhou.cr.aliyuncs.com"
ACR_NAMESPACE = "ali_img_ns"
ACR_USER = "10952138@qq.com"
ACR_PASS = "idurCT!rIq9EzISD"

# K8s 部署信息
K8S_NAMESPACE = "jjb-dragon"
DEPLOY_NAME = "jjb-saas-safety-eval-deploy"        # Deployment 名称
CONTAINER_NAME = "jjb-saas-safety-eval-app"        # 容器名称
KUBECTL = "/usr/bin/kubectl"

# 镜像命名
APP_NAME = "jjb-saas-safety-eval"
ENV_PREFIX = "ota"                                  # 环境前缀

# 项目本地路径 (默认为脚本所在目录的上一级)
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# JAR 相对路径
JAR_REL = os.path.join("safety-eval-start", "target", "safety-eval-start-1.0-SNAPSHOT.jar")

# 远程构建目录
REMOTE_BUILD_DIR = f"/tmp/docker-build-{APP_NAME}"

# 脚本自带的 Dockerfile (与 docs/build-tools/Dockerfile 一致: CentOS7 + JDK1.8.0_202)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOCKERFILE_PATH = os.path.join(SCRIPT_DIR, "Dockerfile")
# 若 push 目录没有 Dockerfile，则回退到 docs/build-tools/Dockerfile
if not os.path.exists(DOCKERFILE_PATH):
    DOCKERFILE_PATH = os.path.join(
        PROJECT_DIR, "docs", "build-tools", "Dockerfile"
    )


# ==================== 工具函数 ====================
def ssh_exec(client, cmd, timeout=600, quiet=False):
    """执行 SSH 命令并打印结果，返回 (stdout, stderr, exit_code)。"""
    if not quiet:
        print(f"  $ {cmd[:150]}{'...' if len(cmd) > 150 else ''}")
    stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout)
    out = stdout.read().decode("utf-8", errors="replace")
    err = stderr.read().decode("utf-8", errors="replace")
    exit_code = stdout.channel.recv_exit_status()
    if not quiet:
        if out.strip():
            for line in out.strip().split("\n")[-15:]:
                print(f"    {line}")
        if err.strip():
            # 过滤掉 docker 下载进度噪音
            important = [
                l for l in err.strip().split("\n")
                if not any(x in l for x in
                           ["Downloading", "Downloaded", "Extracting", "Progress",
                            "WARNING! --password", "Login Succeeded"])
            ]
            if important:
                print(f"    [stderr] {'; '.join(important[:5])}")
    return out.strip(), err.strip(), exit_code


def upload_with_progress(sftp, local, remote):
    """带进度条的上传。"""
    size_mb = os.path.getsize(local) / 1024 / 1024
    start = time.time()
    last_pct = [-1]

    def progress(transferred, total):
        pct = int(transferred * 100 / total)
        if pct >= last_pct[0] + 10:
            last_pct[0] = pct
            mb = transferred / 1024 / 1024
            elapsed = time.time() - start
            speed = mb / elapsed if elapsed > 0 else 0
            print(f"    {pct}% ({mb:.1f} MB) - {speed:.1f} MB/s")

    sftp.put(local, remote, callback=progress)
    elapsed = time.time() - start
    print(f"    上传完成: {elapsed:.1f}s ({size_mb / elapsed:.1f} MB/s)")


def find_local_jar(project_dir):
    """自动查找项目 JAR 包。"""
    candidate = os.path.join(project_dir, JAR_REL)
    if os.path.exists(candidate):
        return os.path.abspath(candidate)
    # 兜底: 在 safety-eval-start/target 下找最大的 jar (非 *.original)
    target_dir = os.path.join(project_dir, "safety-eval-start", "target")
    if os.path.isdir(target_dir):
        jars = [os.path.join(target_dir, f) for f in os.listdir(target_dir)
                if f.endswith(".jar") and not f.endswith(".jar.original")]
        if jars:
            jars.sort(key=os.path.getsize, reverse=True)
            return os.path.abspath(jars[0])
    return None


def local_mvn_build(project_dir):
    """在本地执行 Maven 编译。"""
    print("[0/7] 本地 Maven 编译 (mvn clean package -DskipTests)...")
    cmd = ["mvn", "clean", "package", "-DskipTests"]
    print(f"  $ {' '.join(cmd)}")
    print("  (编译日志较多，仅显示关键输出...)")
    try:
        proc = subprocess.run(
            cmd, cwd=project_dir, shell=(os.name == "nt"),
            capture_output=True, text=True, encoding="utf-8", errors="replace"
        )
    except FileNotFoundError:
        print("  ERROR: 未找到 mvn 命令，请确认本地已安装 Maven 并加入 PATH。")
        sys.exit(1)

    # 打印最后的关键行
    tail = (proc.stdout or "").strip().split("\n")[-25:]
    for line in tail:
        print(f"    {line}")
    if proc.returncode != 0:
        print("\n  ERROR: Maven 编译失败！")
        if proc.stderr:
            print(proc.stderr[-1000:])
        sys.exit(1)
    print("  Maven 编译成功。")


def next_version_suffix(client):
    """查询 master 上当天已存在的镜像 tag，自动递增序号。"""
    date_tag = datetime.now().strftime("%Y%m%d")
    prefix = f"{ENV_PREFIX}-{date_tag}-"
    # 查询本应用当天已构建的 tag (docker images 第二列为 Tag)
    repo_filter = f"prod-aly-{ENV_PREFIX}-dragon-{APP_NAME}"
    out, _, _ = ssh_exec(
        client,
        f"docker images --format '{{{{.Repository}}}}:{{{{.Tag}}}}' "
        f"| grep '{repo_filter}' | awk -F: '{{print $NF}}'",
        quiet=True,
    )
    used = set()
    for line in out.split("\n"):
        line = line.strip()
        if line.startswith(prefix):
            num_part = line[len(prefix):]
            if num_part.isdigit():
                used.add(int(num_part))
    return max(used) + 1 if used else 1


# ==================== 主流程 ====================
def main():
    parser = argparse.ArgumentParser(
        description="safety-eval-service 一键发布到开发环境 K8s",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("--jar", default=None, help="本地 JAR 路径 (不指定则自动查找)")
    parser.add_argument("--build", action="store_true", help="发布前先执行本地 mvn 编译")
    parser.add_argument("--version", default=None, help="镜像版本序号 (默认自动递增)")
    parser.add_argument("--app", default=APP_NAME, help=f"应用名 (默认: {APP_NAME})")
    parser.add_argument("--prefix", default=ENV_PREFIX, help=f"环境前缀 (默认: {ENV_PREFIX})")
    parser.add_argument("--no-deploy", action="store_true", help="只构建推送镜像，不部署 K8s")
    parser.add_argument("--deploy-only", action="store_true",
                        help="只更新 K8s (需配合 --image 指定已存在的镜像)")
    parser.add_argument("--image", default=None, help="指定完整镜像地址 (配合 --deploy-only)")
    parser.add_argument("--restart", action="store_true",
                        help="只重启 K8s (不改镜像，等同 rollout restart)")
    parser.add_argument("--dry-run", action="store_true", help="只打印计划，不实际执行")
    args = parser.parse_args()

    global APP_NAME, ENV_PREFIX
    APP_NAME = args.app
    ENV_PREFIX = args.prefix

    # ---- 仅重启 ----
    if args.restart:
        do_k8s_restart(args.dry_run)
        return

    # ---- 仅部署 (镜像已存在) ----
    if args.deploy_only:
        if not args.image:
            print("ERROR: --deploy-only 需要配合 --image <完整镜像地址>")
            sys.exit(1)
        do_k8s_deploy(args.image, args.dry_run)
        return

    # ==================== 完整流程: 编译+构建+推送+部署 ====================
    date_tag = datetime.now().strftime("%Y%m%d")

    # [0] 本地编译 (可选)
    if args.build:
        local_mvn_build(PROJECT_DIR)

    # [1] 定位 JAR
    jar_local = args.jar or find_local_jar(PROJECT_DIR)
    if not jar_local or not os.path.exists(jar_local):
        print(f"ERROR: 未找到 JAR 文件: {jar_local or '(未自动发现)'}")
        print(f"  可用 --jar 指定路径，或加 --build 自动编译。")
        sys.exit(1)
    jar_size_mb = os.path.getsize(jar_local) / 1024 / 1024
    jar_name = os.path.basename(jar_local)

    # ---- 连接 master 以确定版本号 ----
    print("\n连接 K8s Master 节点...")
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)
    print(f"  SSH 已连接: {MASTER}")

    version_suffix = args.version or next_version_suffix(client)
    image_tag = f"{ENV_PREFIX}-{date_tag}-{version_suffix}"
    full_image = (f"{ACR_REGISTRY}/{ACR_NAMESPACE}/"
                  f"prod-aly-{ENV_PREFIX}-dragon-{APP_NAME}:{image_tag}")

    # ---- 打印发布计划 ----
    print("\n" + "=" * 66)
    print("  safety-eval-service 一键发布到开发环境")
    print("=" * 66)
    print(f"  JAR:       {jar_local} ({jar_size_mb:.1f} MB)")
    print(f"  应用:      {APP_NAME}")
    print(f"  镜像 Tag:  {image_tag}")
    print(f"  完整镜像:  {full_image}")
    print(f"  K8s:       {K8S_NAMESPACE}/{DEPLOY_NAME}")
    print(f"  Master:    {MASTER}")
    print("=" * 66)

    if args.dry_run:
        print("\n[DRY RUN] 以上为发布计划，未实际执行。")
        client.close()
        return

    # [2] 准备远程目录 + 上传 JAR
    print(f"\n[1/6] 上传 JAR 到 master ({jar_size_mb:.1f} MB)...")
    ssh_exec(client, f"rm -rf {REMOTE_BUILD_DIR} && mkdir -p {REMOTE_BUILD_DIR}/target",
             quiet=True)
    sftp = client.open_sftp()
    remote_jar = f"{REMOTE_BUILD_DIR}/target/{jar_name}"
    upload_with_progress(sftp, jar_local, remote_jar)

    # 上传 Dockerfile
    print("  上传 Dockerfile...")
    if os.path.exists(DOCKERFILE_PATH):
        sftp.put(DOCKERFILE_PATH, f"{REMOTE_BUILD_DIR}/Dockerfile")
    else:
        # 回退: 用 heredoc 写入标准模板
        print("  (未找到本地 Dockerfile，使用 master 上的模板)")
        ssh_exec(client,
                 f"cp /opt/docker-templates/Dockerfile {REMOTE_BUILD_DIR}/Dockerfile "
                 f"2>/dev/null || true", quiet=True)
    sftp.close()

    # [3] 准备构建上下文 (复制 JDK)
    print("\n[2/6] 准备构建上下文 (复制 JDK)...")
    ssh_exec(client, f"cp -r /opt/jdk1.8.0_202 {REMOTE_BUILD_DIR}/", timeout=120)
    ssh_exec(client, f"du -sh {REMOTE_BUILD_DIR}/")

    # [4] Docker build
    print("\n[3/6] 构建 Docker 镜像...")
    out, err, code = ssh_exec(
        client, f"docker build -t '{full_image}' {REMOTE_BUILD_DIR}", timeout=600
    )
    if code != 0:
        print("\nERROR: Docker build 失败！")
        client.close()
        sys.exit(1)

    # [5] 登录 + 推送 ACR
    print("\n[4/6] 推送镜像到杭州 ACR...")
    ssh_exec(client,
             f"echo '{ACR_PASS}' | docker login --username={ACR_USER} "
             f"--password-stdin {ACR_REGISTRY}", quiet=True)
    out, err, code = ssh_exec(client, f"docker push '{full_image}'", timeout=900)
    if code != 0:
        print("\nERROR: Docker push 失败！")
        client.close()
        sys.exit(1)
    ssh_exec(client, f"docker images | grep '{APP_NAME}' | head -5")

    # 清理远程构建目录
    ssh_exec(client, f"rm -rf {REMOTE_BUILD_DIR}", quiet=True)

    if args.no_deploy:
        client.close()
        print("\n" + "=" * 66)
        print("  构建推送完成 (--no-deploy 已跳过 K8s 部署)")
        print("=" * 66)
        print(f"\n  镜像地址: {full_image}\n")
        return

    # [6] 部署到 K8s
    print("\n[5/6] 部署到 K8s...")
    do_k8s_deploy(full_image, dry_run=False, client=client)

    # [7] 校验
    print("\n[6/6] 校验 Pod 状态...")
    verify_rollout(client)

    client.close()
    print("\n" + "=" * 66)
    print("  发布完成！")
    print("=" * 66)
    print(f"  镜像: {full_image}")
    print(f"  服务: safety-eval-service (Nacos: {K8S_NAMESPACE})")
    print(f"  网关: http://192.168.20.100:30140/safety-eval/  (Header: filterGatewaName: cqanquan)")
    print("=" * 66)


def do_k8s_deploy(full_image, dry_run, client=None):
    """更新 K8s Deployment 镜像并触发滚动更新。"""
    own_client = client is None
    if own_client:
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)

    # 检查 Deployment 是否存在
    out, _, code = ssh_exec(
        client,
        f"{KUBECTL} get deployment {DEPLOY_NAME} -n {K8S_NAMESPACE} --ignore-not-found",
        quiet=True,
    )
    deploy_exists = code == 0 and DEPLOY_NAME in out

    if dry_run:
        print(f"  [DRY RUN] 将更新镜像: {full_image}")
        if own_client:
            client.close()
        return

    if deploy_exists:
        # 更新镜像
        print(f"  更新 Deployment 镜像...")
        out, err, code = ssh_exec(
            client,
            f"{KUBECTL} set image deployment/{DEPLOY_NAME} "
            f"{CONTAINER_NAME}={full_image} -n {K8S_NAMESPACE}"
        )
        if code != 0:
            print(f"  ERROR: kubectl set image 失败: {err}")
            if own_client:
                client.close()
            sys.exit(1)
    else:
        # 首次部署: 用 docs/deploy 下的 yaml 创建
        print(f"  Deployment 不存在，尝试 apply 部署 yaml...")
        deploy_yaml = f"/tmp/{DEPLOY_NAME}.yaml"
        local_yaml = os.path.join(PROJECT_DIR, "docs", "deploy", "safety-eval-deploy.yaml")
        if os.path.exists(local_yaml):
            sftp = client.open_sftp()
            sftp.put(local_yaml, deploy_yaml)
            sftp.close()
            # 先替换镜像为本次构建的镜像
            ssh_exec(client,
                     f"sed -i 's|image: .*|image: {full_image}|' {deploy_yaml}",
                     quiet=True)
            ssh_exec(client, f"{KUBECTL} apply -f {deploy_yaml} -n {K8S_NAMESPACE}")
            ssh_exec(client, f"rm -f {deploy_yaml}", quiet=True)
        else:
            print("  ERROR: Deployment 不存在且找不到部署 yaml，请先手动创建 Deployment。")
            if own_client:
                client.close()
            sys.exit(1)

    if own_client:
        client.close()


def do_k8s_restart(dry_run):
    """仅重启 K8s (不改镜像)。"""
    print("=" * 66)
    print(f"  重启 K8s Deployment: {K8S_NAMESPACE}/{DEPLOY_NAME}")
    print("=" * 66)
    if dry_run:
        print("  [DRY RUN] 将执行 rollout restart")
        return

    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)
    print("\n触发滚动重启...")
    ssh_exec(client, f"{KUBECTL} rollout restart deployment/{DEPLOY_NAME} -n {K8S_NAMESPACE}")
    verify_rollout(client)
    client.close()


def verify_rollout(client):
    """等待滚动更新完成并打印 Pod 状态与最近日志。"""
    print("  等待滚动更新完成 (最多 300s)...")
    out, err, code = ssh_exec(
        client,
        f"{KUBECTL} rollout status deployment/{DEPLOY_NAME} "
        f"-n {K8S_NAMESPACE} --timeout=300s",
        timeout=320,
    )
    if code != 0:
        print(f"  ⚠ 滚动更新未完成或超时: {err}")
        print("  最近事件:")
        ssh_exec(client,
                 f"{KUBECTL} get events -n {K8S_NAMESPACE} --sort-by=.metadata.creationTimestamp "
                 f"| grep -i 'safety-eval' | tail -15")

    # Pod 状态
    print("\n  Pod 状态:")
    ssh_exec(client, f"{KUBECTL} get pods -n {K8S_NAMESPACE} -o wide | grep 'safety-eval' | head -10")

    # 最近日志
    print("\n  最近启动日志 (最后 40 行):")
    ssh_exec(client,
             f"{KUBECTL} logs deployment/{DEPLOY_NAME} -n {K8S_NAMESPACE} --tail=40")


if __name__ == "__main__":
    main()
