import os
import re
import sys
import pwd
import xml
import yaml
import types
import syslog
import inspect
import argparse
import subprocess
import sunhpc.util
import sunhpc.utils

from io import StringIO
from pathlib import Path
from typing import Dict, Optional, Tuple, List, Union

from sunhpc.logs import Logs
from sunhpc.disks import DiskInfo
from sunhpc.configs import Config
from sunhpc.network import Network
from sunhpc.memory import MemoryInfo
from sunhpc.output import Output
from sunhpc.firewalld import FWManager

from sunhpc.utils.shells import ShellExecutor

from xml.sax import saxutils
from xml.sax import handler
from xml.sax import make_parser

def Abort(message, doExit=1):
    '''Print a standard error message and throw a
    sunhpc.util.CommandError
    '''
    syslog.syslog(syslog.LOG_ERR, message)
    raise sunhpc.util.CommandError(message)

class DocStringHandler(
    handler.ContentHandler, handler.DTDHandler,
    handler.EntityResolver, handler.ErrorHandler):

    def __init__(self, name='', users=[]):
        handler.ContentHandler.__init__(self)
        self.text                   = ''
        self.name                   = name
        self.users                  = users
        self.section                = {}
        self.section['description'] = ''
        self.section['arg']         = []
        self.section['param']       = []
        self.section['example']     = []
        self.section['related']     = []
        self.parser = make_parser()
        self.parser.setContentHandler(self)

    def getDocbookText(self):
        s  = ''
        s += '<section id="sunhpc-%s" xreflabel="%s">\n' % \
			            ('-'.join(self.name.split(' ')), self.name)
        s += '<title>%s</title>\n' % self.name
        s += '<cmdsynopsis>\n'
        s += '\t<command>sunhpc %s</command>\n' % self.name
        for ((name, type, opt, rep), txt) in self.section['arg']:
            if opt:
                choice = 'opt'
            else:
                choice = 'req'
            if rep:
                repeat = 'repeat'
            else:
                repeat = 'norepeat'
            s += '\t<arg rep="%s" choice="%s">%s</arg>\n' % \
				(repeat, choice, name)
        for ((name, type, opt, rep), txt) in self.section['param']:
            if opt:
                choice = 'opt'
            else:
                choice = 'req'
            if rep:
                repeat = 'repeat'
            else:
                repeat = 'norepeat'
            s += '\t<arg rep="%s" choice="%s">' % (repeat, choice)
            s += '%s=<replaceable>%s</replaceable>' % (name, type)
            s += '</arg>\n'
        s += '</cmdsynopsis>\n'
        s += '<para>\n'
        s += saxutils.escape(self.section['description'])
        s += '\n</para>\n'
        if self.section['arg']:
            s += '<variablelist><title>arguments</title>\n'
            for ((name, type, opt, rep), txt) in \
				self.section['arg']:
                s += '\t<varlistentry>\n'
                if opt:
                    term = '<optional>%s</optional>' % name
                else:
                    term = name
            s += '\t<term>%s</term>\n' % term
            s += '\t<listitem>\n'
            s += '\t<para>\n'
            s += saxutils.escape(txt)
            s += '\n\t</para>\n'
            s += '\t</listitem>\n'
            s += '\t</varlistentry>\n'
            s += '</variablelist>\n'
        if self.section['param']:
            s += '<variablelist><title>parameters</title>\n'
            for ((name, type, opt, rep), txt) in \
				self.section['param']:
                s += '\t<varlistentry>\n'
                if opt:
                    optStart = '<optional>'
                    optEnd   = '</optional>'
                else:
                    optStart = ''
                    optEnd   = ''
                key = '%s=' % name
                val = '<replaceable>%s</replaceable>' % type
                s += '\t<term>%s%s%s%s</term>\n' % \
					(optStart, key, val, optEnd)
                s += '\t<listitem>\n'
                s += '\t<para>\n'
                s += saxutils.escape(txt)
                s += '\n\t</para>\n'
                s += '\t</listitem>\n'
                s += '\t</varlistentry>\n'
            s += '</variablelist>\n'
        if self.section['example']:
            s += '<variablelist><title>examples</title>\n'
            for (cmd, txt) in self.section['example']:
                s += '\t<varlistentry>\n'
                s += '\t<term>\n'
                if 'root' in self.users:
                    s += '# '
                else:
                    s += '$ '
                s += 'sunhpc %s' % cmd
                s += '\n\t</term>\n'
                s += '\t<listitem>\n'
                s += '\t<para>\n'
                s += saxutils.escape(txt)
                s += '\n\t</para>\n'
                s += '\t</listitem>\n'
                s += '\t</varlistentry>\n'
            s += '</variablelist>\n'
        if self.section['related']:
            s += '<variablelist><title>related commands</title>\n'
            for related in self.section['related']:
                s += '\t<varlistentry>\n'
                s += '\t<term>'
                s += '<xref linkend="sunhpc-%s">' % '-'.join(related.split(' '))
                s += '</term>\n'
                s += '\t<listitem>\n'
                s += '\t<para>\n'
                s += '\n\t</para>\n'
                s += '\t</listitem>\n'
                s += '\t</varlistentry>\n'
            s += '</variablelist>\n'
        s += '</section>'
        return s

    def getUsageText(self):
        s = ''
        for ((name, type, opt, rep), txt) in self.section['arg']:
            if opt:
                s += '[%s]' % name
            else:
                s += '{%s}' % name
            if rep:
                s += '...'
            s += ' '
        for ((name, type, opt, rep), txt) in self.section['param']:
            if opt:
                s += '[%s=%s]' % (name, type)
            else:
                s += '{%s=%s}' % (name, type)
            if rep:
                s += '...'
            s += ' '
        if s and s[-1] == ' ':
            return s[:-1]
        else:
            return s

    def getSphinxText(self):
        if 'root' in self.users:
            prompt = '#'
        else:
            prompt = '$'

        s  = ':orphan:\n\n'
        s += '%s\n' % self.name
        s += '%s\n\n' % ("-" * len(self.name))
        s += '.. role:: defn\n\n' 
        utxt = self.getUsageText()
        if len(utxt): 
            s += ':defn:`sunhpc %s` *%s*\n' % (self.name, utxt)
        else:         
            s += ':defn:`sunhpc %s` %s\n' % (self.name, utxt)
        s += '\n\n**Description:**\n'
        s += self.section['description'].replace('\t','   ')
        if self.section['arg']:
            s += '\n**Arguments:**\n\n'
            for ((name, type, opt, rep), txt) in \
				self.section['arg']:
                if opt:
                    s += '*[%s]*' % name
                else:
                    s += '*{%s}*' % name
                txt = txt.replace('*', '\*')
                s += '\n%s\n' % txt.replace('\t', '   ')
        if self.section['param']:
            s += '\n**Parameters:**\n\n'
            for ((name, type, opt, rep), txt) in \
				self.section['param']:
                if opt:
                    s += '*[%s=%s]*' % (name, type)
                else:
                    s += '*{%s=%s}*' % (name, type)
                txt = txt.replace('*', '\*')
                s += '\n%s\n' % txt.replace('\t', '   ')
        if self.section['example']:
            s += '\n**Examples:**\n'
            for (cmd, txt) in self.section['example']:
                txt = txt.replace('*', '\*')
                s += '%s::\n\n' % txt.replace('\t','   ')
                s += '        %s sunhpc %s\n' % (prompt, cmd)
        if self.section['related']:
            s += '\n**Related Commands:**\n\n'
            for related in self.section['related']:
                s += '   * :ref:`sunhpc-%s`\n' % related.replace(' ','-')

        word = self.name.split()[0] 
        s += '\n:ref:`%s commands <%s-ref>`\n' % (word, word)
        return s

    def getPlainText(self):
        if 'root' in self.users:
            prompt = '#'
        else:
            prompt = '$'
        s  = ''
        s += '  \n%s sunhpc %s %s' % (prompt, self.name, self.getUsageText())
        s += '\n\nDescription:\n'
        s += self.section['description']
        if self.section['arg']:
            s += '\nArguments:\n'
            for ((name, type, opt, rep), txt) in \
				self.section['arg']:
                if opt:
                    s += '    [%s]' % name
                else:
                    s += '    {%s}' % name
                s += '%s\n' % txt
        if self.section['param']:
            s += '\nParameters:\n'
            for ((name, type, opt, rep), txt) in \
				self.section['param']:
                if opt:
                    s += '    [%s=%s]' % (name, type)
                else:
                    s += '    {%s=%s}' % (name, type)
                s += '%s\n' % txt
        if self.section['example']:
            s += '\nExamples:\n\n'
            for (cmd, txt) in self.section['example']:
                s += '    %s sunhpc %s' % (prompt, cmd)
                s += '%s\n' % txt
        if self.section['related']:
            s += '\nRelated Commands:\n\n'
            for related in self.section['related']:
                s += '    sunhpc %s\n' % related.replace(' ','-')
        return s

    def getParsedText(self):
        return '%s' % self.section

    def startElement(self, name, attrs):
        if not self.section['description']:
            self.section['description'] = self.text
        self.key    = None
        self.text   = ''
        if name in [ 'arg', 'param' ]:
            try:
                dtype = attrs.get('type')
            except:
                dtype = 'string'
            try:
                optional = int(attrs.get('optional'))
            except:
                if name == 'arg':
                    optional = 0
                if name == 'param':
                    optional = 1
            try:
                repeat = int(attrs.get('repeat'))
            except:
                repeat = 0
            name = attrs.get('name')
            self.key = (name, dtype, optional, repeat)
        elif name == 'example':
            self.key = attrs.get('cmd')

    def endElement(self, name):
        if name == 'docstring':
            self.section['param'].sort()
            self.section['related'].sort()
        elif name in [ 'arg', 'param', 'example' ]:
            self.section[name].append((self.key, self.text))
        else:
            if name in self.section:
                self.section[name].append(self.text)

    def characters(self, s):
        self.text += s

class Command:
    '''Base class for all Sunhpc commands the general command line form
    is as follows:

        sunhpc action component object [ <argname args> ... ]
        action(s):
            add
            create
            list
            load
            sync
    '''
    MustBeRoot = 1

    def __init__(self, database):
        self.db         = database
        self.conf       = Config('/etc/sunhpc/sunhpc.yaml')
        self.text       = ''
        self.output     = []

        self.net        = Network()
        self.mem        = MemoryInfo()
        self.disk       = DiskInfo()
        self.fmt        = Output()
        self.sh         = ShellExecutor()
        self.fw         = FWManager('sunhpc')

        self.os         = os.uname()[0].lower()
        self.arch       = os.uname()[4]
        self.user       = pwd.getpwuid(os.getuid()).pw_name

        self.parser     = argparse.ArgumentParser()
        self.parserArgs = []
        self.subcommand = 'Sub Command'
        self.subdesc    = None
        self.epilog     = None

        self._args      = None
        self._params    = None
        if 'SUNHPCDEBUG' in os.environ:
            self._debug = True
            self.log    = Logs(name='Sunhpc', level=0, show_time=True)
        else:
            self._debug = False
            self.log    = Logs(name='Sunhpc', show_time=True)

    def debug(self):
        """Return True if debug mode is enabled"""
        return self._debug
    def abort(self, msg):
        syslog.syslog(syslog.LOG_ERR, msg.split('\n')[0])
        raise sunhpc.util.CommandError(msg)
    def strWordWrap(self, line, indent=''):
        if 'COLUMNS' in os.environ:
            cols = os.environ['COLUMNS']
        else:
            cols = 80
        l = 0
        s = ''
        for word in line.split(' '):
            if l + len(word) < cols:
                s += '%s ' % word
                l += len(word) + 1  # space
            else:
                s += '\n%s%s ' % (indent, word)
                l += len(indent) + len(word) + 1  # space
        return s

    def isRootUser(self):
        return os.getuid() == 0
    def isApacheUser(self):
        return self.user == 'apache'

    def str2bool(self, s):
        if s and s.upper() in [ 'ON', 'YES', 'Y', 'TRUE', '1']:
            return True
        else:
            return False
    def bool2str(self, b):
        if b:
            return 'yes'
        else:
            return 'no'

    def rsync_copy(
            self, 
            src: str, 
            dst: str, 
            options: str = '-ah --info=progress2'
            ) -> bool:
        """
        简化版本的rsync拷贝、专注于进度显示
        
        Args:
            src: 源路径
            dst: 目标路径
        
        Returns:
            bool: 是否成功
        """
        options = options.split()

        cmd = ["rsync"]
        cmd.extend(options)
        
        cmd.append(src)
        cmd.append(dst)
        
        self.log.info(f"执行命令: {' '.join(cmd)}")
        try:
            # 使用Popen实时显示输出
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )
            
            # 实时打印输出
            for line in process.stdout:
                # 去除末尾换行符
                line = line.rstrip('\n\r')

                if line:
                    # 计算需要覆盖的长度（使用退格符或直接回车）
                    # 方法1：使用 \r 回车覆盖（推荐）
                    print(f'\r{line}', end='', flush=True)
                    last_line = line
                else:
                    # 空行，换行
                    print()
            
            # 输出完成后换行，避免下一行输出覆盖进度信息
            if last_line:
                print()
            
            return_code = process.wait()
            
            if return_code == 0:
                self.log.info("\n✓ 拷贝成功完成")
                return True
            else:
                self.log.error(f"\n✗ 拷贝失败，返回码: {return_code}")
                return False
                
        except Exception as e:
            print(f"✗ 执行失败: {e}")
            return False

    def is_remote_path(self, path: str) -> bool:
        """判断是否为远程路径"""
        return '@' in path and ':' in path and not path.startswith(':')


    def dtons(self, data: dict) -> types.SimpleNamespace:
        '''递归将字典转换为 SimpleNamespace'''
        if isinstance(data, dict):
            for key, val in data.items():
                clean_key = key.replace(' ','_')
                data[clean_key] = self.dtons(val)
            return types.SimpleNamespace(**data)
        elif isinstance(data, list):
            return [self.dtons(e) for e in data]
        return data

    def parser_cmd_init(self):
        self.parser.usage = 'sunhpc %s [options]' % self.subcommand
        if self.subdesc:
            self.parser.description = self.subdesc

        # 重新设置 formatter 为 RawTextHelpFormatter
        self.parser.formatter_class = argparse.RawTextHelpFormatter

        if self.epilog is None:
            epilogmsg = "\n"
            epilogmsg += "Help & Documentation:\n"
            epilogmsg += " - Gitea Repository : https://gitea.sunhpc.com/py/sunhpc\n"
            epilogmsg += " - Documentation    : https://gitea.sunhpc.com/py/sunhpc/wiki\n"
            epilogmsg += " - Issue Tracker    : https://gitea.sunhpc.com/py/sunhpc/issues\n"
            #epilogmsg += "SunHPC Tool v1.0.0\n"
            #epilogmsg += "Copyright (c) 2026 SunHPC Team\n"
            #epilogmsg += "This is open-source software released under the Apache 2.0 license.\n"
            self.parser.epilog = epilogmsg

    def parser_cmd_args(self, args):

        original_stderr = sys.stderr
        sys.stderr = StringIO()
        try:
            parsed, unknown = self.parser.parse_known_args(args)
            result =  vars(parsed) # 返回字典格式
            result['unknown'] = unknown
            return result
            #return vars(parsed) # 返回字典格式
        except SystemExit as e:
            error_msg = sys.stderr.getvalue()
            sys.stderr = original_stderr
            if e.code != 0:
                self.log.error(error_msg)
            #pass
            #return {}
            #return {}, args

    def clearText(self):
        self.text = ''
    def addText(self, s):
        if s:
            self.text += s
    def getText(self):
        return self.text

    def beginOutput(self):
        self.output = []
    def addOutput(self, owner, vals):
        'Append a list to the output list buffer.'
        plist = [ '%s:' % owner ]
        if isinstance(vals, (type([]))):
            plist.extend(vals)
        elif isinstance(vals, (type(()))):
            for e in vals:
                plist.append(e)
        else:
            plist.append(vals)

        self.output.append(plist)
    def endOutput(self, header=[], padChar='-', trimOwner=1, linesep='\n'):
        if not self.output:
            return

        showHeader = True
        if 'output-header' in self._params:
            showHeader = self.str2bool(self._params['output-header'])

        self.outputCols = []
        if 'output-col' in self._params:
            showCols = self._params['output-col'].split(',')
            for i in header:
                if i.lower() in showCols:
                    self.outputCols.append(True)
                else:
                    self.outputCols.append(False)

        if trimOwner:
            owner = ''
            self.startOfLine = 1
            for line in self.output:
                if not owner:
                    owner = line[0]
                if not owner == line[0]:
                    self.startOfLine = 0
        else:
            self.startOfLine = 0

        if header and showHeader:
            plist = []
            for field in header:
                plist.append(field.upper())
            output = [plist]
            output.extend(self.output)
        else:
            output = self.output

        colwidth = []
        for line in output:
            for i in range(0, len(line)):
                if len(colwidth) <= i:
                    colwidth.append(0)
                if type(line[i]) != type(''):
                    if line[i] == None:
                        itemlen = 0
                    else:
                        itemlen = len(repr(line[i]))

                else:
                    itemlen = len(line[i])
                
                if itemlen > colwidth[i]:
                    colwidth[i] = itemlen

        o = ''
        for line in output:
            plist = []
            for i in range(self.startOfLine, len(line)):
                if line[i] == None:
                    s = ''
                else:
                    s = str(line[i])
                if padChar != '':
                    if s:
                        o = s.ljust(colwidth[i])
                    else:
                        o = ''.ljust(colwidth[i], padChar)
                else:
                    o = s
                plist.append(o)
            self.addText('%s%s' % (self.outputRow(plist), linesep))
    def outputRow(self, plist):
        if self.outputCols:
            l = []
            for i in range(0, len(plist)):
                if self.outputCols[i + self.startOfLine]:
                    l.append(plist[i])
            return ' '.join(l)
        else:
            return ' '.join(plist)

    def output_dict(self, data={}):
        if not data: return

        max_key_len = max(len(str(key)) for key in data.keys())
        for key, value in data.items():
            print(f"{str(key):<{max_key_len}} : {value}")

    def command(self, command, args=[]):
        '''Import and run a Sunhpc command.
        Returns and output string.
        '''
        modpath = 'sunhpc.commands.%s' % command
        __import__(modpath)
        mod = eval(modpath)
        try:
            o = getattr(mod, 'Command')
            n = ' '.join(command.split('.'))
        except AttributeError:
            return ''

        o.runWrapper(n, args)
        return o.getText()

    def fillParams(self, names, params=None):
        '''
        Returns a list of variables with either default
        values of the values in the PARAMS dictionary.

        : type names: list
        : param names: a list of (key, default) tuples, where key
            is a key name of params dictionary and default
            is the default value if key in not in dict

        : type params: dict
        : param params: optional dictionary

        For example::
            (svc, comp) = self.fillParams (
                ('service', None),
                ('component', None))

        Can also be written as::
            (svc, comp) = self.fillParams(('service',), ('component', ))
        '''
        # make sure names is a list or tuple
        if not isinstance(names, (type([]), type(()))):
            names = [ names ]

        dlist = []
        for e in names:
            if type(e) in [ type(()), type([]) ] \
                and len(e) == 2:
                tuples = ( e[0], e[1] )
            else:
                tuples = ( e[0], None )
            dlist.append(tuples)

        if not params:
            params = self._params

        rlist = []
        for (key, default) in dlist:
            if key in params:
                rlist.append(params[key])
            else:
                rlist.append(default)

        return rlist

    def fillPostArgs(self, names, params=None, args=None):
        '''
        The helper function will allow named parameters
        to be used in lieu of positional arguments
        Example::
            Suppose command is of the form:
                command <arg1> <arg2> <arg3>
            Usually called as:
                command foo bar baz
            However if you name the arguments as parameters, say
                arg1, arg2, arg3
            Then, equivalent calls of the command are
                command arg1=foo arg2=bar arg3=baz
                command foo bar arg3=baz
                command foo bar baz
                command foo arg2=bar baz

            Arguments:
                paramlist = list of parameter names in the order
                            that their unnamed argument counterparts
                            apper eg. paramlist=('iface', 'mac')
                params    = list of parameters (e.g param=value)
                args      = args

        Returns:
            remaining args, Filled parameters
        Example::
            hostlist, iface, mac = self.fillPostArgs(
                    ('iface','mac'), params, args)
        '''

        if not isinstance(names, (type([]), type(()))):
            names = [ names ]

        if not params:
            params = self._params
            
        if not args:
            args = self._args

        plist = []
        for name in names:
            if name in params:
                plist.append(params[name])
            else:
                plist.append(None)

        # now walk backwards through the args and pull off
        # positional arguments that have not already been set
        # as a param=<parameter>

        trimmedArgs = args
        vars = []
        plist.reverse()
        for e in plist:
            if not e and len(trimmedArgs):
                vars.append(trimmedArgs[-1])
                trimmedArgs = trimmedArgs[:-1]
            else:
                vars.append(e)

        # need to reverse the 'vars' to get them in the correct order
        # since we processed them above in reverse order
        vars.reverse()
        rlist = []
        rlist.append(trimmedArgs)
        rlist.extend(vars)
        return rlist

    def runWrapper(self, name, args):

        self.subcommand = name

        if args:
            command = '%s %s' % (name, ' '.join(args))
        else:
            command = name

        syslog.syslog(syslog.LOG_INFO,
            'running command: %s for user: %s' % (command, self.user))

        pdict = {} # flags
        plist = [] # arguments

        nparams = 0
        flagpattern = re.compile(r'^[a-zA-Z0-9-_+]+=')

        for arg in args:
            # 检查 - 或 -- 开头
            if arg.startswith('--') or arg.startswith('-'):
                self.parserArgs.append(arg)
                continue
            
            tokens = arg.split()
            if tokens[0] == 'select':
                plist.append(arg)
            elif flagpattern.match(arg):
                (key, val) = arg.split('=', 1)
                pdict[key] = val
                if nparams == 0:
                    pdict['@SUNHPCPARAM0'] = arg
                nparams += 1
            else:
                plist.append(arg)

        if plist and plist[0] == 'help':
            self.help(name, pdict)
        else:
            if self.MustBeRoot and not \
                (self.isRootUser() or self.isApacheUser()):
                self.abort('command "%s" requires root privilege' % name)
            else:
                self._args      = plist
                self._params    = pdict
                try:
                    self.run(self._params, self._args)
                except sunhpc.util.HostnotfoundException as e:
                    if self.debug():
                        traceback.print_exc()
                    self.abort(str(e))
        
    def run(self, flags, args):
        pass

    def usage(self):
        if self.__doc__:
            handler = DocStringHandler()
            parser = make_parser()
            parser.setContentHandler(handler)
            try:
                parser.feed('<docstring>%s</docstring>' % self.__doc__)
            except:
                return '-- invalid doc string --'
            return handler.getUsageText()
        else:
            return '-- missing doc string --'

    def help(self, command, flags={}):
        if not self.__doc__:
            return
        if self.MustBeRoot:
            users = ['root', 'apache']
        else:
            users = []

        if 'format' in flags:
            format = flags['format'].lower()
        else:
            format = 'plain'

        if format == 'raw':
            i = 1
            for line in self.__doc__.split('\n'):
                self.addText('%d:%s\n' % (i, line))
                i += 1
        else:
            handler = DocStringHandler(command, users)
            parser = make_parser()
            parser.setContentHandler(handler)
            parser.feed('<docstring>%s</docstring>' % self.__doc__)
            if format == 'docbook':
                self.addText(handler.getDockbookText())
            elif format == 'parsed':
                self.addText(handler.getParsedText())
            elif format == 'sphinx':
                self.addText(handler.getSphinxText())
            else:
                self.addText(handler.getPlainText())
