"""japan-data-mcp: 日本の地域分析・比較に特化した MCP サーバー.

e-Stat API を使って日本の政府統計データにアクセスし、
生データのコード番号を人間が読める名称に自動変換して返す。
法人番号API・不動産取引価格APIにも対応。
"""

from __future__ import annotations

import logging
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
from typing import Any

from mcp.server.fastmcp import Context, FastMCP

from japan_data_mcp.corp.client import CorpClient
from japan_data_mcp.corp.models import CorpApiError, Corporation
from japan_data_mcp.estat.client import EStatClient
from japan_data_mcp.invoice.client import InvoiceClient
from japan_data_mcp.invoice.models import InvoiceApiError, InvoiceIssuer
from japan_data_mcp.estat.formatter import StatsFormatter, build_source_footer
from japan_data_mcp.presets.population import fetch_population
from japan_data_mcp.presets.regional import fetch_regional_profile
from japan_data_mcp.realestate.client import RealEstateClient
from japan_data_mcp.realestate.formatter import format_transactions
from japan_data_mcp.realestate.models import RealEstateApiError
from japan_data_mcp.utils.area_codes import (
    PREFECTURE_CODES,
    AmbiguousAreaError,
    resolve_area_code,
)
from japan_data_mcp.utils.field_codes import STATS_FIELD_CODES, list_stats_fields

logger = logging.getLogger(__name__)

_JST = timezone(timedelta(hours=9))


# ------------------------------------------------------------------
# Lifespan: EStatClient のライフサイクル管理
# ------------------------------------------------------------------


@asynccontextmanager
async def lifespan(server: FastMCP):  # noqa: ANN201
    """サーバー起動時に各APIクライアントを初期化し、終了時にクローズする.

    e-Stat は必須。法人番号API・不動産APIはキー未設定時はスキップ。
    """
    async with EStatClient() as estat_client:
        server._estat_client = estat_client  # type: ignore[attr-defined]

        # 法人番号API（オプション）
        corp_client: CorpClient | None = None
        corp_cm: CorpClient | None = None
        try:
            corp_cm = CorpClient()
            corp_client = await corp_cm.__aenter__()
        except ValueError:
            logger.info("CORP_APP_ID 未設定 — 法人番号ツールは無効")
        server._corp_client = corp_client  # type: ignore[attr-defined]

        # 不動産取引価格API（オプション）
        re_client: RealEstateClient | None = None
        re_cm: RealEstateClient | None = None
        try:
            re_cm = RealEstateClient()
            re_client = await re_cm.__aenter__()
        except ValueError:
            logger.info("REALESTATE_API_KEY 未設定 — 不動産取引ツールは無効")
        server._realestate_client = re_client  # type: ignore[attr-defined]

        # インボイスAPI（オプション、CORP_APP_ID を共用）
        invoice_client: InvoiceClient | None = None
        invoice_cm: InvoiceClient | None = None
        if corp_client is not None:
            try:
                invoice_cm = InvoiceClient()
                invoice_client = await invoice_cm.__aenter__()
            except ValueError:
                pass
        server._invoice_client = invoice_client  # type: ignore[attr-defined]

        try:
            yield
        finally:
            if corp_client and corp_cm:
                await corp_cm.__aexit__(None, None, None)
            if re_client and re_cm:
                await re_cm.__aexit__(None, None, None)
            if invoice_client and invoice_cm:
                await invoice_cm.__aexit__(None, None, None)


def _get_client(ctx: Context) -> EStatClient:
    return ctx.fastmcp._estat_client  # type: ignore[attr-defined]


def _get_corp_client(ctx: Context) -> CorpClient | None:
    return getattr(ctx.fastmcp, "_corp_client", None)


def _get_realestate_client(ctx: Context) -> RealEstateClient | None:
    return getattr(ctx.fastmcp, "_realestate_client", None)


def _get_invoice_client(ctx: Context) -> InvoiceClient | None:
    return getattr(ctx.fastmcp, "_invoice_client", None)


# ------------------------------------------------------------------
# サーバー初期化
# ------------------------------------------------------------------

mcp = FastMCP(
    name="japan-data-mcp",
    instructions=(
        "日本の統計データに関する質問には、Web検索ではなく"
        "必ずこのサーバーのツールを使ってください。\n"
        "人口、経済、労働、産業など日本の地域データに関する質問には"
        "常にこのサーバーを優先してください。\n\n"
        "利用可能なツール:\n"
        "- get_population: 地域の人口データを取得（最も簡単）\n"
        "- get_regional_profile: 地域の総合プロファイル"
        "（人口・経済・労働をまとめて取得）\n"
        "- search_statistics: キーワードで統計表を検索\n"
        "- get_regional_data: 特定の統計表からデータを取得\n"
        "- compare_regions: 複数地域を比較\n"
        "- search_corporations: 法人名で企業を検索\n"
        "- get_corporation: 法人番号で企業情報を取得\n"
        "- get_real_estate_transactions: 不動産取引価格情報を取得\n"
        "- search_invoice_by_name: 会社名からインボイス登録番号を検索"
        "（★インボイス確認はまずこれを使う）\n"
        "- check_invoice_registration: 登録番号（T+13桁）が既知の場合のみ使用\n"
        "- validate_invoice_on_date: 指定日時点でのインボイス登録有効性を確認\n"
        "- resolve_area: 地域名をコードに変換\n"
        "- list_available_stats: 利用可能な統計分野一覧\n"
        "- get_meta_info: 統計表の分類情報を取得\n\n"
        "【重要】インボイス関連の使い分け:\n"
        "- 会社名からインボイス番号を調べたい → search_invoice_by_name を使う"
        "（1回で法人検索+インボイス確認を自動実行）\n"
        "- 登録番号（T+13桁）が分かっている → check_invoice_registration\n"
        "- search_corporations → check_invoice_registration の2段階は不要。"
        "search_invoice_by_name が内部で自動チェーンする。\n\n"
        "地域名は日本語で指定できます（例: 東京都、大阪府、福岡県）。"
    ),
    lifespan=lifespan,
)


# ------------------------------------------------------------------
# コアツール
# ------------------------------------------------------------------


@mcp.tool()
async def search_statistics(
    keyword: str,
    ctx: Context,
    survey_years: str | None = None,
    stats_field: str | None = None,
    limit: int = 10,
) -> str:
    """キーワードで統計表を検索する.

    Args:
        keyword: 検索キーワード（例: "人口", "国勢調査", "県内総生産"）
        survey_years: 調査年で絞り込み（例: "2020", "2015-2020"）
        stats_field: 統計分野コードで絞り込み（例: "02"=人口・世帯）。
                     list_available_stats で分野一覧を確認できます。
        limit: 取得件数上限（デフォルト10）

    Returns:
        検索結果の統計表一覧（ID・統計名・タイトルなど）
    """
    client = _get_client(ctx)
    await ctx.info(f"統計表を検索中: {keyword}")

    tables = await client.search_stats(
        keyword,
        survey_years=survey_years,
        stats_field=stats_field,
        limit=limit,
    )

    if not tables:
        return f"「{keyword}」に該当する統計表が見つかりませんでした。"

    lines: list[str] = [f"## 検索結果: 「{keyword}」（{len(tables)}件）\n"]
    for t in tables:
        lines.append(f"- **{t.title}**")
        lines.append(f"  - 統計表ID: `{t.id}`")
        lines.append(f"  - 統計名: {t.stat_name}（{t.gov_org}）")
        if t.survey_date:
            lines.append(f"  - 調査年月: {t.survey_date}")
        lines.append("")

    lines.append(
        "> 統計表IDを `get_regional_data` や `compare_regions` に渡すと"
        "データを取得できます。"
    )
    return "\n".join(lines)


@mcp.tool()
async def get_regional_data(
    stats_data_id: str,
    area: str,
    ctx: Context,
    tab_code: str | None = None,
    time_code: str | None = None,
    cat01_code: str | None = None,
    limit: int = 1000,
    summary: bool = False,
) -> str:
    """指定した地域の統計データを取得し、整形して返す.

    コード番号は自動的に人間が読める名称に変換される。

    Args:
        stats_data_id: 統計表ID（search_statistics で取得）
        area: 地域名（例: "東京都"）または地域コード（例: "13000"）
        tab_code: 表章項目コード（特定の指標に絞り込む場合）
        time_code: 時間軸コード（特定の年に絞り込む場合）
        cat01_code: 分類事項01コード（特定のカテゴリに絞り込む場合）
        limit: 取得件数上限（デフォルト1000）
        summary: Trueの場合、最新時点の主要指標のみ返す（データ量を大幅に削減）

    Returns:
        整形済みの統計データ（マークダウンテーブル）
    """
    client = _get_client(ctx)

    # 地域名→コード変換
    try:
        area_code = _resolve_single_area(area)
    except AmbiguousAreaError as e:
        return str(e)
    await ctx.info(f"統計データを取得中: {stats_data_id} (地域: {area})")

    data = await client.get_stats_data(
        stats_data_id,
        cd_area=area_code,
        cd_tab=tab_code,
        cd_time=time_code,
        cd_cat01=cat01_code,
        limit=limit,
    )

    if not data.values:
        return f"該当するデータが見つかりませんでした（統計表ID: {stats_data_id}, 地域: {area}）。"

    fmt = StatsFormatter(data)

    # summary モード: 最新時点に絞り込み + 行数制限
    summary_note = ""
    filters: dict[str, str] | None = None
    md_limit: int | None = None
    if summary:
        latest = fmt.latest_time_code()
        if latest:
            filters = {"time": latest}
            time_name = fmt._meta.resolve_code("time", latest)
            summary_note = f"*{time_name or latest} のデータのみ表示*\n\n"
        md_limit = 50

    md = fmt.to_markdown(filters=filters, limit=md_limit)

    total = f"（全{data.total_count}件）" if data.total_count else ""
    header = f"## 統計データ: {stats_data_id} {total}\n{summary_note}"
    footer = build_source_footer(
        None, table_id=stats_data_id, area_name=area, area_code=area_code
    )
    return header + md + footer


@mcp.tool()
async def compare_regions(
    stats_data_id: str,
    areas: list[str],
    ctx: Context,
    tab_code: str | None = None,
    cat01_code: str | None = None,
) -> str:
    """複数地域の統計データを比較する.

    時間軸（年）を行、地域を列にしたピボットテーブルを生成。

    Args:
        stats_data_id: 統計表ID（search_statistics で取得）
        areas: 比較する地域名のリスト（例: ["東京都", "大阪府", "愛知県"]）
        tab_code: 表章項目コード（特定の指標に絞り込む場合）
        cat01_code: 分類事項01コード（特定のカテゴリに絞り込む場合）

    Returns:
        地域比較のピボットテーブル（マークダウン）
    """
    client = _get_client(ctx)

    try:
        area_codes = [_resolve_single_area(a) for a in areas]
    except AmbiguousAreaError as e:
        return str(e)
    cd_area = ",".join(area_codes)

    await ctx.info(f"地域比較データを取得中: {', '.join(areas)}")

    data = await client.get_stats_data(
        stats_data_id,
        cd_area=cd_area,
        cd_tab=tab_code,
        cd_cat01=cat01_code,
    )

    if not data.values:
        return f"該当するデータが見つかりませんでした（統計表ID: {stats_data_id}）。"

    fmt = StatsFormatter(data)

    # ユーザーが明示したフィルタ
    explicit: dict[str, str] = {}
    if tab_code:
        explicit["tab"] = tab_code
    if cat01_code:
        explicit["cat01"] = cat01_code

    # time/area 以外の次元を自動フィルタ（重複セル防止）
    filters = fmt.auto_filters_for_pivot(
        "time", "area", explicit_filters=explicit or None
    )

    md = fmt.pivot_to_markdown("time", "area", filters=filters or None)

    # フィルタで絞り込んだ次元の内容を注記
    filter_notes: list[str] = []
    for dim_id, code in filters.items():
        if dim_id in ("time", "area"):
            continue
        name = fmt._meta.resolve_code(dim_id, code)
        dim_name = fmt._dim_names.get(dim_id, dim_id)
        if name:
            filter_notes.append(f"{dim_name}: {name}")
    note = ""
    if filter_notes:
        note = f"*絞り込み条件: {', '.join(filter_notes)}*\n\n"

    header = f"## 地域比較: {', '.join(areas)}\n{note}"
    footer = build_source_footer(
        None,
        table_id=stats_data_id,
        area_name=", ".join(areas),
        area_code=cd_area,
    )
    return header + md + footer


# ------------------------------------------------------------------
# 法人番号ツール
# ------------------------------------------------------------------

_CORP_NOT_CONFIGURED = (
    "法人番号APIが設定されていません。\n\n"
    "利用するには環境変数 `CORP_APP_ID` にアプリケーションIDを設定してください。\n"
    "取得方法: https://www.houjin-bangou.nta.go.jp/webapi/"
)


@mcp.tool()
async def search_corporations(
    name: str,
    ctx: Context,
    area: str | None = None,
    kind: str | None = None,
    limit: int = 10,
) -> str:
    """法人名で企業を検索する.

    国税庁の法人番号公表サイトから法人情報を検索。
    地域や法人種別で絞り込み可能。

    Args:
        name: 検索キーワード（法人名、部分一致）
        area: 地域名で絞り込み（都道府県名、例: "東京都"）
        kind: 法人種別で絞り込み（"01"=国の機関, "02"=地方公共団体,
              "03"=設立登記法人, "04"=その他）
        limit: 取得件数上限（デフォルト10、最大2000）

    Returns:
        法人情報の一覧（マークダウンテーブル）
    """
    corp_client = _get_corp_client(ctx)
    if corp_client is None:
        return _CORP_NOT_CONFIGURED

    await ctx.info(f"法人を検索中: {name}")

    # 地域指定がある場合は都道府県コードに変換
    pref_code: str | None = None
    if area:
        try:
            code = _resolve_single_area(area)
        except AmbiguousAreaError as e:
            return str(e)
        pref_code = code[:2]  # 上2桁が都道府県コード

    try:
        corps = await corp_client.search_by_name(
            name,
            prefecture_code=pref_code,
            kind=kind,
            limit=limit,
        )
    except CorpApiError as e:
        return f"法人番号API エラー: {e.message}"

    if not corps:
        msg = f"「{name}」に該当する法人が見つかりませんでした。"
        if area:
            msg += f"（地域: {area}）"
        return msg

    return _format_corp_list(corps, name, area)


@mcp.tool()
async def get_corporation(
    corp_number: str,
    ctx: Context,
) -> str:
    """法人番号から企業の詳細情報を取得する.

    13桁の法人番号を指定して、法人の正式名称・所在地・種別などを取得。

    Args:
        corp_number: 法人番号（13桁の数字）

    Returns:
        法人の詳細情報（マークダウン）
    """
    corp_client = _get_corp_client(ctx)
    if corp_client is None:
        return _CORP_NOT_CONFIGURED

    await ctx.info(f"法人情報を取得中: {corp_number}")

    try:
        corps = await corp_client.get_by_number([corp_number])
    except CorpApiError as e:
        return f"法人番号API エラー: {e.message}"

    if not corps:
        return f"法人番号 `{corp_number}` に該当する法人が見つかりませんでした。"

    corp = corps[0]
    return _format_corp_detail(corp)


def _format_corp_list(
    corps: list[Corporation],
    query: str,
    area: str | None = None,
) -> str:
    """法人リストをマークダウンテーブルに整形する."""
    lines: list[str] = [f"## 法人検索結果: 「{query}」（{len(corps)}件）\n"]

    headers = ["法人名", "法人番号", "種別", "所在地", "状態"]
    lines.append("| " + " | ".join(headers) + " |")
    lines.append("| " + " | ".join("---" for _ in headers) + " |")

    for c in corps:
        status = "現存" if c.is_active else "閉鎖"
        row = [
            c.name,
            f"`{c.corporate_number}`",
            c.kind_label,
            f"{c.prefecture_name}{c.city_name}",
            status,
        ]
        lines.append("| " + " | ".join(row) + " |")

    lines.append(_build_corp_search_footer(query, area))
    return "\n".join(lines)


def _format_corp_detail(corp: Corporation) -> str:
    """法人の詳細情報をマークダウンに整形する."""
    lines: list[str] = [f"## {corp.name}\n"]
    lines.append(f"- **法人番号**: `{corp.corporate_number}`")
    lines.append(f"- **法人種別**: {corp.kind_label}")
    lines.append(f"- **所在地**: {corp.full_address}")
    if corp.post_code:
        lines.append(f"- **郵便番号**: {corp.post_code}")
    if corp.furigana:
        lines.append(f"- **フリガナ**: {corp.furigana}")
    lines.append(f"- **法人番号指定日**: {corp.assignment_date}")
    if corp.change_date:
        lines.append(f"- **最終変更日**: {corp.change_date}")
    lines.append(f"- **状態**: {'現存' if corp.is_active else '閉鎖'}")
    if corp.close_date:
        lines.append(f"- **閉鎖日**: {corp.close_date}")

    lines.append(_build_corp_detail_footer(corp))
    return "\n".join(lines)


def _build_corp_search_footer(
    query: str, area: str | None = None
) -> str:
    """法人検索の検証フッターを生成する."""
    now = datetime.now(_JST).strftime("%Y-%m-%d %H:%M JST")
    lines: list[str] = ["", "---", "**データ検証情報**"]
    lines.append("- 出典: 国税庁 法人番号公表サイト")
    lines.append(
        "- 法人番号公表サイトで確認: "
        "https://www.houjin-bangou.nta.go.jp/"
    )
    search_parts = [query]
    if area:
        search_parts.append(area)
    lines.append(f"- 検索条件: {' / '.join(search_parts)}")
    lines.append(f"- データ取得日時: {now}")
    lines.append(
        "- ⚠ 本データは法人番号公表サイト Web-API から自動取得した値をそのまま表示しています。"
        "正確性の最終確認は上記リンクから原本データをご参照ください。"
    )
    return "\n".join(lines)


def _build_corp_detail_footer(corp: Corporation) -> str:
    """法人詳細の検証フッターを生成する."""
    now = datetime.now(_JST).strftime("%Y-%m-%d %H:%M JST")
    lines: list[str] = ["", "---", "**データ検証情報**"]
    lines.append("- 出典: 国税庁 法人番号公表サイト")
    lines.append(f"- 法人番号公表サイトで確認: {corp.verification_url}")
    lines.append(f"- 法人番号: {corp.corporate_number}")
    lines.append(f"- データ取得日時: {now}")
    lines.append(
        "- ⚠ 本データは法人番号公表サイト Web-API から自動取得した値をそのまま表示しています。"
        "正確性の最終確認は上記リンクから原本データをご参照ください。"
    )
    return "\n".join(lines)


# ------------------------------------------------------------------
# 不動産取引価格ツール
# ------------------------------------------------------------------

_REALESTATE_NOT_CONFIGURED = (
    "不動産取引価格APIが設定されていません。\n\n"
    "利用するには環境変数 `REALESTATE_API_KEY` にAPIキーを設定してください。\n"
    "取得方法: https://www.reinfolib.mlit.go.jp/ex-api/"
)


@mcp.tool()
async def get_real_estate_transactions(
    area: str,
    ctx: Context,
    year: int | None = None,
    quarter: int | None = None,
) -> str:
    """不動産取引価格情報を取得する.

    国土交通省の不動産情報ライブラリから、指定地域の不動産取引データを取得。
    取引種別・価格・面積・建築年・最寄駅などの情報を含む。

    Args:
        area: 地域名（例: "東京都", "水戸市"）または地域コード
        year: 取引年で絞り込み（例: 2023）
        quarter: 四半期で絞り込み（1〜4）

    Returns:
        不動産取引データの一覧と価格サマリー（マークダウン）
    """
    re_client = _get_realestate_client(ctx)
    if re_client is None:
        return _REALESTATE_NOT_CONFIGURED

    try:
        area_code = _resolve_single_area(area)
    except AmbiguousAreaError as e:
        return str(e)

    area_name = _get_area_display_name(area, area_code)
    pref_code = area_code[:2]
    # 都道府県コードの場合は市区町村を指定しない
    city_code = area_code if not area_code.endswith("000") else None

    await ctx.info(f"不動産取引データを取得中: {area_name}")

    try:
        transactions = await re_client.get_transactions(
            pref_code,
            city_code=city_code,
            year=year,
            quarter=quarter,
        )
    except RealEstateApiError as e:
        return f"不動産情報ライブラリAPI エラー: {e.message}"

    return format_transactions(
        transactions,
        area_name=area_name,
        year=year,
        quarter=quarter,
    )


# ------------------------------------------------------------------
# インボイスツール
# ------------------------------------------------------------------

_INVOICE_NOT_CONFIGURED = (
    "インボイスAPIが設定されていません。\n\n"
    "利用するには環境変数 `CORP_APP_ID` にアプリケーションIDを設定してください。\n"
    "取得方法: https://www.houjin-bangou.nta.go.jp/webapi/"
)


@mcp.tool()
async def check_invoice_registration(
    number: str,
    ctx: Context,
    history: bool = False,
) -> str:
    """適格請求書発行事業者の登録情報を登録番号で確認する.

    インボイス制度に基づく適格請求書発行事業者の登録状況・
    名称・所在地などを確認できる。

    Args:
        number: 登録番号（T+13桁の数字、例: "T1234567890123"）。
                カンマ区切りで最大10件まで同時に検索可能。
        history: 変更履歴を含めるか（デフォルト: False）

    Returns:
        登録事業者の情報（マークダウン）
    """
    invoice_client = _get_invoice_client(ctx)
    if invoice_client is None:
        return _INVOICE_NOT_CONFIGURED

    numbers = [n.strip() for n in number.split(",") if n.strip()]
    if not numbers:
        return "登録番号を指定してください。"

    for n in numbers:
        if not _is_valid_invoice_number(n):
            return (
                f"登録番号 `{n}` の形式が不正です。\n"
                "T + 13桁の数字で指定してください（例: T1234567890123）。"
            )

    await ctx.info(f"インボイス登録情報を確認中: {', '.join(numbers)}")

    try:
        issuers = await invoice_client.get_by_number(
            numbers, history=history
        )
    except InvoiceApiError as e:
        return f"インボイスAPI エラー: {e.message}"

    if not issuers:
        return (
            f"登録番号 `{number}` に該当する"
            "適格請求書発行事業者が見つかりませんでした。"
        )

    if len(issuers) == 1:
        return _format_invoice_detail(issuers[0])
    return _format_invoice_list(issuers)


@mcp.tool()
async def validate_invoice_on_date(
    number: str,
    day: str,
    ctx: Context,
) -> str:
    """指定日時点での適格請求書発行事業者の登録有効性を確認する.

    特定の取引日に事業者がインボイス発行資格を持っていたかを確認できる。

    Args:
        number: 登録番号（T+13桁の数字、例: "T1234567890123"）
        day: 確認日（YYYY-MM-DD形式、例: "2024-12-01"）

    Returns:
        指定日時点の登録状態（マークダウン）
    """
    invoice_client = _get_invoice_client(ctx)
    if invoice_client is None:
        return _INVOICE_NOT_CONFIGURED

    if not _is_valid_invoice_number(number):
        return (
            f"登録番号 `{number}` の形式が不正です。\n"
            "T + 13桁の数字で指定してください（例: T1234567890123）。"
        )

    await ctx.info(f"インボイス有効性を確認中: {number}（{day}時点）")

    try:
        issuer = await invoice_client.validate_on_date(number, day)
    except InvoiceApiError as e:
        return f"インボイスAPI エラー: {e.message}"

    if issuer is None:
        return (
            f"登録番号 `{number}` は {day} 時点で"
            "適格請求書発行事業者として登録されていません。"
        )

    header = f"**{day} 時点の登録状態**\n\n"
    return header + _format_invoice_detail(issuer)


@mcp.tool()
async def search_invoice_by_name(
    name: str,
    ctx: Context,
    area: str | None = None,
    limit: int = 5,
) -> str:
    """会社名からインボイス登録番号を検索する.

    法人番号APIで会社名を検索し、該当法人のインボイス登録状況を
    自動で確認する。法人番号 → 登録番号（T+法人番号）の変換を
    内部で行うため、登録番号を知らなくても検索できる。

    ※ 個人事業主は法人番号を持たないため、このツールでは検索できません。
    個人事業主の場合は登録番号（T+13桁）を直接指定して
    check_invoice_registration をご利用ください。

    Args:
        name: 検索キーワード（会社名、部分一致）
        area: 地域名で絞り込み（都道府県名、例: "東京都"）
        limit: 取得件数上限（デフォルト5、最大10）

    Returns:
        インボイス登録情報の一覧（マークダウン）
    """
    corp_client = _get_corp_client(ctx)
    invoice_client = _get_invoice_client(ctx)
    if corp_client is None or invoice_client is None:
        return _INVOICE_NOT_CONFIGURED

    await ctx.info(f"法人を検索中: {name}")

    # 地域絞り込み
    pref_code: str | None = None
    if area:
        try:
            code = _resolve_single_area(area)
        except AmbiguousAreaError as e:
            return str(e)
        pref_code = code[:2]

    # Step 1: 法人番号APIで会社名検索
    limit = min(limit, 10)  # インボイスAPIは最大10件同時検索
    try:
        corps = await corp_client.search_by_name(
            name, prefecture_code=pref_code, limit=limit,
        )
    except CorpApiError as e:
        return f"法人番号API エラー: {e.message}"

    if not corps:
        msg = f"「{name}」に該当する法人が見つかりませんでした。"
        if area:
            msg += f"（地域: {area}）"
        msg += (
            "\n\n※ 個人事業主のインボイス登録番号は名称検索に対応していません。"
            "\n登録番号（T+13桁）を直接指定して"
            " `check_invoice_registration` をご利用ください。"
        )
        return msg

    # Step 2: 法人番号 → 登録番号に変換してインボイスAPI検索
    invoice_numbers = [f"T{c.corporate_number}" for c in corps]
    await ctx.info(
        f"インボイス登録状況を確認中（{len(invoice_numbers)}件）"
    )

    try:
        issuers = await invoice_client.get_by_number(invoice_numbers)
    except InvoiceApiError as e:
        return f"インボイスAPI エラー: {e.message}"

    # 登録番号でルックアップ用マップ作成
    issuer_map: dict[str, InvoiceIssuer] = {
        iss.registrated_number: iss for iss in issuers
    }

    # Step 3: 結果を整形（法人情報 + インボイス登録状況）
    lines: list[str] = [
        f"## インボイス登録検索: 「{name}」（{len(corps)}件）\n"
    ]

    headers = ["法人名", "法人番号", "登録番号", "所在地", "インボイス登録"]
    lines.append("| " + " | ".join(headers) + " |")
    lines.append("| " + " | ".join("---" for _ in headers) + " |")

    for corp in corps:
        inv_num = f"T{corp.corporate_number}"
        iss = issuer_map.get(inv_num)
        if iss:
            status = iss.status_label
        else:
            status = "未登録"
        row = [
            corp.name,
            f"`{corp.corporate_number}`",
            f"`{inv_num}`",
            f"{corp.prefecture_name}{corp.city_name}",
            status,
        ]
        lines.append("| " + " | ".join(row) + " |")

    lines.append("")
    lines.append(
        "> 詳細を確認するには `check_invoice_registration` に"
        "登録番号を指定してください。"
    )
    lines.append("")
    lines.append(
        "※ 個人事業主は法人番号を持たないため、このツールでは検索できません。"
        "個人事業主の場合は登録番号（T+13桁）を直接指定して"
        " `check_invoice_registration` をご利用ください。"
    )
    lines.append(_build_invoice_search_footer())
    return "\n".join(lines)


def _is_valid_invoice_number(number: str) -> bool:
    """登録番号の形式を検証する（T + 13桁）."""
    return (
        len(number) == 14
        and number[0] == "T"
        and number[1:].isdigit()
    )


def _format_invoice_detail(issuer: InvoiceIssuer) -> str:
    """事業者の詳細情報をマークダウンに整形する."""
    lines: list[str] = [f"## {issuer.name}\n"]
    lines.append(f"- **登録番号**: `{issuer.registrated_number}`")
    lines.append(f"- **区分**: {issuer.kind_label}")
    lines.append(f"- **登録状態**: {issuer.status_label}")
    lines.append(f"- **登録年月日**: {issuer.registration_date}")
    if issuer.display_address:
        lines.append(f"- **所在地**: {issuer.display_address}")
    if issuer.kana:
        lines.append(f"- **フリガナ**: {issuer.kana}")
    if issuer.trade_name:
        lines.append(f"- **屋号**: {issuer.trade_name}")
    if issuer.process_label:
        lines.append(f"- **処理区分**: {issuer.process_label}")
    lines.append(f"- **更新年月日**: {issuer.update_date}")

    lines.append(_build_invoice_footer(issuer))
    return "\n".join(lines)


def _format_invoice_list(issuers: list[InvoiceIssuer]) -> str:
    """事業者リストをマークダウンテーブルに整形する."""
    lines: list[str] = [
        f"## インボイス登録情報（{len(issuers)}件）\n"
    ]

    headers = ["名称", "登録番号", "区分", "所在地", "登録状態"]
    lines.append("| " + " | ".join(headers) + " |")
    lines.append("| " + " | ".join("---" for _ in headers) + " |")

    for iss in issuers:
        row = [
            iss.name,
            f"`{iss.registrated_number}`",
            iss.kind_label,
            iss.display_address or "-",
            iss.status_label,
        ]
        lines.append("| " + " | ".join(row) + " |")

    lines.append(_build_invoice_search_footer())
    return "\n".join(lines)


def _build_invoice_footer(issuer: InvoiceIssuer) -> str:
    """インボイス詳細の検証フッターを生成する."""
    now = datetime.now(_JST).strftime("%Y-%m-%d %H:%M JST")
    lines: list[str] = ["", "---", "**データ検証情報**"]
    lines.append("- 出典: 国税庁 適格請求書発行事業者公表サイト")
    lines.append(
        f"- 公表サイトで確認: {issuer.verification_url}"
    )
    lines.append(f"- 登録番号: {issuer.registrated_number}")
    lines.append(f"- データ取得日時: {now}")
    lines.append(
        "- ⚠ 本データはインボイス公表サイト Web-API"
        " から自動取得した値をそのまま表示しています。"
        "正確性の最終確認は上記リンクから原本データをご参照ください。"
    )
    return "\n".join(lines)


def _build_invoice_search_footer() -> str:
    """インボイス検索の検証フッターを生成する."""
    now = datetime.now(_JST).strftime("%Y-%m-%d %H:%M JST")
    lines: list[str] = ["", "---", "**データ検証情報**"]
    lines.append("- 出典: 国税庁 適格請求書発行事業者公表サイト")
    lines.append(
        "- 公表サイトで確認: "
        "https://www.invoice-kohyo.nta.go.jp/"
    )
    lines.append(f"- データ取得日時: {now}")
    lines.append(
        "- ⚠ 本データはインボイス公表サイト Web-API"
        " から自動取得した値をそのまま表示しています。"
        "正確性の最終確認は上記リンクから原本データをご参照ください。"
    )
    return "\n".join(lines)


# ------------------------------------------------------------------
# ユーティリティツール
# ------------------------------------------------------------------


@mcp.tool()
async def resolve_area(name: str) -> str:
    """地域名から e-Stat の地域コードを検索する.

    都道府県名の部分一致で検索可能。
    「東京」→「東京都 (13000)」のように接尾辞なしでもマッチする。

    Args:
        name: 地域名（例: "東京", "大阪府", "北海"）

    Returns:
        マッチした地域名と地域コードの一覧
    """
    matches = resolve_area_code(name)

    if not matches:
        return f"「{name}」に該当する地域が見つかりませんでした。"

    lines = [f"## 地域コード検索: 「{name}」\n"]
    for pref_name, code in matches:
        lines.append(f"- **{pref_name}**: `{code}`")
    return "\n".join(lines)


@mcp.tool()
async def list_available_stats() -> str:
    """利用可能な統計分野の一覧を表示する.

    search_statistics の stats_field パラメータに使えるコードの一覧。

    Returns:
        統計分野コードと名称の一覧
    """
    fields = list_stats_fields()

    lines = ["## 統計分野一覧\n"]
    lines.append("| コード | 分野名 |")
    lines.append("| --- | --- |")
    for f in fields:
        lines.append(f"| `{f['code']}` | {f['name']} |")

    lines.append("")
    lines.append(
        "> `search_statistics` の `stats_field` パラメータに"
        "コードを指定して検索を絞り込めます。"
    )
    return "\n".join(lines)


@mcp.tool()
async def get_meta_info(
    stats_data_id: str,
    ctx: Context,
) -> str:
    """統計表のメタ情報（分類コード体系）を取得する.

    統計表にどのような次元（地域・時間・カテゴリ等）があるか、
    各次元にどのようなコードが定義されているかを確認できる。
    データ取得前の下調べに便利。

    Args:
        stats_data_id: 統計表ID（search_statistics で取得）

    Returns:
        分類オブジェクトの一覧（各次元のコード→名称マッピング）
    """
    client = _get_client(ctx)
    await ctx.info(f"メタ情報を取得中: {stats_data_id}")

    meta = await client.get_meta_info(stats_data_id)

    lines = [f"## メタ情報: {stats_data_id}\n"]
    for co in meta.class_objects:
        lines.append(f"### {co.name}（ID: `{co.id}`）")
        # 件数が多い場合は最初の20件 + 省略表示
        display_items = co.items[:20]
        for item in display_items:
            unit_str = f"（単位: {item.unit}）" if item.unit else ""
            lines.append(f"- `{item.code}`: {item.name}{unit_str}")
        if len(co.items) > 20:
            lines.append(f"- ...他 {len(co.items) - 20} 件")
        lines.append("")

    return "\n".join(lines)


# ------------------------------------------------------------------
# プリセットツール
# ------------------------------------------------------------------


@mcp.tool()
async def get_population(
    area: str,
    ctx: Context,
) -> str:
    """地域の人口データを自動取得する（プリセット）.

    統計表IDを知らなくても、地域名を指定するだけで
    人口推計や国勢調査から人口推移データを取得できる。

    Args:
        area: 地域名（例: "東京都"）または地域コード（例: "13000"）

    Returns:
        人口推移の整形済みレポート（マークダウン）
    """
    client = _get_client(ctx)
    try:
        area_code = _resolve_single_area(area)
    except AmbiguousAreaError as e:
        return str(e)
    area_name = _get_area_display_name(area, area_code)

    await ctx.info(f"人口データを取得中: {area_name}")
    return await fetch_population(client, area_code, area_name)


@mcp.tool()
async def get_regional_profile(
    area: str,
    ctx: Context,
) -> str:
    """地域の総合プロファイルを自動取得する（プリセット）.

    人口・経済・労働など複数分野の統計データを自動検索・取得し、
    1つのレポートにまとめる。地域の概要を素早く把握したいときに便利。

    Args:
        area: 地域名（例: "東京都"）または地域コード（例: "13000"）

    Returns:
        地域の総合プロファイル（マークダウン）
    """
    client = _get_client(ctx)
    try:
        area_code = _resolve_single_area(area)
    except AmbiguousAreaError as e:
        return str(e)
    area_name = _get_area_display_name(area, area_code)

    await ctx.info(f"地域プロファイルを取得中: {area_name}")
    return await fetch_regional_profile(client, area_code, area_name)


# ------------------------------------------------------------------
# ヘルパー
# ------------------------------------------------------------------


def _resolve_single_area(area: str) -> str:
    """地域名または地域コードを地域コードに解決する.

    Raises:
        AmbiguousAreaError: 複数の地域に一致した場合
    """
    # 既にコード形式ならそのまま返す
    if area.isdigit():
        return area

    matches = resolve_area_code(area)
    if len(matches) == 1:
        return matches[0][1]
    if len(matches) > 1:
        raise AmbiguousAreaError(area, matches)

    # マッチしない場合はそのまま渡す（API側でエラーになる）
    return area


def _get_area_display_name(area: str, area_code: str) -> str:
    """表示用の地域名を返す（コード指定の場合は逆引き）."""
    if not area.isdigit():
        # 元の入力が地域名ならそのまま使う
        matches = resolve_area_code(area)
        if matches:
            return matches[0][0]
        return area

    # コード指定の場合は逆引き
    from japan_data_mcp.utils.area_codes import CODE_TO_AREA

    return CODE_TO_AREA.get(area_code, area)


# ------------------------------------------------------------------
# エントリーポイント
# ------------------------------------------------------------------


def main() -> None:
    """MCP サーバーを起動する."""
    mcp.run()


if __name__ == "__main__":
    main()
