"""
概要:
    各種ファイル形式の変換処理を統一的に管理・実行するスクリプトです。
詳細説明:
    Word、Excel、PowerPoint、MarkdownなどのファイルをPDFなどの目的の形式に変換します。
    変換器はレジストリに登録され、ディレクトリ内の再帰的な一括変換や単一ファイルの変換をサポートします。
"""
import os
import sys
import fnmatch
import argparse
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Tuple, Any
import tempfile
import shutil


OUTPUT_MODE_FILE = "file"
OUTPUT_MODE_DIR = "dir"


@dataclass
class ConverterSpec:
    """
    概要:
        変換器の仕様を定義するデータクラスです。
    詳細説明:
        入力と出力の拡張子、実行する変換関数、およびその他のメタデータを保持します。
    引数:
        :param input_ext: 入力ファイルの拡張子
        :type input_ext: str
        :param output_ext: 出力ファイルの拡張子
        :type output_ext: str
        :param converter: 変換処理を実行する関数
        :type converter: Callable
        :param description: 変換器の説明文
        :type description: str
        :param ignore_temp_prefixes: 無視する一時ファイルの接頭辞のタプル
        :type ignore_temp_prefixes: tuple
        :param output_mode: 出力モードを示す文字列
        :type output_mode: str
        :param options: 変換器に渡す追加オプションの辞書
        :type options: dict
    """
    input_ext: str
    output_ext: str
    converter: Callable[..., Any]
    description: str = ""
    ignore_temp_prefixes: Tuple[str, ...] = ()
    output_mode: str = OUTPUT_MODE_FILE
    options: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        """
        概要:
            インスタンス生成後の初期化処理を行います。
        詳細説明:
            拡張子を正規化し、出力モードの値が正しいか検証します。
        例外:
            :raises ValueError: 無効な output_mode が指定された場合
        """
        self.input_ext = normalize_ext(self.input_ext)
        self.output_ext = normalize_ext(self.output_ext)
        if self.output_mode not in (OUTPUT_MODE_FILE, OUTPUT_MODE_DIR):
            raise ValueError(f"Invalid output_mode: {self.output_mode}")


class ConverterRegistry:
    """
    概要:
        変換器を管理するレジストリクラスです。
    詳細説明:
        入出力の拡張子に応じた適切な変換器を登録および取得する機能を提供します。
    """
    def __init__(self):
        """
        概要:
            レジストリの初期化を行います。
        """
        self._specs: Dict[Tuple[str, str], ConverterSpec] = {}
        self._import_errors: Dict[str, Exception] = {}

    def register(self, spec: ConverterSpec):
        """
        概要:
            変換器の仕様をレジストリに登録します。
        引数:
            :param spec: 登録する変換器の仕様
            :type spec: ConverterSpec
        """
        key = (spec.input_ext, spec.output_ext)
        self._specs[key] = spec

    def set_import_error(self, module_name: str, error: Exception):
        """
        概要:
            モジュールのインポート時に発生したエラーを記録します。
        引数:
            :param module_name: インポートに失敗したモジュール名
            :type module_name: str
            :param error: 発生した例外オブジェクト
            :type error: Exception
        """
        self._import_errors[module_name] = error

    def get(self, input_ext: str, output_ext: str) -> Optional[ConverterSpec]:
        """
        概要:
            指定された拡張子の組み合わせに対応する変換器を取得します。
        引数:
            :param input_ext: 入力ファイルの拡張子
            :type input_ext: str
            :param output_ext: 出力ファイルの拡張子
            :type output_ext: str
        戻り値:
            :returns: 変換器の仕様オブジェクト、見つからない場合はNone
            :rtype: ConverterSpec
        """
        return self._specs.get((normalize_ext(input_ext), normalize_ext(output_ext)))

    def has(self, input_ext: str, output_ext: str) -> bool:
        """
        概要:
            指定された拡張子の組み合わせに対応する変換器が存在するか判定します。
        引数:
            :param input_ext: 入力ファイルの拡張子
            :type input_ext: str
            :param output_ext: 出力ファイルの拡張子
            :type output_ext: str
        戻り値:
            :returns: 変換器が存在する場合はTrue、それ以外はFalse
            :rtype: bool
        """
        return self.get(input_ext, output_ext) is not None

    def list_specs(self):
        """
        概要:
            登録されているすべての変換器の仕様をリストとして返します。
        戻り値:
            :returns: 変換器仕様オブジェクトのリスト
            :rtype: list
        """
        return list(self._specs.values())

    def list_output_exts(self):
        """
        概要:
            登録されているすべての出力拡張子をソートされたリストとして返します。
        戻り値:
            :returns: 出力拡張子のリスト
            :rtype: list
        """
        return sorted({spec.output_ext for spec in self._specs.values()})

    def print_available_converters(self):
        """
        概要:
            利用可能な変換器の一覧と、利用できないモジュールを標準出力に表示します。
        """
        print("Available converters:")
        for spec in sorted(self._specs.values(), key=lambda s: (s.output_ext, s.input_ext, s.output_mode)):
            desc = f" ({spec.description})" if spec.description else ""
            mode = f" [{spec.output_mode}]" if spec.output_mode != OUTPUT_MODE_FILE else ""
            print(f"  {spec.input_ext} -> {spec.output_ext}{mode}{desc}")

        if self._import_errors:
            print("\nUnavailable modules:")
            for module_name, err in self._import_errors.items():
                print(f"  {module_name}: {err}")


def normalize_ext(ext: str) -> str:
    """
    概要:
        拡張子を正規化します。
    詳細説明:
        小文字に変換し、先頭にピリオドがない場合は付与します。
    引数:
        :param ext: 元の拡張子
        :type ext: str
    戻り値:
        :returns: 正規化された拡張子
        :rtype: str
    """
    ext = ext.strip().lower()
    if not ext.startswith("."):
        ext = "." + ext
    return ext


def build_output_path(input_path: str, output_ext: str, output_mode: str = OUTPUT_MODE_FILE) -> str:
    """
    概要:
        出力先のファイルまたはディレクトリパスを構築します。
    詳細説明:
        出力モードがディレクトリの場合は拡張子に s を付与した名前を返します。
    引数:
        :param input_path: 入力ファイルのパス
        :type input_path: str
        :param output_ext: 対象の出力拡張子
        :type output_ext: str
        :param output_mode: 出力モード
        :type output_mode: str
    戻り値:
        :returns: 構築された出力パス
        :rtype: str
    """
    stem = os.path.splitext(input_path)[0]
    if output_mode == OUTPUT_MODE_DIR:
        return stem + normalize_ext(output_ext) + "s"
    return stem + normalize_ext(output_ext)


def should_ignore_file(filename: str, spec: ConverterSpec) -> bool:
    """
    概要:
        ファイルが一時ファイルとして無視すべき対象かどうかを判定します。
    引数:
        :param filename: 判定するファイル名
        :type filename: str
        :param spec: 適用する変換器の仕様
        :type spec: ConverterSpec
    戻り値:
        :returns: 無視すべきファイルの場合はTrue、それ以外はFalse
        :rtype: bool
    """
    return any(filename.startswith(prefix) for prefix in spec.ignore_temp_prefixes)


def path_exists_for_mode(path: str, output_mode: str) -> bool:
    """
    概要:
        指定された出力モードにおいてパスが有効に存在するかを確認します。
    詳細説明:
        ディレクトリモードの場合はディレクトリが存在し、かつ空でないことを確認します。
    引数:
        :param path: 確認するパス
        :type path: str
        :param output_mode: 出力モード
        :type output_mode: str
    戻り値:
        :returns: パスが存在し条件を満たす場合はTrue、それ以外はFalse
        :rtype: bool
    """
    if output_mode == OUTPUT_MODE_DIR:
        return os.path.isdir(path) and any(True for _ in os.scandir(path))
    return os.path.exists(path)


def get_latest_mtime(path: str, output_mode: str) -> Optional[float]:
    """
    概要:
        指定されたパスの最新の更新日時を取得します。
    詳細説明:
        ディレクトリモードの場合は再帰的にファイルを走査し、最も新しい更新日時を返します。
    引数:
        :param path: 対象のパス
        :type path: str
        :param output_mode: 出力モード
        :type output_mode: str
    戻り値:
        :returns: 最新の更新日時を表すタイムスタンプ、存在しない場合はNone
        :rtype: float
    """
    if output_mode == OUTPUT_MODE_FILE:
        if not os.path.exists(path):
            return None
        return os.path.getmtime(path)

    if not os.path.isdir(path):
        return None

    latest = os.path.getmtime(path)
    found_any = False
    for dirpath, dirnames, filenames in os.walk(path):
        for name in dirnames + filenames:
            found_any = True
            candidate = os.path.join(dirpath, name)
            try:
                latest = max(latest, os.path.getmtime(candidate))
            except OSError:
                pass
    return latest if found_any else None


def should_convert(input_path: str, output_path: str, output_mode: str, update: bool, overwrite: bool) -> Tuple[bool, str]:
    """
    概要:
        変換を実行すべきかどうかとその理由を判定します。
    詳細説明:
        上書き設定やファイルの更新日時を比較して、変換の要否を決定します。
    引数:
        :param input_path: 入力ファイルのパス
        :type input_path: str
        :param output_path: 出力ファイルのパス
        :type output_path: str
        :param output_mode: 出力モード
        :type output_mode: str
        :param update: 更新のみ行うかどうかのフラグ
        :type update: bool
        :param overwrite: 強制的に上書きするかどうかのフラグ
        :type overwrite: bool
    戻り値:
        :returns: 変換実行の要否と判定理由の文字列のタプル
        :rtype: tuple
    """
    if overwrite:
        return True, "overwrite=1"

    exists = path_exists_for_mode(output_path, output_mode)
    if not exists:
        return True, "output does not exist"

    if not update:
        return False, "output exists and update=0"

    output_mtime = get_latest_mtime(output_path, output_mode)
    if output_mtime is None:
        return True, "output exists but is empty/incomplete"

    input_mtime = os.path.getmtime(input_path)
    if input_mtime > output_mtime:
        return True, "source is newer than output"

    return False, "up to date"


def parse_target_patterns(target_text: Optional[str]) -> Optional[List[str]]:
    """
    概要:
        セミコロン区切りの対象ファイルパターンの文字列をリストにパースします。
    引数:
        :param target_text: 対象パターンの文字列
        :type target_text: str
    戻り値:
        :returns: パースされたパターンのリスト、入力が空の場合はNone
        :rtype: list
    """
    if not target_text:
        return None
    patterns = [p.strip() for p in target_text.split(";") if p.strip()]
    return patterns or None


def matches_target(filename: str, target_patterns: Optional[List[str]]) -> bool:
    """
    概要:
        ファイル名が対象パターンのいずれかに一致するか判定します。
    引数:
        :param filename: 判定するファイル名
        :type filename: str
        :param target_patterns: 対象パターンのリスト
        :type target_patterns: list
    戻り値:
        :returns: 一致する場合はTrue、それ以外はFalse
        :rtype: bool
    """
    if not target_patterns:
        return True

    lower_name = filename.lower()
    for pattern in target_patterns:
        if fnmatch.fnmatch(lower_name, pattern.lower()):
            return True
    return False


# ---------- importable wrappers ----------
def wrap_simple(converter: Callable[[str, Optional[str]], Any]) -> Callable[..., bool]:
    """
    概要:
        シンプルな変換関数を標準的なインターフェースでラップします。
    引数:
        :param converter: 元の変換関数
        :type converter: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        result = converter(input_path, output_path)
        return bool(result is not None)
    return _wrapped


def wrap_image_to_dir(converter: Callable[..., Any], rename_func: Optional[Callable[[str], Any]] = None) -> Callable[..., bool]:
    """
    概要:
        画像をディレクトリに出力する変換関数をラップします。
    引数:
        :param converter: 元の変換関数
        :type converter: Callable
        :param rename_func: 出力後のリネーム処理を行う関数
        :type rename_func: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        os.makedirs(output_path, exist_ok=True)
        converter(input_path, output_path, "png")
        if rename_func is not None:
            rename_func(output_path)
        return True
    return _wrapped


def wrap_docx_to_png(docx_to_pdf: Callable[[str, str], Any], pdf_to_images: Callable[..., Any]) -> Callable[..., bool]:
    """
    概要:
        WordファイルをPDF経由でPNG画像のディレクトリに変換する処理をラップします。
    引数:
        :param docx_to_pdf: WordからPDFへの変換関数
        :type docx_to_pdf: Callable
        :param pdf_to_images: PDFから画像への変換関数
        :type pdf_to_images: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        os.makedirs(output_path, exist_ok=True)
        tmp_pdf = os.path.splitext(input_path)[0] + ".pdf"
        docx_to_pdf(input_path, tmp_pdf)
        pdf_to_images(tmp_pdf, output_path, "png")
        return True
    return _wrapped


def wrap_md_with_images(convert_func: Callable[..., Any]) -> Callable[..., bool]:
    """
    概要:
        画像ディレクトリを伴うMarkdown変換関数をラップします。
    引数:
        :param convert_func: 元の変換関数
        :type convert_func: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        image_dir = os.path.splitext(output_path)[0] + ".images"
        result = convert_func(input_path, output_path, image_dir)
        return bool(result is not None or os.path.exists(output_path))
    return _wrapped


def wrap_pdf2md(convert_func: Callable[[str, Optional[str]], Any]) -> Callable[..., bool]:
    """
    概要:
        PDFからMarkdownへの変換関数をラップします。
    引数:
        :param convert_func: 元の変換関数
        :type convert_func: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        result = convert_func(input_path, output_path)
        return bool(result is not None or os.path.exists(output_path))
    return _wrapped


def wrap_pandoc(target: str, default_template: Optional[str] = None) -> Callable[..., bool]:
    """
    概要:
        Pandocを使用した変換関数をラップします。
    引数:
        :param target: 変換先のフォーマット指定
        :type target: str
        :param default_template: デフォルトのテンプレートパス
        :type default_template: str
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        from pandoc import convert_md, find_template

        pandoc_path = kwargs.get("pandoc_path") or "pandoc"
        template = kwargs.get("template") or default_template
        if template:
            template = find_template(template)

        return bool(convert_md(
            infile_md=input_path,
            target=target,
            pandoc_path=pandoc_path,
            outfile=output_path,
            template=template,
            toc=bool(kwargs.get("toc", 0)),
            css=kwargs.get("css"),
            mathml=bool(kwargs.get("mathml", False)),
            no_yaml=bool(kwargs.get("no_yaml", False)),
            verbose=bool(kwargs.get("verbose", False)),
            smart_conversion=bool(kwargs.get("smart_conversion", False)),
        ))
    return _wrapped


def wrap_ipynb_json_to_md(convert_func: Callable[..., Any]) -> Callable[..., bool]:
    """
    概要:
        Jupyter NotebookまたはJSONファイルからMarkdownへの変換関数をラップします。
    引数:
        :param convert_func: 元の変換関数
        :type convert_func: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        result = convert_func(input_path, output_path)
        return bool(result is not None or os.path.exists(output_path))
    return _wrapped


def wrap_ipynb_json_to_pdf(ipynb_or_json_to_md: Callable[..., Any], md_to_pdf: Callable[..., Any]) -> Callable[..., bool]:
    """
    概要:
        Jupyter NotebookまたはJSONファイルをMarkdown経由でPDFに変換する関数をラップします。
    詳細説明:
        一時ディレクトリを作成し、Markdownへ変換したのちPDFへの変換を実行します。
    引数:
        :param ipynb_or_json_to_md: Markdownへの変換関数
        :type ipynb_or_json_to_md: Callable
        :param md_to_pdf: MarkdownからPDFへの変換関数
        :type md_to_pdf: Callable
    戻り値:
        :returns: ラップされた関数
        :rtype: Callable
    """
    def _wrapped(input_path: str, output_path: str, **kwargs) -> bool:
        tmp_dir = tempfile.mkdtemp(prefix="convert_pipeline_")
        tmp_md = os.path.join(tmp_dir, os.path.splitext(os.path.basename(input_path))[0] + ".md")
        try:
            md_result = ipynb_or_json_to_md(input_path, tmp_md)
            if md_result is False or not os.path.exists(tmp_md):
                return False
            pdf_result = md_to_pdf(tmp_md, output_path)
            return bool(pdf_result is not None or os.path.exists(output_path))
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)
    return _wrapped


# ---------- registry ----------
def safe_import_registry(registry: ConverterRegistry):
    """
    概要:
        利用可能な各種モジュールを安全にインポートし、レジストリに登録します。
    詳細説明:
        インポートに失敗した場合はエラー情報をレジストリに記録し、処理を続行します。
    引数:
        :param registry: 登録先のレジストリインスタンス
        :type registry: ConverterRegistry
    """
    try:
        from docx2pdf import docx_to_pdf
    except Exception as e:
        registry.set_import_error("docx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".docx",
            output_ext=".pdf",
            converter=wrap_simple(docx_to_pdf),
            description="Word to PDF",
        ))

    try:
        from xlsx2pdf import xlsx_to_pdf
    except Exception as e:
        registry.set_import_error("xlsx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".xlsx",
            output_ext=".pdf",
            converter=wrap_simple(xlsx_to_pdf),
            description="Excel to PDF",
            ignore_temp_prefixes=("~$",),
        ))

    try:
        from pptx2pdf import pptx_to_pdf
    except Exception as e:
        registry.set_import_error("pptx2pdf", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".pdf",
            converter=wrap_simple(pptx_to_pdf),
            description="PowerPoint to PDF",
        ))

    try:
        from pptx2pdf_with_notes_importable import pptx_to_pdf_with_notes
    except Exception as e:
        registry.set_import_error("pptx2pdf_with_notes_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".notes.pdf",
            converter=wrap_simple(pptx_to_pdf_with_notes),
            description="PowerPoint to PDF with speaker notes",
        ))

    try:
        from html2pdf_importable import html_to_pdf
    except Exception as e:
        registry.set_import_error("html2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".html",
            output_ext=".pdf",
            converter=wrap_simple(html_to_pdf),
            description="HTML to PDF",
        ))
        registry.register(ConverterSpec(
            input_ext=".htm",
            output_ext=".pdf",
            converter=wrap_simple(html_to_pdf),
            description="HTML to PDF",
        ))

    try:
        from md2pdf_importable import md_to_pdf
    except Exception as e:
        registry.set_import_error("md2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".md",
            output_ext=".pdf",
            converter=wrap_simple(md_to_pdf),
            description="Markdown to PDF",
        ))

    try:
        from txt2pdf_importable import txt_to_pdf
    except Exception as e:
        registry.set_import_error("txt2pdf_importable", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".txt",
            output_ext=".pdf",
            converter=wrap_simple(txt_to_pdf),
            description="Text to PDF",
        ))

    try:
        from img2pdf_importable import image_to_pdf
    except Exception as e:
        registry.set_import_error("img2pdf_importable", e)
    else:
        for ext in (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"):
            registry.register(ConverterSpec(
                input_ext=ext,
                output_ext=".pdf",
                converter=wrap_simple(image_to_pdf),
                description="Image to PDF",
            ))

    # Markdown converters with formula/image extraction
    try:
        import docx2md
    except Exception as e:
        registry.set_import_error("docx2md", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".docx",
            output_ext=".md",
            converter=wrap_md_with_images(docx2md.convert),
            description="Word to Markdown with equations/images",
        ))

    try:
        import pptx2md2
    except Exception as e:
        registry.set_import_error("pptx2md2", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".md",
            converter=wrap_md_with_images(pptx2md2.extract_content_to_markdown),
            description="PowerPoint to Markdown with equations/images",
        ))

    try:
        import pdf2md
    except Exception as e:
        registry.set_import_error("pdf2md", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pdf",
            output_ext=".md",
            converter=wrap_pdf2md(pdf2md.convert),
            description="PDF to Markdown",
        ))

    try:
        import pdf2pptx
    except Exception as e:
        registry.set_import_error("pdf2pptx", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pdf",
            output_ext=".pptx",
            converter=pdf2pptx.convert,
            description="PDF to PowerPoint slides as images",
        ))

    try:
        import pptx2img
    except Exception as e:
        registry.set_import_error("pptx2img", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pptx",
            output_ext=".png",
            converter=wrap_image_to_dir(pptx2img.export_img, rename_func=getattr(pptx2img, "rename_img", None)),
            description="PowerPoint to PNG images",
            output_mode=OUTPUT_MODE_DIR,
        ))

    try:
        import docx2img
    except Exception as e:
        registry.set_import_error("docx2img", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".docx",
            output_ext=".png",
            converter=wrap_docx_to_png(docx2img.convert_word_to_pdf, docx2img.convert_pdf_to_images),
            description="Word to PNG images",
            output_mode=OUTPUT_MODE_DIR,
        ))

    try:
        import pdf2img
    except Exception as e:
        registry.set_import_error("pdf2img", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".pdf",
            output_ext=".png",
            converter=wrap_image_to_dir(pdf2img.convert_pdf_to_images),
            description="PDF to PNG images",
            output_mode=OUTPUT_MODE_DIR,
        ))

    try:
        import ipynb2md
    except Exception as e:
        registry.set_import_error("ipynb2md", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".ipynb",
            output_ext=".md",
            converter=wrap_ipynb_json_to_md(ipynb2md.convert_ipynb_to_md),
            description="Jupyter Notebook to Markdown",
        ))
        registry.register(ConverterSpec(
            input_ext=".json",
            output_ext=".md",
            converter=wrap_ipynb_json_to_md(ipynb2md.convert_json_to_md),
            description="JSON to Markdown",
        ))

        try:
            from md2pdf_importable import md_to_pdf as md_to_pdf_for_pipeline
        except Exception:
            try:
                from md2pdf import md_to_pdf as md_to_pdf_for_pipeline
            except Exception as e:
                registry.set_import_error("md2pdf pipeline backend", e)
            else:
                registry.register(ConverterSpec(
                    input_ext=".ipynb",
                    output_ext=".pdf",
                    converter=wrap_ipynb_json_to_pdf(ipynb2md.convert_ipynb_to_md, md_to_pdf_for_pipeline),
                    description="Jupyter Notebook to PDF via Markdown pipeline",
                ))
                registry.register(ConverterSpec(
                    input_ext=".json",
                    output_ext=".pdf",
                    converter=wrap_ipynb_json_to_pdf(ipynb2md.convert_json_to_md, md_to_pdf_for_pipeline),
                    description="JSON to PDF via Markdown pipeline",
                ))
        else:
            registry.register(ConverterSpec(
                input_ext=".ipynb",
                output_ext=".pdf",
                converter=wrap_ipynb_json_to_pdf(ipynb2md.convert_ipynb_to_md, md_to_pdf_for_pipeline),
                description="Jupyter Notebook to PDF via Markdown pipeline",
            ))
            registry.register(ConverterSpec(
                input_ext=".json",
                output_ext=".pdf",
                converter=wrap_ipynb_json_to_pdf(ipynb2md.convert_json_to_md, md_to_pdf_for_pipeline),
                description="JSON to PDF via Markdown pipeline",
            ))

    # pandoc.py 由来の追加変換
    try:
        import pandoc as pandoc_module
        _ = pandoc_module.find_pandoc
        _ = pandoc_module.convert_md
    except Exception as e:
        registry.set_import_error("pandoc", e)
    else:
        registry.register(ConverterSpec(
            input_ext=".md",
            output_ext=".docx",
            converter=wrap_pandoc("docx"),
            description="Markdown to Word via Pandoc",
        ))
        registry.register(ConverterSpec(
            input_ext=".md",
            output_ext=".pptx",
            converter=wrap_pandoc("pptx"),
            description="Markdown to PowerPoint via Pandoc",
        ))
        registry.register(ConverterSpec(
            input_ext=".md",
            output_ext=".html",
            converter=wrap_pandoc("html"),
            description="Markdown to HTML via Pandoc",
        ))


def convert_file(
    input_path: str,
    output_ext: str,
    registry: ConverterRegistry,
    update: bool = True,
    overwrite: bool = False,
    converter_kwargs: Optional[Dict[str, Any]] = None,
) -> bool:
    """
    概要:
        単一のファイルを変換します。
    詳細説明:
        指定されたファイルに対してレジストリから適切な変換器を取得し、変換を実行します。
    引数:
        :param input_path: 入力ファイルのパス
        :type input_path: str
        :param output_ext: 対象の出力拡張子
        :type output_ext: str
        :param registry: 変換器レジストリ
        :type registry: ConverterRegistry
        :param update: 出力ファイルが存在し、入力より新しい場合はスキップするかのフラグ
        :type update: bool
        :param overwrite: 常に変換を実行するかのフラグ
        :type overwrite: bool
        :param converter_kwargs: 変換器に渡す追加引数
        :type converter_kwargs: dict
    戻り値:
        :returns: 変換に成功した場合はTrue、それ以外はFalse
        :rtype: bool
    """
    input_path = os.path.abspath(input_path)
    input_ext = normalize_ext(os.path.splitext(input_path)[1])
    converter_kwargs = converter_kwargs or {}

    spec = registry.get(input_ext, output_ext)
    if spec is None:
        print(f"Skipping unsupported file: '{input_path}'")
        return False

    filename = os.path.basename(input_path)
    if should_ignore_file(filename, spec):
        print(f"Skipping temporary file: '{input_path}'")
        return False

    output_path = build_output_path(input_path, output_ext, spec.output_mode)
    do_convert, reason = should_convert(
        input_path=input_path,
        output_path=output_path,
        output_mode=spec.output_mode,
        update=update,
        overwrite=overwrite,
    )

    if do_convert:
        print(f"Converting: '{input_path}' -> '{output_path}' ({reason})")
        kwargs = dict(spec.options)
        kwargs.update(converter_kwargs)
        result = spec.converter(input_path, output_path, **kwargs)
        return bool(result)

    print(f"Skipping: '{input_path}' ({reason})")
    return False


def walk_and_convert(
    root_dir: str,
    output_ext: str,
    registry: ConverterRegistry,
    max_level: int = -1,
    update: bool = True,
    overwrite: bool = False,
    target_patterns: Optional[List[str]] = None,
    converter_kwargs: Optional[Dict[str, Any]] = None,
):
    """
    概要:
        ディレクトリツリーを走査し、条件に一致するファイルを一括で変換します。
    詳細説明:
        再帰的にディレクトリを辿り、指定された対象パターンや更新条件を満たすファイルを変換します。
    引数:
        :param root_dir: 走査を開始するルートディレクトリ
        :type root_dir: str
        :param output_ext: 対象の出力拡張子
        :type output_ext: str
        :param registry: 変換器レジストリ
        :type registry: ConverterRegistry
        :param max_level: 最大の再帰深度であり、-1の場合は制限なし
        :type max_level: int
        :param update: 更新対象のみ変換するかのフラグ
        :type update: bool
        :param overwrite: 常に上書き変換するかのフラグ
        :type overwrite: bool
        :param target_patterns: 変換対象とするファイル名のパターンのリスト
        :type target_patterns: list
        :param converter_kwargs: 変換器に渡す追加引数
        :type converter_kwargs: dict
    戻り値:
        :returns: 走査した対象ファイル数と変換されたファイル数のタプル
        :rtype: tuple
    """
    if not os.path.isdir(root_dir):
        print(f"Error: Root directory '{root_dir}' does not exist.")
        return 0, 0

    root_dir_abs = os.path.abspath(root_dir)
    output_ext = normalize_ext(output_ext)

    print(f"Searching in '{root_dir_abs}'")
    print(f"Target output format: {output_ext}")
    print(f"Max level: {max_level}")
    print(f"update: {int(update)}")
    print(f"overwrite: {int(overwrite)}")
    if converter_kwargs:
        for key, value in sorted(converter_kwargs.items()):
            if value not in (None, ""):
                print(f"{key}: {value}")
    if target_patterns:
        print(f"Target patterns: {';'.join(target_patterns)}")
    else:
        print("Target patterns: (all matching input files)")
    print("")

    converted = 0
    scanned = 0

    for dirpath, dirnames, filenames in os.walk(root_dir_abs):
        current_level = dirpath.count(os.sep) - root_dir_abs.count(os.sep)

        if max_level != -1 and current_level >= max_level:
            del dirnames[:]

        for filename in filenames:
            if not matches_target(filename, target_patterns):
                continue

            input_path = os.path.join(dirpath, filename)
            input_ext = normalize_ext(os.path.splitext(filename)[1])

            if registry.has(input_ext, output_ext):
                scanned += 1
                if convert_file(
                    input_path=input_path,
                    output_ext=output_ext,
                    registry=registry,
                    update=update,
                    overwrite=overwrite,
                    converter_kwargs=converter_kwargs,
                ):
                    converted += 1

    print("\nConversion process finished.")
    print(f"Scanned supported files: {scanned}")
    print(f"Converted files: {converted}")
    return scanned, converted


def parse_args():
    """
    概要:
        コマンドライン引数をパースします。
    戻り値:
        :returns: パース結果を格納した名前空間オブジェクト
        :rtype: argparse.Namespace
    """
    parser = argparse.ArgumentParser(
        description="Convert files in a directory tree using registered converters."
    )
    parser.add_argument("root_dir", nargs="?", default=".", help="Root directory to scan")
    parser.add_argument("--infile", default="", help="Convert only this file and skip recursive search when specified")
    parser.add_argument("output_ext", nargs="?", default=".pdf", help="Target output extension, e.g. pdf or .notes.pdf")
    parser.add_argument("--max_level", nargs="?", default="-1", help="Maximum recursion depth (-1 means unlimited)")
    parser.add_argument("--target", default=None, help='Filename patterns separated by ";" , e.g. --target="*.pptx;*.docx"')
    parser.add_argument("--update", type=int, choices=[0, 1], default=1, help="If 0, skip when output already exists even if source is newer")
    parser.add_argument("--overwrite", type=int, choices=[0, 1], default=0, help="If 1, always convert regardless of timestamps")
    parser.add_argument("--pandoc_path", default=None, help="Path to pandoc executable for Pandoc-based conversions")
    parser.add_argument("--template_docx", default=None, help="Pandoc reference doc/template for md -> docx")
    parser.add_argument("--template_pptx", default=None, help="Pandoc reference doc/template for md -> pptx")
    parser.add_argument("--css", default=None, help="CSS for md -> html via Pandoc")
    parser.add_argument("--toc", type=int, choices=[0, 1], default=0, help="Enable table of contents for Pandoc docx output")
    parser.add_argument("--mathml", action="store_true", help="Use MathML for md -> html via Pandoc")
    parser.add_argument("--no_yaml", action="store_true", help="Pass no_yaml option to Pandoc conversion")
    parser.add_argument("--verbose", action="store_true", help="Verbose mode for Pandoc conversion")
    parser.add_argument("--smart_conversion", type=int, choices=[0, 1], default=0, help="Enable smart_conversion in pandoc.py")
    parser.add_argument("--pdf-dpi", type=int, default=None, help="DPI for PDF -> PPTX image rendering. Default is pdf2pptx.py default, usually 200")
    parser.add_argument("--pdf-slide-size", choices=["pdf", "wide", "standard"], default=None, help="Slide size for PDF -> PPTX: pdf, wide, or standard")
    parser.add_argument("--pdf-margin", type=float, default=None, help="Margin in inches for PDF -> PPTX")
    parser.add_argument("--pdf-split-vertical-half", action="store_true", help="Split each PDF page into upper/lower halves for PDF -> PPTX")
    parser.add_argument("--list", action="store_true", help="List available converters and exit")
    parser.add_argument("--no-pause", action="store_true", help="Do not wait for ENTER before exit")
    return parser.parse_args()


def build_converter_kwargs(args, output_ext: str) -> Dict[str, Any]:
    """
    概要:
        コマンドライン引数から変換器に渡すキーワード引数を構築します。
    引数:
        :param args: パース済みのコマンドライン引数
        :type args: argparse.Namespace
        :param output_ext: 対象の出力拡張子
        :type output_ext: str
    戻り値:
        :returns: 変換器用の追加引数が格納された辞書
        :rtype: dict
    """
    output_ext = normalize_ext(output_ext)
    kwargs: Dict[str, Any] = {}

    if args.pandoc_path:
        kwargs["pandoc_path"] = args.pandoc_path
    if output_ext == ".docx" and args.template_docx:
        kwargs["template"] = args.template_docx
    elif output_ext == ".pptx" and args.template_pptx:
        kwargs["template"] = args.template_pptx
    elif output_ext == ".html" and args.css:
        kwargs["css"] = args.css

    kwargs["toc"] = bool(args.toc)
    kwargs["mathml"] = bool(args.mathml)
    kwargs["no_yaml"] = bool(args.no_yaml)
    kwargs["verbose"] = bool(args.verbose)
    kwargs["smart_conversion"] = bool(args.smart_conversion)

    # Options used by pdf2pptx.convert when input is PDF and output is PPTX.
    # They are harmless for other .pptx converters because wrappers ignore unknown kwargs.
    if args.pdf_dpi is not None:
        kwargs["pdf_dpi"] = args.pdf_dpi
    if args.pdf_slide_size is not None:
        kwargs["pdf_slide_size"] = args.pdf_slide_size
    if args.pdf_margin is not None:
        kwargs["pdf_margin"] = args.pdf_margin
    if args.pdf_split_vertical_half:
        kwargs["pdf_split_vertical_half"] = True

    return kwargs


def main():
    """
    概要:
        スクリプトのメイン処理を実行します。
    詳細説明:
        コマンドライン引数の解析、レジストリの初期化、指定されたモードに応じた変換処理を行います。
    戻り値:
        :returns: 終了コード
        :rtype: int
    """
    args = parse_args()

    try:
        max_level = int(args.max_level)
    except ValueError:
        print(f"Error: Invalid max_level '{args.max_level}'.")
        return 1

    registry = ConverterRegistry()
    safe_import_registry(registry)

    if args.list:
        registry.print_available_converters()
        return 0

    output_ext = normalize_ext(args.output_ext)
    available_outputs = registry.list_output_exts()

    if output_ext not in available_outputs:
        print(f"Error: No converters registered for output format '{output_ext}'.")
        if available_outputs:
            print("Registered output formats:", ", ".join(available_outputs))
        registry.print_available_converters()
        return 1

    target_patterns = parse_target_patterns(args.target)
    converter_kwargs = build_converter_kwargs(args, output_ext)

    if args.infile:
        infile = os.path.abspath(args.infile)
        if not os.path.exists(infile):
            print(f"Error: infile '{args.infile}' does not exist.")
            return 1
        print(f"Single-file mode: {infile}")
        converted = convert_file(
            input_path=infile,
            output_ext=output_ext,
            registry=registry,
            update=bool(args.update),
            overwrite=bool(args.overwrite),
            converter_kwargs=converter_kwargs,
        )
        print("\nConversion process finished.")
        print("Scanned supported files: 1" if registry.has(os.path.splitext(infile)[1], output_ext) else "Scanned supported files: 0")
        print(f"Converted files: {1 if converted else 0}")
        return 0 if converted or registry.has(os.path.splitext(infile)[1], output_ext) else 1

    walk_and_convert(
        root_dir=args.root_dir,
        output_ext=output_ext,
        registry=registry,
        max_level=max_level,
        update=bool(args.update),
        overwrite=bool(args.overwrite),
        target_patterns=target_patterns,
        converter_kwargs=converter_kwargs,
    )
    return 0


if __name__ == "__main__":
    rc = main()
    print("\nProgram execution completed.")
    if "--no-pause" not in sys.argv:
        input("\nPress ENTER to terminate>>\n")
    raise SystemExit(rc)