#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""文档构建相关任务模块。

包含文档清理、构建、国际化、测试等相关任务以及文档站点配置集合创建功能。"""

from pathlib import Path
import sys
import logging
from dataclasses import dataclass, field
from shutil import rmtree
from tempfile import mkdtemp
from typing import List, Dict, Union, overload, Optional
from invoke.tasks import task
from invoke.context import Context
from invoke.collection import Collection

# 导入sphinx_intl模块
import sphinx_intl.basic as sphinx_intl_basic


def _get_sphinx_paths(ctx: Context, source: Optional[str], target: Optional[str]) -> tuple[str, str]:
    """安全地从上下文获取 sphinx 的 source/target 路径，若没有 ctx.sphinx 则使用默认值。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
        source: 源目录路径，如果为 None 则使用配置值或默认值 'doc'。
        target: 目标目录路径，如果为 None 则使用配置值或默认值 'doc/_build/html'。

    Returns:
        tuple[str, str]: 包含 source 和 target 路径的元组。
    """
    sphinx_cfg = getattr(ctx, 'sphinx', None)
    
    # 使用 SphinxConfig 数据类来结构化配置获取过程
    if sphinx_cfg:
        config = SphinxConfig(
            source=getattr(sphinx_cfg, "source", "doc"),
            target=getattr(sphinx_cfg, "target", "doc/_build/html")
        )
    else:
        config = SphinxConfig()
    
    src = source or config.source
    dst = target or config.target
    return str(src), str(dst)


logger = logging.getLogger(__name__)

# 在非 Windows 平台上使用 pty
PTY = sys.platform != 'win32'

# 全局 ctx 变量，用于支持测试用例中的 patch
ctx = None


@dataclass
class SphinxConfig:
    """Sphinx 配置数据类。
    
    用于存储 Sphinx 文档构建的配置信息，包括源代码目录和构建输出目录。
    """
    source: str = "doc"
    target: str = "doc/_build/html"


@dataclass
class ProjectConfig:
    """项目配置数据类。
    
    用于存储单个文档项目的配置信息，包括名称、源代码目录、构建输出目录和子文档目录。
    """
    name: str
    source: str = "doc"
    target: str = "doc/_build/html"
    children: str = ""

@task
def clean(ctx: Context) -> None:
    """清除文档构建目标目录，以便下次构建是干净的。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
    """
    try:
        _, target = _get_sphinx_paths(ctx, None, None)
    except Exception:
        sphinx_cfg = getattr(ctx, "sphinx", None)
        target = getattr(sphinx_cfg, "target", None) if sphinx_cfg is not None else None
        if not target:
            logger.warning("无法确定 sphinx.target，跳过 clean 操作")
            return

    # 确保 target 是字符串类型
    target_str = str(target)
    output = Path(target_str)
    if output.exists():
        logger.info(f'删除 {output}')
        rmtree(output)


@task(default=True)
def build(ctx: Context,
          builder: str = "html",
          opts: str = "",
          language: str | None = None,
          source: str | None = None,
          target: str | None = None,
          nitpick: bool = False,
          jobs: str = "auto",
          keep_going: bool = True) -> None:
    """构建项目的 Sphinx 文档。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
        builder: Sphinx 构建器类型，默认为 'html'。
        opts: Sphinx 构建额外选项/参数。
        language: 文档语言，默认为 None。
        source: 源目录，覆盖配置设置。
        target: 输出目录，覆盖配置设置。
        nitpick: 是否启用更严格的警告/错误检查。
        jobs: 并行作业数，默认为 'auto'。
        keep_going: 遇到错误是否继续构建，默认为 True。
    """
    source, target = _get_sphinx_paths(ctx, source, target)

    if language:
        opts = f"{opts} -D language={language}".strip()
        target = f"{target}/{language}"

    if nitpick:
        opts = f"{opts} -n -W -T".strip()

    opts = f"{opts} -j {jobs}".strip()
    if keep_going:
        opts = f"{opts} --keep-going".strip()

    cmd = f"sphinx-build -b {builder} {opts} {source} {target}"
    logger.info(f"{builder}@{source} => {target}")
    try:
        ctx.run(cmd, pty=PTY)
    except ValueError as e:
        if sys.platform == 'win32' and "closed file" in str(e):
            logger.warning("构建被用户中断")
        else:
            raise
    except KeyboardInterrupt:
        logger.warning("构建被用户中断")
    except OSError as e:
        logger.error(f"文件系统错误: {e}")
        raise
    except Exception as e:
        logger.error(f"构建失败: {e}")
        raise

@task
def intl(ctx: Context, language: str = 'en') -> None:
    """更新 POT 文件并调用 `sphinx-intl update` 命令。

    用于更新多语言文档翻译。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
        language: 目标语言代码，默认为 'en'（英语）。
    """
    opts = "-b gettext"
    _, configured_target = _get_sphinx_paths(ctx, None, None)
    target = Path(configured_target).parent / 'gettext'

    if language == 'en':
        if target.exists():
            rmtree(target)
        build(ctx, target=str(target), opts=opts)
    elif language:
        if not target.exists():
            build(ctx, target=str(target), opts=opts)
        # 使用sphinx_intl.basic.update函数代替命令行调用
        sphinx_intl_basic.update(
            locale_dir='locales',
            pot_dir=str(target),
            languages=(language,),
        )
        # 以下代码已注释掉，因为当前项目可能不需要
        # for DIR in ['pages', 'posts', 'shop']:
        #     rmtree(f'locales/{language}/LC_MESSAGES/{DIR}/')

@task
def doctest(ctx: Context) -> None:
    """运行 Sphinx 的 doctest 构建器进行文档测试。

    这将像测试运行一样，显示测试结果，如果所有测试未通过，则以非零状态退出。
    使用临时目录作为构建目标，因为唯一的输出是自动打印的文本文件。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
    """
    tmpdir = mkdtemp()
    try:
        opts = "-b doctest"
        target = tmpdir
        # 使用临时目录进行 doctest 构建。不要清理常规模板输出目录以避免意外删除用户构建结果。
        logger.info(f"使用临时目录进行 doctest: {tmpdir}")
        build(ctx, target=target, opts=opts)
    finally:
        try:
            rmtree(tmpdir)
        except Exception:
            logger.warning(f"无法删除临时目录 {tmpdir}")

@task
def tree(ctx: Context) -> None:
    """显示文档目录的树形结构。

    Args:
        ctx: Invoke 上下文对象，包含 sphinx 配置信息。
    """
    ignore = ".git|*.pyc|*.swp|dist|*.egg-info|_static|_build|_templates"
    try:
        ctx.run(f'tree -Ca -I "{ignore}" {ctx.sphinx.source}', pty=PTY)
    except Exception:
        sphinx_cfg = getattr(ctx, 'sphinx', None)
        root = Path(getattr(sphinx_cfg, 'source', 'doc') if sphinx_cfg else 'doc')
        for p in sorted(root.rglob("*")):
            rel = p.relative_to(root)
            if any(part in ignore.split("|") for part in rel.parts):
                continue
            print(rel)


# 文档站点配置集合创建函数
# Collection is imported from invoke.collection at the top of the file


@overload
def create_docs(source: str = 'doc', target: str = '.temp/html', children: str = '') -> Collection:
    ...

@overload
def create_docs(source: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]]) -> Collection:
    ...

@overload
def create_docs(source: Union[List[ProjectConfig], Dict[str, ProjectConfig]]) -> Collection:
    ...

def create_docs(
    source: Union[str, List[Dict[str, str]], Dict[str, Dict[str, str]], List[ProjectConfig], Dict[str, ProjectConfig], None] = None,
    target: str = 'doc/_build/html',
    children: str = ''
) -> Collection:
    """创建文档站点配置集合。

    统一的文档站点配置创建函数，支持单个项目和多个项目的配置。

    Args:
        source: 文档源代码目录或项目配置。可以是：
            - 字符串：文档源代码目录，默认为 'doc'
            - 列表：多个项目配置的列表，每个元素是包含 source、target、name 的字典
            - 字典：多个项目配置的字典，键为项目名称，值为包含 source、target 的字典
            - 列表[ProjectConfig]：多个项目配置的数据类列表
            - 字典[str, ProjectConfig]：多个项目配置的数据类字典
            - None：使用默认值 'doc'
        target: 文档构建输出目录，仅在处理单个项目时有效，默认为 '.temp/html'。
        children: 子文档目录，仅在处理单个项目时有效，如果指定则覆盖 source。

    Returns:
        配置好的 Invoke Collection 对象，包含 doc 子命令或多个命名的 doc 子命令集合。

    Raises:
        无直接异常，由底层函数抛出 RemoteConfigError 等。
    """
    def _make_project_namespace(project_config: ProjectConfig) -> Collection:
        """创建项目命名空间。
        
        Args:
            project_config: 项目配置数据类。
            
        Returns:
            配置好的 Invoke Collection 对象。
        """
        project_namespace = Collection(project_config.name)
        # 使用 Collection.from_module 确保传入的是一个 Collection 对象（而不是 module）
        project_namespace.add_collection(Collection.from_module(sys.modules[__name__]))
        actual_source = project_config.children if project_config.children else project_config.source
        project_config_dict = {
            "sphinx": {
                "source": actual_source,
                "target": f"{project_config.target}/{project_config.children}" if project_config.children else project_config.target
            }
        }
        project_namespace.collections[__name__.split('.')[-1]].configure(project_config_dict)  # type: ignore[attr-defined]
        return project_namespace

    # 处理单个项目的情况
    if source is None or isinstance(source, str):
        # 确保 source 有值
        actual_source_str = source if source is not None else 'doc'
        actual_source = children if children else actual_source_str

        # 创建并配置命令集合（与以前保持行为一致）
        _config = {
            "sphinx": {
                "source": actual_source,
                "target": f"{target}/{children}" if children else target
            }
        }

        namespace = Collection()
        namespace.add_collection(Collection.from_module(sys.modules[__name__]))
        namespace.collections[__name__.split('.')[-1]].configure(_config)  # type: ignore[attr-defined]

        return namespace

    # 处理多个项目的情况（列表或字典）
    project_configs = source  # type: ignore
    main_namespace = Collection()

    if isinstance(project_configs, list):
        # 列表形式：可以是字典列表或 ProjectConfig 列表
        for i, config in enumerate(project_configs):
            if isinstance(config, dict):
                # 从字典创建 ProjectConfig 对象
                name = config.get('name', f'doc_{i+1}')
                proj_source = config.get('source', 'doc')
                proj_target = config.get('target', f'doc/_build/html/{name}')
                proj_children = config.get('children', '')
                project_config = ProjectConfig(name=name, source=proj_source, target=proj_target, children=proj_children)
            else:
                # 已经是 ProjectConfig 对象，添加类型断言
                project_config = config  # type: ignore[assignment]
            
            project_namespace = _make_project_namespace(project_config)
            main_namespace.add_collection(project_namespace)
    else:
        # 字典形式：键为项目名称
        for name, config in project_configs.items():
            if isinstance(config, dict):
                # 从字典创建 ProjectConfig 对象
                proj_source = config.get('source', 'doc')
                proj_target = config.get('target', f'doc/_build/html/{name}')
                proj_children = config.get('children', '')
                project_config = ProjectConfig(name=name, source=proj_source, target=proj_target, children=proj_children)
            else:
                # 已经是 ProjectConfig 对象，更新名称
                project_config = config  # type: ignore[assignment]
                project_config.name = name
            
            project_namespace = _make_project_namespace(project_config)
            main_namespace.add_collection(project_namespace)

    return main_namespace


def sites(source: str = 'doc', target: str = 'doc/_build/html', children: str = '') -> Collection:
    """创建文档站点配置集合。

    为不同的文档站点创建配置好的 Invoke 集合，用于构建 Sphinx 文档。

    Args:
        source: 文档源代码目录，默认为 'doc'。
        target: 文档构建输出目录，默认为 '.temp/html'。
        children: 子文档目录，如果指定则覆盖 source。

    Returns:
        配置好的 Invoke Collection 对象，包含 doc 子命令。
    """
    return create_docs(source, target, children)


def multi_sites(project_configs: Union[List[Dict[str, str]], Dict[str, Dict[str, str]]]) -> Collection:
    """创建多文档站点配置集合。

    支持同时对多个 doc 项目进行构建的功能。

    Args:
        project_configs: 项目配置列表或字典。可以是：
            - 列表形式：每个元素是包含 source、target、name 的字典
            - 字典形式：键为项目名称，值为包含 source、target 的字典

    Returns:
        配置好的 Invoke Collection 对象，包含多个命名的 doc 子命令集合。
    """
    return create_docs(project_configs)  # type: ignore
