"""Performance bottleneck analyzer using AST.

Detects performance anti-patterns in Python code:
- I/O calls inside loops
- Object recreation in loops
- Nested loops
- Pandas row-wise operations
- Sync calls in async context
"""

from __future__ import annotations

import ast
import logging
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# 점수 테이블
PATTERN_SCORES = {
    # 루프 내부 I/O (가장 심각)
    "loop_io_requests": 15,
    "loop_io_db": 12,
    "loop_io_file": 10,
    "loop_io_network": 12,
    
    # 중첩 루프
    "nested_loop_depth_2": 8,
    "nested_loop_depth_3": 15,
    "nested_loop_depth_4+": 25,
    
    # 루프 내부 객체 재생성
    "loop_object_list": 3,
    "loop_object_dict": 3,
    "loop_object_regex": 5,
    "loop_object_pattern": 5,
    
    # pandas 행 단위
    "pandas_iterrows": 10,
    "pandas_apply": 6,
    "pandas_itertuples": 4,
    
    # async 컨텍스트
    "sync_io_in_async": 8,
    "blocking_call_in_async": 10,
    
    # 기타
    "recursive_call": 5,
    "large_collection_loop": 6,
}


class PerformanceBottleneckVisitor(ast.NodeVisitor):
    """AST Visitor that detects performance bottleneck patterns."""
    
    def __init__(self):
        self.function_issues = {}  # {function_name: [issues]}
        self.current_function = None
        self.loop_stack = []  # [(type, depth, start_line)]
        self.function_contexts = {}  # {function_name: {line_count, start_line}}
        self.in_async = False
    
    def reset(self):
        """Reset visitor state for new file."""
        self.function_issues = {}
        self.current_function = None
        self.loop_stack = []
        self.function_contexts = {}
        self.in_async = False
    
    def visit_FunctionDef(self, node):
        """Visit function definition."""
        old_func = self.current_function
        old_async = self.in_async
        self.current_function = node.name
        self.loop_stack = []
        self.in_async = False
        
        start_line = node.lineno
        self.generic_visit(node)  # 함수 본문 탐색
        
        # 함수 끝 라인 추정
        end_line = self._estimate_function_end(node)
        line_count = end_line - start_line
        
        self.function_contexts[node.name] = {
            "line_count": line_count,
            "start_line": start_line,
        }
        
        self.current_function = old_func
        self.in_async = old_async
    
    def visit_AsyncFunctionDef(self, node):
        """Visit async function definition."""
        old_async = self.in_async
        self.in_async = True
        self.visit_FunctionDef(node)  # 재사용
        self.in_async = old_async
    
    def visit_For(self, node):
        """Visit for loop."""
        depth = len(self.loop_stack)
        self.loop_stack.append(("for", depth, node.lineno))
        self.generic_visit(node)
        self.loop_stack.pop()
    
    def visit_While(self, node):
        """Visit while loop."""
        depth = len(self.loop_stack)
        self.loop_stack.append(("while", depth, node.lineno))
        self.generic_visit(node)
        self.loop_stack.pop()
    
    def visit_Call(self, node):
        """Visit function call."""
        if not self.current_function:
            return
        
        func_name = self._get_call_name(node)
        in_loop = len(self.loop_stack) > 0
        nesting_depth = len(self.loop_stack)
        
        # 패턴 감지
        if in_loop:
            # 루프 내부 I/O
            if self._is_io_call(func_name):
                io_type = self._classify_io_type(func_name)
                self._add_issue({
                    "type": f"loop_io_{io_type}",
                    "line": node.lineno,
                    "nesting_depth": nesting_depth,
                    "description": f"루프 내부에서 I/O 호출: {func_name}",
                })
            
            # 루프 내부 객체 재생성
            if self._is_object_creation(func_name):
                obj_type = self._classify_object_type(func_name)
                self._add_issue({
                    "type": f"loop_object_{obj_type}",
                    "line": node.lineno,
                    "nesting_depth": nesting_depth,
                    "description": f"루프 내부에서 객체 재생성: {func_name}",
                })
            
            # pandas 행 단위 연산
            if self._is_pandas_rowwise(func_name):
                pandas_type = self._classify_pandas_type(func_name)
                self._add_issue({
                    "type": f"pandas_{pandas_type}",
                    "line": node.lineno,
                    "nesting_depth": nesting_depth,
                    "description": f"pandas 행 단위 연산: {func_name}",
                })
        
        # 중첩 루프 체크 (루프 내부에서 또 다른 루프 시작 감지는 visit_For/While에서)
        if nesting_depth >= 2:
            depth_key = f"nested_loop_depth_{nesting_depth}" if nesting_depth < 4 else "nested_loop_depth_4+"
            self._add_issue({
                "type": depth_key,
                "line": node.lineno,
                "nesting_depth": nesting_depth,
                "description": f"{nesting_depth}중 중첩 루프",
            })
        
        # async 컨텍스트에서 동기 I/O
        if self.in_async and not self._has_await(node):
            if self._is_blocking_io(func_name):
                self._add_issue({
                    "type": "sync_io_in_async",
                    "line": node.lineno,
                    "nesting_depth": 0,
                    "description": f"async 컨텍스트에서 동기 I/O: {func_name}",
                })
        
        self.generic_visit(node)
    
    def _add_issue(self, issue: dict[str, Any]):
        """Add issue to current function's issue list."""
        if self.current_function:
            if self.current_function not in self.function_issues:
                self.function_issues[self.current_function] = []
            self.function_issues[self.current_function].append(issue)
    
    def _get_call_name(self, node: ast.Call) -> str:
        """Extract function name from call node."""
        if isinstance(node.func, ast.Attribute):
            # obj.method() 형태
            obj = self._get_attr_base(node.func)
            return f"{obj}.{node.func.attr}"
        elif isinstance(node.func, ast.Name):
            return node.func.id
        return ""
    
    def _get_attr_base(self, node: ast.Attribute) -> str:
        """Get base object name from attribute."""
        if isinstance(node.value, ast.Name):
            return node.value.id
        elif isinstance(node.value, ast.Attribute):
            return f"{self._get_attr_base(node.value)}.{node.value.attr}"
        return ""
    
    def _is_io_call(self, func_name: str) -> bool:
        """Check if function call is I/O operation."""
        io_patterns = [
            "requests.get", "requests.post", "requests.put", "requests.delete",
            "open", "read", "write", "readlines",
            "query", "execute", "fetch", "fetchall", "fetchone",
            "get", "post", "put", "delete",  # HTTP 메서드
            "urllib", "httpx", "aiohttp",
        ]
        return any(pattern in func_name.lower() for pattern in io_patterns)
    
    def _classify_io_type(self, func_name: str) -> str:
        """Classify I/O type."""
        func_lower = func_name.lower()
        if "requests" in func_lower or "http" in func_lower:
            return "requests"
        elif "query" in func_lower or "execute" in func_lower or "fetch" in func_lower:
            return "db"
        elif "open" in func_lower or "read" in func_lower or "write" in func_lower:
            return "file"
        else:
            return "network"
    
    def _is_object_creation(self, func_name: str) -> bool:
        """Check if function call creates objects that should be cached."""
        creation_patterns = [
            "re.compile", "Pattern",
            "list(", "dict(", "set(",
        ]
        return any(pattern in func_name for pattern in creation_patterns)
    
    def _classify_object_type(self, func_name: str) -> str:
        """Classify object creation type."""
        if "re.compile" in func_name or "Pattern" in func_name:
            return "regex"
        elif "list" in func_name:
            return "list"
        elif "dict" in func_name:
            return "dict"
        else:
            return "pattern"
    
    def _is_pandas_rowwise(self, func_name: str) -> bool:
        """Check if function call is pandas row-wise operation."""
        return any(x in func_name for x in [".iterrows", ".apply", ".itertuples"])
    
    def _classify_pandas_type(self, func_name: str) -> str:
        """Classify pandas operation type."""
        if ".iterrows" in func_name:
            return "iterrows"
        elif ".apply" in func_name:
            return "apply"
        elif ".itertuples" in func_name:
            return "itertuples"
        return "apply"
    
    def _is_blocking_io(self, func_name: str) -> bool:
        """Check if function is blocking I/O."""
        blocking_patterns = [
            "requests.", "urllib", "open(", "read(", "write(",
            "time.sleep", "input(",
        ]
        return any(pattern in func_name.lower() for pattern in blocking_patterns)
    
    def _has_await(self, node: ast.Call) -> bool:
        """Check if call is awaited (simplified check)."""
        # 부모 노드가 Await인지 확인 (간단한 버전)
        # 실제로는 부모 노드를 추적해야 하지만, 여기서는 간단히 처리
        return False  # 대부분의 경우 await는 별도로 체크
    
    def _estimate_function_end(self, node: ast.FunctionDef) -> int:
        """Estimate function end line."""
        if not node.body:
            return node.lineno
        
        # 마지막 노드의 라인 번호 추정
        last_node = node.body[-1]
        if hasattr(last_node, "lineno"):
            return last_node.lineno + 10  # 여유있게
        return node.lineno + 50


def calculate_score(issues: list[dict[str, Any]], function_context: dict[str, Any]) -> int:
    """Calculate performance bottleneck score from issues."""
    base_score = 0
    multipliers = []
    
    # 1. 각 패턴별 기본 점수 합산
    for issue in issues:
        pattern_type = issue["type"]
        base = PATTERN_SCORES.get(pattern_type, 1)
        
        # 2. 중첩 깊이 가중치
        nesting = issue.get("nesting_depth", 0)
        if nesting > 0:
            base *= (1 + nesting * 0.5)  # 깊이 1: 1.5배, 깊이 2: 2배
        
        base_score += base
    
    # 3. 함수 크기 고려
    func_lines = function_context.get("line_count", 0)
    if func_lines > 200:
        multipliers.append(1.5)
    elif func_lines > 100:
        multipliers.append(1.2)
    
    # 4. 패턴 조합 가중치
    pattern_types = {i["type"] for i in issues}
    
    # 루프 + I/O + 객체 생성 = 매우 위험
    has_loop_io = any("loop_io" in pt for pt in pattern_types)
    has_loop_obj = any("loop_object" in pt for pt in pattern_types)
    has_nested = any("nested_loop" in pt for pt in pattern_types)
    
    if has_nested and has_loop_io:
        multipliers.append(1.5)
    
    if has_loop_io and has_loop_obj:
        multipliers.append(1.3)
    
    # 5. 최종 점수 계산
    final_score = base_score
    for mult in multipliers:
        final_score *= mult
    
    return int(final_score)


def analyze_performance_bottlenecks(
    file_entries: list[dict[str, Any]] | None = None,
    files: list[Path] | None = None,
) -> list[dict[str, Any]]:
    """Analyze performance bottlenecks using AST.
    
    Args:
        file_entries: List of file entries with content (from input_context)
        files: List of file paths (for direct file reading)
    
    Returns:
        List of suspicious regions with scores
    """
    visitor = PerformanceBottleneckVisitor()
    suspicious_regions = []
    
    # 파일별로 AST 분석
    if file_entries:
        # input_context에서 파일 가져오기
        for entry in file_entries:
            path_str = entry.get("path", "")
            if not path_str:
                continue
            
            path = Path(path_str)
            if path.suffix != ".py":
                continue
            
            source = entry.get("content", "")
            if not source:
                continue
            
            try:
                tree = ast.parse(source, filename=str(path))
                visitor.reset()
                visitor.visit(tree)
                
                # 함수별로 점수 집계
                for func_name, issues in visitor.function_issues.items():
                    func_context = visitor.function_contexts.get(func_name, {})
                    score = calculate_score(issues, func_context)
                    
                    if score > 0:
                        suspicious_regions.append({
                            "file": str(path),
                            "function": func_name,
                            "line": issues[0]["line"] if issues else func_context.get("start_line", 0),
                            "score": score,
                            "issues": issues,
                            "function_context": func_context,
                        })
            except SyntaxError as e:
                logger.warning(f"Syntax error in {path}: {e}")
                continue
            except Exception as e:
                logger.warning(f"Error analyzing {path}: {e}")
                continue
    
    elif files:
        # 직접 파일 읽기
        for path in files:
            if path.suffix != ".py":
                continue
            
            try:
                source = path.read_text(encoding="utf-8")
                tree = ast.parse(source, filename=str(path))
                visitor.reset()
                visitor.visit(tree)
                
                # 함수별로 점수 집계
                for func_name, issues in visitor.function_issues.items():
                    func_context = visitor.function_contexts.get(func_name, {})
                    score = calculate_score(issues, func_context)
                    
                    if score > 0:
                        suspicious_regions.append({
                            "file": str(path),
                            "function": func_name,
                            "line": issues[0]["line"] if issues else func_context.get("start_line", 0),
                            "score": score,
                            "issues": issues,
                            "function_context": func_context,
                        })
            except (SyntaxError, UnicodeDecodeError) as e:
                logger.warning(f"Error reading {path}: {e}")
                continue
            except Exception as e:
                logger.warning(f"Error analyzing {path}: {e}")
                continue
    
    return suspicious_regions


def filter_top_suspicious(
    all_suspicious: list[dict[str, Any]],
    top_percent: int = 10,
    min_items: int = 1,
    max_items: int = 20,
) -> list[dict[str, Any]]:
    """Filter top N% of suspicious regions by score.
    
    Args:
        all_suspicious: All suspicious regions
        top_percent: Percentage to keep (default 10%)
        min_items: Minimum items to return
        max_items: Maximum items to return
    
    Returns:
        Top suspicious regions sorted by score
    """
    if not all_suspicious:
        return []
    
    # 점수 내림차순 정렬
    sorted_regions = sorted(all_suspicious, key=lambda x: x["score"], reverse=True)
    
    # 상위 N% 계산
    percent_count = len(sorted_regions) * top_percent // 100
    top_n = max(min_items, min(percent_count, max_items))
    
    filtered = sorted_regions[:top_n]
    
    # 디버깅: 필터링 결과 로그
    logger.debug(
        f"필터링: 전체 {len(all_suspicious)}개 → "
        f"상위 {top_percent}% ({percent_count}개) → "
        f"최대 {max_items}개 제한 → "
        f"최종 {len(filtered)}개 선택"
    )
    
    return filtered
