#!/usr/bin/env python
# coding=utf-8
# Copyright (c) 2025 Huawei Technologies Co., Ltd.

import os
import json
import tempfile
import stat
import subprocess
import shlex
import time
import threading
import collections
from concurrent.futures import ThreadPoolExecutor, Future, as_completed
from bmcgo.errors import BmcGoException
from bmcgo.utils.tools import Tools

tools = Tools("build_parallel")
log = tools.log


class BuildConanParallel(object):
    def __init__(self, lockfile, orderinfo, log_dir, remote_list):
        self.lockfile = lockfile
        self.orderinfo = orderinfo
        self.log_dir = log_dir
        self.remote_list = remote_list
        if not os.path.isdir(log_dir):
            os.makedirs(log_dir)
        self.swbom_data_dir = os.path.join(self.log_dir, "swbom_data")
        if not os.path.exists(self.swbom_data_dir):
            os.makedirs(self.swbom_data_dir)
        self.build_status_schedule = collections.OrderedDict()
        self.lock = threading.Lock()
    
    def build(self):
        cwd = os.getcwd()
        tmp = tempfile.TemporaryDirectory()
        os.chdir(tmp.name)
        self._build()
        os.chdir(cwd)

    def _wait_dependency(self, pkgname):
        pkg_deps = self.build_status_schedule.get(pkgname, {}).get("depends", [])
        if pkg_deps is None:
            return
        for dep in pkg_deps:
            dep_name = dep.split("/")[0]
            while True:
                if self.build_status_schedule.get(dep_name) is None:
                    break
                if self.build_status_schedule.get(dep_name, {}).get("status") != "finish":
                    time.sleep(3)
                else:
                    break

    def _build_package(self, option, ref_id, build_args, public_args, depends):
        if option == "Cache":
            return
        pkgname = ref_id.split("/")[0]
        depends = list(depends)
        with self.lock:
            # 检查pkgname是否已存在于构建队列,如果存在重命名为pkgname_2，并将自身添加到依赖列表
            if self.build_status_schedule.get(pkgname) is not None:
                original_pkgname = pkgname
                pkgname = f"{pkgname}_2"
                depends.append(original_pkgname)
            # 注册pkgname
            self.build_status_schedule[pkgname] = {
                "status": "start",
                "depends": depends
            }
        self._wait_dependency(pkgname)
        logfile = f"{self.log_dir}/conan_{option}_{pkgname}.log"
        result = False
        if option == "Build":
            comp_swbom_json_path = os.path.join(self.swbom_data_dir, f"dependencies_{pkgname}.json")
            cmd = f"conan install {build_args} {public_args} --lockfile={self.lockfile}"
            cmd += f" -f json --out-file={comp_swbom_json_path}"
            result = self._run_conan_command(pkgname, option, logfile, cmd)
        if option == "Download":
            for remote in self.remote_list:
                cmd = f"conan download {ref_id} -r {remote}"
                result = self._run_conan_command(pkgname, option, logfile, cmd)
                if result:
                    break
        if not result:
            log.info(f"{option} {pkgname} not ok.")

    def _run_conan_command(self, pkgname, option, logfile, cmd):
        logfd = os.fdopen(os.open(logfile, os.O_RDWR | os.O_CREAT, stat.S_IWUSR | stat.S_IRUSR), "a+")
        log.debug(f">>>> {cmd}")
        log.info(f">>>> {option} {pkgname} start, logfile: {logfile}")
        max_retry = 3
        for i in range(max_retry):
            real_cmd = shlex.split(cmd)
            pipe = subprocess.Popen(real_cmd, stdout=logfd, stderr=logfd)
            start_time = time.time()
            while True:
                if pipe.poll() is not None:
                    break
                if time.time() - start_time > 1800:
                    pipe.kill()
                    log.info(f"================== {pkgname} 构建超时 ==================")
                    break
                time.sleep(1)
            if pipe.returncode != 0:
                if i == max_retry - 1:
                    with self.lock:
                        self.build_status_schedule[pkgname].update({"status": "finish"})
                    return False
                continue
            else:
                break
        with self.lock:
            self.build_status_schedule[pkgname].update({"status": "finish"})
        log.success(f"<<<< {option} {pkgname} finished")
        return True

    def _build(self):
        with open(self.orderinfo, "r") as fp:
            order_info = json.load(fp)
        public_args = order_info.get("profiles", {}).get("self", {}).get("args", "")
        for orders in order_info.get("order", []):
            thread_pool = ThreadPoolExecutor(max_workers=(os.cpu_count() + 1) // 2)
            results: list[Future] = []
            for order in orders:
                ref = order.get("ref")
                packages = order.get("packages", [])
                for package in packages:
                    for pkg in package:
                        binary = pkg.get("binary", "")
                        depends = order.get("depends", [])
                        package_id = pkg.get("package_id", "")
                        ref_id = f"{ref}:{package_id}"
                        build_args = pkg.get("build_args", "")
                        result = thread_pool.submit(self._build_package, binary, ref_id, build_args, public_args, depends)
                        results.append(result)
            for result in as_completed(results):
                result.result()