"""
概要: ウェブサイトのHTMLサイトマップツリービューを生成するスクリプト。

詳細説明:
このスクリプトは、指定されたルートURLからウェブサイトを再帰的にクロールし、発見されたリンクから
HTML形式の階層的なサイトマップツリービューを生成します。
コマンドライン引数でルートURL、出力ファイル名、ファイル除外設定、および特定のパスパターンに基づいて
URLを除外する機能も持ちます。検出された各ページのタイトルと最終更新日もサイトマップに含められます。

関連リンク: :doc:`MakeSiteMap_usage`
"""
import sys
import re
import requests
from urllib.parse import urljoin, urlparse, urlunparse
import charset_normalizer
from datetime import datetime

# デフォルト設定値
RootURL = "http://d2mate.mdxes.iir.isct.ac.jp/D2MatE/D2MatE_programs.html?page=statistcs"
outpath = "sitemap.html" # 出力ファイルをHTMLに変更
EXCLUDE_FILES = True 
# 一般的なファイル拡張子のリスト（必要に応じて追加/調整可能）
# .html / .shtml は上記で既に許可されている
exclude_file_extensions = [
            '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', 
            '.zip', '.rar', '.tar', '.gz', 
            '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.webp',
            '.mp4', '.mp3', '.mov', '.avi', '.wav',
            '.xml', '.txt', '.md', '.json', '.csv', '.ini', '.list', '.prm', '.cfg',
            '.cif',
            '.py', '.pm', '.pl', '.c', '.cpp', '.js', '.f', '.f90', '.f77', '.bas', '.sh', '.csh', '.bat', '.lib',
        ]

# 新規追加: 除外したいパスパターンのリスト (正規表現)
EXCLUDE_PATHS = [
    r".*/__pycache__/",
    r".*/download/",
    r".*/database/",
    r".*/db/",
]

# EXCLUDE_PATHSを正規表現オブジェクトにコンパイルしておく（効率化のため）
COMPILED_EXCLUDE_PATHS = [re.compile(p) for p in EXCLUDE_PATHS]


argv = sys.argv
nargs = len(argv)
if nargs > 1:
    RootURL = argv[1]
if nargs > 2:
    outpath = argv[2]
if nargs > 3:
    EXCLUDE_FILES = argv[3].lower() in ('true', '1', 't')

base_domain = urlparse(RootURL).netloc

print()
print("Crawling and generating HTML Sitemap Tree View...")
print()
print(f"usage: python {argv[0]} [RootURL] [OutputFile] [ExcludeFiles(True/False)]")
print()
print(f"RootURL: {RootURL}")
print(f"Output File: {outpath}")
print(f"Base Domain: {base_domain}")
print(f"Exclude Files: {EXCLUDE_FILES}")
print(f"Exclude Path Patterns: {EXCLUDE_PATHS}") # 新しい除外パターンを表示
print()

def clean_html_content(html_content):
    """
    概要: HTMLコンテンツから改行文字を削除し、前後の空白をトリムする。

    詳細説明:
    与えられたHTML文字列内のすべての改行文字をスペースに置換し、
    結果として生じる文字列の前後の空白を削除します。

    :param html_content: str: 処理するHTML文字列。
    :returns: str: 改行とトリムが適用されたHTML文字列。
    """
    return " ".join(html_content.split())

def get_page_title(html_content):
    """
    概要: HTMLコンテンツ内から<title>を抽出する。

    詳細説明:
    与えられたHTMLコンテンツから`<title>`タグの内容を正規表現で検索し抽出します。
    大文字・小文字を区別せず検索し、見つかった場合はその内容をトリムして返します。
    `<title>`タグが見つからない場合は「No Title」を返します。

    :param html_content: str: タイトルを抽出するHTML文字列。
    :returns: str: 抽出されたページのタイトル、または「No Title」。
    """
    title_match = re.search(r"<title>(.*?)</title>", html_content, re.IGNORECASE)
    if title_match:
        return title_match.group(1).strip()
    return "No Title"

def is_target_url(url):
    """
    概要: URLがサイトマップに含めるべき対象かどうかを判断する。

    詳細説明:
    URLがHTMLファイル（.htmlまたは.shtml）であるか、ファイル名がない（ディレクトリパス）である場合、
    またはパスもクエリも存在しない場合はTrueを返します。
    `EXCLUDE_FILES`が`True`に設定されている場合、`exclude_file_extensions`リストに定義された
    一般的なファイル拡張子を持つURLは対象外とします。

    :param url: str: 判定するURL。
    :returns: bool: 対象のURLであればTrue、そうでなければFalse。
    """
    parsed_url = urlparse(url)
    path = parsed_url.path
    
    # 1. 基本的なHTMLとディレクトリパスを許可
    if path.endswith(".html") or path.endswith(".shtml") or path.endswith("/"):
        return True
    
    # 2. パスが空（ルートドメインなど）で、クエリがない場合はHTMLと見なす
    if not path and not parsed_url.query:
        return True

    # 3. EXCLUDE_FILESがTrueの場合、ファイル拡張子をチェックして排除
    if EXCLUDE_FILES:
        # パスに拡張子が含まれていないかチェック
        # パスにドットが含まれていても、最後のセグメントが拡張子リストにマッチしなければOK
        if '.' in path:
            last_segment = path.split('/')[-1]
            if any(last_segment.lower().endswith(ext) for ext in exclude_file_extensions):
                return False # ファイル拡張子にマッチした場合、除外
                
    # 4. それ以外は許可
    return True

def is_excluded_path(url):
    """
    概要: URLパスが定義された除外パターンにマッチするかどうかをチェックする。

    詳細説明:
    `COMPILED_EXCLUDE_PATHS`グローバル変数に定義された正規表現パターンに対して、
    与えられたURLのパス部分（クエリパラメータを含む場合あり）がマッチするかどうかを検査します。
    パスが'/'で始まっていない場合、正規表現とのマッチングのために先頭に'/'が追加されます。

    :param url: str: チェックするURL。
    :returns: bool: 除外パターンにマッチすればTrue、そうでなければFalse。
    """
    parsed_url = urlparse(url)
    # クエリパラメータも除外対象に含めるため、pathとqueryを結合
    path_with_query = parsed_url.path
    if parsed_url.query:
        path_with_query += f"?{parsed_url.query}"
    
    # パスが/で始まっていない場合は先頭に追加（正規表現とのマッチングのため）
    if not path_with_query.startswith('/'):
        path_with_query = '/' + path_with_query

    for pattern in COMPILED_EXCLUDE_PATHS:
        if pattern.search(path_with_query):
            return True
    return False


def normalize_url(url):
    """
    概要: URLを標準化し、フラグメント（#以降）を削除する。

    詳細説明:
    `urllib.parse`モジュールを使用してURLを解析し、URLのフラグメント部分
    （'#'に続く部分）を除去した新しいURLを再構築します。
    これにより、同じコンテンツを指すがフラグメントが異なるURLが重複して扱われるのを防ぎます。
    クエリパラメータは保持されます。

    :param url: str: 標準化するURL。
    :returns: str: フラグメントが削除された標準化されたURL。
    """
    parsed_url = urlparse(url)
    normalized = urlunparse(parsed_url._replace(fragment=""))  # フラグメントを削除
    return normalized

def find_urls_in_html(html_content, base_url):
    """
    概要: HTMLコンテンツからリンクを抽出し、絶対URLに変換して返す。

    詳細説明:
    与えられたHTMLコンテンツから、`href`, `src`, `url()`パターンにマッチするURLを
    正規表現を使って抽出します。
    抽出された相対URLは`base_url`を基に絶対URLに変換されます。
    メーリングリスト (`mailto:`), 電話番号 (`tel:`), ページの内部アンカー (`#`),
    CSSファイル (`.css`), JavaScriptファイル (`.js`) などの特定の種類のURLは除外されます。
    さらに、`is_target_url`関数を使って、サイトマップに含めるべきファイルタイプ
    （例: HTMLファイルやディレクトリパス）のURLのみを結果に含めます。

    :param html_content: str: リンクを抽出するHTML文字列。
    :param base_url: str: 相対URLを絶対URLに変換するための基準となるURL。
    :returns: set[str]: 抽出され、フィルタリングされた絶対URLのセット。
    """
    urls = set()
    # href, src, url()パターンをまとめて検索
    pattern = re.compile(r"url:\s*[\"']([^\"']+)[\"']|href=[\"']([^\"']+)[\"']|src=[\"']([^\"']+)[\"']")
    matches = pattern.findall(html_content)

    for match in matches:
        # 見つかったURLのうち、Noneでない最初の要素を取得
        url = match[0] or match[1] or match[2]
        
        if url.startswith("#") or url.startswith("mailto:") or url.startswith("tel:") or url.endswith(".css") or url.endswith(".js"):
            continue

        if url.startswith("/"):
            full_url = urljoin(base_url, url)
        elif not url.startswith("http"):
            full_url = urljoin(base_url, url)
        else:
            full_url = url
            
        # is_target_urlでファイルの除外をチェック
        if is_target_url(full_url):
            urls.add(full_url)
            
    return urls

def detect_encoding(response_content):
    """
    概要: レスポンスコンテンツから文字コードを判別する。

    詳細説明:
    `charset_normalizer`ライブラリを使用して、HTTPレスポンスのバイト列コンテンツの
    文字コードを自動的に推測します。

    :param response_content: bytes: 文字コードを判別するバイト列コンテンツ。
    :returns: str: 判別された文字コード名。
    """
    result = charset_normalizer.detect(response_content)
    return result['encoding']

def crawl_recursive(start_url, visited, base_domain, parent=""):
    """
    概要: 指定されたURLからウェブサイトを再帰的にクロールし、検出されたページのメタデータを収集する。

    詳細説明:
    この関数は、ウェブサイトを深さ優先で再帰的にクロールします。
    - URLを標準化し、すでに訪問済みのURLや`is_excluded_path`で除外対象と判断されたURLはスキップします。
    - `requests`ライブラリを使用してURLをフェッチし、HTTPステータスコードが200でない場合はエラーとして扱います。
    - `detect_encoding`で文字コードを判別し、コンテンツを適切にデコードします。
    - HTTPレスポンスヘッダーの`Last-Modified`からページの最終更新日を取得し、ISOフォーマットに変換します。
    - `get_page_title`でページのタイトルを抽出します。
    - `find_urls_in_html`で現在のページ内のすべてのリンクを抽出し、`base_domain`と一致する内部リンクのみをフィルタリングします。
    - 検出された子URLに対して再帰的に自身を呼び出し、すべてのURLとそのメタデータを収集します。

    :param start_url: str: クロールを開始するURL。
    :param visited: set[str]: 訪問済みの標準化されたURLを追跡するためのセット。
    :param base_domain: str: クロール対象のベースドメイン（異なるドメインへのリンクは追跡しない）。
    :param parent: str: (オプション) 現在のURLをリンクしている親URL。デバッグやトレースのために使用されます。
    :returns: dict[str, dict[str, str | None]]: URLをキーとし、タイトルと最終更新日を含む辞書。
    """
    normalized_url = normalize_url(start_url)
    
    # 新規追加: 除外パスにマッチする場合、即座にクロールを中断
    if is_excluded_path(start_url):
        print(f"Skipping (Excluded Path): {start_url}")
        return {}
    
    # クエリパラメータを含めた正規化されたURLで訪問済みチェック
    if normalized_url in visited:
        return {}

    print(f"Crawling: {start_url}")
    visited.add(normalized_url)

    try:
        response = requests.get(start_url)
        if response.status_code != 200:
            print(f"Failed to fetch: {start_url} in {parent} (Status: {response.status_code})")
            return {}

        # 文字コード判別
        encoding = detect_encoding(response.content)
        decoded_content = response.content.decode(encoding, errors='replace')
        print(f" Encoding: {encoding}")

        # Last-Modifiedヘッダーを取得
        lastmod = response.headers.get("Last-Modified")
        if lastmod:
            try:
                # HTTPの日付フォーマットをISOフォーマットに変換
                lastmod = datetime.strptime(lastmod, "%a, %d %b %Y %H:%M:%S %Z").strftime("%Y-%m-%d")
                print(f" lastmod: {lastmod}")
            except ValueError:
                lastmod = None # パースエラーの場合はNone
        else:
            lastmod = None

    except Exception as e:
        print(f"Error fetching {start_url}: {e}")
        return {}

    html_content = clean_html_content(decoded_content)
    title = get_page_title(html_content)
    print(f" title: {title}")

    found_urls = find_urls_in_html(html_content, start_url)

    # ベースドメインが一致するURLのみにフィルタリング
    filtered_urls = {url for url in found_urls if urlparse(url).netloc == base_domain}

    # 現在のURLのメタデータを追加
    all_urls = {start_url: {"title": title, "lastmod": lastmod}}
    
    # 子URLを再帰的にクロール
    for url in filtered_urls:
        all_urls.update(crawl_recursive(url, visited, base_domain, parent=start_url))

    return all_urls

def build_hierarchy_dict(urls_with_metadata, base_domain):
    """
    概要: フラットなURLリストから、パスに基づいた階層的な辞書構造を構築する。

    詳細説明:
    クロールによって収集されたURLとそれに対応するメタデータ（タイトル、最終更新日）のフラットな辞書を受け取り、
    URLのパスセグメントを基に、ファイルシステムのようなネストされた辞書構造を作成します。
    これにより、サイトマップツリービューのレンダリングに適した階層データが準備されます。
    クエリパラメータもパスの一部として扱われ、一意のURLがノードとして保持されます。
    ルートURLのメタデータは、ツリー構造の最上位ノードとして特別に扱われます。

    :param urls_with_metadata: dict[str, dict[str, str | None]]: URLとそれに対応するメタデータ（タイトル、最終更新日）の辞書。
    :param base_domain: str: サイトマップを構築する対象のベースドメイン。
    :returns: dict: 階層的に整理されたサイトマップデータ構造。
    """
    # 階層のルートとして、ベースドメインのノードを初期化
    root_node = {"__children": {}}
    
    # データをURLでソートし、安定したツリーの順序を確保
    sorted_urls = sorted(urls_with_metadata.items(), key=lambda x: x[0])

    for url, metadata in sorted_urls:
        parsed = urlparse(url)
        
        if parsed.netloc != base_domain:
            continue
            
        path = parsed.path
        
        # クエリパラメータもパスの一部として扱う
        path_with_query = path
        if parsed.query:
            path_with_query += f"?{parsed.query}"
            
        # パスのセグメントに分割 (先頭/末尾の'/'は無視)
        segments = [s for s in path_with_query.split('/') if s]

        current_level = root_node["__children"]
        
        # ルートURL (例: http://domain.com/) の処理
        if not segments or (not segments and not parsed.query):
            # ルートURL自体をルートノードのメタデータとして保存
            root_node.update({
                "url": url,
                "title": metadata.get("title", "No Title"),
                "lastmod": metadata.get("lastmod", "N/A"),
            })
            continue

        for i, segment in enumerate(segments):
            is_last_segment = (i == len(segments) - 1)
            
            if is_last_segment:
                # 最終セグメントはメタデータ（URL自体）を保持
                # キーはURL全体（一意性を確保するため）
                current_level[url] = {
                    "title": metadata.get("title", "No Title"),
                    "lastmod": metadata.get("lastmod", "N/A"),
                    "url": url
                }
            else:
                # 途中のセグメントはネストされた辞書（子ノード）を保持
                current_level = current_level.setdefault(segment, {"__children": {}})
                current_level = current_level["__children"]

    return root_node

def render_tree(data, is_root=False):
    """
    概要: 階層的な辞書構造からHTMLのツリー構造を再帰的に生成する。

    詳細説明:
    `build_hierarchy_dict`関数によって構築された階層的なデータ構造を受け取り、
    それをHTMLの`details`/`summary`タグと`ul`/`li`タグを組み合わせたツリービューとしてレンダリングします。
    ディレクトリに相当するノードは`<details>`タグでラップされ、初期状態で開いた状態(`open`属性)で表示されます。
    ファイル（URL）に相当するノードは`<li>`タグ内にリンク、タイトル、URLパス、最終更新日が表示されます。
    出力されるHTMLには、Tailwind CSSクラスが適用され、モダンなデザインのツリービューを提供します。

    :param data: dict: 階層的なサイトマップデータ構造。
    :param is_root: bool: (オプション) 現在処理中のノードがツリーのルートであるかを示すフラグ。デフォルトはFalse。
    :returns: str: 生成されたHTML文字列。
    """
    html_content = ""
    
    # ノードをキー（セグメント名またはURL）でソート
    sorted_keys = sorted(data.keys())

    for key in sorted_keys:
        item = data[key]
        
        if "__children" in item:
            # これはディレクトリ/パスのノード
            segment_name = key
            
            # ディレクトリノードをdetailsでラップし、open属性で展開
            html_content += f'<details open class="group my-1 ml-4 border-l border-indigo-300 pl-3">'
            html_content += f'<summary class="cursor-pointer font-semibold text-indigo-700 hover:text-indigo-900 list-none flex items-center">'
            # 展開/折りたたみを示すSVGアイコン
            html_content += f'<svg class="w-4 h-4 mr-2 group-open:rotate-90 transition-transform text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>'
            html_content += f'{segment_name}/'
            html_content += '</summary>'
            
            # 子ノードを ul で再帰的に処理
            html_content += '<ul class="mt-1">'
            html_content += render_tree(item["__children"], is_root=False)
            html_content += '</ul>'
            html_content += '</details>'
        else:
            # これはURL（リーフ）ノード
            url_data = item
            display_url = url_data['url']
            title = url_data['title']
            lastmod = url_data['lastmod']
            
            # URLとメタデータを li で表示
            html_content += f'<li class="mb-2 ml-4 p-2 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">'
            html_content += f'<a href="{display_url}" target="_blank" class="text-blue-600 hover:text-blue-800 font-medium break-all">'
            html_content += f'{title}'
            
            # クエリパラメータを含む場合、URLのパス部分を表示
            path_part = urlparse(display_url).path
            query_part = urlparse(display_url).query
            
            if query_part:
                path_part += f"?{query_part}"
                
            html_content += f'<span class="text-sm text-gray-500 ml-2">({path_part})</span>'

            html_content += '</a>'
            
            if lastmod and lastmod != "N/A":
                html_content += f'<p class="text-xs text-gray-400 mt-1">最終更新日: {lastmod}</p>'
            html_content += '</li>'
            
    return html_content

def generate_html_sitemap_tree(urls_with_metadata, root_url, output_file):
    """
    概要: クローリング結果から階層的なHTMLサイトマップファイルを生成し、指定されたファイルパスに保存する。

    詳細説明:
    `crawl_recursive`関数によって収集されたURLとメタデータの辞書を受け取り、
    `build_hierarchy_dict`関数でこのデータを階層的な構造に変換します。
    次に、`render_tree`関数を呼び出して、その階層構造をHTMLツリービューとしてレンダリングします。
    生成されたHTMLコンテンツは、Tailwind CSSを含む事前定義されたHTMLテンプレートに埋め込まれ、
    最終的なHTMLサイトマップファイルとして`output_file`に保存されます。

    :param urls_with_metadata: dict[str, dict[str, str | None]]: クロールによって収集されたURLとメタデータの辞書。
    :param root_url: str: クロールの起点となったルートURL。サイトマップの基点として表示されます。
    :param output_file: str: サイトマップを保存するHTMLファイルのパス。
    :returns: None
    """
    base_domain = urlparse(root_url).netloc
    
    # 階層構造を構築
    tree_data = build_hierarchy_dict(urls_with_metadata, base_domain)
    
    # ルートURL（ドメイン）のメタデータを取得
    root_title = tree_data.get("title", f"Root: {base_domain}")
    root_lastmod = tree_data.get("lastmod", "N/A")
    root_url_link = tree_data.get("url", f"http://{base_domain}/")
    
    # ツリーのHTMLコンテンツをレンダリング
    tree_html = render_tree(tree_data["__children"], is_root=True)

    
    html_template = f"""
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>サイトマップツリービュー - {base_domain}</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {{ font-family: 'Inter', sans-serif; background-color: #f4f7f9; }}
        /* detailsの展開アイコンをカスタマイズするための非表示 */
        summary::-webkit-details-marker,
        summary::marker {{
            display: none;
        }}
    </style>
</head>
<body class="p-4 md:p-8">
    <div class="max-w-4xl mx-auto bg-white p-6 md:p-10 rounded-xl shadow-2xl">
        <h1 class="text-4xl font-extrabold text-indigo-800 mb-2">サイトマップツリービュー</h1>
        <p class="text-gray-600 mb-6 border-b pb-4">
            クローリングルート: <code class="bg-gray-100 p-1 rounded text-sm text-indigo-600">{root_url}</code>
        </p>
        <p class="text-sm text-gray-500 mb-2">
            <span class="font-semibold text-gray-700">ファイル除外モード:</span> {'有効' if EXCLUDE_FILES else '無効'}
        </p>
        <p class="text-sm text-gray-500 mb-6">
            <span class="font-semibold text-gray-700">パス除外パターン:</span> {', '.join(EXCLUDE_PATHS)}
        </p>
        
        <div class="space-y-4 text-gray-800">
            <!-- Root Node -->
            <div class="p-3 bg-indigo-50 border border-indigo-200 rounded-lg shadow-inner">
                <a href="{root_url_link}" target="_blank" class="text-lg font-bold text-indigo-700 hover:text-indigo-900">
                    {root_title}
                </a>
                <p class="text-xs text-gray-400 mt-1">
                    最終更新日: {root_lastmod}
                </p>
            </div>
            
            <!-- Tree Structure -->
            <div id="sitemap-tree" class="pl-2">
                <ul class="list-none p-0 m-0">
                    {tree_html}
                </ul>
            </div>
        </div>
        
    </div>
</body>
</html>
    """
    
    with open(output_file, "w", encoding="utf-8") as f:
        f.write(html_template)
    
    print(f"HTML Sitemap Tree View has been saved as '{output_file}'")


def main():
    """
    概要: スクリプトのメイン実行関数。

    詳細説明:
    この関数は、コマンドライン引数を処理し、`RootURL`, `outpath`, `EXCLUDE_FILES`
    といったグローバル変数を設定します。
    次に、`crawl_recursive`関数を呼び出してウェブサイトを再帰的にクロールし、
    すべての発見されたURLとそのメタデータを収集します。
    最後に、`generate_html_sitemap_tree`関数を呼び出して、
    収集したデータから階層的なHTMLサイトマップファイルを生成します。

    :returns: None
    """
    visited = set()

    print(f"Starting crawl from: {RootURL}")
    all_urls_with_metadata = crawl_recursive(RootURL, visited, base_domain)

    print("-" * 50)
    print(f"Found total URLs: {len(all_urls_with_metadata)}")
    
    generate_html_sitemap_tree(all_urls_with_metadata, RootURL, outpath)
    print("-" * 50)

if __name__ == "__main__":
    main()