"""
MarkdownファイルをPDFに変換するユーティリティ。

MarkdownテキストをHTMLに変換し、さらにFPDFライブラリを使用してPDFドキュメントとして
レンダリングします。日本語フォントの自動検出や、コードブロック、箇条書き、見出しなどの
基本的なMarkdown要素のサポートを含みます。

:doc:`md2pdf_usage`
"""
import os
import re
from pathlib import Path

import markdown
from fpdf import FPDF, XPos, YPos


def insert_soft_breaks(s: str, hard_chunk=60) -> str:
    """
    改行できない長大トークン（URL等）にゼロ幅スペースを挿入して折り返しを許可します。

    URLやファイルパスなどの連続した文字の塊、または指定された`hard_chunk`よりも
    長いトークンに対して、ゼロ幅スペース(U+200B)を挿入することで、PDFレンダリング時に
    テキストが適切に折り返されるようにします。これにより、レイアウトの崩れを防ぎます。

    :param s: 処理対象の文字列。
    :type s: str
    :param hard_chunk: ゼロ幅スペースを挿入する最小の文字塊の長さ。これよりも長いトークンが検出された場合に適用されます。
    :type hard_chunk: int
    :returns: ゼロ幅スペースが挿入された文字列。
    :rtype: str
    """
    s = re.sub(r'([/_\-.&?=#:])', r'\1\u200b', s)

    def break_long_token(m):
        token = m.group(0)
        out = []
        for i in range(0, len(token), hard_chunk):
            out.append(token[i:i + hard_chunk])
        return '\u200b'.join(out)

    s = re.sub(r'[^\s\u200b]{' + str(hard_chunk * 2) + r',}', break_long_token, s)
    return s


def md_to_html(md_text: str) -> str:
    """
    MarkdownテキストをHTMLに変換します。

    Python-Markdownライブラリを使用し、追加の拡張機能
    ("extra", "fenced_code", "tables")を有効にしてMarkdownテキストをHTML文字列に変換します。

    :param md_text: 変換するMarkdown形式の文字列。
    :type md_text: str
    :returns: 変換されたHTML形式の文字列。
    :rtype: str
    """
    return markdown.markdown(md_text, extensions=["extra", "fenced_code", "tables"])


def extract_code_blocks(html: str):
    """
    HTML文字列から`<pre><code>...</code></pre>`形式のコードブロックを抽出し、プレースホルダに置換します。

    FPDFがHTMLのコードブロックを直接処理できないため、この関数はコードブロックのコンテンツを
    一時的なリストに保存し、元のHTML内では`@@@CODEBLOCK_N@@@`という形式のプレースホルダに置き換えます。
    これにより、HTMLの残りの部分が通常の段落として処理された後、コードブロックを個別にレンダリングできます。

    :param html: 処理対象のHTML文字列。
    :type html: str
    :returns: プレースホルダに置換されたHTML文字列と、抽出されたコードブロックのリストのタプル。
    :rtype: tuple[str, list[str]]
    """
    code_list = []

    def repl(m):
        code = m.group(1)
        code_list.append(code)
        return f"@@@CODEBLOCK_{len(code_list) - 1}@@@"

    html2 = re.sub(
        r"<pre><code>(.*?)</code></pre>",
        repl,
        html,
        flags=re.IGNORECASE | re.DOTALL,
    )
    return html2, code_list


def find_font_path(font_path=None):
    """
    システム上の日本語フォントのパスを検索します。

    指定されたフォントパスを最優先で確認し、その後、一般的なWindows、macOS、Linuxの
    日本語フォントパスの候補を探索します。最初に見つかった既存のファイルのパスを返します。
    これにより、異なるOS環境でも日本語が正しく表示されるようにします。

    :param font_path: ユーザーが指定したフォントファイルのパス。Noneの場合はシステムデフォルトを検索します。
    :type font_path: str or None
    :returns: 見つかったフォントファイルの絶対パス文字列、または見つからなかった場合はNone。
    :rtype: str or None
    """
    candidates = []
    if font_path:
        candidates.append(Path(font_path))

    candidates.append(Path(r"C:\Windows\Fonts\msgothic.ttc"))
    candidates += [
        Path("/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc"),
        Path("/Library/Fonts/Arial Unicode.ttf"),
        Path("/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"),
        Path("/usr/share/fonts/truetype/noto/NotoSansCJKjp-Regular.otf"),
    ]

    for p in candidates:
        if p.is_file():
            return str(p)
    return None


class PDF(FPDF):
    """
    MarkdownコンテンツをPDFとしてレンダリングするためのFPDF拡張クラス。

    FPDFの基本機能に加え、H1、H2、段落、箇条書き、コードブロックといった
    Markdownの要素をPDFに適切に描画するためのカスタムメソッドを提供します。
    ページのヘッダーとフッターは表示されません。
    """
    def header(self):
        """
        ページのヘッダーを生成しないようにオーバーライドします。
        """
        pass

    def footer(self):
        """
        ページのフッターを生成しないようにオーバーライドします。
        """
        pass

    def _body_width(self) -> float:
        """
        PDFドキュメントのボディ部分の幅（左右のマージンを除く）を計算します。

        :returns: ボディ部分の幅（mm単位）。
        :rtype: float
        """
        return self.w - self.l_margin - self.r_margin

    def h1(self, text: str):
        """
        H1見出しをPDFに書き込みます。

        指定されたテキストを大きなフォントサイズで描画し、前後に余白を追加します。

        :param text: H1見出しのテキスト。
        :type text: str
        """
        self.set_font("DOCFONT", "", 18)
        self.ln(2)
        self.multi_cell(self._body_width(), 10, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
        self.ln(1)

    def h2(self, text: str):
        """
        H2見出しをPDFに書き込みます。

        指定されたテキストをH1よりわずかに小さいフォントサイズで描画し、前後に余白を追加します。

        :param text: H2見出しのテキスト。
        :type text: str
        """
        self.set_font("DOCFONT", "", 16)
        self.ln(1)
        self.multi_cell(self._body_width(), 9, text, new_x=XPos.LMARGIN, new_y=YPos.NEXT)

    def para(self, text: str):
        """
        標準の段落テキストをPDFに書き込みます。

        指定されたテキストを通常のフォントサイズで描画します。
        テキストが長すぎてFPDFの`multi_cell`で処理できない場合、
        `insert_soft_breaks`関数を使用してゼロ幅スペースを挿入し、折り返しを試みます。

        :param text: 段落のテキスト。
        :type text: str
        """
        self.set_font("DOCFONT", "", 12)
        try:
            self.multi_cell(self._body_width(), 7, text)
        except Exception:
            self.multi_cell(self._body_width(), 7, insert_soft_breaks(text))
        self.ln(0.5)

    def bullet(self, text: str):
        """
        箇条書き項目をPDFに書き込みます。

        "• "を先頭に付加し、残りのテキストを右にインデントして描画します。
        テキストが長すぎてFPDFの`multi_cell`で処理できない場合、
        `insert_soft_breaks`関数を使用してゼロ幅スペースを挿入し、折り返しを試みます。

        :param text: 箇条書き項目のテキスト。
        :type text: str
        """
        self.set_font("DOCFONT", "", 12)
        bullet_w = 6
        remain = self._body_width() - bullet_w
        if remain < 20:
            return self.para(f"• {text}")
        self.cell(bullet_w, 7, "• ")
        try:
            self.multi_cell(remain, 7, text)
        except Exception:
            self.multi_cell(remain, 7, insert_soft_breaks(text))

    def code_block(self, code: str):
        """
        コードブロックをPDFに書き込みます。

        コードブロックは背景色がグレーのボックスで囲まれ、コードがその中に表示されます。
        コードの行が長い場合、`insert_soft_breaks`関数を使用してゼロ幅スペースを挿入し、
        適切に折り返されるようにします。

        :param code: コードブロック内のテキスト。
        :type code: str
        """
        self.set_font("DOCFONT", "", 11)
        lines = code.replace("\t", "    ").splitlines() or [""]
        h = max(7 * len(lines) + 6, 14)
        x0, y0 = self.get_x(), self.get_y()
        w = self._body_width()

        self.set_fill_color(245, 245, 245)
        self.rect(x0, y0, w, h, style="F")
        self.set_draw_color(200, 200, 200)
        self.rect(x0, y0, w, h)

        self.set_xy(x0 + 2, y0 + 3)
        for ln in lines:
            ln2 = insert_soft_breaks(ln, hard_chunk=80)
            self.multi_cell(w - 4, 6, ln2)
        self.ln(3)


def html_to_pdf(html: str, output_filename: str, font_path=None):
    """
    HTML文字列をPDFファイルに変換します。

    HTMLを解析し、`PDF`クラスのメソッドを使用してコンテンツをレンダリングします。
    事前に`extract_code_blocks`で抽出されたコードブロックは、この関数内で
    PDFのコードブロックとして特別に処理されます。また、日本語フォントの検出と設定も行われます。

    :param html: 変換するHTML形式の文字列。
    :type html: str
    :param output_filename: 出力するPDFファイルのパス。
    :type output_filename: str
    :param font_path: 使用する日本語フォントファイルのパス。Noneの場合はシステムデフォルトを検索します。
    :type font_path: str or None
    :raises FileNotFoundError: 日本語フォントが見つからない場合。
    """
    html, code_list = extract_code_blocks(html)

    pdf = PDF(format="A4")
    pdf.set_auto_page_break(auto=True, margin=15)
    pdf.add_page()

    chosen_font = find_font_path(font_path)
    if not chosen_font:
        raise FileNotFoundError(
            "Japanese font not found. Specify a TTF/OTF/TTC font path."
        )

    pdf.add_font("DOCFONT", "", chosen_font)

    lines = html.splitlines()

    for raw in lines:
        s = raw.strip()
        if not s:
            continue

        m = re.match(r"^@@@CODEBLOCK_(\d+)@@@$", s)
        if m:
            idx = int(m.group(1))
            code = code_list[idx]
            pdf.code_block(code)
            continue

        m = re.match(r"^<h1[^>]*>(.*?)</h1>\s*$", s, re.IGNORECASE)
        if m:
            pdf.h1(re.sub(r"<[^>]+>", "", m.group(1)))
            continue

        m = re.match(r"^<h2[^>]*>(.*?)</h2>\s*$", s, re.IGNORECASE)
        if m:
            pdf.h2(re.sub(r"<[^>]+>", "", m.group(1)))
            continue

        m = re.match(r"^<li[^>]*>(.*?)</li>\s*$", s, re.IGNORECASE)
        if m:
            text = re.sub(r"<[^>]+>", "", m.group(1))
            pdf.bullet(text)
            continue

        m = re.match(r"^<p[^>]*>(.*?)</p>\s*$", s, re.IGNORECASE)
        if m:
            text = re.sub(r"<[^>]+>", "", m.group(1))
            pdf.para(text)
            continue

        # タグが一致しない場合は、タグを除去して通常の段落として処理
        pdf.para(re.sub(r"<[^>]+>", "", s))

    pdf.output(output_filename)


def md_to_pdf(input_path, output_path=None, font_path=None):
    """
    MarkdownファイルをPDFに変換します。

    指定されたMarkdownファイルを読み込み、`md_to_html`でHTMLに変換後、
    `html_to_pdf`で最終的なPDFファイルを作成します。
    入力ファイルの存在チェック、空ファイルチェック、エラーハンドリングを行います。
    出力パスが指定されない場合は、入力ファイル名と同じ名前で拡張子を.pdfに変更して出力します。

    :param input_path: 入力となるMarkdownファイルのパス。
    :type input_path: str
    :param output_path: 出力するPDFファイルのパス。Noneの場合は入力ファイルパスから自動生成されます。
    :type output_path: str or None
    :param font_path: 使用する日本語フォントファイルのパス。Noneの場合はシステムデフォルトを検索します。
    :type font_path: str or None
    :returns: 変換に成功した場合は出力PDFファイルの絶対パス、失敗した場合はNone。
    :rtype: str or None
    """
    input_path = os.path.abspath(input_path)
    if output_path is None:
        output_path = os.path.splitext(input_path)[0] + ".pdf"
    else:
        output_path = os.path.abspath(output_path)

    print(f"  Converting '{os.path.basename(input_path)}' to PDF...")

    try:
        if not os.path.exists(input_path):
            print(f"  Error converting '{input_path}' to PDF: file does not exist")
            return None

        if os.path.getsize(input_path) == 0:
            print(f"  Error converting '{input_path}' to PDF: file is empty")
            return None

        md_text = Path(input_path).read_text(encoding="utf-8")
        html = md_to_html(md_text)
        html_to_pdf(html, output_path, font_path=font_path)

        print(f"  Successfully converted to PDF: '{output_path}'")
        return output_path

    except Exception as e:
        print(f"  Error converting '{input_path}' to PDF: {e}")
        return None


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="Convert Markdown to PDF (simple, Japanese-capable)."
    )
    parser.add_argument("input_path", help="input markdown file path")
    parser.add_argument("output_path", nargs="?", default=None, help="output pdf file path")
    parser.add_argument("--font", default=None, help="path to a TTF/OTF/TTC font for Japanese")
    parser.add_argument("--no-pause", action="store_true", help="do not wait for ENTER before exit")
    args = parser.parse_args()

    rc = 0 if md_to_pdf(args.input_path, args.output_path, font_path=args.font) else 1
    print("\nProgram execution completed.")
    if not args.no_pause:
        input("\nPress ENTER to terminate>>\n")
    raise SystemExit(rc)
