"""
InterfaceReviewAgent for Gar

인터페이스 요약을 먼저 보고, 필요한 경우에만 파일 일부를 읽어오는
LLM Agent 기반 코드 리뷰어
"""

from __future__ import annotations

import json
import logging
import os
from pathlib import Path
from typing import Any

from common.console import console
from common.context import ReviewContext, ReviewFinding
from common.ui import update_file_read_progress
from interface.llm.client import AgentLLMBackend, LocalOpenAIAgentBackend
from interface.reviewers.interface.analyzer import build_interface_summary
from interface.reviewers.interface.prompts import build_system_prompt, build_user_prompt

logger = logging.getLogger(__name__)


class InterfaceReviewAgent:
    """인터페이스 중심 코드 리뷰를 수행하는 에이전트."""

    def __init__(self, backend: AgentLLMBackend | None = None) -> None:
        self._backend = backend or LocalOpenAIAgentBackend()
        self._read_files: set[Path] = set()  # 읽은 파일 추적 (중복 카운트 방지)

    # ----------------------------- Public API ---------------------------------
    def run(self, ctx: ReviewContext) -> list[ReviewFinding]:
        """주어진 컨텍스트에 대해 인터페이스 리뷰를 실행한다."""

        # 1) 파일 목록 검증
        if not ctx.target.files:
            logger.warning("리뷰 대상 파일이 없습니다.")
            return [
                ReviewFinding(
                    file=None,
                    line=None,
                    category="system",
                    severity="info",
                    message="리뷰 대상 파일이 없습니다. 검사할 파일 경로를 확인하세요.",
                )
            ]

        # 2) 인터페이스 요약 생성 (가벼운 AST 분석)
        input_context = ctx.state.input_context or {}
        file_entries = input_context.get("files") if isinstance(input_context, dict) else None
        interface_summary = build_interface_summary(ctx.target.files, file_entries=file_entries)
        ctx.state.interface_summary = interface_summary

        stats = interface_summary.get("stats", {})
        modules = interface_summary.get("modules", [])
        parse_failures = interface_summary.get("parse_failures", [])

        if not ctx.quiet and os.getenv("GAR_DEBUG") == "1":
            from rich.table import Table as RichTable
            
            # 인터페이스 분석 통계를 테이블로 표시
            stats_table = RichTable(
                title="[bold cyan]인터페이스 분석 통계[/bold cyan]",
                show_header=False,
                box=None,
                padding=(0, 1),
            )
            stats_table.add_column(style="bold cyan", width=20)
            stats_table.add_column(style="white")
            
            stats_table.add_row("📦 모듈", f"[bold]{stats.get('analyzed_modules', 0)}개[/bold]")
            stats_table.add_row("🔧 함수", f"[bold]{stats.get('total_functions', 0)}개[/bold]")
            stats_table.add_row("🏗️  클래스", f"[bold]{stats.get('total_classes', 0)}개[/bold]")
            
            if stats.get("parse_failures", 0) > 0:
                stats_table.add_row("⚠️  파싱 실패", f"[bold red]{stats['parse_failures']}개[/bold red]")
            
            console.print(stats_table)
            
            if stats.get("parse_failures", 0) > 0:
                console.print()
                failure_table = RichTable(
                    title="[bold yellow]파싱 실패 파일[/bold yellow]",
                    show_header=True,
                    header_style="bold yellow",
                    box=None,
                )
                failure_table.add_column("파일 경로", style="path", overflow="fold")
                failure_table.add_column("오류", style="error")
                
                for failure in parse_failures[:5]:  # 최대 5개만 표시
                    failure_table.add_row(
                        failure.get("path", "Unknown"),
                        failure.get("parse_error", "Unknown"),
                    )
                if len(parse_failures) > 5:
                    failure_table.add_row(
                        f"[dim]... 외 {len(parse_failures) - 5}개[/dim]",
                        "",
                    )
                console.print(failure_table)
            console.print()

        # 3) 파싱 실패 파일에 대한 자동 Finding 생성
        auto_findings: list[ReviewFinding] = []
        for failure in parse_failures:
            if failure.get("parse_error"):
                auto_findings.append(
                    ReviewFinding(
                        file=Path(failure["path"]) if failure.get("path") else None,
                        line=None,
                        category="syntax_error",
                        severity="high",
                        message=f"파일 파싱 실패: {failure['parse_error']}",
                        suggestion="문법 오류를 수정하세요. 올바른 Python 문법인지 확인이 필요합니다.",
                    )
                )
        
        # 3-1) 인터페이스 요약에서 명백한 문제 자동 감지
        for module in modules:
            module_path = module.get("path")
            
            # 타입힌트 없는 함수 감지
            for func in module.get("functions", []):
                func_name = func.get("name") if isinstance(func, dict) else getattr(func, "name", None)
                returns = func.get("returns") if isinstance(func, dict) else getattr(func, "returns", None)
                lineno = func.get("lineno") if isinstance(func, dict) else getattr(func, "lineno", None)
                
                if returns is None or returns == "None":
                    # 타입힌트가 없거나 None인 경우 (실제로 뭔가 반환하는데 None인 경우 의심)
                    auto_findings.append(
                        ReviewFinding(
                            file=Path(module_path) if module_path else None,
                            line=lineno,
                            category="missing_type_hint",
                            severity="medium",
                            message=f"함수 '{func_name}'의 반환 타입힌트가 없거나 None입니다. 실제 반환값과 일치하는지 확인이 필요합니다.",
                            suggestion="반환 타입힌트를 명시적으로 추가하세요. 예: -> dict, -> Optional[dict]",
                        )
                    )

        # 4) 모듈이 전혀 없는 경우
        if not modules:
            logger.warning("분석 가능한 모듈이 없습니다.")
            if auto_findings:
                ctx.state.findings.extend(auto_findings)
                return ctx.state.findings
            return [
                ReviewFinding(
                    file=None,
                    line=None,
                    category="system",
                    severity="warning",
                    message="분석 가능한 Python 모듈이 없습니다. 파일 경로와 확장자를 확인하세요.",
                )
            ]

        # 5) 초기 프롬프트 구성
        system_prompt = build_system_prompt(ctx.config.language)
        user_prompt = build_user_prompt(interface_summary)

        messages: list[dict[str, str]] = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ]

        tools = self._build_tools_schema()

        # 디버깅: 초기 프롬프트 로깅
        if not ctx.quiet:
            logger.debug(f"시스템 프롬프트 길이: {len(system_prompt)} 문자")
            logger.debug(f"사용자 프롬프트 길이: {len(user_prompt)} 문자")
            logger.debug(f"리뷰 대상 파일 수: {len(ctx.target.files)}")

        # 6) 에이전트 루프
        step = 0
        max_steps = max(ctx.max_retries * 2, 6) if ctx.max_retries > 0 else 6
        forced_read_done = False  # 최소 한 번의 read_file 호출 여부 추적
        
        # 진행 상황 표시 (Progress bar 사용)
        progress = None
        task_id = None
        if not ctx.quiet and os.getenv("GAR_DEBUG") == "1":
            from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
            
            progress = Progress(
                SpinnerColumn(),
                TextColumn("[bold blue]{task.description}[/bold blue]"),
                BarColumn(bar_width=20, style="blue", complete_style="green", finished_style="green"),
                TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
                TextColumn("•"),
                TextColumn("[dim]{task.completed}/{task.total} 스텝[/dim]"),
                TextColumn("•"),
                TextColumn("[bold cyan]{task.fields[findings]}건 발견[/bold cyan]"),
                TimeElapsedColumn(),
                console=console,
                transient=False,
            )
            progress.start()
            task_id = progress.add_task(
                "LLM 분석 중...",
                total=max_steps,
                findings=0,
            )
        
        while step < max_steps:
            step += 1
            if not ctx.quiet and progress and task_id is not None:
                findings_count = len(ctx.state.findings)
                progress.update(
                    task_id,
                    advance=1,
                    description=f"[bold blue]분석 중... (스텝 {step}/{max_steps})[/bold blue]",
                    findings=findings_count,
                )

            step_result = self._backend.run_step(
                messages=messages,
                tools=tools,
                model=ctx.model,
                temperature=ctx.temperature,
                max_tokens=ctx.max_output_tokens,
            )

            messages = step_result.messages
            ctx.state.conversation_log = messages

            # LLM이 tool 호출 없이 종료하려 할 때 강제 개입
            if step_result.is_final and not step_result.tool_calls and not forced_read_done:
                # 아직 read_file을 한 번도 안 했으면 강제로 호출
                if ctx.target.files and not ctx.state.findings:
                    if not ctx.quiet and os.getenv("GAR_DEBUG") == "1":
                        console.print("[warning]LLM이 조기 종료하려 합니다. 추가 분석 진행...[/warning]")
                    
                    # 문제가 있을 가능성이 높은 파일 선택 (파싱 실패 > 일반 파일)
                    target_file = None
                    for failure in parse_failures:
                        if failure.get("path"):
                            target_file = Path(failure["path"])
                            break
                    
                    if not target_file:
                        # 파싱 실패 파일이 없으면 첫 번째 파일 선택
                        target_file = ctx.target.files[0]
                    
                    # 강제 read_file 실행
                    read_result = self._tool_read_file(ctx, {"path": str(target_file)})
                    forced_read_done = True
                    
                    # 읽은 내용을 기반으로 LLM에게 다시 분석 요청
                    force_message = (
                        f"파일을 읽었습니다:\n\n"
                        f"파일: {read_result.get('path', target_file)}\n"
                        f"내용:\n```python\n{read_result.get('content', '')[:2000]}\n```\n\n"
                        f"이 코드에서 인터페이스 관점의 문제점을 찾아 `report_finding`으로 보고하세요. "
                        f"타입힌트와 docstring의 불일치, None 반환 가능성 등을 확인하세요."
                    )
                    messages.append({"role": "user", "content": force_message})
                    continue  # 다시 LLM 호출
            
            if step_result.tool_calls:
                # 툴 호출 처리
                for tool_call in step_result.tool_calls:
                    # tool_call 구조: {"id": "...", "type": "function", "function": {"name": "...", "arguments": "..."}}
                    func_info = tool_call.get("function", {})
                    tool_name = func_info.get("name")
                    raw_args = func_info.get("arguments") or "{}"
                    tool_call_id = tool_call.get("id", f"call_{step}_{len(messages)}")

                    try:
                        args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
                    except (json.JSONDecodeError, TypeError):
                        args = {}

                    try:
                        if tool_name == "read_file":
                            result = self._tool_read_file(ctx, args)
                        elif tool_name == "get_interface_summary":
                            result = self._tool_get_interface_summary(ctx, args)
                        elif tool_name == "report_finding":
                            # 실시간 출력 제거 - 최종 리포트에서만 표시
                            result = self._tool_report_finding(ctx, args)
                        else:
                            result = {"error": f"Unknown tool: {tool_name}"}
                    except Exception as e:
                        logger.error(f"도구 실행 실패 ({tool_name}): {e}", exc_info=True)
                        result = {"error": f"Tool execution failed: {str(e)}"}

                    # OpenAI tool call 형식에 맞는 tool 응답 메시지 추가
                    messages.append(
                        {
                            "role": "tool",
                            "tool_call_id": tool_call_id,
                            "content": json.dumps(result, ensure_ascii=False),
                        }
                    )
                # 다음 스텝에서 LLM이 tool 결과를 보고 계속 진행
                continue

            if step_result.is_final:
                if not ctx.quiet and progress and task_id is not None:
                    findings_count = len(ctx.state.findings)
                    progress.update(
                        task_id,
                        completed=max_steps,
                        description="[bold green]✓ 분석 완료[/bold green]",
                        findings=findings_count,
                    )
                    progress.stop()
                    if os.getenv("GAR_DEBUG") == "1":
                        console.print()
                break

        if step >= max_steps:
            if not ctx.quiet and progress and task_id is not None:
                findings_count = len(ctx.state.findings)
                progress.update(
                    task_id,
                    completed=max_steps,
                    description="[bold yellow]⚠ 최대 스텝 도달[/bold yellow]",
                    findings=findings_count,
                )
                progress.stop()
                if os.getenv("GAR_DEBUG") == "1":
                    console.print()
        
        # Progress가 아직 실행 중이면 정리
        if progress:
            try:
                progress.stop()
            except Exception:
                pass

        # 7) 루프 종료 후 방어 로직 - auto_findings는 항상 추가
        # LLM이 보고한 것과 중복되지 않는 auto_findings 추가
        if auto_findings:
            reported_issues = {
                (str(f.file), f.line, f.category) 
                for f in ctx.state.findings if f.file
            }
            for auto_finding in auto_findings:
                key = (str(auto_finding.file), auto_finding.line, auto_finding.category)
                if key not in reported_issues:
                    ctx.state.findings.append(auto_finding)
            
            if not ctx.quiet and os.getenv("GAR_DEBUG") == "1":
                added_count = len([f for f in ctx.state.findings if f.category in ("syntax_error", "missing_type_hint")])
                if added_count > 0:
                    from rich.panel import Panel
                    console.print(Panel(
                        f"[bold cyan]🤖 자동 감지된 문제 {added_count}건이 추가되었습니다.[/bold cyan]",
                        border_style="cyan",
                    ))
        
        # 그래도 findings가 비어있으면 시스템 메시지 추가
        if not ctx.state.findings:
            pass  # 시스템 메시지는 아래에서 추가됨
            ctx.state.findings.append(
                ReviewFinding(
                    file=None,
                    line=None,
                    category="system",
                    severity="info",
                    message="인터페이스 리뷰 완료: 발견된 문제가 없습니다.",
                    suggestion="코드의 공개 인터페이스가 적절하게 정의되어 있습니다.",
                )
            )

        return ctx.state.findings

    # ---------------------------- Tool schema ---------------------------------
    def _build_tools_schema(self) -> list[dict[str, Any]]:
        """OpenAI function calling 스타일의 tool 스키마 정의"""
        return [
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "특정 파일의 일부분을 읽어 인터페이스 관련 구현 세부사항을 확인한다.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {"type": "string", "description": "읽을 파일의 절대 경로 또는 프로젝트 내 경로"},
                            "start_line": {
                                "type": "integer",
                                "description": "시작 줄 번호(1 기반). 지정하지 않으면 파일의 처음부터.",
                                "minimum": 1,
                            },
                            "end_line": {
                                "type": "integer",
                                "description": "끝 줄 번호(포함). 지정하지 않으면 파일 끝까지.",
                                "minimum": 1,
                            },
                        },
                        "required": ["path"],
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "get_interface_summary",
                    "description": "이미 계산된 인터페이스 요약 정보를 다시 가져온다.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "특정 모듈 경로를 지정하면 해당 모듈 요약만 반환한다.",
                            }
                        },
                    },
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "report_finding",
                    "description": "발견한 인터페이스 문제를 구조화된 형태로 보고한다.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "file": {"type": "string", "description": "문제가 발견된 파일 경로"},
                            "line": {"type": "integer", "description": "관련된 줄 번호(모르면 생략 가능)", "minimum": 1},
                            "category": {
                                "type": "string",
                                "description": "예: api_contract, breaking_change, doc_mismatch, naming, syntax_error 등",
                            },
                            "severity": {
                                "type": "string",
                                "description": "예: low, medium, high, critical",
                            },
                            "message": {
                                "type": "string",
                                "description": "문제가 되는 이유를 자세히 설명.",
                            },
                            "suggestion": {
                                "type": "string",
                                "description": "개선 제안 또는 수정 방향을 작성.",
                            },
                            "extra": {
                                "type": "object",
                                "description": "추가 메타데이터(선택). anchors 배열(코드 토큰 1~3개)을 포함한다.",
                            },
                        },
                        "required": ["file", "category", "severity", "message"],
                    },
                },
            },
        ]

    # ------------------------------ Tools -------------------------------------
    def _tool_read_file(self, ctx: ReviewContext, args: dict[str, Any]) -> dict[str, Any]:
        raw_path = args.get("path")
        if not raw_path:
            return {"error": "path is required"}

        path = Path(raw_path)
        if not path.is_absolute():
            # 대상 파일 목록에서 매칭 시도
            for candidate in ctx.target.files:
                if str(candidate).endswith(raw_path):
                    path = candidate
                    break

        cached = None
        input_context = ctx.state.input_context
        if isinstance(input_context, dict):
            for entry in input_context.get("files", []):
                if entry.get("path") == raw_path or entry.get("path") == str(path):
                    cached = entry
                    break
        if cached:
            content = str(cached.get("content", ""))
            lines = content.splitlines()
            end_line = int(args.get("end_line") or len(lines))
            start_line = int(args.get("start_line") or 1)
            start_idx = max(start_line - 1, 0)
            end_idx = max(min(end_line, len(lines)), start_idx + 1)
            snippet = "\n".join(lines[start_idx:end_idx])
            return {
                "path": cached.get("path"),
                "start_line": start_line,
                "end_line": end_idx,
                "total_lines": len(lines),
                "content": snippet,
            }
        if not path.exists():
            return {"error": f"file not found: {path}"}

        start_line = int(args.get("start_line") or 1)
        end_line = int(args.get("end_line") or 0)

        try:
            content = path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            try:
                content = path.read_text(encoding="latin-1")
            except Exception as e:
                return {"error": f"파일 읽기 실패: {e}"}
        except Exception as e:
            return {"error": f"파일 읽기 실패: {e}"}
        
        # 파일 읽기 진행률 업데이트 (중복 카운트 방지)
        if path not in self._read_files:
            self._read_files.add(path)
            # progress handle 가져오기
            if isinstance(input_context, dict):
                progress_handle = input_context.get("_ui_handle")
                if progress_handle:
                    total_files = len(ctx.target.files)
                    update_file_read_progress(progress_handle, total=total_files)
                    update_file_read_progress(progress_handle, completed=len(self._read_files))

        lines = content.splitlines()
        if end_line <= 0 or end_line > len(lines):
            end_line = len(lines)

        # 1-based index 보정
        start_idx = max(start_line - 1, 0)
        end_idx = max(min(end_line, len(lines)), start_idx + 1)

        snippet = "\n".join(lines[start_idx:end_idx])
        return {
            "path": str(path),
            "start_line": start_line,
            "end_line": end_line,
            "total_lines": len(lines),
            "content": snippet,
        }

    def _tool_get_interface_summary(self, ctx: ReviewContext, args: dict[str, Any]) -> dict[str, Any]:
        summary = ctx.state.interface_summary or {}
        path_filter = args.get("path")
        if not path_filter:
            return summary

        modules = summary.get("modules", [])
        filtered = [m for m in modules if m.get("path") == path_filter]
        return {"modules": filtered}

    def _tool_report_finding(self, ctx: ReviewContext, args: dict[str, Any]) -> dict[str, Any]:
        file_str = args.get("file")
        path = Path(file_str) if file_str else None
        line_val = args.get("line")
        line = int(line_val) if isinstance(line_val, int) else None

        finding = ReviewFinding(
            file=path,
            line=line,
            category=str(args.get("category") or ""),
            severity=str(args.get("severity") or ""),
            message=str(args.get("message") or ""),
            suggestion=(str(args["suggestion"]) if "suggestion" in args and args["suggestion"] is not None else None),
            extra=(dict(args["extra"]) if isinstance(args.get("extra"), dict) else {}),
        )
        ctx.state.findings.append(finding)
        return {"status": "ok", "finding_count": len(ctx.state.findings)}
