"""Issue Explorer Agent

Extracts linked issues from merge request description, fetches full issue details
with comments and linked issues, and summarizes the context for downstream agents.

Three-phase pipeline:
1. Discovery: Find all issue references (text parsing + API)
2. Fetch: Get full context (issues, comments, linked issues)
3. Summarize: LLM synthesizes findings for downstream agents
"""

from __future__ import annotations

import asyncio
import logging
import re
from typing import TYPE_CHECKING, Annotated, cast

from pydantic import BaseModel, ConfigDict, Field

from ..adaptors.issue import IssueHandler
from ..adaptors.issue.schema import (
    Issue,
    IssueComment,
    IssueSummary,
    IssueWithContext,
    LinkedIssue,
)
from ..utils import aggregate_token_usage
from .base import AgentConfig, AgentResponse, BaseAgentImpl, TokenUsage
from .output_parser import OutputParserAgent
from .utils import format_template

if TYPE_CHECKING:
    from ..workflows.context import PipelineContext

logger = logging.getLogger(__name__)


class ExtractedIssue(BaseModel):
    """Single extracted issue reference"""

    model_config = ConfigDict(extra="forbid")

    project_id: Annotated[
        str | None,
        Field(
            default=None,
            description='The full project path (e.g., "marketing/crm" or "owner/repo"). Null if not specified.',
        ),
    ]
    issue_id: Annotated[int, Field(description="The issue number")]


class ExtractedIssues(BaseModel):
    """Output schema for extracted issues"""

    model_config = ConfigDict(extra="forbid")

    issues: Annotated[
        list[ExtractedIssue], Field(description="List of issues found in the description")
    ]


class IssueAnalysis(BaseModel):
    """Analysis result containing issues with full details and summary"""

    model_config = ConfigDict(extra="forbid")

    issues: Annotated[
        list[Issue], Field(description="List of issues with full details from the platform")
    ]
    summary: Annotated[
        IssueSummary | None,
        Field(default=None, description="Summarized context for downstream agents"),
    ]


class IssueExplorerAgent:
    """Agent that discovers, fetches, and summarizes linked issues.

    Three-phase pipeline:
    1. Discovery: Parse MR description + use platform APIs to find linked issues
    2. Fetch: Get full context (issues, comments, linked issues with depth limit)
    3. Summarize: LLM synthesizes findings into actionable context

    Cost controls:
    - MAX_ISSUES: Maximum number of issues to process
    - MAX_COMMENTS_PER_ISSUE: Maximum comments to fetch per issue
    - MAX_LINKED_DEPTH: How deep to follow linked issues (0=none, 1=direct)
    """

    # Cost control settings
    MAX_ISSUES = 10
    MAX_COMMENTS_PER_ISSUE = 50
    MAX_LINKED_DEPTH = 1

    def __init__(
        self,
        config: Annotated[AgentConfig, Field(description="LLM configuration")],
    ):
        """Initialize IssueExplorerAgent

        Args:
            config: LLM configuration
        """
        self.base = BaseAgentImpl(
            name="IssueExplorer",
            description="Discovers and summarizes linked issues for code review context",
            config=config,
        )

    async def analyse(
        self,
        issue_handler: Annotated[IssueHandler, Field(description="Issue handler adapter")],
        repo: Annotated[
            str,
            Field(
                description='Repository identifier (e.g., "owner/repo") - used as fallback if project_id doesn\'t match'
            ),
        ],
        description: Annotated[
            str, Field(description="The merge request description to parse for issue links")
        ],
        output_parser: Annotated[
            OutputParserAgent, Field(description="Output parser for retry logic")
        ],
        mr_number: Annotated[
            int | None,
            Field(default=None, description="MR/PR number for API-based discovery"),
        ] = None,
    ) -> AgentResponse[IssueAnalysis]:
        """Analyze merge request and extract linked issues with full context.

        Three-phase pipeline:
        1. Discovery: Find all issue references (text + API)
        2. Fetch: Get full context (issues, comments, linked issues)
        3. Summarize: LLM synthesizes findings

        Args:
            issue_handler: Issue handler adapter (GitHub, GitLab, etc.)
            repo: Repository identifier (e.g., "owner/repo") - used as fallback
            description: The merge request description to parse for issue links
            output_parser: Output parser agent for fixing invalid JSON responses
            mr_number: Optional MR/PR number for API-based discovery

        Returns:
            AgentResponse containing IssueAnalysis with issues and summary

        Raises:
            ReviewateError: If analysis fails
        """
        # Phase 1: Discovery
        logger.info("Phase 1: Discovering linked issues")
        discovered_refs = await self._discover_issues(
            issue_handler, repo, description, mr_number, output_parser
        )
        total_usage = discovered_refs.metadata

        # Limit issues to process
        refs_to_process = discovered_refs.output.issues[: self.MAX_ISSUES]
        logger.info(
            f"Found {len(discovered_refs.output.issues)} issues, processing {len(refs_to_process)}"
        )

        if not refs_to_process:
            # No issues found - return empty analysis
            return AgentResponse(
                output=IssueAnalysis(issues=[], summary=None),
                metadata=total_usage,
            )

        # Phase 2: Fetch context
        logger.info("Phase 2: Fetching issue context")
        issues_with_context = await self._fetch_issue_context(issue_handler, repo, refs_to_process)
        logger.info(f"Fetched context for {len(issues_with_context)} issues")

        if not issues_with_context:
            # Failed to fetch any issues
            return AgentResponse(
                output=IssueAnalysis(issues=[], summary=None),
                metadata=total_usage,
            )

        # Phase 3: Summarize
        logger.info("Phase 3: Summarizing issue context")
        summary_response = await self._summarize_issues(issues_with_context, output_parser)
        total_usage = aggregate_token_usage(total_usage, summary_response.metadata)

        # Return complete analysis
        return AgentResponse(
            output=IssueAnalysis(
                issues=[ic.issue for ic in issues_with_context],
                summary=summary_response.output,
            ),
            metadata=total_usage,
        )

    async def _discover_issues(
        self,
        issue_handler: IssueHandler,
        repo: str,
        description: str,
        mr_number: int | None,
        output_parser: OutputParserAgent,
    ) -> AgentResponse[ExtractedIssues]:
        """Phase 1: Discover all issue references.

        Combines text parsing (LLM) with platform API discovery.

        Args:
            issue_handler: Issue handler adapter
            repo: Repository identifier
            description: MR description to parse
            mr_number: Optional MR number for API discovery
            output_parser: Output parser for retry logic

        Returns:
            AgentResponse containing ExtractedIssues
        """
        refs: set[tuple[str, int]] = set()  # (repo, issue_number)

        # Method 1: Text parsing via LLM (existing logic)
        text_refs = await self._extract_issues_from_text(description, output_parser)
        for ref in text_refs.output.issues:
            target_repo = ref.project_id if ref.project_id else repo
            refs.add((target_repo, ref.issue_id))

        # Method 2: Platform API discovery (if mr_number provided)
        if mr_number:
            try:
                api_refs = await issue_handler.fetch_pr_linked_issues(repo, mr_number)
                for linked in api_refs:
                    refs.add((repo, linked.issue.number))
                logger.info(f"API discovery found {len(api_refs)} linked issues")
            except Exception as e:
                logger.warning(f"API-based issue discovery failed: {e}")

        # Convert back to ExtractedIssues format
        extracted = ExtractedIssues(
            issues=[ExtractedIssue(project_id=r[0], issue_id=r[1]) for r in refs]
        )

        return AgentResponse(
            output=extracted,
            metadata=text_refs.metadata,
        )

    async def _extract_issues_from_text(
        self,
        description: str,
        output_parser: OutputParserAgent,
    ) -> AgentResponse[ExtractedIssues]:
        """Extract linked issues from merge request description using LLM.

        Args:
            description: The merge request description to parse
            output_parser: Output parser for retry logic

        Returns:
            AgentResponse containing ExtractedIssues
        """
        ctx = {"description": description}
        template = self.base.load_prompt("issue_explorer_agent.txt")
        system_prompt = format_template(self.base.jinja_env, template, ctx)
        output_format = self.base.get_output_format(ExtractedIssues)

        return await self.base.analyze(
            system_prompt=system_prompt,
            user_prompt=output_format,
            response_model=ExtractedIssues,
            output_parser=output_parser,
        )

    async def _fetch_issue_context(
        self,
        issue_handler: IssueHandler,
        repo: str,
        refs: list[ExtractedIssue],
    ) -> list[IssueWithContext]:
        """Phase 2: Fetch full context for each issue.

        Fetches issue details, comments, and linked issues (depth 1).

        Args:
            issue_handler: Issue handler adapter
            repo: Repository identifier (fallback)
            refs: List of issue references to fetch

        Returns:
            List of IssueWithContext objects
        """
        results: list[IssueWithContext] = []
        seen_issues: set[tuple[str, int]] = set()

        for ref in refs:
            target_repo = ref.project_id if ref.project_id else repo
            key = (target_repo, ref.issue_id)

            if key in seen_issues:
                continue
            seen_issues.add(key)

            try:
                # Fetch issue details
                issue = await issue_handler.fetch_issue(target_repo, ref.issue_id)

                # Fetch comments (parallel with linked issues)
                comments_task = issue_handler.fetch_issue_comments(
                    target_repo, ref.issue_id, limit=self.MAX_COMMENTS_PER_ISSUE
                )

                # Fetch linked issues (depth 1 only)
                linked_task = None
                if self.MAX_LINKED_DEPTH > 0:
                    linked_task = issue_handler.fetch_linked_issues(target_repo, ref.issue_id)

                # Await concurrently
                if linked_task:
                    comments, linked = await asyncio.gather(
                        comments_task,
                        linked_task,
                        return_exceptions=True,
                    )
                else:
                    comments = await comments_task
                    linked = []

                # Handle exceptions from gather with explicit typing for mypy
                if isinstance(comments, Exception):
                    logger.warning(
                        f"Failed to fetch comments for {target_repo}#{ref.issue_id}: {comments}"
                    )
                    comments_result: list[IssueComment] = []
                else:
                    comments_result = cast(list[IssueComment], comments)

                if isinstance(linked, Exception):
                    logger.warning(
                        f"Failed to fetch linked issues for {target_repo}#{ref.issue_id}: {linked}"
                    )
                    linked_result: list[LinkedIssue] = []
                else:
                    linked_result = cast(list[LinkedIssue], linked)

                # Parse task list items from issue body
                task_items = self._parse_task_list(issue.description)

                # Detect if this is an epic (has children or specific labels)
                is_epic = self._detect_epic(issue, linked_result)

                results.append(
                    IssueWithContext(
                        issue=issue,
                        comments=comments_result,
                        linked_issues=linked_result,
                        task_list_items=task_items,
                        is_epic=is_epic,
                    )
                )

            except Exception as e:
                logger.warning(
                    f"Failed to fetch issue context for {target_repo}#{ref.issue_id}: {e}"
                )
                continue

        return results

    def _parse_task_list(self, description: str | None) -> list[str]:
        """Parse markdown task list items from issue description.

        Args:
            description: Issue description text

        Returns:
            List of task list item texts
        """
        if not description:
            return []

        # Match both checked and unchecked markdown task items
        # - [ ] Task item
        # - [x] Completed task
        pattern = r"^\s*[-*]\s*\[[ xX]\]\s*(.+)$"
        matches = re.findall(pattern, description, re.MULTILINE)
        return [m.strip() for m in matches]

    def _detect_epic(self, issue: Issue, linked: list[LinkedIssue]) -> bool:
        """Detect if an issue is an epic/parent issue.

        Args:
            issue: The issue to check
            linked: Linked issues

        Returns:
            True if this appears to be an epic
        """
        # Check labels for epic indicators
        epic_labels = {"epic", "parent", "master", "umbrella", "tracking"}
        if any(label.lower() in epic_labels for label in issue.labels):
            return True

        # Check if it has child issues
        for linked_issue in linked:
            if linked_issue.relation.value == "child":
                return True

        return False

    async def _summarize_issues(
        self,
        issues: list[IssueWithContext],
        output_parser: OutputParserAgent,
    ) -> AgentResponse[IssueSummary]:
        """Phase 3: LLM summarizes all issue context.

        Args:
            issues: List of issues with full context
            output_parser: Output parser for retry logic

        Returns:
            AgentResponse containing IssueSummary
        """
        if not issues:
            return AgentResponse(
                output=IssueSummary(
                    acceptance_criteria=[],
                    key_requirements=[],
                    discussion_highlights=[],
                    unresolved_questions=[],
                    related_context="",
                ),
                metadata=TokenUsage(
                    input_tokens=0,
                    output_tokens=0,
                    total_tokens=0,
                    cached_tokens=0,
                    model="none",
                ),
            )

        # Build context for LLM (limit token usage)
        issues_data = []
        for ic in issues:
            issue_dict = {
                "title": ic.issue.title,
                "description": ic.issue.description[:2000] if ic.issue.description else "",
                "labels": ic.issue.labels,
                "state": ic.issue.state,
                "url": ic.issue.web_url,
                "comments": [
                    {"author": c.author, "body": c.body[:500]}
                    for c in ic.comments[:10]  # Limit for token budget
                ],
                "task_list": ic.task_list_items[:20],
                "is_epic": ic.is_epic,
                "linked_issue_count": len(ic.linked_issues),
            }
            issues_data.append(issue_dict)

        ctx = {"issues": issues_data}
        template = self.base.load_prompt("issue_summarizer.txt")
        system_prompt = format_template(self.base.jinja_env, template, ctx)
        output_format = self.base.get_output_format(IssueSummary)

        return await self.base.analyze(
            system_prompt=system_prompt,
            user_prompt=output_format,
            response_model=IssueSummary,
            output_parser=output_parser,
        )

    async def run(
        self,
        ctx: PipelineContext,
        issue_handler: IssueHandler | None,
        output_parser: OutputParserAgent,
    ) -> None:
        """Execute as a pipeline step.

        Reads merge_request from ctx, discovers and fetches issues, sets ctx.issues and ctx.issue_summary.
        """
        if ctx.merge_request is None:
            raise ValueError("merge_request must be set before running issue explorer")

        # Skip if no issue handler (local mode)
        if issue_handler is None:
            logger.info("IssueExplorer skipped: no issue handler")
            ctx.issues = []
            ctx.issue_summary = None
            return

        logger.info("Starting IssueExplorer")
        result = await self.analyse(
            issue_handler=issue_handler,
            repo=ctx.repo,
            description=ctx.merge_request.description,
            output_parser=output_parser,
            mr_number=int(ctx.merge_id),
        )

        ctx.issues = result.output.issues
        ctx.issue_summary = result.output.summary
        ctx.track("issue_explorer", result.metadata)
        ac_count = len(ctx.issue_summary.acceptance_criteria) if ctx.issue_summary else 0
        logger.info(
            f"IssueExplorer completed: {len(ctx.issues)} issues, {ac_count} acceptance criteria"
        )

    @classmethod
    def default(cls) -> IssueExplorerAgent:
        """Create IssueExplorerAgent with default config from environment.

        Returns:
            IssueExplorerAgent with configuration from environment variables
        """
        from ..config import Config

        config = Config.from_env()
        return cls(config.create_agent_config("issue_explorer"))
