import typing
import argparse
import sys
from typing import Any, Callable, Dict, List, Tuple
from pathlib import Path
from html import escape
import yaml

# from zyplib.utils.fs import write_json
from zyplib.utils.print import print_error, print_info, print_warning


def str2bool(v):
    """Convert a string to a boolean value for argparse."""
    if isinstance(v, bool):
        return v
    if v.lower() in ('yes', 'true', 't', '1', 'y'):
        return True
    if v.lower() in ('no', 'false', 'f', '0', 'n'):
        return False
    raise argparse.ArgumentTypeError('Boolean value expected.')


class CommandRegistry:
    """Advanced CLI command registry with argparse integration."""

    def __init__(self):
        self.commands: Dict[str, Dict[str, Any]] = {}

    def register_command(
        self,
        name: str,
        help: str | None = None,
        args: typing.Sequence[Tuple[str, typing.Mapping]] | None = None,
    ):
        """Register a CLI command with its parameters.

        Parameters
        ----------
        - `name` : `str`
            - Name of the command to register
        - `help` : `str`, optional
            - Help text for the commandF
        - `args` : `List[Tuple[str, Dict]]`, optional
            - List of argument specifications for argparse

        Returns
        ----------
        - `callable`
            - Decorator function that registers the command
        """

        def decorator(func):
            self.commands[name] = {
                'func': func,
                'help': help
                or (func.__doc__.strip().split('\n')[0] if func.__doc__ else ''),
                'args': args or [],
            }
            return func

        return decorator

    def get_command(self, name: str) -> Callable | None:
        """Get a registered command function by name."""
        command_info = self.commands.get(name)
        return command_info['func'] if command_info else None

    def list_commands(self) -> Dict[str, Callable]:
        """List all registered command functions."""
        return {name: info['func'] for name, info in self.commands.items()}

    def build_parsers(self, subparsers):
        """Build argparse subparsers for all registered commands.

        Parameters
        ----------
        - `subparsers` : `argparse._SubParsersAction`
            - The subparsers object from argparse
        """
        for name, info in self.commands.items():
            parser = subparsers.add_parser(name, help=info['help'])

            # Add arguments for this command
            for arg_spec in info['args']:
                if len(arg_spec) == 2:
                    arg_name, arg_kwargs = arg_spec
                    parser.add_argument(arg_name, **arg_kwargs)

            # Set the function to call for this command
            parser.set_defaults(func=info['func'])


# Global registry instance
registry = CommandRegistry()


def _get_skill_root() -> Path:
    """获取内置 SKILL 目录路径。"""
    return Path(__file__).resolve().parent / '__SKILL__'


def _extract_front_matter_text(text: str) -> str | None:
    """提取 front matter 区块文本（不含分隔符）。"""
    bom = '\ufeff'
    if text.startswith(bom):
        text = text[len(bom) :]
    text = text.strip()
    lines = text.splitlines()
    if len(lines) < 3 or lines[0].strip() != '---':
        return None

    for idx in range(1, len(lines)):
        if lines[idx].strip() == '---':
            return '\n'.join(lines[1:idx]).strip()

    return None


def _parse_skill_front_matter(skill_file: Path) -> tuple[str, str]:
    """解析 SKILL.md 的 front matter，提取名称与描述。"""
    try:
        text = skill_file.read_text(encoding='utf-8')
    except Exception:
        return skill_file.parent.name, '读取失败'

    front_matter_text = _extract_front_matter_text(text)
    if not front_matter_text:
        return skill_file.parent.name, '无描述'

    try:
        loaded = yaml.safe_load(front_matter_text)
    except Exception:
        loaded = None

    if not isinstance(loaded, dict):
        return skill_file.parent.name, '无描述'

    name = str(loaded.get('name', skill_file.parent.name))
    desc = str(loaded.get('description', '无描述'))
    return name, desc


def _collect_skills() -> list[dict[str, str]]:
    """扫描 __SKILL__ 目录并收集技能信息。"""
    skill_root = _get_skill_root()
    if not skill_root.exists():
        return []

    skills: list[dict[str, str]] = []
    for skill_file in sorted(skill_root.rglob('SKILL.md')):
        name, description = _parse_skill_front_matter(skill_file)
        skills.append(
            {
                'name': name,
                'description': description,
                'path': str(skill_file),
                'folder': skill_file.parent.name,
            }
        )

    return skills


def _extract_skill_body(text: str) -> str:
    """提取 SKILL.md 正文（去除 front matter）。"""
    lines = text.splitlines()
    if len(lines) < 3 or lines[0].strip() != '---':
        return text.strip()

    for idx in range(1, len(lines)):
        if lines[idx].strip() == '---':
            return '\n'.join(lines[idx + 1 :]).strip()

    return text.strip()


def _collect_skill_resources(skill_file: Path) -> list[str]:
    """收集技能目录下可用资源文件（相对路径）。"""
    skill_dir = skill_file.parent
    resources: list[str] = []
    for file_path in sorted(skill_dir.rglob('*')):
        if not file_path.is_file():
            continue
        if file_path.name == 'SKILL.md':
            continue
        resources.append(file_path.relative_to(skill_dir).as_posix())
    return resources


def register_command(
    name: str,
    help: str | None = None,
    args: typing.Sequence[Tuple[str, typing.Mapping]] | None = None,
):
    """Register a CLI command with its parameters.

    Parameters
    ----------
    - `name` : `str`
        - Name of the command to register
    - `help` : `str`, optional
        - Help text for the command
    - `args` : `List[Tuple[str, Dict]]`, optional
        - List of argument specifications for argparse

    Returns
    ----------
    - `callable`
        - Decorator function that registers the command
    """
    return registry.register_command(name, help, args)


@register_command('list_skills', help='列出 zyplib 内置 SKILL')
def list_skills(args):
    """列出 zyplib 内置 SKILL

    扫描 zyplib/__SKILL__ 目录下的所有 SKILL.md，并展示技能名称与描述。

    Returns
    ----------
    - `None`
        - 打印技能列表到控制台
    """
    skills = _collect_skills()
    if not skills:
        print_warning('未找到可用 SKILL，请检查 zyplib/__SKILL__ 目录')
        return

    fn_spaces = lambda level: '  ' * level

    lines = ['<available_skills>']
    for item in skills:
        lines.append(f'{fn_spaces(1)}<skill>')
        lines.append(f'{fn_spaces(2)}<name>{(item["name"])}</name>')
        lines.append(
            f'{fn_spaces(2)}<description>\n{fn_spaces(3)}{(item["description"])}\n{fn_spaces(2)}</description>'
        )
        lines.append(f'{fn_spaces(2)}<location>{(item["path"])}</location>')
        lines.append(f'{fn_spaces(1)}</skill>')
    lines.append('</available_skills>')
    lines.append(
        'Run `python -m zyplib.cli read_skills --name <skill_name>` to view details of a skill.'
    )

    print('\n'.join(lines))


@register_command(
    'read_skills',
    help='读取指定 SKILL 的内容',
    args=[
        (
            '--name',
            {
                'type': str,
                'required': True,
                'help': 'SKILL 名称或目录名称（可先通过 list_skills 查看）',
            },
        )
    ],
)
def read_skills(args):
    """读取指定 SKILL 的内容

    根据技能名或目录名定位 SKILL.md，并打印完整内容。

    Parameters
    ----------
    - `name` : `str`
        - SKILL 名称或目录名称

    Returns
    ----------
    - `None`
        - 打印 SKILL 文件内容到控制台
    """
    skills = _collect_skills()
    if not skills:
        print_warning('未找到可用 SKILL，请先确认 zyplib/__SKILL__ 目录')
        return

    needle = args.name.strip().lower()
    target = next(
        (
            item
            for item in skills
            if item['name'].lower() == needle or item['folder'].lower() == needle
        ),
        None,
    )

    if target is None:
        print_error(f'未找到 SKILL: {args.name}')
        print_info('可用 SKILL:')
        for item in skills:
            print(f"- {item['name']} ({item['folder']})")
        return

    try:
        skill_file = Path(target['path'])
        content = skill_file.read_text(encoding='utf-8')
    except Exception as e:
        print_error(f'读取 SKILL 失败: {e}')
        return

    body = _extract_skill_body(content)
    skill_dir = skill_file.parent
    resources = _collect_skill_resources(skill_file)

    lines = [f'<skill_content name="{escape(target["name"])}">']
    if body:
        lines.append(body)
    lines.append('')
    lines.append('---')
    lines.append('')
    lines.append(f'Skill directory: {skill_dir}')
    lines.append('Relative paths in this skill are relative to the skill directory.')
    lines.append('')
    lines.append('<skill_resources>')
    for resource in resources:
        lines.append(f'  <file>{escape(resource)}</file>')
    lines.append('</skill_resources>')
    lines.append('</skill_content>')

    print('\n'.join(lines))


@register_command(
    'config-file',
    help='管理 zyplib 配置文件',
    args=[
        ('--reset', {'action': 'store_true', 'help': '重置配置文件为默认值'}),
        ('--update', {'action': 'store_true', 'help': '更新配置文件，添加缺失的字段'}),
        (
            '--target',
            {
                'type': str,
                'choices': ['global', 'local'],
                'default': 'local',
                'help': '操作目标：global (全局配置) 或 local (本地配置), 默认为 local',
            },
        ),
    ],
)
def manage_config_file(args):
    """管理 zyplib 配置文件

    管理配置文件，支持重置为默认值或更新缺失字段。

    Parameters
    ----------
    - `reset` : `bool`, optional
        - 重置配置文件为默认值, by default False
    - `update` : `bool`, optional
        - 更新配置文件，添加缺失的字段, by default False
    - `target` : `str`, optional
        - 操作目标，'global' 或 'local', by default 'local'

    Returns
    ----------
    - `None`
        - 根据参数创建或更新配置文件

    Examples
    ----------
    ```bash
    # 创建/重置本地配置文件
    python -m zyplib.cli config-file --reset

    # 更新全局配置文件（添加缺失字段）
    python -m zyplib.cli config-file --update --target global

    # 重置全局配置文件
    python -m zyplib.cli config-file --reset --target global
    ```
    """
    from . import _config

    # 如果没有指定任何操作，默认为更新
    if not args.reset and not args.update:
        args.update = True

    if args.reset and args.update:
        print_error('--reset 和 --update 不能同时使用')
        return

    if args.reset:
        _config.reset_zyplib_envfile(args.target)
        print_info(f'已重置 {args.target} 配置文件为默认值')
    elif args.update:
        updated = _config.update_zyplib_envfile(args.target)
        if not updated:
            print_info(f'{args.target.capitalize()} 配置文件已是最新版本')


@register_command('show-config', help='显示当前配置')
def show_config(args):
    """显示当前配置

    打印当前生效的全局配置信息。

    Returns
    ----------
    - `None`
        - 打印配置信息到控制台
    """
    from . import _config

    print_info(_config.config.as_dict())


@register_command(
    'show-caches',
    help='显示当前缓存',
    args=[
        ('--dir', {'type': str, 'default': None, 'help': '指定缓存目录（可选）'}),
        (
            '--table',
            {
                'type': str2bool,
                'default': True,
                'help': '是否以表格形式显示 (true/false)',
            },
        ),
    ],
)
def show_caches(args):
    """显示当前缓存

    显示系统中的缓存信息。

    Parameters
    ----------
    - `table` : `bool`, optional
        - 是否以表格形式显示, by default True
    - `dir` : `str`, optional
        - 指定缓存目录, by default None

    Returns
    ----------
    - `None`
        - 打印缓存信息到控制台
    """
    from zyplib.utils.cache import display_caches

    display_caches(cache_dir=args.dir, table=args.table)


@register_command(
    'viz-eeg',
    help='可视化脑电信号并标注',
    args=[
        ('--path', {'type': str, 'help': '脑电信号数据文件路径'}),
        ('--srate', {'type': int, 'help': '采样率'}),
    ],
)
def visualize_and_annotate(args):
    """可视化脑电信号并标注

    启动脑电信号可视化和标注工具。

    Parameters
    ----------
    - `path` : `str`, optional
        - 脑电信号数据文件路径
    - `srate` : `int`, optional
        - 采样率

    Returns
    ----------
    - `None`
        - 启动可视化标注工具
    """
    from zyplib.apps.annotators import launch_app

    launch_app(fpath=args.path, srate=args.srate)


@register_command(
    'inspect-keyval',
    help='在目录中递归查看所有 key-value 文件中指定 key 的取值',
    args=[
        ('--dir', {'type': str, 'required': True, 'help': '要搜索的目录路径'}),
        ('--key', {'type': str, 'required': True, 'help': '要查找的键名'}),
    ],
)
def inspect_key_value(args):
    """在目录中递归查看所有 YAML/JSON 等 key-value 文件中指定 key 名称的取值

    递归搜索指定目录中的所有键值文件，查找特定键的值并以表格形式展示。

    Parameters
    ----------
    - `dir` : `str`
        - 要搜索的目录路径
    - `key` : `str`
        - 要查找的键名

    Returns
    ----------
    - `None`
        - 以表格形式打印查找结果

    Examples
    ----------
    ```bash
    python -m zyplib.cli inspect-keyval --dir "./results" --key "score"
    ```
    """
    from prettytable import PrettyTable

    from zyplib.utils.kv import collect_key_path

    try:
        # 搜索文件并获取结果
        results = collect_key_path(args.dir, args.key)

        # 展示结果
        for filename, key_paths in results.items():
            table = PrettyTable(['Path', '取值'])
            for k, v in key_paths.items():
                table.add_row([k, v])

            if len(table.rows) > 0:
                print_info(f'{filename}'.center(100, '='))
                print(table)
    except Exception as e:
        print_error(f'执行命令时发生错误: {e}')


@register_command(
    'help',
    help='显示命令帮助信息',
    args=[('--command', {'type': str, 'help': '要显示详细帮助的命令名称'})],
)
def show_help(args):
    """显示命令帮助信息

    显示所有可用命令的列表，或特定命令的详细信息。

    Parameters
    ----------
    - `command` : `str`, optional
        - 要显示详细帮助的命令名称

    Returns
    ----------
    - `None`
        - 打印帮助信息到控制台

    Examples
    ----------
    ```bash
    python -m zyplib.cli help
    python -m zyplib.cli help --command "inspect-keyval"
    ```
    """
    command_name = args.command

    if command_name:
        command_func = registry.get_command(command_name)
        if command_func:
            print_info(f'命令: {command_name}')
            print(command_func.__doc__ or '无详细描述')
        else:
            print_error(f'未知的命令: {command_name}')
            print(help_doc())
    else:
        print(help_doc())


def help_doc():
    """Generate help documentation for all registered commands.

    Returns
    ----------
    - `str`
        - Formatted help text containing all command descriptions
    """
    if not registry.list_commands():
        return '没有可用的命令。'
    from zyplib.utils.envs import get_system_fingerprint

    system = get_system_fingerprint()
    docs = [f'当前系统 ID: {system}', '可用命令:']
    for command, func in registry.list_commands().items():
        # 提取docstring的第一行作为简短描述
        doc = func.__doc__ or '无描述'
        first_line = doc.strip().split('\n')[0]
        docs.append(f'  {command:20} - {first_line}')

    docs.append("\n使用 'python -m zyplib.cli <command>' 运行命令")
    docs.append('使用 --help 查看具体命令的参数格式')
    return '\n'.join(docs)


def main():
    """Main entry point for the CLI application.

    Parses command line arguments and executes the appropriate command.
    """
    parser = argparse.ArgumentParser(
        description='ZypLib CLI tool',
        epilog=help_doc(),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    # Create subparsers for commands
    subparsers = parser.add_subparsers(dest='command', help='可用的子命令')

    # Build all command parsers from registry
    registry.build_parsers(subparsers)

    args = parser.parse_args()

    # Execute the command
    if hasattr(args, 'func'):
        args.func(args)
    else:
        print_warning('请指定一个命令')
        print(help_doc())

    sys.exit(0)


if __name__ == '__main__':
    main()
