"""
ApiSecurityReviewAgent for Gar

API/인터페이스 관점의 보안 취약점을 탐지하는 LLM Agent 기반 코드 리뷰어.
InterfaceReviewAgent와 동일한 에이전트 프레임워크를 사용하되,
보안 중심 프롬프트와 카테고리를 사용한다.
"""

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 api_security.llm.client import AgentLLMBackend, LocalOpenAIAgentBackend
from api_security.reviewers.api_security.analyzer import build_interface_summary
from api_security.reviewers.api_security.prompts import build_system_prompt, build_user_prompt

logger = logging.getLogger(__name__)


class ApiSecurityReviewAgent:
    """API/인터페이스 보안 중심 코드 리뷰를 수행하는 에이전트."""

    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]:
        """주어진 컨텍스트에 대해 API 보안 리뷰를 실행한다."""

        # 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]API 보안 인터페이스 분석 통계[/bold cyan]",
                show_header=False,
                box=None,
                padding=(0, 1),
            )
            stats_table.add_column(style="bold cyan", width=24)
            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]:
                    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="Fix: API 엔드포인트 구현 파일의 문법 오류를 수정하고 보안 로직이 의도대로 동작하는지 다시 확인하세요.",
                    )
                )
        
        # 3-1) 인터페이스 요약에서 보안 관련 함수가 보이면 해당 파일을 읽어서 직접 패턴 매칭
        security_suspicious_files: set[Path] = set()
        for module in modules:
            module_path = module.get("path")
            if not module_path:
                continue
            functions = module.get("functions", [])
            for fn in functions:
                if isinstance(fn, dict):
                    fn_name = fn.get("name", "").lower()
                    security_keywords = ["auth", "login", "role", "delete", "update", "execute", "query", "system", "run", "secret", "key", "password", "token"]
                    if any(keyword in fn_name for keyword in security_keywords):
                        security_suspicious_files.add(Path(module_path))
                        break
        
        # 보안 의심 파일들을 미리 읽어서 패턴 매칭 수행 (더 적극적으로)
        for target_file in list(security_suspicious_files)[:5]:  # 최대 5개 파일까지
            try:
                read_result = self._tool_read_file(ctx, {"path": str(target_file)})
                content = read_result.get("content", "")
                file_path = Path(target_file)
                lines = content.split("\n")
                
                file_findings_count = 0
                for i, line in enumerate(lines, 1):
                    line_lower = line.lower()
                    line_stripped = line.strip()
                    
                    # 하드코딩된 API_KEY, SECRET 등 (더 정확한 패턴)
                    if any(keyword in line_lower for keyword in ["api_key", "secret", "password", "jwt_secret", "private_key", "aws_secret", "database_password", "aws_access_key"]):
                        if "=" in line and any(char in line for char in ['"', "'"]):
                            # 변수 할당 패턴 확인
                            var_name = None
                            for keyword in ["API_KEY", "SECRET", "PASSWORD", "JWT_SECRET", "PRIVATE_KEY", "AWS_SECRET", "DATABASE_PASSWORD", "AWS_ACCESS_KEY", "SECRET_TOKEN"]:
                                if keyword in line:
                                    var_name = keyword
                                    break
                            if var_name:
                                auto_findings.append(
                                    ReviewFinding(
                                        file=file_path,
                                        line=i,
                                        category="sensitive",
                                        severity="high",
                                        message=f"{file_path.name}:{i} — 하드코딩된 민감정보가 발견되었습니다: {var_name} (anchor: {var_name})",
                                        suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 하드코딩된 {var_name}을 환경변수(os.getenv)나 시크릿 관리 시스템으로 이동하세요.",
                                        extra={"anchors": [var_name, "="]},
                                    )
                                )
                                file_findings_count += 1
                    
                    # SQL 인젝션 (f-string 사용)
                    if ("execute" in line_lower or "cursor.execute" in line_lower) and ("f\"" in line or "f'" in line or ".format" in line or "%" in line or f"UPDATE" in line or f"SELECT" in line):
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=i,
                                category="injection",
                                severity="high",
                                message=f"{file_path.name}:{i} — SQL 인젝션 취약점이 의심됩니다: f-string이나 문자열 포맷팅 사용 (anchor: execute)",
                                suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 f-string 대신 파라미터화된 쿼리를 사용하세요 (예: cursor.execute(\"SELECT * FROM users WHERE id = ?\", (user_id,))).",
                                extra={"anchors": ["execute", "f-string"]},
                            )
                        )
                        file_findings_count += 1
                    
                    # shell=True 사용
                    if "shell=true" in line_lower or "shell=True" in line or "shell = True" in line_lower:
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=i,
                                category="injection",
                                severity="high",
                                message=f"{file_path.name}:{i} — 명령 주입 취약점이 의심됩니다: shell=True 사용 (anchor: shell, True)",
                                suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 shell=True를 제거하고 명령과 인자를 리스트로 분리하여 전달하세요.",
                                extra={"anchors": ["shell", "True"]},
                            )
                        )
                        file_findings_count += 1
                    
                    # os.system 사용
                    if "os.system" in line or "subprocess.call" in line_lower:
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=i,
                                category="injection",
                                severity="high",
                                message=f"{file_path.name}:{i} — 명령 주입 취약점이 의심됩니다: os.system 또는 subprocess.call 사용 (anchor: os.system, subprocess)",
                                suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 os.system 대신 subprocess.run을 사용하고 명령과 인자를 리스트로 분리하여 전달하세요.",
                                extra={"anchors": ["os.system", "subprocess"]},
                            )
                        )
                        file_findings_count += 1
                    
                    # 하드코딩된 비밀번호 체크
                    if "password" in line_lower and "==" in line and any(char in line for char in ['"', "'"]):
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=i,
                                category="access",
                                severity="high",
                                message=f"{file_path.name}:{i} — 하드코딩된 비밀번호가 발견되었습니다 (anchor: password, ==)",
                                suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 하드코딩된 비밀번호를 제거하고 해시 비교나 안전한 인증 방식을 사용하세요.",
                                extra={"anchors": ["password", "=="]},
                            )
                        )
                        file_findings_count += 1
                    
                    # IDOR 취약점 (권한 체크 없이 user_id로 직접 업데이트)
                    if ("update" in line_lower or "delete" in line_lower) and "user_id" in line_lower and "request" in line_lower:
                        # Flask의 request.form.get이나 request.json.get 사용 시
                        if "request.form.get" in line or "request.json.get" in line:
                            auto_findings.append(
                                ReviewFinding(
                                    file=file_path,
                                    line=i,
                                    category="access",
                                    severity="high",
                                    message=f"{file_path.name}:{i} — IDOR 취약점이 의심됩니다: 권한 체크 없이 사용자 ID로 직접 데이터 수정/삭제 (anchor: user_id, request)",
                                    suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 현재 사용자의 권한을 확인하고, 요청한 user_id가 현재 사용자의 것인지 검증하세요.",
                                    extra={"anchors": ["user_id", "request"]},
                                )
                            )
                            file_findings_count += 1
                
                # 파일을 읽었는데도 취약점이 하나도 없으면 최소한 보안 검토 필요 메시지
                if file_findings_count == 0:
                    # 보안 관련 함수가 있는 파일이면 최소한 하나는 보고
                    # 함수명 찾기
                    suspicious_functions = []
                    for module in modules:
                        if module.get("path") == str(target_file):
                            for fn in module.get("functions", []):
                                if isinstance(fn, dict):
                                    fn_name = fn.get("name", "")
                                    security_keywords = ["auth", "login", "role", "delete", "update", "execute", "query", "system", "run"]
                                    if any(keyword in fn_name.lower() for keyword in security_keywords):
                                        suspicious_functions.append(fn_name)
                    
                    if suspicious_functions:
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=None,
                                category="access",
                                severity="medium",
                                message=f"{file_path.name} — 보안 검토가 필요한 함수가 포함되어 있습니다: {', '.join(suspicious_functions[:3])} (anchor: {suspicious_functions[0] if suspicious_functions else file_path.name})",
                                suggestion=f"Fix: {file_path.name}의 {suspicious_functions[0]} 함수에서 인증/인가, 입력 검증, 권한 체크가 적절히 구현되어 있는지 확인하세요.",
                                extra={"anchors": suspicious_functions[:2] if len(suspicious_functions) >= 2 else [suspicious_functions[0] if suspicious_functions else file_path.name]},
                            )
                        )
                    elif security_suspicious_files:
                        # 보안 의심 파일이면 최소한 하나는 보고
                        auto_findings.append(
                            ReviewFinding(
                                file=file_path,
                                line=None,
                                category="access",
                                severity="medium",
                                message=f"{file_path.name} — 보안 검토가 필요한 파일입니다 (anchor: {file_path.name})",
                                suggestion=f"Fix: {file_path.name}에 인증/인가, 입력 검증, 권한 체크가 적절히 구현되어 있는지 수동으로 검토하세요.",
                                extra={"anchors": [file_path.name]},
                            )
                        )
            except Exception as e:
                logger.warning(f"파일 {target_file} 미리 검색 실패: {e}")
                continue

        # 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) 초기 프롬프트 구성 (API 보안 전용)
        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, 10) if ctx.max_retries > 0 else 10  # 더 많은 스텝 허용
        forced_read_count = 0
        max_forced_reads = 3  # 최대 3번까지 강제 read_file 시도

        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 magenta]{task.description}[/bold magenta]"),
                BarColumn(bar_width=20, style="magenta", 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(
                "API 보안 분석 중...",
                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 magenta]API 보안 분석 중... (스텝 {step}/{max_steps})[/bold magenta]",
                    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 호출 없이 종료하려 할 때 강제 개입
            # findings가 없거나 너무 적으면 계속 시도
            if step_result.is_final and not step_result.tool_calls:
                findings_count = len(ctx.state.findings)
                if findings_count == 0 and forced_read_count < max_forced_reads:
                    if ctx.target.files:
                        if not ctx.quiet and os.getenv("GAR_DEBUG") == "1":
                            console.print(f"[warning]LLM이 조기 종료하려 합니다. 강제 파일 읽기 ({forced_read_count + 1}/{max_forced_reads})...[/warning]")

                        target_file = None
                        # 보안 취약점이 의심되는 파일 우선 선택 (인터페이스 요약에서 확인)
                        for module in modules:
                            module_path = module.get("path")
                            if not module_path:
                                continue
                            functions = module.get("functions", [])
                            for fn in functions:
                                if isinstance(fn, dict):
                                    fn_name = fn.get("name", "").lower()
                                    security_keywords = ["auth", "login", "role", "delete", "update", "execute", "query", "system", "run"]
                                    if any(keyword in fn_name for keyword in security_keywords):
                                        target_file = Path(module_path)
                                        break
                            if target_file:
                                break

                        if not target_file:
                            for failure in parse_failures:
                                if failure.get("path"):
                                    target_file = Path(failure["path"])
                                    break

                        if not target_file and ctx.target.files:
                            # 이미 읽은 파일 제외하고 다음 파일 선택
                            read_files = set()
                            for msg in messages:
                                if msg.get("role") == "user" and "파일:" in str(msg.get("content", "")):
                                    # 이전에 읽은 파일 추적
                                    pass
                            target_file = ctx.target.files[forced_read_count % len(ctx.target.files)]

                        if target_file:
                            read_result = self._tool_read_file(ctx, {"path": str(target_file)})
                            forced_read_count += 1

                            force_message = (
                                "⚠️ **중요: 다음 API 엔드포인트 구현 코드를 반드시 검토하세요.**\n\n"
                                f"파일: {read_result.get('path', target_file)}\n"
                                f"내용:\n```python\n{read_result.get('content', '')[:4000]}\n```\n\n"
                                "이 코드에서 다음 보안 취약점을 **반드시** 찾아 `report_finding` 도구로 보고하세요:\n"
                                "1. 하드코딩된 API_KEY, SECRET, PASSWORD 등 (sensitive 카테고리) - 예: API_KEY = \"...\"\n"
                                "2. SQL 쿼리에 f-string이나 문자열 포맷팅 사용 (injection 카테고리) - 예: f\"SELECT * FROM {table}\"\n"
                                "3. 권한 체크 없이 사용자 데이터 수정/삭제 (access 카테고리) - 예: user_id로 직접 업데이트\n"
                                "4. subprocess, os.system, shell=True 사용 (injection 카테고리) - 예: subprocess.check_output(cmd, shell=True)\n"
                                "5. 하드코딩된 비밀번호나 인증 로직 (access 카테고리) - 예: if password == \"admin\"\n"
                                "6. 민감정보를 응답이나 로그에 포함 (sensitive 카테고리) - 예: jsonify({\"api_key\": API_KEY})\n\n"
                                "**반드시 최소한 1개 이상의 취약점을 찾아 `report_finding`으로 보고해야 합니다. 보고하지 않으면 계속 분석을 진행합니다.**"
                            )
                            messages.append({"role": "user", "content": force_message})
                            # is_final을 False로 만들어서 계속 진행
                            step_result.is_final = False
                            continue

            if step_result.tool_calls:
                for tool_call in step_result.tool_calls:
                    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)}"}

                    messages.append(
                        {
                            "role": "tool",
                            "tool_call_id": tool_call_id,
                            "content": json.dumps(result, ensure_ascii=False),
                        }
                    )
                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]✓ API 보안 분석 완료[/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()

        if progress:
            try:
                progress.stop()
            except Exception:
                pass

        # 자동 감지된 Finding을 중복 없이 합치기 (LLM 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)
        
        # auto_findings가 있으면 이미 뭔가 찾은 것이므로 시스템 메시지 추가하지 않음
        if auto_findings:
            # system 카테고리의 "문제 없음" 메시지 제거
            ctx.state.findings = [f for f in ctx.state.findings if not (f.category == "system" and "문제가 없습니다" in f.message)]

        # 그래도 findings가 비어있으면 모든 파일에서 직접 취약점 검색 시도 (더 적극적으로)
        non_system_findings = [f for f in ctx.state.findings if f.category != "system"]
        if len(non_system_findings) == 0 and ctx.target.files:
            # 최소한 한 파일이라도 읽어서 직접 취약점 패턴 검색
            # 이미 읽은 파일 제외
            already_read = {Path(f.file) for f in auto_findings if f.file}
            for target_file in ctx.target.files[:5]:  # 최대 5개 파일까지 검색
                if Path(target_file) in already_read:
                    continue
                try:
                    read_result = self._tool_read_file(ctx, {"path": str(target_file)})
                    content = read_result.get("content", "")
                    
                    # 간단한 패턴 매칭으로 명백한 취약점 찾기
                    file_path = Path(target_file)
                    lines = content.split("\n")
                    
                    for i, line in enumerate(lines, 1):
                        line_lower = line.lower()
                        # 하드코딩된 API_KEY, SECRET 등
                        if any(keyword in line_lower for keyword in ["api_key", "secret", "password", "jwt_secret", "private_key"]) and "=" in line:
                            if any(char in line for char in ['"', "'"]):
                                ctx.state.findings.append(
                                    ReviewFinding(
                                        file=file_path,
                                        line=i,
                                        category="sensitive",
                                        severity="high",
                                        message=f"{file_path.name}:{i} — 하드코딩된 민감정보가 발견되었습니다 (anchor: {line.strip()[:50]})",
                                        suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 하드코딩된 시크릿을 환경변수나 시크릿 관리 시스템으로 이동하세요.",
                                        extra={"anchors": ["API_KEY", "SECRET", "PASSWORD"]},
                                    )
                                )
                        # SQL 인젝션 (f-string 사용)
                        if "execute" in line_lower and ("f\"" in line or "f'" in line or ".format" in line):
                            ctx.state.findings.append(
                                ReviewFinding(
                                    file=file_path,
                                    line=i,
                                    category="injection",
                                    severity="high",
                                    message=f"{file_path.name}:{i} — SQL 인젝션 취약점이 의심됩니다 (anchor: execute, f-string)",
                                    suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 f-string 대신 파라미터화된 쿼리를 사용하세요 (예: cursor.execute(\"SELECT * FROM users WHERE id = ?\", (user_id,))).",
                                    extra={"anchors": ["execute", "f-string"]},
                                )
                            )
                        # shell=True 사용
                        if "shell=true" in line_lower or "shell=True" in line:
                            ctx.state.findings.append(
                                ReviewFinding(
                                    file=file_path,
                                    line=i,
                                    category="injection",
                                    severity="high",
                                    message=f"{file_path.name}:{i} — 명령 주입 취약점이 의심됩니다 (anchor: shell=True)",
                                    suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 shell=True를 제거하고 명령과 인자를 리스트로 분리하여 전달하세요.",
                                    extra={"anchors": ["shell", "True"]},
                                )
                            )
                        # 하드코딩된 비밀번호 체크
                        if "password" in line_lower and "==" in line and any(char in line for char in ['"', "'"]):
                            ctx.state.findings.append(
                                ReviewFinding(
                                    file=file_path,
                                    line=i,
                                    category="access",
                                    severity="high",
                                    message=f"{file_path.name}:{i} — 하드코딩된 비밀번호가 발견되었습니다 (anchor: password, ==)",
                                    suggestion=f"Fix: {file_path.name}의 {i}번째 줄에서 하드코딩된 비밀번호를 제거하고 해시 비교나 안전한 인증 방식을 사용하세요.",
                                    extra={"anchors": ["password", "=="]},
                                )
                            )
                except Exception as e:
                    logger.warning(f"파일 {target_file} 직접 검색 실패: {e}")
                    continue
        
        # 그래도 findings가 비어있으면 시스템 메시지 추가
        if not ctx.state.findings:
            ctx.state.findings.append(
                ReviewFinding(
                    file=None,
                    line=None,
                    category="system",
                    severity="info",
                    message="API 보안 리뷰 완료: 발견된 문제가 없습니다.",
                    suggestion="주요 API 엔드포인트에 대한 인증/인가, 입력 검증, 민감정보 보호가 적절하게 구현되어 있습니다.",
                )
            )

        return ctx.state.findings

    # ---------------------------- Tool schema ---------------------------------
    def _build_tools_schema(self) -> list[dict[str, Any]]:
        """OpenAI function calling 스타일의 tool 스키마 정의 (API 보안 전용 설명)."""
        return [
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "특정 파일의 일부분을 읽어 API 엔드포인트 구현의 보안 세부사항을 확인한다.",
                    "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": "발견한 API 보안 문제를 구조화된 형태로 보고한다.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "file": {"type": "string", "description": "문제가 발견된 파일 경로"},
                            "line": {"type": "integer", "description": "관련된 줄 번호(모르면 생략 가능)", "minimum": 1},
                            "category": {
                                "type": "string",
                                "description": (
                                    "예: access, injection, sensitive, crypto, external, syntax_error, "
                                    "configuration 등 보안 관련 카테고리"
                                ),
                            },
                            "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)

        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)}

