#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import json
import time
import socket
import platform
import subprocess
import re
import urllib.request
import urllib.error
import ssl
import configparser

VERSION = "1.0.0"

# 忽略 SSL 证书校验，以便在自签证书环境下正常上报
ssl_context = ssl._create_unverified_context()

def read_config():
    """读取配置文件"""
    config_paths = [
        "/etc/asset-agent/agent.conf",
        "agent.conf",
        "agent/agent.conf"
    ]
    
    config = {
        'server_url': 'http://127.0.0.1:5000',
        'report_interval': 300,
        'asset_no': '',
        'department': '',
        'user': '',
        'location': '',
        'remark': ''
    }
    
    for path in config_paths:
        if os.path.exists(path):
            try:
                parser = configparser.ConfigParser()
                parser.read(path, encoding='utf-8')
                if 'agent' in parser:
                    sect = parser['agent']
                    config['server_url'] = sect.get('server_url', config['server_url']).rstrip('/')
                    config['report_interval'] = int(sect.get('report_interval', str(config['report_interval'])))
                    config['asset_no'] = sect.get('asset_no', config['asset_no'])
                    config['department'] = sect.get('department', config['department'])
                    config['user'] = sect.get('user', config['user'])
                    config['location'] = sect.get('location', config['location'])
                    config['remark'] = sect.get('remark', config['remark'])
                break
            except Exception as e:
                print(f"Warning: Failed to read config from {path}: {e}")
                
    return config

def get_physical_interfaces():
    """获取所有物理网卡及其 IP 和 MAC 地址，排除虚拟网卡"""
    interfaces = []
    net_dir = '/sys/class/net'
    if not os.path.exists(net_dir):
        # 降级通过 ip link / ip addr 解析
        return interfaces
        
    for name in os.listdir(net_dir):
        if name == 'lo':
            continue
        # 物理网卡在 sysfs 中一定存在指向物理总线设备的 device 软链接，而虚拟网卡没有
        device_path = os.path.join(net_dir, name, 'device')
        if os.path.exists(device_path):
            # 排除 docker, veth 等常见的虚拟命名前缀
            if any(name.startswith(p) for p in ['docker', 'veth', 'br-', 'virbr', 'tun', 'tap', 'wg']):
                continue
                
            mac_path = os.path.join(net_dir, name, 'address')
            mac = ""
            if os.path.exists(mac_path):
                try:
                    with open(mac_path, 'r') as f:
                        mac = f.read().strip()
                except Exception:
                    pass
                    
            if mac:
                # 获取 IP
                ip = get_interface_ip(name)
                interfaces.append({
                    'name': name,
                    'mac': mac,
                    'ip': ip
                })
    return interfaces

def get_interface_ip(name):
    """获取特定网卡的 IPv4 地址"""
    try:
        res = subprocess.run(['ip', '-o', '-4', 'addr', 'show', name],
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
        out = res.stdout.decode('utf-8', errors='ignore')
        match = re.search(r'inet\s+([0-9\.]+)', out)
        if match:
            return match.group(1)
    except Exception:
        pass
    return ""

def get_cpu_model():
    """获取 CPU 型号"""
    model = ""
    if os.path.exists('/proc/cpuinfo'):
        try:
            with open('/proc/cpuinfo', 'r', encoding='utf-8', errors='ignore') as f:
                for line in f:
                    if 'model name' in line.lower() or 'processor' in line.lower():
                        parts = line.split(':', 1)
                        if len(parts) > 1:
                            val = parts[1].strip()
                            # 过滤仅包含数字的 processor 标识，只保留含有字母的 CPU 型号描述
                            if re.search(r'[A-Za-z]', val):
                                model = val
                                break
        except Exception:
            pass
            
    if not model:
        try:
            res = subprocess.run(['lscpu'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
            out = res.stdout.decode('utf-8', errors='ignore')
            for line in out.splitlines():
                if 'Model name' in line or '型号名称' in line:
                    parts = line.split(':', 1)
                    if len(parts) > 1:
                        model = parts[1].strip()
                        break
        except Exception:
            pass
            
    return model or "Unknown CPU"

def get_memory_gb():
    """获取物理内存总量 (GB)"""
    if os.path.exists('/proc/meminfo'):
        try:
            with open('/proc/meminfo', 'r') as f:
                for line in f:
                    if 'MemTotal' in line:
                        match = re.search(r'(\d+)', line)
                        if match:
                            kb = int(match.group(1))
                            return round(kb / (1024.0 * 1024.0), 2)
        except Exception:
            pass
    return 0.0

def get_disk_gb():
    """计算非虚拟分区的磁盘总容量 (GB)，排除 cdrom"""
    total_kb = 0
    try:
        res = subprocess.run(['df', '-P'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
        out = res.stdout.decode('utf-8', errors='ignore')
        lines = out.splitlines()
        for line in lines[1:]:
            parts = line.split()
            if len(parts) >= 6:
                device = parts[0]
                mountpoint = parts[5]
                # 只统计物理设备分区且排除只读光盘介质与虚拟分区
                if device.startswith('/dev/'):
                    if any(x in device.lower() for x in ['sr0', 'loop', 'cdrom']) or \
                       any(x in mountpoint.lower() for x in ['cdrom', 'media', 'mnt/iso']):
                        continue
                    try:
                        total_kb += int(parts[1])
                    except ValueError:
                        pass
    except Exception:
        pass
        
    if total_kb > 0:
        return round(total_kb / (1024.0 * 1024.0), 2)
        
    # 降级采用根分区空间
    try:
        stat = os.statvfs('/')
        return round((stat.f_blocks * stat.f_frsize) / (1024.0 * 1024.0 * 1024.0), 2)
    except Exception:
        return 0.0

def get_os_version():
    """获取操作系统版本"""
    if os.path.exists('/etc/kylin-release'):
        try:
            with open('/etc/kylin-release', 'r', encoding='utf-8', errors='ignore') as f:
                return f.read().strip()
        except Exception:
            pass
            
    if os.path.exists('/etc/os-release'):
        try:
            pretty_name = ""
            with open('/etc/os-release', 'r', encoding='utf-8', errors='ignore') as f:
                for line in f:
                    if line.startswith('PRETTY_NAME='):
                        pretty_name = line.split('=', 1)[1].strip().strip('"')
                        break
            if pretty_name:
                return pretty_name
        except Exception:
            pass
            
    if os.path.exists('/etc/redhat-release'):
        try:
            with open('/etc/redhat-release', 'r', encoding='utf-8', errors='ignore') as f:
                return f.read().strip()
        except Exception:
            pass
            
    return f"{platform.system()} {platform.release()}"

def get_serial_number():
    """获取物理硬件序列号 (DMI System Serial Number)"""
    sn = ""
    # 尝试读取 sysfs 中的 DMI 序列号 (需要 root 权限)
    paths = [
        "/sys/class/dmi/id/product_serial",
        "/sys/devices/virtual/dmi/id/product_serial",
        "/sys/class/dmi/id/board_serial"
    ]
    for p in paths:
        if os.path.exists(p):
            try:
                with open(p, 'r') as f:
                    val = f.read().strip()
                    # 排除一些无意义的占位符
                    if val and not any(x in val.lower() for x in ['not specified', 'none', 'default string', 'to be filled by o.e.m.']):
                        sn = val
                        break
            except Exception:
                pass
                
    if not sn:
        # 尝试通过 dmidecode 命令行读取
        try:
            res = subprocess.run(['dmidecode', '-s', 'system-serial-number'],
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
            val = res.stdout.decode('utf-8', errors='ignore').strip()
            if val and not any(x in val.lower() for x in ['not specified', 'none', 'default string', 'to be filled by o.e.m.']):
                sn = val
        except Exception:
            pass
            
    return sn or "Unknown-SN"

def get_system_serial():
    """获取麒麟系统服务序列号 (解析 /etc/.kyinfo)"""
    kyinfo_path = "/etc/.kyinfo"
    if not os.path.exists(kyinfo_path):
        return "Unknown-System-Serial"
        
    # 首先尝试通过正则精确匹配，因为 .kyinfo 可能存在非标准的 INI 语法
    try:
        with open(kyinfo_path, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
        
        # 常见的麒麟序列号标识：key, service_id, system_serial, milestone等
        for pattern in [r'key\s*=\s*(.*)', r'service_id\s*=\s*(.*)', r'system_serial\s*=\s*(.*)', r'milestone\s*=\s*(.*)']:
            match = re.search(pattern, content, re.IGNORECASE)
            if match:
                val = match.group(1).strip()
                if val:
                    return val
    except Exception:
        pass
        
    # 降级尝试使用 ConfigParser
    try:
        parser = configparser.ConfigParser(strict=False)
        parser.read(kyinfo_path, encoding='utf-8')
        for section in parser.sections():
            for option in ['key', 'service_id', 'system_serial', 'milestone']:
                if parser.has_option(section, option):
                    val = parser.get(section, option).strip()
                    if val:
                        return val
    except Exception:
        pass
        
    return "Unknown-System-Serial"

def get_cpu_usage():
    """计算 CPU 当前使用率 (%)"""
    if not os.path.exists('/proc/stat'):
        return 0.0
    try:
        def read_cpu_ticks():
            with open('/proc/stat', 'r') as f:
                line = f.readline()
            parts = line.split()
            if len(parts) >= 5 and parts[0] == 'cpu':
                ticks = [float(x) for x in parts[1:]]
                # idle_ticks = idle + iowait
                # total_ticks = sum(all)
                idle = ticks[3] + (ticks[4] if len(ticks) > 4 else 0)
                total = sum(ticks)
                return idle, total
            return None
            
        t1 = read_cpu_ticks()
        if not t1:
            return 0.0
        time.sleep(0.5)
        t2 = read_cpu_ticks()
        if not t2:
            return 0.0
            
        idle_delta = t2[0] - t1[0]
        total_delta = t2[1] - t1[1]
        if total_delta > 0:
            return round(100.0 * (1.0 - idle_delta / total_delta), 1)
    except Exception:
        pass
    return 0.0

def get_mem_usage():
    """获取内存使用率 (%)"""
    if os.path.exists('/proc/meminfo'):
        try:
            mem = {}
            with open('/proc/meminfo', 'r') as f:
                for line in f:
                    parts = line.split()
                    if len(parts) >= 2:
                        key = parts[0].rstrip(':')
                        mem[key] = int(parts[1])
            total = mem.get('MemTotal', 0)
            if total > 0:
                # 如果有 MemAvailable 优先使用
                if 'MemAvailable' in mem:
                    avail = mem['MemAvailable']
                    used = total - avail
                else:
                    free = mem.get('MemFree', 0)
                    buffers = mem.get('Buffers', 0)
                    cached = mem.get('Cached', 0)
                    reclaimable = mem.get('SReclaimable', 0)
                    used = total - free - buffers - cached - reclaimable
                return round((used / total) * 100.0, 1)
        except Exception:
            pass
    return 0.0

def get_disk_usage():
    """获取根路径磁盘使用率 (%)"""
    try:
        stat = os.statvfs('/')
        total = stat.f_blocks
        free = stat.f_bfree
        if total > 0:
            used = total - free
            return round((used / total) * 100.0, 1)
    except Exception:
        pass
    return 0.0

def get_sys_load():
    """获取系统负载 (1, 5, 15分钟)"""
    if os.name != 'nt':
        try:
            # 优先使用 os.getloadavg()
            load = os.getloadavg()
            return f"{load[0]:.2f}, {load[1]:.2f}, {load[2]:.2f}"
        except Exception:
            pass
            
    if os.path.exists('/proc/loadavg'):
        try:
            with open('/proc/loadavg', 'r') as f:
                line = f.readline().strip()
            parts = line.split()
            if len(parts) >= 3:
                return f"{parts[0]}, {parts[1]}, {parts[2]}"
        except Exception:
            pass
    return "0.00, 0.00, 0.00"

def get_disk_partitions():
    """获取所有物理分区的挂载详情，并剔除 cdrom 以防止误报"""
    partitions = []
    mounts_path = '/proc/mounts'
    if not os.path.exists(mounts_path):
        mounts_path = '/etc/mtab'
        if not os.path.exists(mounts_path):
            return partitions
            
    try:
        seen_mountpoints = set()
        with open(mounts_path, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                parts = line.split()
                if len(parts) >= 3:
                    device = parts[0]
                    mountpoint = parts[1]
                    fstype = parts[2]
                    
                    # 只统计 /dev/ 开头的物理分区，并强力排除虚拟文件系统及只读光盘介载挂载
                    if not device.startswith('/dev/'):
                        continue
                        
                    is_cdrom = any(x in device.lower() for x in ['sr0', 'cdrom', 'loop']) or \
                               any(x in mountpoint.lower() for x in ['cdrom', 'media', 'mnt/iso']) or \
                               any(x in fstype.lower() for x in ['iso9660', 'udf'])
                               
                    if is_cdrom:
                        continue
                        
                    if mountpoint in seen_mountpoints:
                        continue
                    seen_mountpoints.add(mountpoint)
                    
                    try:
                        stat = os.statvfs(mountpoint)
                        total_blocks = stat.f_blocks
                        free_blocks = stat.f_bfree
                        avail_blocks = stat.f_bavail
                        block_size = stat.f_frsize
                        
                        total_gb = round((total_blocks * block_size) / (1024.0 ** 3), 2)
                        free_gb = round((avail_blocks * block_size) / (1024.0 ** 3), 2)
                        used_gb = round(((total_blocks - free_blocks) * block_size) / (1024.0 ** 3), 2)
                        
                        percent = 0.0
                        if total_blocks > 0:
                            percent = round(((total_blocks - free_blocks) / total_blocks) * 100.0, 1)
                            
                        partitions.append({
                            'device': device,
                            'mountpoint': mountpoint,
                            'fstype': fstype,
                            'total_gb': total_gb,
                            'free_gb': free_gb,
                            'used_gb': used_gb,
                            'percent': percent
                        })
                    except Exception:
                        pass
    except Exception as e:
        print(f"Error parsing disk partitions: {e}")
        
    return partitions

def scan_software():
    """扫描系统已安装的软件 (非 quick 模式下调用，限制最多前 500 个以防数据上报过大)"""
    apps = {}
    if os.path.exists('/usr/bin/rpm'):
        try:
            res = subprocess.run(['rpm', '-qa', '--qf', '%{NAME}\t%{VERSION}\n'],
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
            out = res.stdout.decode('utf-8', errors='ignore')
            lines = out.splitlines()
            for line in lines[:500]:
                parts = line.split('\t', 1)
                if len(parts) == 2:
                    apps[parts[0]] = parts[1]
        except Exception:
            pass
    elif os.path.exists('/usr/bin/dpkg-query'):
        try:
            res = subprocess.run(['dpkg-query', '-W', "-f=${Package}\t${Version}\n"],
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
            out = res.stdout.decode('utf-8', errors='ignore')
            lines = out.splitlines()
            for line in lines[:500]:
                parts = line.split('\t', 1)
                if len(parts) == 2:
                    apps[parts[0]] = parts[1]
        except Exception:
            pass
    return apps

def scan_services():
    """扫描常见核心系统服务的状态"""
    services = {}
    core_services = ['ssh', 'sshd', 'nginx', 'mysql', 'docker', 'firewalld', 'asset-server', 'asset-agent']
    for s in core_services:
        try:
            # 检查服务文件是否存在或是否 active
            res = subprocess.run(['systemctl', 'is-active', s],
                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=3)
            out = res.stdout.decode('utf-8', errors='ignore').strip()
            if out == 'active':
                services[s] = 'running'
            else:
                services[s] = 'stopped'
        except Exception:
            pass
    return services

def collect_all_data(quick=False):
    """收集所有硬件资产与状态指标数据"""
    interfaces = get_physical_interfaces()
    
    # 选取主网卡信息：优先选取有有效 IPv4 且非 127.0.0.1 物理网卡的 MAC/IP
    main_mac = ""
    main_ip = ""
    if interfaces:
        for face in interfaces:
            if face['ip'] and face['ip'] != "127.0.0.1":
                main_mac = face['mac']
                main_ip = face['ip']
                break
        # 若所有物理网卡均未获得 IP，则 fallback 默认选择第一个物理网卡
        if not main_mac:
            main_mac = interfaces[0]['mac']
            main_ip = interfaces[0]['ip']
        
    if not main_mac:
        # fallback 尝试读取任意网卡 MAC
        try:
            for name in os.listdir('/sys/class/net'):
                if name == 'lo': continue
                with open(f'/sys/class/net/{name}/address', 'r') as f:
                    val = f.read().strip()
                    if val:
                        main_mac = val
                        break
        except Exception:
            pass
            
    if not main_mac:
        try:
            import uuid
            node = uuid.getnode()
            main_mac = ':'.join(['{:02x}'.format((node >> i) & 0xff) for i in range(0, 8*6, 8)][::-1])
        except Exception:
            pass
            
    if not main_ip:
        try:
            main_ip = socket.gethostbyname(socket.gethostname())
        except Exception:
            main_ip = "127.0.0.1"

    config = read_config()
    
    data = {
        'mac': main_mac,
        'ip': main_ip,
        'hostname': socket.gethostname(),
        'cpu_model': get_cpu_model(),
        'cpu_cores': os.cpu_count() or 1,
        'cpu_arch': platform.machine(),
        'memory_gb': get_memory_gb(),
        'disk_gb': get_disk_gb(),
        'os_version': get_os_version(),
        'kernel_version': platform.release(),
        'serial_number': get_serial_number(),
        'system_serial': get_system_serial(),
        'version': VERSION,
        
        # 动态指标
        'cpu_usage': 0.0 if quick else get_cpu_usage(),
        'mem_usage': 0.0 if quick else get_mem_usage(),
        'disk_usage': 0.0 if quick else get_disk_usage(),
        'sys_load': "0.00, 0.00, 0.00" if quick else get_sys_load(),
        'disk_partitions': [] if quick else get_disk_partitions(),
        
        # 软件与服务
        'services': {} if quick else scan_services(),
        'installed_apps': {} if quick else scan_software(),
        
        # 静态业务属性
        'asset_number': config['asset_no'],
        'department': config['department'],
        'responsible_person': config['user'],
        'location': config['location'],
        'remark': config['remark']
    }
    
    return data, config['server_url']

def handle_upgrade(download_url):
    """静默下载更新并异步执行升级安装包"""
    print(f"检测到新版本，开始静默升级：{download_url}")
    try:
        # 区分是 RPM 还是 DEB 体系
        is_rpm = download_url.lower().endswith('.rpm') or 'format=rpm' in download_url.lower()
        ext = '.rpm' if is_rpm else '.deb'
        local_file = f"/tmp/asset-agent-update{ext}"
        
        # 下载文件
        req = urllib.request.Request(
            download_url,
            headers={'User-Agent': 'Asset-Agent-Upgrade'}
        )
        with urllib.request.urlopen(req, context=ssl_context, timeout=60) as response:
            with open(local_file, 'wb') as f:
                f.write(response.read())
        print(f"升级包下载成功，保存至: {local_file}")
        
        # 构造非阻塞的安装命令以防止当前 python 进程被系统直接中断
        if is_rpm:
            cmd = f"sleep 2 && (dnf localinstall -y {local_file} || yum localinstall -y {local_file} || rpm -Uvh --force {local_file}) && rm -f {local_file}"
        else:
            cmd = f"sleep 2 && dpkg -i {local_file} && apt-get install -f -y && rm -f {local_file}"
            
        print(f"触发后台异步升级安装流程，执行命令: {cmd}")
        subprocess.Popen(
            f"nohup sh -c '{cmd}' >/dev/null 2>&1 &",
            shell=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            close_fds=True
        )
        print("异步更新指令已发出，Agent 正在退出准备更新。")
        sys.exit(0)
    except Exception as e:
        print(f"Silent upgrade failed: {e}")

def main():
    try:
        # 参数解析
        output_file = None
        quick_mode = False
        
        for i in range(1, len(sys.argv)):
            if sys.argv[i] in ('-o', '--output') and i + 1 < len(sys.argv):
                output_file = sys.argv[i+1]
            elif sys.argv[i].startswith('--output='):
                output_file = sys.argv[i].split('=', 1)[1]
            elif sys.argv[i] in ('-q', '--quick'):
                quick_mode = True
                
        data, server_url = collect_all_data(quick=quick_mode)
        if not data['mac']:
            print("Error: Could not determine host MAC address. Aborting.")
            sys.exit(1)
            
        json_output = json.dumps(data, indent=2, ensure_ascii=False)
        
        if output_file:
            with open(output_file, 'w', encoding='utf-8') as f:
                f.write("=== 麒麟资产管理系统 离线硬件资产采集报告 ===\n")
                f.write(f"生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
                f.write("===============================================\n\n")
                f.write(json_output)
                f.write("\n")
            print(f"采集完成！硬件资产信息已成功保存至本地文件: {output_file}")
            sys.exit(0)
            
        print("Collected Asset Info:")
        print(json_output)
        
        # 发送上报请求
        report_url = f"{server_url}/api/report"
        req_data = json.dumps(data).encode('utf-8')
        req = urllib.request.Request(
            report_url,
            data=req_data,
            headers={'Content-Type': 'application/json', 'User-Agent': 'Asset-Agent'}
        )
        
        with urllib.request.urlopen(req, context=ssl_context, timeout=15) as response:
            resp_body = response.read().decode('utf-8')
            res_json = json.loads(resp_body)
            print("Server response:", res_json)
            
            # 校验静默升级
            if res_json.get('need_upgrade') and res_json.get('download_url'):
                latest_v = res_json.get('latest_version', VERSION)
                if latest_v != VERSION:
                    handle_upgrade(res_json['download_url'])
                    
    except Exception as e:
        print(f"Agent execution failed: {e}")
        sys.exit(1)

if __name__ == '__main__':
    main()