"Specification phase (requirements clarification)."

import json
import os
from datetime import datetime
from pathlib import Path
from typing import List, Optional

import yaml

from cafe.agents.manager import AgentManager
from cafe.core.git import GitOperations
from cafe.core.permission import PermissionHandler
from cafe.core.phase import Phase
from cafe.core.status_codes import PhaseStatusCode, StatusCodeParser, generate_status_code_prompt
from cafe.core.types import PhaseProgress, PhaseResult, PhaseStatus
from cafe.ui.display import Display
from cafe.ui.phase_prompts import prompt_for_input_method, prompt_for_rigor, fetch_github_issue
from cafe.utils.git_utils import get_github_repo_name, get_repo_root, to_cwd_relative_path
from cafe.utils.github import GitHubOps, GitHubError
from cafe.ui.interactive_qa import interactive_qa_flow
from cafe.utils.prompt_utils import format_checklist_instruction

# Maximum number of clarification iterations to prevent infinite loops
MAX_CLARIFICATION_ITERATIONS = 10


def create_github_issue(content: str) -> str:
    """Create a new GitHub issue with content.

    Args:
        content: Issue content

    Returns:
        Issue ID

    Note:
        This is a placeholder. Actual implementation should use gh CLI.
    """
    # TODO: Implement using gh CLI
    # gh issue create --title "..." --body "..."
    raise NotImplementedError("GitHub issue creation not yet implemented")


def update_github_issue(issue_id: str, content: str) -> None:
    """Update existing GitHub issue.

    Args:
        issue_id: Issue ID
        content: Updated content

    Note:
        This is a placeholder. Actual implementation should use gh CLI.
    """
    # TODO: Implement using gh CLI
    # gh issue edit <issue_id> --body "..."
    raise NotImplementedError("GitHub issue update not yet implemented")


class SpecPhase(Phase):
    """Specification phase: Requirements clarification with PM agent."""

    def __init__(
        self,
        agent_manager: AgentManager,
        permission_handler: PermissionHandler,
        git_ops: GitOperations,
        pm_agent: str = "Roger",
        interactive: bool = True,
        issue_name: Optional[str] = None,
        rigor: Optional["SpecRigor"] = None,
        user_input: str = "",
        fetch_issue_id: Optional[int] = None,
        spec_file: Optional[str] = None,  # Deprecated: kept for backward compatibility
        template_path: Optional[Path] = None,
        template_mode: str = "auto",
        sync_github: Optional[bool] = None,
    ) -> None:
        """Initialize requirements phase.

        Args:
            agent_manager: Agent manager
            permission_handler: Permission handler
            git_ops: Git operations
            pm_agent: PM agent name (default: Roger)
            interactive: Enable interactive mode for user input (default: True)
            issue_name: Issue name for history tracking (default: derived from current branch)
            rigor: Specification rigor level (default: medium)
            user_input: User input for non-interactive mode (default: "")
            fetch_issue_id: GitHub issue number to fetch content from (optional)
            spec_file: (Deprecated) Spec file path - ignored, kept for backward compatibility
            template_path: Path to spec template file (optional)
            template_mode: Template selection mode ('auto' or 'manual', default: 'auto')
            sync_github: Whether to sync to GitHub (None=use config default based on issue_id presence)
        """
        super().__init__(interactive=interactive, git_ops=git_ops)

        from cafe.core.types import SpecRigor

        self.agent_manager = agent_manager
        self.permission_handler = permission_handler
        self.pm_agent = pm_agent
        self.user_input = user_input
        self.fetch_issue_id = fetch_issue_id
        self.template_path = template_path
        self.template_mode = template_mode
        self.phase_name = "spec"  # For base class progress tracking

        # Track if rigor was explicitly set (for interactive prompting)
        if rigor is not None:
            self.rigor = rigor
            self._rigor_explicitly_set = True
        else:
            self.rigor = SpecRigor.MEDIUM  # Default, will prompt if interactive
            self._rigor_explicitly_set = False

        # Track sync_github setting (resolved at load time if None)
        self._sync_github_explicit = sync_github  # Store CLI-provided value
        self._sync_github: bool = False  # Default, will be set by _load_issue_config()

        self.iteration = 0

        # Determine issue name for history tracking
        if issue_name:
            self.issue_name = issue_name
        else:
            # Derive from current branch (issue_dir is set by base class)
            self.issue_name = self.issue_dir.name

        # Phase directory for spec phase
        # Path: .cafe/issues/{issue_name}/spec
        self.phase_dir = self.issue_dir / "spec"

        # spec_file will be set in execute() based on iteration number
        self.spec_file: str = ""

        # Config-based settings (loaded from issue.yaml by _load_issue_config())
        self._config_input_method: Optional[str] = None
        self._config_issue_id: Optional[int] = None

        # Load issue_id from issue.yaml if exists (for comment posting after resume)
        self._load_issue_config()

        # Store original requirement (from first iteration)
        self.original_requirement: Optional[str] = None

        # Track requirements and questions
        self.confirmed_requirements = []
        self.pending_questions = []

        # Initialize display for better input handling
        self.display = Display()

        # Restore state from last iteration file (if resuming)
        self.iteration = self._load_iteration_counter()

        # Load phase-specific data from last iteration
        if self.iteration > 0:
            existing_iterations = sorted(self.phase_dir.glob("iteration_*/context.json"))
            if existing_iterations:
                last_context_file = existing_iterations[-1]
                with open(last_context_file, "r", encoding="utf-8") as f:
                    last_data = json.load(f)
                if "confirmed_requirements" in last_data:
                    self.confirmed_requirements = last_data["confirmed_requirements"]
                if "pending_questions" in last_data:
                    self.pending_questions = last_data["pending_questions"]

    def execute(self) -> PhaseResult:
        """Execute requirements clarification phase.

        Returns:
            Phase result
        """
        try:
            # Check if phase is already completed (avoid re-running completed phases)
            from cafe.core.status_codes import PhaseStatusCode
            early_exit_result = self._check_if_already_completed([
                PhaseStatusCode.CONFIRMED,
            ])
            if early_exit_result:
                return early_exit_result

            # Ensure phase directory exists
            self.phase_dir.mkdir(parents=True, exist_ok=True)

            # Get next iteration number and setup versioned file path
            try:
                iteration_number = self._get_next_iteration_number("spec", self.phase_dir)
            except ValueError as e:
                # Exceeded 999 iterations
                # spec_file may not be set yet, use hasattr to check
                spec_file = self.spec_file if hasattr(self, 'spec_file') else None
                return PhaseResult(
                    status=PhaseStatus.FAILED,
                    message=f"Spec phase failed: {str(e)}",
                    data={"spec_file": spec_file},
                )

            # Set self.iteration based on versioned files and history
            # iteration_number is the next file number (count + 1)
            # self.iteration is the current execution iteration
            if self.iteration == 0:  # No history
                # If files exist, this is a resume of the last iteration
                # If no files, this is the first iteration
                self.iteration = max(1, iteration_number - 1)
            else:  # Has history
                # Continue from last completed iteration
                self.iteration += 1

            # Note: Copy of previous version is deferred until just before agent execution
            # to avoid creating new iterations when interrupted before agent is called

            # Set versioned spec_file path
            spec_file_path = self._get_versioned_file_path("spec", iteration_number, self.phase_dir)
            self.spec_file = str(spec_file_path)

            # Fetch issue content from GitHub if --issue-id is provided
            if self.fetch_issue_id:
                error_result = self._fetch_github_issue(self.fetch_issue_id)
                if error_result:
                    return error_result

            # Check if already confirmed - skip execution
            already_completed = self._check_if_already_completed([PhaseStatusCode.CONFIRMED])
            if already_completed:
                return already_completed

            # Check if spec file exists
            spec_path = Path(self.spec_file)
            if spec_path.exists():
                # File already exists (resume case), skip to execution
                pass
            elif iteration_number == 1:
                    # File doesn't exist AND this is first iteration - get initial user story
                    if self.interactive:
                        # If --issue-id not provided, check config or ask user to choose input method
                        if not self.fetch_issue_id:
                            # Check if config has input_method specified
                            if self._config_input_method:
                                # Use config values
                                if self._config_input_method == "github" and self._config_issue_id:
                                    # Fetch from GitHub Issue
                                    error_result = self._fetch_github_issue(self._config_issue_id)
                                    if error_result:
                                        return error_result
                                else:
                                    # Manual input
                                    self._prompt_for_user_story()
                            else:
                                # No config, prompt user
                                method, issue_id = self._prompt_for_input_method()

                                if method == "github" and issue_id:
                                    # Fetch from GitHub Issue
                                    error_result = self._fetch_github_issue(issue_id)
                                    if error_result:
                                        return error_result
                                else:
                                    # Manual input
                                    self._prompt_for_user_story()
                        else:
                            # --issue-id already provided, skip to user story prompt
                            self._prompt_for_user_story()

                    else:
                        # Non-interactive mode: use user_input if provided, otherwise try stdin
                        if self.user_input is None:
                            # Explicitly set to None - not allowed to read from stdin
                            return PhaseResult(
                                status=PhaseStatus.FAILED,
                                message="No user story provided in non-interactive mode",
                                data={"iterations": 0},
                            )
                        elif self.user_input:
                            # User input provided
                            user_story = self.user_input.strip()
                        else:
                            # Try reading from stdin
                            import sys
                            user_story = sys.stdin.read().strip()

                        # Remove END marker if present
                        if user_story.upper().endswith("END"):
                            lines = user_story.split('\n')
                            if lines[-1].strip().upper() == "END":
                                user_story = '\n'.join(lines[:-1]).strip()

                        if not user_story:
                            return PhaseResult(
                                status=PhaseStatus.FAILED,
                                message="No user story provided in non-interactive mode",
                                data={"iterations": 0},
                            )

                        # Create spec file with user story
                        spec_path.write_text(user_story, encoding="utf-8")

            # Ask for rigor level if interactive and not set (after input method selection)
            if self.interactive and iteration_number == 1:
                self._prompt_for_rigor()

            # Capture original requirement before entering clarification loop
            # Note: Iteration is determined by history files, not versioned spec files
            # For iteration 1: read from spec_001.md (initial requirement)
            # For iteration 2+: read from previous iteration's file (spec_{iteration-1}.md)
            if self.iteration > 1:
                # Read from previous iteration's completed spec file
                prev_spec_path = self._get_versioned_file_path("spec", self.iteration - 1, self.phase_dir)
                if prev_spec_path.exists():
                    self.original_requirement = prev_spec_path.read_text(encoding="utf-8").strip()
            else:
                # First iteration: read from spec_001.md if it exists
                first_spec_path = self._get_versioned_file_path("spec", 1, self.phase_dir)
                if first_spec_path.exists():
                    self.original_requirement = first_spec_path.read_text(encoding="utf-8").strip()

            # NOTE: self.iteration is already set from versioned files at line 183
            # No need to increment here as it's handled by _get_next_iteration_number()

            # Safety check: prevent infinite loops
            max_iterations_result = self._check_max_iterations(
                MAX_CLARIFICATION_ITERATIONS,
                "Requirements clarification"
            )
            if max_iterations_result:
                return max_iterations_result

            # Generate checklist for this iteration
            from cafe.utils.checklist_generator import generate_spec_checklist

            checklist_path = self._get_iteration_dir(self.iteration) / "checklist.md"
            prev_spec_file = None
            if self.iteration > 1:
                prev_spec_path = self._get_versioned_file_path("spec", self.iteration - 1, self.phase_dir)
                if prev_spec_path.exists():
                    prev_spec_file = str(prev_spec_path)

            # Define basic principles (language selection is dynamic based on sync setting)
            language_instruction = 'the same language as the "Initial Requirements" section in the spec document' if self._sync_github else "your native language"
            basic_principles = f"""- Write analysis results in {language_instruction}
- Do not include technical details (implementation, architecture, languages, frameworks, databases)
- You may provide 2-3 high-level approach options for the user to consider, but avoid prescribing specific technical solutions
- Treat users as stakeholders unfamiliar with implementation - do NOT ask "how is this currently implemented" or "how should this be implemented"; instead, note such questions in the spec for developers"""

            # Get template file path (convert to relative path for display)
            template_file = None
            if self.template_path:
                try:
                    template_file = to_cwd_relative_path(self.template_path)
                except (ValueError, OSError):
                    template_file = str(self.template_path)

            # Compute questions.xml path for this iteration
            questions_xml_path = self._get_iteration_dir(self.iteration) / "questions.xml"
            try:
                questions_xml_file = to_cwd_relative_path(questions_xml_path)
            except (ValueError, OSError):
                questions_xml_file = str(questions_xml_path.resolve())

            generate_spec_checklist(
                iteration=self.iteration,
                agent_name=self.pm_agent,
                current_spec_file=self.spec_file,
                prev_spec_file=prev_spec_file,
                checklist_file_path=checklist_path,
                basic_principles=basic_principles,
                template_file=template_file,
                template_mode=self.template_mode,
                questions_xml_file=questions_xml_file,
            )

            # Prepare user_input for this iteration
            result_or_input = self._prepare_user_input_for_iteration()
            if isinstance(result_or_input, PhaseResult):
                # Method returned a PhaseResult (completion/failure/pause)
                return result_or_input
            # Otherwise, it's the user input string
            current_user_input = result_or_input

            # Prepare allowed tools with write/edit permission for spec file and checklist
            # Convert to relative path (without / prefix) - plain relative path
            # Use path relative to current working directory (supports worktree)
            try:
                spec_file_pattern = to_cwd_relative_path(self.spec_file)
            except ValueError:
                # Fallback to absolute path if file is not under cwd
                spec_file_pattern = str(Path(self.spec_file).resolve())

            # Get checklist path for this iteration
            iteration_dir = self._get_iteration_dir(self.iteration)
            checklist_file = iteration_dir / "checklist.md"
            try:
                checklist_pattern = to_cwd_relative_path(checklist_file)
            except ValueError:
                checklist_pattern = str(checklist_file.resolve())

            # Get questions.xml path for allowed_tools
            try:
                questions_xml_pattern = to_cwd_relative_path(questions_xml_path)
            except (ValueError, OSError):
                questions_xml_pattern = str(questions_xml_path.resolve())

            # Merge base tools with previous iteration's tools (if any)
            base_allowed_tools = [
                "read",
                "grep",
                "glob",
                "ls",
                "web_fetch",
                "web_search",
                f"edit({spec_file_pattern})",
                f"edit({checklist_pattern})",
                f"edit({questions_xml_pattern})",
            ]
            allowed_tools = self._merge_allowed_tools(base_allowed_tools)

            # Copy previous version just before calling agent (if iteration > 1)
            # This ensures we only create a new iteration when we actually execute the agent
            self._copy_previous_version("spec", iteration_number, self.phase_dir)

            # Execute full agent interaction cycle (generate prompt, execute, handle status)
            result, response = self._execute_and_handle_agent_response(
                agent_name=self.pm_agent,
                user_input=current_user_input,
                valid_status_codes=[
                    PhaseStatusCode.READY_FOR_REVIEW,
                    PhaseStatusCode.NEED_CLARIFICATION,
                ],
                allowed_tools=allowed_tools,
                complete_codes=[PhaseStatusCode.READY_FOR_REVIEW, PhaseStatusCode.NEED_CLARIFICATION],
                continue_codes=[],
            )

            # In mock mode or if agent doesn't use write tool, write spec from response
            self._ensure_spec_file_written(response)

            # Verify content was actually updated (not identical to input/previous version)
            content_check_result, final_response = self._verify_content_updated(
                response=response,
                spec_file_pattern=spec_file_pattern,
                allowed_tools=allowed_tools,
            )

            # Use the final response if content was updated
            if final_response:
                response = final_response

            # Phase-specific post-processing: Sync spec to GitHub (no-op in local mode)
            self._sync_spec_to_github("")

            # Save rigor setting to issue.yaml after each iteration
            self._save_issue_config()

            # Validate questions.xml when agent returns CAFE_NEED_CLARIFICATION
            from cafe.core.status_codes import StatusCodeParser
            status_code = StatusCodeParser.extract(response)
            if status_code == PhaseStatusCode.NEED_CLARIFICATION:
                self._validate_and_retry_questions_xml(
                    xml_path=questions_xml_path,
                    agent_name=self.pm_agent,
                    allowed_tools=allowed_tools,
                )

            # Since we removed the while loop, if result is None (meaning need to continue),
            # we should return IN_PROGRESS with the status code from response
            if result is None:

                return PhaseResult(
                    status=PhaseStatus.IN_PROGRESS,
                    message=f"Spec phase needs more iterations (iteration {self.iteration})",
                    data={
                        "iterations": self.iteration,
                        "status_code": status_code.value if status_code else None,
                        "spec_file": self.spec_file,  # Issue 1: Add full file path
                    },
                )

            return result

        except KeyboardInterrupt:
            # User paused with Ctrl+C - save progress and allow resume
            return self._handle_keyboard_interrupt("spec", {"spec_file": self.spec_file})
        except Exception as e:
            # Use base class helper to handle critical errors
            result = self._handle_exception_in_execute(e, "Spec phase failed")
            # Add phase-specific data
            spec_file = self.spec_file if hasattr(self, 'spec_file') else None
            if spec_file:
                result.data["spec_file"] = spec_file
            return result

    def _get_allowed_directories(self) -> List[str]:
        """Get list of allowed directories for spec phase.

        Extends base class to include spec templates directory.

        Returns:
            List of allowed directories
        """
        from cafe.utils.prompt_utils import get_template_allowed_directories

        dirs = super()._get_allowed_directories()
        dirs.extend(get_template_allowed_directories(self.template_mode, self.template_path, "spec"))
        return dirs

    def _get_completion_data(self) -> dict:
        """Get additional data when phase completes (provided to base class's _handle_standard_status_codes).

        Note: This method is NOT called when user confirms (CONFIRMED status),
        because confirmation bypasses agent execution and returns PhaseResult directly.
        GitHub sync for CONFIRMED status is handled in _sync_confirmed_spec_to_github().

        Returns:
            Dict containing phase-specific data, will be merged into PhaseResult.data
        """
        data = {}

        # Always include spec_file in completion data (Issue 1: Add full file path)
        # Find the latest existing output.md file (in case current iteration doesn't have one)
        latest_output = self._get_latest_versioned_file("spec", self.phase_dir)
        if latest_output:
            data["spec_file"] = str(latest_output)
        elif hasattr(self, 'spec_file'):
            data["spec_file"] = self.spec_file

        return data

    def _prepare_user_input_for_iteration(self) -> "PhaseResult | str":
        """Prepare user input for current iteration.

        Get all required user input at the beginning:
        - Interactive: Ask user from stdin
        - Non-interactive: Get from self.user_input

        Subsequent processing logic is identical, no distinction between interactive/non-interactive.

        Returns:
            PhaseResult: If phase needs to end/pause
            str: User input content (for agent use)
        """
        # Iteration 1: user_input is the initial user story from spec file
        if self.iteration == 1:
            spec_file_path = Path(self.spec_file)
            return spec_file_path.read_text() if spec_file_path.exists() else ""

        # Iteration 2+: Check if current iteration was interrupted (has user_input but no response)
        current_data = self._load_current_iteration_data()
        if current_data and not current_data.get("response"):
            # Resume interrupted iteration, load user_input from user_input.md or context.json
            user_input = self._load_user_input(self.iteration)
            if user_input:
                return user_input

        # Iteration 2+: Display current spec content (interactive only)
        if self.interactive:
            self._display_current_spec()

        # Iteration 2+: Load previous iteration data
        prev_data = self._load_previous_iteration_data()
        if not prev_data:
            return ""

        prev_status = prev_data.get("status_code", "")

        # Get user input based on previous round status
        if prev_status == "CAFE_READY_FOR_REVIEW":
            # Need user choice: confirm/modify
            if self.interactive:
                # Display delta from previous iteration before asking for decision
                if self.iteration > 1:
                    self._display_iteration_delta()

                choice = self._ask_user_for_review_decision("Requirements specification", agent_name="PM")
            else:
                choice = self.user_input
                # Non-interactive mode: clear after use to ensure no reuse
                self.user_input = ""

                if not choice:
                    # Non-interactive but no input provided → fail immediately
                    return PhaseResult(
                        status=PhaseStatus.FAILED,
                        message=f"Spec phase failed after iteration {self.iteration - 1}: received READY_FOR_REVIEW in non-interactive mode without user input",
                        data={
                            "iterations": self.iteration - 1,
                            "last_response": prev_data.get("response", ""),
                            "status_code": "CAFE_READY_FOR_REVIEW",
                        },
                    )

            # Handle user choice (no longer distinguish interactive/non-interactive)
            result_or_input = self._process_review_decision(
                choice,
                prev_data,
                "Specification",
                {"pm_agent": self.pm_agent},
            )

            # If user confirmed (returned PhaseResult with CONFIRMED status),
            # sync spec to GitHub before returning (if sync is enabled)
            if isinstance(result_or_input, PhaseResult):
                if result_or_input.data.get("status_code") == PhaseStatusCode.CONFIRMED.value:
                    if self._sync_github:
                        self._sync_confirmed_spec_to_github()

            return result_or_input

        elif prev_status == "CAFE_NEED_CLARIFICATION":
            return self._handle_need_clarification_input(prev_data, agent_display_name="PM")
        else:
            return ""

    def _display_current_spec(self) -> None:
        """Display current spec.md content (interactive mode only)."""
        prev_iteration = self.iteration - 1
        if prev_iteration > 0:
            prev_spec_file = self._get_versioned_file_path("spec", prev_iteration, self.phase_dir)
            self.display.console.print(f"\n💾 Loading latest requirements specification file: {prev_spec_file}\n")
            spec_content = prev_spec_file.read_text() if prev_spec_file.exists() else "(File not generated)"
        else:
            spec_content = "(File not generated)"

        # Get PM CLI info for display
        pm_cli = self.agent_manager.get_agent_config(self.pm_agent).cli.value

        self.display.console.print(f"\n{'='*60}")
        self.display.console.print(f"PM ({self.pm_agent} by {pm_cli}) - Current specification content (Iteration {self.iteration - 1}):")
        self.display.console.print(f"{'='*60}")
        self.display.console.print(spec_content)
        self.display.console.print(f"{'='*60}\n")

    def _display_iteration_delta(self) -> None:
        """Display changes from previous iteration."""
        from cafe.ui.cli import _display_iteration_delta

        # Get previous iteration's file path (which is the current READY_FOR_REVIEW version)
        prev_iteration = self.iteration - 1
        prev_spec_file = self._get_versioned_file_path("spec", prev_iteration, self.phase_dir)

        _display_iteration_delta(
            prev_iteration,
            str(prev_spec_file),
            self.display.console,
        )

    def _ask_user_for_clarification(self) -> str:
        """Ask user for answer to NEED_CLARIFICATION using interactive Q&A when available.

        Overrides base class to check for questions.xml from previous iteration.
        If found and valid, uses interactive_qa_flow(); otherwise falls back to prompt_multiline().

        Returns:
            str: User's answer
        """
        from cafe.core.questions_schema import parse_questions_xml, validate_questions_xml
        from cafe.ui.inquirer_prompts import prompt_multiline

        # Look for questions.xml in the previous iteration directory
        if self.iteration > 1:
            prev_iter_dir = self._get_iteration_dir(self.iteration - 1)
            xml_path = prev_iter_dir / "questions.xml"

            if xml_path.exists() and validate_questions_xml(xml_path):
                questions = parse_questions_xml(xml_path)
                return interactive_qa_flow(questions)

        # Fallback to original multiline prompt
        return prompt_multiline("Please answer the question")

    def _prompt_for_rigor(self) -> None:
        """Prompt user to select rigor level if not already set."""
        from cafe.core.types import SpecRigor

        # Check if rigor is already explicitly set (not default)
        # We only prompt if it's still at default value
        if self._rigor_explicitly_set:
            return

        # Use shared prompt function
        rigor_str = prompt_for_rigor(self.display)
        self.rigor = SpecRigor(rigor_str)

    def _prompt_for_input_method(self) -> tuple[str, Optional[int]]:
        """Ask user to select requirements input method (manual vs GitHub Issue)

        Returns:
            Tuple of (method, issue_id):
            - method: "manual" or "github"
            - issue_id: Issue ID (int) if GitHub selected, None otherwise
        """
        # Use shared prompt function
        gh_ops = GitHubOps()
        return prompt_for_input_method(self.display, gh_ops)

    def _fetch_github_issue(self, issue_id: int) -> Optional[PhaseResult]:
        """Fetch issue content from GitHub

        Args:
            issue_id: GitHub issue ID

        Returns:
            PhaseResult if error occurred, None if success
        """
        try:
            # Get repository name from .git/config
            repo_name = get_github_repo_name()

            # Fetch issue content using shared function
            gh_ops = GitHubOps()
            fetched_content, image_urls = fetch_github_issue(gh_ops, issue_id)

            # Download images if present
            if image_urls:
                images_dir = self.phase_dir / "images"
                try:
                    saved_paths = gh_ops.download_issue_images(image_urls, images_dir)
                    if saved_paths:
                        self.display.console.print()
                        self.display.console.print(f"✅ Downloaded {len(saved_paths)} image(s):")
                        for path in saved_paths:
                            size_bytes = path.stat().st_size
                            if size_bytes >= 1024 * 1024:
                                size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
                            elif size_bytes >= 1024:
                                size_str = f"{size_bytes / 1024:.1f} KB"
                            else:
                                size_str = f"{size_bytes} B"
                            self.display.console.print(f"   {path} ({size_str})")
                    if len(saved_paths) < len(image_urls):
                        failed_count = len(image_urls) - len(saved_paths)
                        self.display.console.print(f"⚠️  Warning: {failed_count} image(s) failed to download")
                except Exception as e:
                    # Don't fail the whole process if image download fails
                    self.display.console.print(f"⚠️  Warning: Failed to download images: {e}")

            # Override user_input with fetched content
            self.user_input = fetched_content

            # Store issue_id for sync
            self._config_issue_id = int(issue_id)

            # Save issue config
            self._save_issue_config()

            # Write fetched content to spec file (same as _prompt_for_user_story)
            # If spec_file is not set (when called directly without execute()),
            # compute it using the next iteration number
            if not self.spec_file:
                iteration_number = self._get_next_iteration_number("spec", self.phase_dir)
                spec_path = self._get_versioned_file_path("spec", iteration_number, self.phase_dir)
            else:
                spec_path = Path(self.spec_file)

            spec_path.parent.mkdir(parents=True, exist_ok=True)
            # Extract title from fetched_content if it exists
            # fetched_content format: "# {title}\n\n{body}" or just "{body}"
            lines = fetched_content.split('\n', 1)
            if lines[0].startswith('# '):
                # Title is present as H1
                issue_title = lines[0][2:].strip()  # Remove "# " prefix
                issue_body = lines[1].strip() if len(lines) > 1 else ""
                # Write with title explicitly included at the top
                if issue_body:
                    spec_path.write_text(f"# Initial Requirements\n\n**Issue Title:** {issue_title}\n\n{issue_body}\n")
                else:
                    spec_path.write_text(f"# Initial Requirements\n\n**Issue Title:** {issue_title}\n")
            else:
                # No title in fetched_content, write as-is
                spec_path.write_text(f"# Initial Requirements\n\n{fetched_content}\n")

            self.display.console.print()
            self.display.console.print(f"✅ Requirements loaded from GitHub Issue and written to {spec_path}")
            self.display.console.print("   Starting clarification...")
            self.display.console.print()

            return None  # Success

        except FileNotFoundError as e:
            return PhaseResult(
                status=PhaseStatus.FAILED,
                message=f"Failed to get repository info: {e}",
            )
        except RuntimeError as e:
            # From fetch_github_issue when gh CLI not authenticated
            return PhaseResult(
                status=PhaseStatus.FAILED,
                message=str(e),
            )
        except GitHubError as e:
            return PhaseResult(
                status=PhaseStatus.FAILED,
                message=f"Failed to fetch GitHub issue: {e}",
            )

    def _prompt_for_user_story(self) -> None:
        """Prompt user to write initial requirement when no requirements file exists."""
        self.display.console.print("\n" + "="*70)
        self.display.console.print("Please describe your requirements:")
        self.display.console.print("="*70)
        self.display.console.print()
        self.display.console.print("Recommended to write as user stories:")
        self.display.console.print("   Format: As a [role], I want [feature], so that [purpose/value]")
        self.display.console.print()
        self.display.console.print("   Examples:")
        self.display.console.print("   - As a product manager, I want to quickly understand project progress to report development status to the team")
        self.display.console.print("   - As a user, I want to see clear error messages to know what went wrong and how to fix it")
        self.display.console.print()
        self.display.console.print("Or describe requirements in general terms:")
        self.display.console.print("   - Add a CSV export feature")
        self.display.console.print("   - Fix bug where login page cannot submit")
        self.display.console.print("   - Optimize homepage loading speed")
        self.display.console.print()

        # Get user's requirement using prompt_multiline for better UX
        from cafe.ui.inquirer_prompts import prompt_multiline
        user_requirement = prompt_multiline("Please enter your requirements").strip()

        if not user_requirement:
            raise ValueError("No requirements provided, cannot continue")

        # Save requirement as initial spec
        spec_path = Path(self.spec_file)
        spec_path.parent.mkdir(parents=True, exist_ok=True)
        spec_path.write_text(f"# Initial Requirements\n\n{user_requirement}\n")

        self.display.console.print()
        self.display.console.print("✅ Requirements recorded, starting clarification...")
        self.display.console.print()

    def _backup_spec(self, spec_path: Path) -> None:
        """Backup original spec file.

        Args:
            spec_path: Path to spec file
        """
        backup_path = Path(f"{spec_path}.backup")
        if not backup_path.exists():
            backup_path.write_text(spec_path.read_text())

    def _ensure_spec_file_written(self, response: str) -> None:
        """Ensure spec file is written (for mock mode or when agent does not use write tool).
        
        In mock mode, agent will not actually call write tool, so content needs to be extracted from response and written.
        
        Args:
            response: Agent response content
        """
        import os
        
        # Only process in mock mode
        if not os.getenv("CAFE_MOCK_AGENTS"):
            return
            
        # Extract content after status code
        lines = response.strip().split("\n")
        if not lines:
            return
            
        # Skip first line (status code) and empty lines
        content_lines = []
        skip_first = True
        for line in lines:
            if skip_first and line.startswith("CAFE_"):
                continue
            skip_first = False
            content_lines.append(line)
        
        content = "\n".join(content_lines).strip()
        if not content:
            return
            
        # Write to file
        spec_path = Path(self.spec_file)
        spec_path.parent.mkdir(parents=True, exist_ok=True)
        spec_path.write_text(content, encoding="utf-8")

    def _sync_spec_to_github(self, response: str) -> None:
        """Sync spec file to GitHub issue (no-op in local mode).

        Note: This method is now a no-op since GitHub mode is removed.

        Args:
            response: Agent response content (unused)
        """
        # No-op: GitHub mode has been removed
        pass

    def _sync_confirmed_spec_to_github(self) -> None:
        """Sync confirmed spec to GitHub issue as a comment.

        Only syncs when:
        1. Spec was loaded from GitHub issue (has _config_issue_id)
        2. User has confirmed the spec (CAFE_CONFIRMED status)
        """
        if not self._config_issue_id:
            return

        try:
            # Get latest versioned spec file (the confirmed spec)
            latest_spec_path = self._get_latest_versioned_file("spec", self.phase_dir)
            if not latest_spec_path:
                return

            spec_content = latest_spec_path.read_text(encoding="utf-8")

            # Format comment body
            comment_body = f"### 📋 Requirements Specification (Confirmed)\n\n{spec_content}"

            # Post comment to GitHub issue
            gh_ops = GitHubOps()
            if not gh_ops.check_gh_installed():
                return

            if not gh_ops.check_gh_auth():
                self.display.console.print(f"[yellow]Warning: gh CLI not authenticated, skipping spec sync to GitHub issue #{self._config_issue_id}[/yellow]")
                return

            self.display.console.print(f"Syncing spec to GitHub issue #{self._config_issue_id}...")
            gh_ops.add_issue_comment(str(self._config_issue_id), comment_body)
            self.display.console.print(f"[green]✅ Spec synced to GitHub issue #{self._config_issue_id} as a comment.[/green]")

        except GitHubError as e:
            # Log error but don't fail the phase
            self.display.console.print(f"[yellow]Warning: Failed to sync spec to GitHub: {e}[/yellow]")
        except Exception as e:
            # Log unexpected errors but don't fail
            self.display.console.print(f"[yellow]Warning: Unexpected error during GitHub sync: {e}[/yellow]")

    def _create_github_issue(self, content: str) -> str:
        """Create a new GitHub issue with requirements.

        Args:
            content: Requirements content

        Returns:
            Issue ID
        """
        return create_github_issue(content)

    def _update_github_issue(self, content: str) -> None:
        """Update existing GitHub issue with requirements.

        Args:
            content: Requirements content
        """
        update_github_issue(self.issue_id, content)

    def _generate_prompt(self, user_input: str = "") -> str:
        """Generate prompt for current iteration.

        Args:
            user_input: User's response/clarification for this iteration

        Returns:
            Prompt string
        """
        return self._generate_local_prompt(user_input)

    def _get_non_technical_guidelines(self) -> str:
        """Get non-technical guidelines for PM.

        Returns:
            Guidelines string
        """
        return """**Important: Absolutely no technical details!**\n- ❌ Do not mention implementation methods, technical architecture, programming languages, frameworks, databases, etc.\n- ❌ Do not suggest any technical solutions\n- ❌ Do not modify code yourself\n- ✅ Only focus on \"what users want\" \"why they want it\" \"what the expected outcome is\"\n- ✅ Think from product and business perspectives"""

    def _get_status_code_prompt(self) -> str:
        """Get status code prompt.

        Returns:
            Status code prompt string
        """
        return generate_status_code_prompt(
            valid_codes=[
                PhaseStatusCode.READY_FOR_REVIEW,
                PhaseStatusCode.NEED_CLARIFICATION,
            ],
            descriptions={
                PhaseStatusCode.READY_FOR_REVIEW: "Requirements specification completed, ready for user confirmation",
                PhaseStatusCode.NEED_CLARIFICATION: "Requirements have unclear parts that need clarification",
            },
        )

    def _get_rigor_guidelines(self) -> str:
        """Get rigor level guidelines for PM.

        Returns:
            Rigor guidelines string
        """
        from cafe.core.types import SpecRigor

        if self.rigor == SpecRigor.LOW:
            return """**Rigor level: Low (fast development)**\n- Only ask for the most critical information (what is the core function)\n- Do not ask for details that developers can decide themselves\n- Allow some ambiguity, trust developers to make reasonable choices\n- If requirements are basically clear, can confirm\n- Goal: Quickly start development, adjust as you go"""
        elif self.rigor == SpecRigor.HIGH:
            return """**Rigor level: High (precise specification)**\n- Ask all details in depth (including edge cases, error handling, special scenarios)\n- Ensure every function's input, output, and behavior is clearly defined\n- Ask for acceptance criteria to ensure requirements are testable\n- Do not allow any vague or \"it depends\" descriptions\n- Must reach the level where test cases can be directly written\n- Goal: The clearer the specification, the better, reduce subsequent communication costs"""
        else:  # MEDIUM (default)
            return """**Rigor level: Medium (balanced mode)**\n- Ask important details and key scenarios\n- Ask about obviously unclear areas, but don't over-pursue minor details\n- Ensure main functions and expected behaviors are clear\n- Accept reasonable flexibility for secondary details\n- Ask for acceptance criteria, but don't require excessive detail\n- Goal: Balance between speed and precision"""

    def _generate_local_prompt(self, user_input: str = "") -> str:
        """Generate prompt for local workflow.

        Args:
            user_input: User's response/clarification for this iteration

        Returns:
            Prompt string
        """
        non_technical = self._get_non_technical_guidelines()
        status_code_prompt = self._get_status_code_prompt()
        rigor_guidelines = self._get_rigor_guidelines()

        # Calculate current and previous file names (using relative paths)
        # self.spec_file is the current file to write (already set in execute())
        from pathlib import Path
        current_spec_path = Path(self.spec_file)
        if not current_spec_path.is_absolute():
            current_spec_path = current_spec_path.resolve()

        # Convert to path relative to current working directory (for prompt)
        # Use to_cwd_relative_path to support worktree environment
        try:
            current_spec_file = to_cwd_relative_path(current_spec_path)
        except (ValueError, OSError):
            # If cannot convert (file not under cwd), use absolute path
            current_spec_file = str(current_spec_path)

        # Previous round file: only exists when iteration > 1
        prev_spec_file = None
        if self.iteration > 1:
            # List existing files and get the newest one (as previous round file)
            # Current file (self.spec_file) may not exist yet (about to be created), so the newest file is from previous round
            existing_specs = sorted(self.phase_dir.glob("spec_*.md"))
            if existing_specs:
                # Get the newest file (highest number) as previous round file
                prev_spec_path = existing_specs[-1]
                if not prev_spec_path.is_absolute():
                    prev_spec_path = prev_spec_path.resolve()
                # Convert to path relative to current working directory
                try:
                    prev_spec_file = to_cwd_relative_path(prev_spec_path)
                except (ValueError, OSError):
                    prev_spec_file = str(prev_spec_path)

        # --- 1. Determine context-specific sections ---
        initial_instruction = ""
        context_section = ""
        restriction = ""

        if self.iteration == 1:
            # Round 1: Read user_input (initial requirements), write to spec_001.md
            template_instruction = ""
            if self.template_path:
                # Convert template path to relative path for display
                try:
                    template_file = to_cwd_relative_path(self.template_path)
                except (ValueError, OSError):
                    template_file = str(self.template_path)
                template_instruction = f"""\n**Template Reference:**\nUse Read tool to read {template_file} as a reference for output format and structure."""

            # Check for images
            images_dir = self.phase_dir / "images"
            images_instruction = ""
            if images_dir.exists() and any(images_dir.iterdir()):
                image_files = sorted(images_dir.iterdir())
                image_paths = []
                for img in image_files:
                    try:
                        image_paths.append(to_cwd_relative_path(img))
                    except (ValueError, OSError):
                        image_paths.append(str(img.resolve()))
                image_list = "\n".join(f"  - `{p}`" for p in image_paths)
                images_instruction = f"""\n\n**Images:** This issue includes screenshots/images. Use the Read tool to view these images for UI/UX requirements and visual context:\n{image_list}"""

            initial_instruction = f"""**Round 1 Requirements Clarification**\n\nRead {current_spec_file} for initial requirements content.{template_instruction}{images_instruction}\n\n**Important:** Preserve the original requirements content (including "Initial Requirements" section and "Issue Title" if present). Only add your analysis and clarification below, do not modify or remove the original content."""
            context_section = ""
        else:  # Iteration 2+
            # Round 2 onwards: Read previous round spec file and user_input, write new spec file
            initial_instruction = f"""**Round {self.iteration} Requirements Clarification**\n\n1. Use Read tool to read {prev_spec_file}(previous round analysis results)\n2. View user's latest answer (see below)\n3. Modify {current_spec_file}, update content (new version)\n\n**Important:** Preserve the original requirements content from the first iteration (including "Initial Requirements" section and "Issue Title" if present). Only update your analysis sections, do not modify or remove the original requirements."""
            
            if user_input:
                context_section = f"""\n**User's Answer:**\n{user_input}\n"""
            if self.iteration >= 4:
                restriction = f"""\n⚠️ **Important Constraints:**\n- You are now in round {self.iteration} , can only continue asking about \"pending questions\".\n- **Cannot propose new questions**.\n- Can only deeply clarify questions already raised.\n"""

        # --- 2. Get agent file path for reference ---
        from cafe.agents.manager import AgentManager

        agent_file = AgentManager.get_agent_file_path(self.pm_agent, "pm")

        # --- 3. Get checklist file path ---
        iteration_dir = self._get_iteration_dir(self.iteration)
        checklist_file = iteration_dir / "checklist.md"
        try:
            checklist_path = to_cwd_relative_path(checklist_file)
        except ValueError:
            checklist_path = str(checklist_file.resolve())

        # --- 4. Build prompt ---
        checklist_instruction = format_checklist_instruction(checklist_path)
        base_prompt = f"""# Specification Phase\n\n{checklist_instruction}\n\n{initial_instruction}\n{context_section}\n{rigor_guidelines}\n\n{status_code_prompt}\n"""

        # --- 5. Return the final prompt ---
        return base_prompt

    def _load_issue_config(self) -> None:
        """Load issue configuration (issue_id, rigor, input_method, sync_github) from issue.yaml if exists."""
        from cafe.core.types import SpecRigor

        # Path: .cafe/issues/{issue_name}/issue.yaml
        config_file = self.issue_dir / "issue.yaml"

        config_data = self._read_issue_config(config_file)
        if config_data:
            # Load from spec section
            spec_config = config_data.get("spec", {})

            # Load input_method and issue_id from spec section
            if "input_method" in spec_config:
                self._config_input_method = spec_config["input_method"]

            # Load issue_id if present (independent of input_method)
            if spec_config.get("issue_id"):
                self._config_issue_id = int(spec_config["issue_id"])
                # Set _fetched_issue_id for sync functionality
                self._fetched_issue_id = str(spec_config["issue_id"])

            # Load rigor from spec section
            if "rigor" in spec_config and not self._rigor_explicitly_set:
                try:
                    self.rigor = SpecRigor(spec_config["rigor"])
                except (ValueError, KeyError):
                    # Invalid rigor value in config, use default
                    pass

            # Load sync_github from spec section
            from cafe.utils.config import resolve_sync_github_config

            self._sync_github = resolve_sync_github_config(
                cli_value=self._sync_github_explicit,
                config_value=bool(spec_config["sync_github"]) if "sync_github" in spec_config else None,
                has_issue_id=bool(self._config_issue_id)
            )
        else:
            # No config file: use CLI value if provided, otherwise default to False
            from cafe.utils.config import resolve_sync_github_config

            self._sync_github = resolve_sync_github_config(
                cli_value=self._sync_github_explicit,
                config_value=None,
                has_issue_id=False
            )

    def _save_issue_config(self) -> None:
        """Save issue configuration (issue_id, rigor, sync_github) to issue.yaml."""
        # Path: .cafe/issues/{issue_name}/issue.yaml
        config_file = self.issue_dir / "issue.yaml"

        # Read existing config to preserve base_branch, feature_branch, and worktree_path
        existing_config = self._read_issue_config(config_file) or {}

        # Prepare new config data
        config_data = {**existing_config}

        # Ensure spec section exists
        if "spec" not in config_data:
            config_data["spec"] = {}

        # Save issue_id if available
        if self._config_issue_id:
            config_data["spec"]["issue_id"] = self._config_issue_id
            config_data["spec"]["input_method"] = "github"

        # Always save rigor (even if it's the default) so subsequent iterations use the same value
        config_data["spec"]["rigor"] = self.rigor.value

        # Save sync_github setting
        config_data["spec"]["sync_github"] = self._sync_github

        # Write config
        self._write_issue_config(config_file, config_data)

    def get_status_file(self) -> Path:
        """Public method to get status file path for workflow integration.

        Returns:
            Path to status.json
        """
        return self._get_status_file()

    def _get_status_analysis_prompt(self) -> str:
        """Get prompt for analyzing status code.

        Returns:
            Prompt for analyzing spec file status
        """
        # Use absolute path
        from pathlib import Path
        spec_path = Path(self.spec_file)
        if not spec_path.is_absolute():
            spec_path = spec_path.resolve()
        
        return f"""Please use Read tool to read {spec_path}  and analyze current status.\n\nBased on the following conditions, determine which status code to return:\n\n- CAFE_READY_FOR_REVIEW: Requirements specification completed, all necessary information clarified, no pending questions\n- CAFE_NEED_CLARIFICATION: Specification still has issues that need user confirmation, or unclear details\n\nPlease return only one status code (e.g., CAFE_READY_FOR_REVIEW), no other content."""

    def _detect_written_output_files(self) -> List[Path]:
        """Check if spec file was written before failure.

        Returns:
            List[Path]: If spec_{iteration}.md exists, return list containing it, otherwise return empty list
        """
        spec_file = self._get_versioned_file_path("spec", self.iteration, self.phase_dir)
        return [Path(spec_file)] if Path(spec_file).exists() else []

    def _validate_and_retry_questions_xml(
        self,
        xml_path: Path,
        agent_name: str,
        allowed_tools: List[str],
        max_retries: int = 3,
    ) -> bool:
        """Validate questions.xml and retry with agent if invalid.

        Args:
            xml_path: Path to questions.xml file
            agent_name: Agent name for retry execution
            allowed_tools: Tools allowed for agent
            max_retries: Maximum number of retry attempts (default: 3)

        Returns:
            True if XML is valid (or was fixed), False otherwise
        """
        from cafe.core.questions_schema import validate_questions_xml

        # Check if file exists
        if not xml_path.exists():
            return False

        # First validation attempt
        if validate_questions_xml(xml_path):
            return True

        # Retry loop: ask agent to fix invalid XML
        for retry in range(max_retries):
            print(f"\n⚠️  questions.xml format is invalid, asking agent to fix... (attempt {retry + 1}/{max_retries})")

            retry_prompt = (
                f"The questions XML file at {xml_path} has invalid format. "
                f"Please fix it so that:\n"
                f"- Root element is <questions>\n"
                f"- Each <question> has a unique id attribute, a <title>, and <options> with at least one <option>\n"
                f"- The file is well-formed XML\n\n"
                f"Read the file, fix the issues, and write the corrected XML back to {xml_path}."
            )

            try:
                self.agent_manager.execute(
                    agent_name,
                    retry_prompt,
                    allowed_tools=allowed_tools,
                )
            except Exception as e:
                print(f"⚠️  Error during XML fix retry: {e}")
                continue

            # Check if fixed
            if xml_path.exists() and validate_questions_xml(xml_path):
                print(f"✓ Agent successfully fixed questions.xml")
                return True

        # All retries failed - delete invalid file and fallback
        print(f"\n❌ questions.xml still invalid after {max_retries} attempts, falling back to original Q&A")
        if xml_path.exists():
            xml_path.unlink()
        return False

    def _verify_content_updated(
        self,
        response: str,
        spec_file_pattern: str,
        allowed_tools: List[str],
        max_retries: int = 3,
    ) -> tuple[Optional[PhaseResult], Optional[str]]:
        """Verify that agent actually updated the spec content.

        Checks:
        1. For iteration 1: output.md should differ from user_input.md
        2. For iteration n: output.md should differ from previous iteration's output.md

        Args:
            response: Agent's response
            spec_file_pattern: File pattern for spec file
            allowed_tools: Tools allowed for agent
            max_retries: Maximum number of retry attempts (default: 3)

        Returns:
            Tuple of (verification_result, final_response)
            - verification_result: PhaseResult if verification failed, None if ok
            - final_response: Updated response if agent corrected the output, None to use original
        """
        from cafe.core.status_codes import StatusCodeParser

        # Extract status code from response
        status_code = StatusCodeParser.extract(response)
        if not status_code:
            return None, None

        # Read current spec file
        spec_path = Path(self.spec_file)
        if not spec_path.exists():
            return None, None

        # Use shared method to check if content was updated
        was_updated = self._check_output_file_updated(
            output_file=spec_path,
            iteration=self.iteration,
            phase_dir=self.phase_dir,
        )

        # Determine comparison label for error messages
        compare_label = "initial requirements (user_input.md)" if self.iteration == 1 else "previous iteration"

        # If content was not updated (identical to comparison), ask agent to update
        if not was_updated:
            retry_count = 0

            while retry_count < max_retries:
                retry_count += 1
                print(f"\n⚠️  Spec content is identical to {compare_label}, asking agent to update... (attempt {retry_count}/{max_retries})")

                # Build retry prompt
                retry_prompt = f"""Your previous output in {spec_file_pattern} is IDENTICAL to the {compare_label}.

This means you did not actually update or analyze the requirements - you just copied the input.

Please:
1. Read the current content in {spec_file_pattern}
2. Perform your analysis and make meaningful updates
3. Write the UPDATED analysis to {spec_file_pattern}
4. Return ONLY the status code: {status_code.value}

Remember: The content must be DIFFERENT from the input. Add your analysis, clarifications, or refinements."""

                try:
                    # Execute retry with agent
                    retry_response, _ = self.agent_manager.execute(
                        agent_name=self.pm_agent,
                        prompt=retry_prompt,
                        allowed_tools=allowed_tools,
                    )

                    # Check if content was updated
                    updated_content = spec_path.read_text(encoding="utf-8").strip()
                    if updated_content != compare_content:
                        # Content was updated!
                        print(f"✓ Agent successfully updated the spec content")

                        # Extract status code from retry response
                        retry_status = StatusCodeParser.extract(retry_response)
                        if retry_status:
                            return None, retry_response.strip()
                        else:
                            # No valid status code but content was updated, use original
                            return None, None

                    # Content still identical, continue retry loop

                except Exception as e:
                    print(f"⚠️  Error during content verification retry: {e}")
                    # Continue retry loop

            # Max retries exceeded
            print(f"\n❌ Agent did not update spec content after {max_retries} attempts")
            # Don't fail the phase, just warn and continue
            return None, None

        # Content is different, all good
        return None, None
