import argparse
import inspect
import logging
import os
import sys
from pathlib import Path

from cpbox.tool import datatypes
from cpbox.tool import system

class BaseCliApp(object):

    def __init__(self):
        self.cli_root_dir = self._get_cli_root_dir()
        from cpbox.app.rootcontext import gcontext
        gcontext.try_enable_framework_mode()
        self.logger = logging.getLogger(self.__class__.__name__)

    def _get_cli_root_dir(self):
        if hasattr(sys.modules['__main__'], '__file__'):
            return Path(sys.argv[0]).resolve().parent
        else:
            return Path.cwd()

    def run_cmd_ret(self, cmd, log=True):
        return self.run_cmd(cmd, log=log)[1]

    def run_cmd(self, cmd, log=True):
        if log:
            self.logger.info('run_cmd: %s', cmd)
        return system.run_cmd(cmd)

    def shell_run(self, cmd, keep_pipeline=True, exit_on_error=True, dry_run=False, log=True):
        if log:
            self.logger.info('shell_run: %s', cmd)
        if dry_run:
            return 0
        return system.shell_run(cmd, keep_pipeline=keep_pipeline, exit_on_error=exit_on_error)

def append_cli_for_composition(container, cli):
    cli_map = {}
    if hasattr(container, '__cli_map__'):
        cli_map = container.__cli_map__
    for key, attr in cli.__class__.__dict__.items():
        if key[0:1] != '_' and callable(attr):
            cli_map[key] = cli
            setattr(container, key, attr.__get__(cli))

def merge_class_for_composition(*proxy_cls_list):
    def real_decorator(cls):
        cls.__proxy_src_cls__ = cls
        cls.__proxy_cls_list__ = list(proxy_cls_list)
        return cls
    return real_decorator

def keep_class_for_inheritance(cls):
    cls.keep_class_for_cli = True
    return cls

def keep_method_for_inheritance(f):
    f.keep_method_for_cli = True
    return f

class _MethodSpec(object):

    def __init__(self, func, base_method_spec=None):
        self.func = func
        self.method_name = func.__name__
        spec = inspect.getfullargspec(func)

        pos_args = [arg for arg in spec.args if arg != 'self']
        defaults = spec.defaults or ()
        default_values = dict(zip(spec.args[-len(defaults):], defaults)) if defaults else {}
        kwonlyargs = spec.kwonlyargs or []
        kwonlydefaults = spec.kwonlydefaults or {}
        varargs = spec.varargs

        self.named_args = pos_args + list(kwonlyargs)
        self.default_values = dict(default_values, **kwonlydefaults)
        self.varargs = varargs
        self.annotations = self._extract_annotations(spec)
        self.method_origin_args = pos_args + ([self.varargs] if self.varargs else []) + list(kwonlyargs)

        if base_method_spec is not None:
            self.named_args = list(dict.fromkeys(self.named_args + base_method_spec.named_args))
            self.default_values = dict(base_method_spec.default_values, **self.default_values)
            self.annotations = dict(base_method_spec.annotations, **self.annotations)
        self.parameters = self._build_parameters()

    def _extract_annotations(self, spec):
        # 注解值: 未开 PEP 563 时内置标量为类型对象 (int/str/bool/float);
        # 开了 from __future__ import annotations 则全为字符串
        source_annotations = spec.annotations or {}
        annotations = {}
        for arg in self.named_args:
            if arg in source_annotations:
                annotations[arg] = source_annotations[arg]
        if self.varargs and self.varargs in source_annotations:
            annotations[self.varargs] = source_annotations[self.varargs]
        return annotations

    def _build_parameters(self):
        parameters = {}
        for arg_name in self.named_args:
            annotation = self.annotations.get(arg_name)
            annotation_type_name = datatypes.resolve_scalar_type_name_from_annotation(annotation)
            if annotation is not None and annotation_type_name is None:
                raise ValueError('unsupported annotation for %s.%s: %s' % (self.method_name, arg_name, annotation))
            type_name = annotation_type_name
            if type_name is None and arg_name in self.default_values:
                type_name = datatypes.resolve_scalar_type_name_from_value(self.default_values[arg_name])
            if type_name is None:
                type_name = 'str'
            des = 'type: %s' % type_name
            if arg_name in self.default_values:
                des = '%s, default value is: %s' % (des, self.default_values[arg_name])
            parameters[arg_name] = {'type': type_name, 'des': des}
        return parameters

    def build_help_text(self):
        parameters = self.parameters or {}
        default_values = self.default_values or {}
        args = self.named_args or []
        varargs = self.varargs
        parts = []
        for arg in args:
            param = parameters.get(arg, {})
            arg_type = param.get('type', 'str')
            if len(arg) == 1:
                display_arg = '-' + arg
            else:
                display_arg = _build_main_long_option(arg)
            if arg in default_values:
                parts.append(f"{display_arg}[{arg_type}={default_values.get(arg)}]")
            else:
                parts.append(f"{display_arg}[{arg_type}*]")
        if varargs:
            parts.append(f"{varargs}... (remainder)")
        if not parts:
            return ''
        return ', '.join(parts)

class AppSpec(object):

    def __init__(self, app):
        self.base_method_spec = _MethodSpec(app.__init__)
        self.app = app
        self._method_processor = _MethodProcessor(app, self.base_method_spec)
        self._method_names_by_order = self._method_processor.get_method_names_by_order()
        self._methods_by_name = self._method_processor.get_methods_by_name()
        self._method_spec_map = self._method_processor.get_method_spec_map()
        self.method_will_call = None

    def get_method_names_by_order(self):
        return list(self._method_names_by_order)

    def get_methods_by_name(self):
        return dict(self._methods_by_name)

    def get_method_spec_map(self):
        return dict(self._method_spec_map)

    def fix_terminal_columns(self):
        if os.name == 'nt':
            return
        if sys.stdin.isatty() and sys.stdout.isatty():
            rows, columns = os.popen('stty size', 'r').read().split()
            os.environ['COLUMNS'] = columns

    def resolve(self, default_method=None):
        self.fix_terminal_columns()

        raw_method_name = None
        if len(sys.argv) > 1:
            raw_method_name = sys.argv[1]
        method_will_call = self._method_processor.resolve_method_name(raw_method_name, default_method)
        if not method_will_call:
            self.show_help_then_exit()

        self.method_will_call = method_will_call
        self._parse_args()

    def _parse_args(self):
        method_will_call = self.method_will_call
        method_spec = self._method_spec_map[method_will_call]
        kwargs = build_argparser_for_func(method_will_call, method_spec)

        self.base_kwargs = {}
        common_args_keys = set(self.base_method_spec.named_args)
        methods_args_keys = set(method_spec.method_origin_args)
        method_kwargs = {}
        for key, value in kwargs.items():
            if key in common_args_keys:
                self.base_kwargs[key] = value
            if key in methods_args_keys:
                method_kwargs[key] = value
        self.kwargs = method_kwargs

    def run(self):
        # kwargs 含 --a --b 等具名参数及 varargs 的 list（命令行剩余项）。
        # method(a,b,*rest) 需 a,b 作位置、*rest 作可变位置；method(*rest,a,b) 需 *rest 作可变位置、a,b 作 keyword-only。
        # 按 method_origin_args 顺序拆分：before_varargs 按位置传，varargs 按 * 展开，after_varargs 按关键字传。
        kwargs = dict(self.kwargs)
        method_spec = self._method_spec_map[self.method_will_call]
        # origin_args 如 [a,b,rest] 或 [rest,a,b]，保持 Python 签名顺序；varargs_name 如 'rest'、'files'，无则 None
        origin_args = method_spec.method_origin_args
        varargs_name = method_spec.varargs
        idx = origin_args.index(varargs_name) if varargs_name else len(origin_args)
        # before_varargs：varargs 前的参数，必须按位置传（如 method(a,b,*rest) 的 a,b）
        # after_varargs：varargs 后的参数，必须按关键字传（如 method(*rest,a,b) 的 a,b）
        before_varargs = origin_args[:idx]
        after_varargs = origin_args[idx + 1:] if varargs_name else []
        # positional_vals 为前 N 个位置参数；varargs_val 为 *args 的 tuple；keyword_kwargs 为 keyword-only 参数字典
        positional_vals = [kwargs.pop(x) for x in before_varargs]
        varargs_val = tuple(kwargs.pop(varargs_name, [])) if varargs_name else ()
        keyword_kwargs = {x: kwargs.pop(x) for x in after_varargs}
        _app = self.app(**self.base_kwargs)
        func = getattr(_app, self.method_will_call)
        # 按签名顺序调用：位置 + *varargs + keyword-only
        func(*positional_vals, *varargs_val, **keyword_kwargs)

    def show_help_then_exit(self):
        parser = argparse.ArgumentParser()
        parser.allow_abbrev = False
        subparsers = parser.add_subparsers(dest='method_name', metavar='cmd')
        for method_name in self._method_names_by_order:
            help_text = self._method_processor.build_help_for_method(method_name)
            subparsers.add_parser(_to_kebab(method_name), help=help_text, add_help=False)
        parser.print_help()
        if any(arg in ('-h', '--help') for arg in sys.argv[1:]):
            sys.exit(0)
        sys.exit(1)

class _MethodProcessor(object):

    def __init__(self, app, base_method_spec):
        self.app = app
        self.base_method_spec = base_method_spec
        self._method_names_by_order = []
        self._methods_by_name = {}
        self._build_method_index()
        self._method_spec_map = self._get_method_spec_map()

    def get_method_names_by_order(self):
        return list(self._method_names_by_order)

    def get_methods_by_name(self):
        return dict(self._methods_by_name)

    def get_method_spec_map(self):
        return dict(self._method_spec_map)

    def resolve_method_name(self, raw_method_name, default_method):
        method_name = raw_method_name if raw_method_name is not None else default_method
        if method_name is None:
            return None
        method_name = _to_snake(method_name.lstrip('-'))
        if method_name in self._method_spec_map:
            return method_name
        return None

    def _get_method_spec_map(self):
        method_spec_map = {}
        for method_name in self._method_names_by_order:
            method_spec_map[method_name] = self._get_method_spec(method_name)
        return method_spec_map

    def _get_method_spec(self, method_name):
        method_attr = self._methods_by_name[method_name]
        return _MethodSpec(method_attr, self.base_method_spec)

    def build_help_for_method(self, method_name):
        method_spec = self._method_spec_map.get(method_name)
        if not method_spec:
            return ''
        return method_spec.build_help_text()

    def _append_methods_with_order_override(self, new_methods):
        for method_name, method_attr in new_methods.items():
            if method_name in self._methods_by_name:
                self._method_names_by_order.remove(method_name)
            self._method_names_by_order.append(method_name)
            self._methods_by_name[method_name] = method_attr

    def _build_method_index(self):
        # 每次重建都先清空：避免同一个 AppSpec 实例被重复初始化时累计旧状态，保证顺序与映射始终从当前 app 重新计算。
        self._method_names_by_order = []
        self._methods_by_name = {}
        # 组合模式：__proxy_src_cls__ 是当前 App 本体，__proxy_cls_list__ 是 merge_class_for_composition 注入的扩展类列表。
        # 处理顺序固定为“本体 -> 扩展类列表”，同名方法按 _append_methods_with_order_override 规则后者覆盖并后移到末尾。
        if '__proxy_cls_list__' in self.app.__dict__:
            proxy_src_cls = self.app.__dict__['__proxy_src_cls__']
            proxy_cls_list = self.app.__dict__['__proxy_cls_list__']
            self._append_methods_with_order_override(self._get_cls_cli_methods(proxy_src_cls, True))
            for cls in proxy_cls_list:
                self._append_methods_with_order_override(self._get_cls_cli_methods(cls, False))
        else:
            # 普通模式：仅扫描当前 app 类的 CLI 可见方法（按继承过滤规则），无需处理组合覆盖链。
            self._append_methods_with_order_override(self._get_cls_cli_methods(self.app, True))

    def _get_cls_cli_methods(self, cls, exclude_parent=True):
        methods = self._get_cls_public_methods(cls)
        if exclude_parent:
            parent_methods = self._get_cls_parent_methods_for_exclude(cls)
            return {key: value for key, value in methods.items() if key not in parent_methods}
        return methods

    def _get_cls_public_methods(self, cls):
        members = inspect.getmembers(cls, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x))
        return {item[0]: item[1] for item in members if item[0][0:1] != '_'}

    def _get_cls_parent_methods_for_exclude(self, cls):
        methods = {}
        for parent_cls in cls.__bases__:
            if parent_cls.__dict__.get('keep_class_for_cli', False):
                continue
            parent_methods = self._get_cls_public_methods(parent_cls)
            parent_methods = {
                key: val for key, val in parent_methods.items()
                if not val.__dict__.get('keep_method_for_cli', False)
            }
            methods.update(parent_methods)
        return methods

def _convert_value_for_type(value, type):
    default = False if type == 'bool' else None
    strict = type != 'bool'
    return datatypes.convert_scalar_value(value, type, default=default, strict=strict)

def _to_kebab(name):
    return name.replace('_', '-')

def _to_snake(name):
    return name.replace('-', '_')

def _build_main_long_option(name):
    return '--' + _to_kebab(name)

def _build_compat_long_option(name):
    if '_' in name:
        return '--' + name
    return None

def _build_method_help_prog(method_name):
    script_name = Path(sys.argv[0]).name
    return f'{script_name} {_to_kebab(method_name)}'

def _token_has_long_option(token, option):
    return token == option or token.startswith(option + '=')

def _check_mixed_long_option_styles(canonical_names, tokens):
    for name in canonical_names:
        compat_option = _build_compat_long_option(name)
        if compat_option is None:
            continue
        main_option = _build_main_long_option(name)
        has_main = any(_token_has_long_option(token, main_option) for token in tokens)
        has_compat = any(_token_has_long_option(token, compat_option) for token in tokens)
        if has_main and has_compat:
            raise ValueError('mixed option styles for %s: use either %s or %s' % (name, main_option, compat_option))

def _add_option_for_argument(parser, arg_name, metavar, help_text, has_default=False, default=None):
    if len(arg_name) == 1:
        parser.add_argument('-' + arg_name, metavar=metavar, help=help_text, default=default, nargs=1, required=not has_default)
        return
    main_option = _build_main_long_option(arg_name)
    compat_option = _build_compat_long_option(arg_name)
    if compat_option is None:
        parser.add_argument(main_option, dest=arg_name, metavar=metavar, help=help_text, default=default, nargs=1, required=not has_default)
        return
    if not has_default:
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument(main_option, dest=arg_name, metavar=metavar, help=help_text, nargs=1)
        group.add_argument(compat_option, dest=arg_name, metavar=metavar, help=argparse.SUPPRESS, nargs=1)
        return
    parser.add_argument(main_option, dest=arg_name, metavar=metavar, help=help_text, default=default, nargs=1, required=False)
    parser.add_argument(compat_option, dest=arg_name, metavar=metavar, help=argparse.SUPPRESS, default=default, nargs=1, required=False)

def _add_method_placeholder_options(parser, method_name):
    # 兼容解析层：注册并隐藏 --cmd 占位，让 legacy 方法选项被 argparse 消费而不进入 remainder。
    main_option = _build_main_long_option(method_name)
    parser.add_argument(main_option, dest=method_name, action='store_true', help=argparse.SUPPRESS)
    compat_option = _build_compat_long_option(method_name)
    if compat_option is None:
        return
    parser.add_argument(compat_option, dest=method_name, action='store_true', help=argparse.SUPPRESS)

def build_argparser_for_func(method_name, method_spec):
    """根据 method_spec 构建 argparse，解析命令行，返回 kwargs（含 varargs 的 list）。

    参数映射：
    - 具名参数：单字符用 -x，多字符用 --name；有默认值则 optional，否则 required。
    - varargs：不建选项，由 parse_known_args 的 unknown 获取 remainder，作为 list 传入。
    - 子命令兼容占位：添加 --method-name（兼容 --method_name）仅用于兼容解析，不作为主调用方式展示。
    """
    args = list(method_spec.named_args)
    default_values = method_spec.default_values
    varargs_name = method_spec.varargs
    # varargs 不建 --option，从 remainder 获取
    if varargs_name:
        args = [a for a in args if a != varargs_name]

    parser = argparse.ArgumentParser(prog=_build_method_help_prog(method_name))
    # 子命令兼容占位，便于 parse_known_args 识别 legacy --cmd 输入
    _add_method_placeholder_options(parser, method_name)

    # 含 des、type 等，用于 help 与类型转换
    parameters = method_spec.parameters
    check_names = [method_name]
    for arg_name in args:
        parameter = parameters.get(arg_name)
        help = parameter.get('des', '')
        metavar = ''
        # 单字符用 -x；多字符主写法 --arg-name，兼容 --arg_name；nargs=1 统一取单值
        has_default = default_values is not None and arg_name in default_values
        default = default_values[arg_name] if has_default else None
        _add_option_for_argument(parser, arg_name, metavar, help, has_default=has_default, default=default)
        if len(arg_name) > 1:
            check_names.append(arg_name)
    _check_mixed_long_option_styles(check_names, sys.argv[1:])
    # unknown 为未被识别的 token，即 remainder（含 varargs）
    args_input, unknown = parser.parse_known_args()
    kwargs = {}
    for arg_name in args:
        parameter = parameters.get(arg_name)
        attr = getattr(args_input, arg_name)
        raw_value = _get_first_from_list_or_item_self(attr)
        kwargs[arg_name] = _convert_value_for_type(raw_value, parameter['type'])
    if varargs_name:
        remainder = list(unknown)
        # 子命令以词形式传入（如 manager run_tests）时会进 unknown，以选项传入（manager --run_tests）则被消费；若在首位则剔除。
        if remainder and _to_snake(remainder[0].lstrip('-')) == method_name:
            remainder = remainder[1:]
        # 剩余项作为 *args 的 list 传入
        kwargs[varargs_name] = remainder
    return kwargs

def _get_first_from_list_or_item_self(attr):
    # nargs=1 gives list (unwrap to scalar); with default and no arg, argparse may return scalar as-is.
    if isinstance(attr, (list, tuple)):
        return attr[0] if len(attr) > 0 else None
    return attr

def run_app(app, default_method=None, **kwargs):
    if 'base_args_option' in kwargs:
        raise ValueError('base_args_option is removed; put common args in app __init__ signature')
    app_spec = AppSpec(app)
    app_spec.resolve(default_method)
    app_spec.run()
