#!/usr/bin/env python3
# File: src/noveler/presentation/cli/cli_adapter.py
# Purpose: Provide a thin CLI facade delegating Noveler commands to
#          presentation/domain adapters while maintaining shared logging
#          and event-loop setup.
# Context: Presentation layer entry point invoked by `noveler` executable.
#          Integrates with MCP modules for local command execution.
"""CLI Facade (presentation layer)

- Single entrypoint: `run(argv: list[str] | None = None) -> int`
- Handles: `mcp-server`, `mcp call <tool> '{json}'`, `check <episode|file> [--auto-fix]`, `write <episode> [--dry-run]`
- Event-loop initialization and logging/console setup are constrained to this layer.
"""

from __future__ import annotations

import asyncio
import importlib
import inspect
import json
import os
import sys
from collections.abc import Awaitable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import yaml

from noveler.application.adapters.cli_adapter_factory import CliAdapterFactory
from noveler.application.use_cases.universal_llm_use_case import UniversalLLMUseCase
from noveler.application.use_cases.universal_prompt_request import UniversalPromptRequest
from noveler.domain.value_objects.project_context import ProjectContext
from noveler.domain.value_objects.universal_prompt_execution import PromptType
from noveler.infrastructure.integrations.universal_claude_code_service import (
    UniversalClaudeCodeService,
)
from noveler.presentation.cli.cli_mapping import iter_mappings
from noveler.presentation.cli.mcp_client import MCPClient, MCPClientError
from noveler.presentation.shared.shared_utilities import get_console
from noveler.tools.ddd_validator import DDDValidator, format_cli_output, get_exit_code

_CLIENT = MCPClient()
_CONFIG_MANAGER = CliAdapterFactory.create_config_manager()


def _ensure_event_loop() -> None:
    """Ensure a default event loop exists only when running the CLI."""
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        asyncio.set_event_loop(asyncio.new_event_loop())


def _run_coroutine_sync(coro: Awaitable[Any]) -> Any:
    """Execute an awaitable while coping with already-initialised loops."""

    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro)

    if loop.is_running():
        temp_loop = asyncio.new_event_loop()
        try:
            return temp_loop.run_until_complete(coro)
        finally:
            temp_loop.close()

    return loop.run_until_complete(coro)


def _call_tool_with_fallback(tool: str, payload: dict[str, Any]) -> Any:
    """Invoke an MCP tool, preferring legacy shims when available."""

    legacy_module = sys.modules.get("mcp_servers.noveler.main")
    if legacy_module is None:
        try:
            legacy_module = importlib.import_module("mcp_servers.noveler.main")
        except Exception:
            legacy_module = None

    legacy_executor = getattr(legacy_module, f"execute_{tool}", None) if legacy_module else None

    if callable(legacy_executor):
        try:
            result = legacy_executor(payload)
        except TypeError as original_err:
            try:
                result = legacy_executor(**payload)
            except TypeError:
                raise original_err
        if inspect.isawaitable(result):
            return _run_coroutine_sync(result)
        return result

    return _CLIENT.call_tool(tool, payload)


def _render_help() -> str:
    """Generate help text from CLI command mappings."""
    lines = [
        "Usage:",
        "  noveler mcp-server",
        "  noveler mcp call <tool> '{JSON}'",
        "  noveler ddd validate [--path <path>] [--threshold <score>]",
        "  noveler check <episode|file> [--auto-fix] [--exclude-dialogue]",
        "  noveler write <episode> [--dry-run]",
        "",
        "Available Commands:",
    ]

    for mapping in iter_mappings():
        lines.append(f"  {mapping.cli_command}")
        lines.append(f"    → {mapping.mcp_tool}: {mapping.description}")

    lines.extend([
        "",
        "Examples:",
        "  noveler mcp call run_quality_checks '{\"episode_number\":1,\"file_path\":\"README.md\",\"exclude_dialogue_lines\":true}'",
        "  noveler mcp call enhanced_get_writing_tasks '{\"episode_number\":1}'",
        "  noveler ddd validate --path . --threshold 85",
    ])

    return "\n".join(lines)


def _parse_args(argv: list[str]) -> tuple[str, list[str]]:
    if not argv:
        return "", []
    return argv[0], argv[1:]


def _parse_write_args(tokens: list[str]) -> tuple[int, bool]:
    """Parse arguments for the ``write`` command.

    Returns the selected episode number and dry-run flag. When no explicit
    episode number is supplied the function defaults to episode 1 so that
    invocations such as ``noveler write --dry-run`` remain intuitive.
    """

    episode: int | None = None
    dry_run = False

    for token in tokens:
        if token == "--dry-run":
            dry_run = True
            continue

        if token.startswith("--"):
            raise ValueError(f"unknown option: {token}")

        if episode is None:
            episode = int(token)
            continue

        raise ValueError("multiple episode numbers provided")

    if episode is None:
        episode = 1

    return episode, dry_run


def _extract_score(payload: Any, *, fallback: float = 0.0) -> float:
    """Best-effort extraction of a numeric score from MCP responses.

    Handles the three response envelopes we currently see:
    1) 旧来の `{score: ...}` フラット辞書
    2) `{"result": {"score": ...}}` の軽量MCPレスポンス
    3) `{"result": {"data": {"score": ...}}}` のJSON-RPC風ラッパー
    """
    try:
        if isinstance(payload, dict):
            if "score" in payload:
                return float(payload.get("score") or fallback)
            inner = payload.get("result")
            if isinstance(inner, dict):
                if "score" in inner:
                    return float(inner.get("score") or fallback)
                inner_data = inner.get("data")
                if isinstance(inner_data, dict) and "score" in inner_data:
                    return float(inner_data.get("score") or fallback)
    except Exception:
        return fallback
    return fallback


def _extract_success(payload: Any, *, default: bool = True) -> bool:
    """Best-effort extraction of success flags from MCP responses.

    想定パターン:
    - 旧来の `{success: bool}`
    - MCPの `{"result": {"success": bool}}` ラッパー
    将来のフォーマット拡張時はここに分岐を足す。
    """
    if isinstance(payload, dict):
        if "success" in payload:
            return bool(payload["success"])
        inner = payload.get("result")
        if isinstance(inner, dict) and "success" in inner:
            return bool(inner["success"])
    return default


def _render_ddd_usage() -> str:
    return "Usage: noveler ddd validate [--path=<project_root>] [--threshold=<score>]"


def _parse_ddd_args(tokens: list[str]) -> tuple[Path, float]:
    project_path = Path.cwd()
    threshold = 90.0

    for token in tokens:
        if token.startswith("--path="):
            value = token.split("=", 1)[1]
            if not value:
                raise ValueError("path value is empty")
            project_path = Path(value).expanduser().resolve()
        elif token.startswith("--threshold="):
            value = token.split("=", 1)[1]
            if not value:
                raise ValueError("threshold value is empty")
            threshold = float(value)
        elif token in ("-h", "--help"):
            raise ValueError("help requested")
        else:
            raise ValueError(f"unknown option: {token}")

    return project_path, threshold


def run(argv: list[str] | None = None) -> int:
    """CLI facade entrypoint.

    Returns an exit code (0=success, non-zero=failure).
    """
    console = get_console()
    _ensure_event_loop()

    args = list(sys.argv[1:] if argv is None else argv)

    # Basic help
    if not args or args[0] in ("-h", "--help"):
        console.print(_render_help())
        return 0

    try:
        cmd, rest = _parse_args(args)

        if cmd == "mcp-server":
            # Lazy import to avoid unnecessary dependencies outside this path
            mcp_main = importlib.import_module("mcp_servers.noveler.main").main

            asyncio.run(mcp_main())
            return 0

        if cmd == "mcp":
            if not rest or rest[0] != "call" or len(rest) < 2:
                console.print("Usage: noveler mcp call <tool> '{JSON}'")
                return 2
            tool = rest[1]
            json_str = rest[2] if len(rest) >= 3 else "{}"
            try:
                args_obj: dict[str, Any] = json.loads(json_str)
            except Exception as e:
                console.print(f"❌ JSONパースエラー: {e}")
                return 2
            try:
                result = _call_tool_with_fallback(tool, args_obj)
            except MCPClientError as exc:
                console.print(f"❌ MCPツール呼び出しに失敗しました: {exc}")
                return 2
            except Exception as exc:  # pragma: no cover
                console.print(f"❌ MCPツール呼び出しに失敗しました: {exc}")
                return 2

            try:
                console.print(json.dumps(result, ensure_ascii=False, indent=2))
            except Exception:
                console.print(str(result))

            success = True
            if isinstance(result, dict):
                success = bool(result.get("success", True))
            return 0 if success else 1

        if cmd == "ddd":
            subcommand = rest[0] if rest else "validate"
            if subcommand != "validate":
                console.print(_render_ddd_usage())
                return 2

            try:
                project_path, threshold = _parse_ddd_args(rest[1:])
            except ValueError as err:
                if str(err) == "help requested":
                    console.print(_render_ddd_usage())
                    return 0
                console.print(f"❌ ddd引数エラー: {err}")
                console.print(_render_ddd_usage())
                return 2

            if not project_path.exists():
                console.print(f"❌ 指定パスが存在しません: {project_path}")
                return 2

            validator = DDDValidator(project_root=project_path)
            result = validator.validate_project(project_path)

            console.print(format_cli_output(result))

            if result.layer_violations:
                console.print("\nLayer violations:")
                for violation in result.layer_violations:
                    path = Path(violation.file_path)
                    try:
                        path = path.relative_to(project_path)
                    except ValueError:
                        pass
                    location = f"{path}:{violation.line_number or '-'}"
                    console.print(f"  - {location} → {violation.message}")

            if result.repository_violations:
                console.print("\nRepository violations:")
                for violation in result.repository_violations:
                    path = Path(violation.file_path)
                    try:
                        path = path.relative_to(project_path)
                    except ValueError:
                        pass
                    location = f"{path}:{violation.line_number or '-'}"
                    console.print(f"  - {location} ({violation.class_name}) → {violation.message}")

            if result.anemic_warnings:
                console.print("\nAnemic domain model warnings:")
                for warning in result.anemic_warnings:
                    console.print(f"  - {warning.entity_name}: {warning.message}")

            if result.custom_rule_violations:
                console.print("\nCustom rule findings:")
                for key, messages in sorted(result.custom_rule_violations.items()):
                    for message in messages:
                        console.print(f"  - {key}: {message}")

            exit_code = get_exit_code(result, threshold=threshold)
            return exit_code

        if cmd == "check":
            # 互換: noveler check <episode|file> [--auto-fix]
            if not rest:
                console.print("Usage: noveler check <episode|file> [--auto-fix]")
                return 2
            target = rest[0]
            auto_fix = "--auto-fix" in rest[1:]
            exclude_dialogue_flag = "--exclude-dialogue" in rest[1:]
            # episode or file
            episode: int | None = None
            file_path: str | None = None
            try:
                episode = int(target)
            except Exception:
                file_path = target

            def _build_run_payload() -> dict[str, Any]:
                additional_params: dict[str, Any] = {
                    "format": "summary",
                    "severity_threshold": "medium",
                    "exclude_dialogue_lines": exclude_dialogue_flag
                    or _CONFIG_MANAGER.get_bool("NOVELER_EXCLUDE_DIALOGUE", default=False),
                }
                if file_path is not None:
                    additional_params["file_path"] = file_path
                return {
                    "episode_number": episode or 1,
                    "additional_params": additional_params,
                }

            def _call(tool: str, payload: dict[str, Any]) -> tuple[Any | None, bool]:
                try:
                    result = _call_tool_with_fallback(tool, payload)
                except MCPClientError as exc:
                    console.print(f"❌ MCPツール呼び出しに失敗しました ({tool}): {exc}")
                    return None, False
                except Exception as exc:
                    console.print(f"❌ MCPツール呼び出しに失敗しました ({tool}): {exc}")
                    return None, False
                return result, True

            run_payload = _build_run_payload()
            run_result, ok = _call("run_quality_checks", run_payload)
            if not ok or run_result is None:
                return 2

            score = _extract_score(run_result)
            console.print(f"品質スコア(現状): {score:.1f}点")

            if auto_fix:
                improved_score = score
                improve_payload = {
                    "episode_number": episode or 1,
                    "additional_params": {
                        "file_path": file_path,
                        "target_score": 80,
                        "max_iterations": 3,
                        "include_diff": False,
                    },
                }
                improve_result, improve_ok = _call("improve_quality_until", improve_payload)
                if improve_ok and improve_result is not None:
                    improved_score = _extract_score(improve_result, fallback=improved_score)
                    console.print(f"自動改善を実行しました（最終スコア）: {improved_score:.1f}点")
                else:
                    # フォールバック: 単回の安全Fixを適用
                    fix_payload = {
                        "episode_number": episode or 1,
                        "additional_params": {"file_path": file_path, "dry_run": False},
                    }
                    fix_result, fix_ok = _call("fix_quality_issues", fix_payload)
                    if not fix_ok or fix_result is None:
                        return 2
                    metadata = fix_result.get("metadata") if isinstance(fix_result, dict) else {}
                    applied = 0
                    if isinstance(metadata, dict):
                        applied = int(metadata.get("applied", 0) or 0)
                    console.print(f"自動修正を適用しました: {applied}件")

                rerun_result, rerun_ok = _call("run_quality_checks", run_payload)
                if not rerun_ok or rerun_result is None:
                    return 2
                rerun_score = _extract_score(rerun_result)
                score = max(improved_score, rerun_score)
                console.print(f"品質スコア(修正後): {score:.1f}点")

            return 0 if score >= 80.0 else 1

        if cmd == "write":
            try:
                episode, dry_run = _parse_write_args(rest)
            except ValueError as err:
                console.print(f"❌ write引数エラー: {err}")
                return 2
            except Exception as err:
                console.print(f"❌ write解析エラー: {err}")
                return 2

            project_root_s = os.getcwd()

            command_str = f"write {episode}"
            if dry_run:
                command_str += " --dry-run"
            mcp_payload = {
                "command": command_str,
                "project_root": project_root_s,
                "options": {
                    "episode_number": episode,
                    "dry_run": dry_run,
                },
            }
            try:
                result = _CLIENT.call_tool("noveler", mcp_payload)
            except MCPClientError as exc:
                console.print_warning(f"⚠️ MCP経由のwrite実行に失敗しました: {exc}")
            else:
                success = _extract_success(result)
                if not success:
                    console.print("❌ write実行エラー: MCP応答が失敗を示しました")
                return 0 if success else 1

            async def _run() -> int:
                try:
                    result = await execute_18_step_writing(
                        episode=episode,
                        dry_run=dry_run,
                        project_root=project_root_s,
                    )
                except Exception as exc:
                    console.print(f"❌ write実行エラー: {exc}")
                    return 1
                return 0 if result.get("success") else 1

            return asyncio.run(_run())

        # 不明コマンド
        console.print("❌ サポートされていないコマンドです")
        console.print("対応コマンド: mcp-server | mcp call | ddd | check | write")
        return 1

    except ImportError as e:
        console.print(f"❌ インポートエラー: {e}")
        return 1
    except Exception as e:
        console.print(f"❌ 実行エラー: {e}")
        return 1


# ===== Write 18-steps (kept for compatibility) =====
from noveler.application.use_cases.universal_llm_use_case import UniversalLLMUseCase


def _ensure_write_result_shape(result: dict[str, Any], *, default_total_steps: int = 19) -> dict[str, Any]:
    """Ensure MCP write results expose the legacy CLI contract structure."""

    normalized = dict(result)
    raw_metadata = normalized.get("metadata")
    metadata: dict[str, Any] = dict(raw_metadata) if isinstance(raw_metadata, dict) else {}
    normalized["metadata"] = metadata

    total_steps = normalized.get("total_steps")
    if not isinstance(total_steps, int):
        total_steps = metadata.get("total_steps")
    if not isinstance(total_steps, int):
        exec_log = normalized.get("execution_log")
        if isinstance(exec_log, list) and exec_log:
            total_steps = len(exec_log)
        else:
            total_steps = default_total_steps
    total_steps = int(total_steps)
    normalized["total_steps"] = total_steps
    metadata.setdefault("total_steps", total_steps)

    completed_steps = normalized.get("completed_steps")
    if not isinstance(completed_steps, int):
        completed_steps = metadata.get("completed_steps")
    if not isinstance(completed_steps, int):
        exec_log = normalized.get("execution_log")
        if isinstance(exec_log, list):
            completed_steps = len(exec_log)
        else:
            completed_steps = total_steps
    completed_steps = int(completed_steps)
    normalized["completed_steps"] = completed_steps
    metadata.setdefault("completed_steps", completed_steps)

    execution_log = normalized.get("execution_log")
    if not isinstance(execution_log, list):
        execution_log = []
    normalized["execution_log"] = execution_log

    saved_files = normalized.get("saved_files")
    if isinstance(saved_files, list):
        normalized["saved_files"] = [str(entry) for entry in saved_files]
    elif isinstance(saved_files, str):
        normalized["saved_files"] = [saved_files]
    else:
        normalized["saved_files"] = []

    content_length = normalized.get("content_length")
    if not isinstance(content_length, int):
        content_source = normalized.get("content") or normalized.get("final_content") or ""
        try:
            content_length = int(content_length)  # type: ignore[arg-type]
        except Exception:
            content_length = len(content_source) if isinstance(content_source, str) else 0
    normalized["content_length"] = max(int(content_length), 0)

    normalized.setdefault("file_manager_used", "EnhancedFileManager")

    completion_rate = normalized.get("completion_rate")
    if not isinstance(completion_rate, str):
        rate = (completed_steps / total_steps * 100) if total_steps else 0.0
        normalized["completion_rate"] = f"{rate:.1f}%"

    success = normalized.get("success")
    if not isinstance(success, bool):
        success_meta = metadata.get("success")
        normalized["success"] = bool(success_meta if isinstance(success_meta, bool) else success)

    metadata.setdefault("success", normalized["success"])

    return normalized


def _extract_write_result_from_envelope(payload: Any) -> dict[str, Any] | None:
    """Normalize MCP noveler tool responses for the write workflow.

    Args:
        payload: Raw response returned by the MCP noveler tool.

    Returns:
        dict[str, Any] | None: Extracted result dictionary compatible with the
        legacy CLI expectations when recognised, otherwise ``None``.
    """
    if not isinstance(payload, dict):
        return None

    # Direct legacy format already matches expectations.
    success_value = payload.get("success")
    if isinstance(success_value, bool):
        return payload

    result_block = payload.get("result")
    if not isinstance(result_block, dict):
        return None

    extracted: dict[str, Any] | None = None

    data_block = result_block.get("data")
    if isinstance(data_block, dict):
        candidate = data_block.get("result")
        if isinstance(candidate, dict):
            extracted = dict(candidate)

    if extracted is None:
        candidate = result_block.get("result")
        if isinstance(candidate, dict):
            extracted = dict(candidate)

    if extracted is None:
        return None

    if isinstance(result_block.get("success"), bool) and "success" not in extracted:
        extracted["success"] = bool(result_block["success"])

    return _ensure_write_result_shape(extracted)


async def execute_18_step_writing(episode: int, dry_run: bool, project_root: str) -> dict:
    """18ステップ執筆システムの実行（共通基盤使用版）"""
    console = get_console()

    payload = {
        "command": f"write {episode}",
        "project_root": project_root,
        "options": {
            "dry_run": dry_run,
            "episode_number": episode,
        },
    }

    try:
        response = await _CLIENT.call_tool_async("noveler", payload)
    except MCPClientError as err:
        raise RuntimeError(f"❌ MCP write委譲に失敗しました（フォールバック不可）: {err}") from err
    except Exception as exc:
        raise RuntimeError(f"❌ MCP write委譲で予期せぬエラーが発生しました（フォールバック不可）: {exc}") from exc

    tool_result = _extract_write_result_from_envelope(response)
    if tool_result is not None:
        needs_saved_file = not dry_run
        saved_files = tool_result.get("saved_files") if isinstance(tool_result, dict) else None
        content_length = tool_result.get("content_length") if isinstance(tool_result, dict) else None
        missing_saved = needs_saved_file and not saved_files
        missing_content = content_length is None or int(content_length) <= 0
        if missing_saved or missing_content:
            reason = "保存先" if missing_saved else "原稿内容"
            raise RuntimeError(
                f"❌ MCP write応答に{reason}が含まれなかったため処理を中止します（フォールバック不可）"
            )
        return tool_result
    raise RuntimeError("❌ MCP write委譲の応答形式が不明のため処理を中止します（フォールバック不可）")


async def _execute_writing_step(step: dict, episode: int, project_path: Path) -> str:
    """個別の執筆ステップを実行してコンテンツを生成"""
    step_id = step["id"]
    step_name = step["name"]
    step_phase = step["phase"]

    # プロジェクト設定.yamlを読み込み
    try:
        project_config_path = project_path / "プロジェクト設定.yaml"
        project_config: dict[str, Any] = {}
        if project_config_path.exists():
            with project_config_path.open("r", encoding="utf-8") as f:
                project_config = yaml.safe_load(f) or {}
    except Exception:
        project_config = {}

    target_word_count = int(project_config.get("target_word_count", 10000))

    # Step 11（初稿生成）の場合のみClaude統合システムを使用
    if step_phase == "執筆実装" and step_id == 11:
        return await _generate_manuscript_with_claude(episode, target_word_count, project_path, project_config)

    content_map = {
        ("構造設計", 0): f"第{episode:03d}話のスコープ: 基本設定と目標の定義（目標文字数: {target_word_count}文字）",
        ("構造設計", 1): "章の目的: 主人公の成長と課題解決",
        ("構造設計", 2): "段階目標: 導入→展開→解決の3段階構成",
        ("構造設計", 3): "テーマ検証: 成長とチャレンジの独自性確認",
        ("構造設計", 4): "バランス設計: 導入20%、展開60%、解決20%",
        ("構造設計", 5): "シーン構成: 開始シーン、展開シーン、解決シーン",
        ("構造設計", 6): "論理検証: 物語の因果関係と整合性確認",
        ("構造設計", 7): "キャラクター一貫性: 主人公の行動・思考パターン確認",
        ("構造設計", 8): "会話設計: 自然な対話と感情表現の設計",
        ("構造設計", 9): "感情曲線: 緊張→緩和→クライマックスの感情変化",
        ("構造設計", 10): "世界観設定: 具体的な場所、時間、雰囲気の描写",
        ("執筆実装", 12): f"文字数調整: 目標{target_word_count}文字に向けて適切な長さに調整し、読みやすさを向上",
        ("執筆実装", 13): "文体改善: 自然な表現と読みやすい構成に修正",
        ("品質保証", 14): "品質チェック: 誤字脱字、構成、整合性の確認完了",
        ("品質保証", 15): "最終確認: 全体品質基準クリア、公開準備完了",
        ("公開", 16): "公開準備: タイトル、タグ、説明文の最終確認完了",
    }

    return content_map.get((step_phase, step_id), f"{step_name}の処理を完了")


async def _generate_manuscript_with_claude(
    episode: int, target_word_count: int, project_path: Path, project_config: dict
) -> str:
    """Claude統合システムを使用した原稿生成"""
    console = get_console()
    console.print(
        f"[blue]🤖 Claude統合執筆システムで第{episode:03d}話を生成中...（目標: {target_word_count}文字）[/blue]"
    )

    try:
        # UniversalLLMUseCaseを初期化
        claude_service = UniversalClaudeCodeService()
        claude_use_case = UniversalLLMUseCase(claude_service)

        # プロジェクトコンテキスト作成
        project_context = ProjectContext(
            project_name=project_config.get("title", "ガイドプロジェクト"),
            project_root=project_path,
        )

        # 執筆プロンプト作成
        writing_prompt = f"""
あなたは熟練した小説家です。以下の条件に従って、第{episode:03d}話の原稿を執筆してください。

## 執筆条件
- 目標文字数: **{target_word_count}文字**
- ジャンル: {project_config.get("genre", "ファンタジー")}
- タイトル: {project_config.get("title", "ガイドプロジェクト")}
- エピソード番号: 第{episode:03d}話

## 構成要求
1. 導入部（約20%）: 設定と状況の説明
2. 展開部（約60%）: 主人公の行動と変化
3. 解決部（約20%）: 結末と次への展望

## 品質要求
- 読みやすい文体で執筆
- 会話と地の文のバランスを取る
- 感情描写を豊かに
- 必ず目標文字数に近づける

原稿をMarkdown形式で出力してください。
"""

        # UniversalPromptRequestを作成
        request = UniversalPromptRequest(
            prompt_type=PromptType.WRITING,
            prompt_content=writing_prompt,
            project_context=project_context,
            output_format="text",
            max_turns=1,
            type_specific_config={
                "target_word_count": target_word_count,
                "episode_number": episode,
                "genre": project_config.get("genre", "ファンタジー"),
            },
        )

        # Claude統合実行
        response = await claude_use_case.execute_with_fallback(request, fallback_enabled=True)

        if response.is_success():
            manuscript = response.response_content
            word_count = len(manuscript)
            console.print(f"[green]✅ Claude執筆完了: {word_count}文字生成[/green]")

            if word_count < target_word_count * 0.8:
                console.print(f"[yellow]⚠️ 文字数不足（目標:{target_word_count}, 生成:{word_count}）[/yellow]")

            return manuscript

        console.print("[yellow]⚠️ Claude実行失敗、フォールバックコンテンツを使用[/yellow]")
        return (
            f"# 第{episode:03d}話 フォールバック原稿\n\n"
            "Claude統合システムが利用できない場合のフォールバック原稿です。\n\n"
            "実際の執筆では、Claude統合により{target_word_count}文字の完全な小説が生成される予定です。\n\n"
            "現在はシステム調整中のため、この簡潔なコンテンツを表示しています。\n"
        )

    except Exception as e:
        console.print(f"[red]❌ Claude統合エラー: {e}[/red]")
        return (
            f"# 第{episode:03d}話 エラー時フォールバック原稿\n\n"
            "システムエラーが発生したため、フォールバック原稿を表示しています。\n\n"
            f"エラー詳細: {str(e)[:100]}...\n\n"
            "実際の執筆では、Claude統合により{target_word_count}文字の完全な小説が生成される予定です。\n"
        )


__all__ = [
    "execute_18_step_writing",
    "iter_mappings",
    "run",
]
