#!/usr/bin/env python3
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Script to find unused functions in Python files.
Analyzes all .py files in the codebase and reports functions that are never called.
"""

import os
import re
import ast
from pathlib import Path
from typing import Dict, List, Set, Tuple
import logging

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

class FunctionUsageAnalyzer:
    def __init__(self, root_dir: str):
        self.root_dir = Path(root_dir)
        self.all_functions: Dict[str, List[Tuple[str, str]]] = {}  # function_name -> [(file_path, line_number)]
        self.function_calls: Set[str] = set()
        self.imports: Dict[str, Set[str]] = {}  # file_path -> set of imported modules/classes
        
    def find_python_files(self) -> List[Path]:
        """Find all Python files in the codebase."""
        python_files = []
        for py_file in self.root_dir.rglob("*.py"):
            if "venv" not in str(py_file) and "env" not in str(py_file):
                python_files.append(py_file)
        return python_files
    
    def extract_functions_from_file(self, file_path: Path) -> List[Tuple[str, int]]:
        """Extract function definitions from a Python file."""
        functions = []
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
                
            tree = ast.parse(content)
            
            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef):
                    functions.append((node.name, node.lineno))
                elif isinstance(node, ast.AsyncFunctionDef):
                    functions.append((node.name, node.lineno))
                    
        except Exception as e:
            logger.warning(f"Error parsing {file_path}: {e}")
            
        return functions
    
    def extract_function_calls_from_file(self, file_path: Path) -> Set[str]:
        """Extract function calls from a Python file."""
        calls = set()
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
                
            tree = ast.parse(content)
            
            for node in ast.walk(tree):
                if isinstance(node, ast.Call):
                    if isinstance(node.func, ast.Name):
                        calls.add(node.func.id)
                    elif isinstance(node.func, ast.Attribute):
                        calls.add(node.func.attr)
                        
        except Exception as e:
            logger.warning(f"Error parsing {file_path}: {e}")
            
        return calls
    
    def extract_imports_from_file(self, file_path: Path) -> Set[str]:
        """Extract imports from a Python file."""
        imports = set()
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
                
            tree = ast.parse(content)
            
            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for alias in node.names:
                        imports.add(alias.name)
                elif isinstance(node, ast.ImportFrom):
                    if node.module:
                        imports.add(node.module)
                    for alias in node.names:
                        if alias.name != "*":
                            imports.add(alias.name)
                        
        except Exception as e:
            logger.warning(f"Error parsing {file_path}: {e}")
            
        return imports
    
    def analyze_codebase(self):
        """Analyze the entire codebase for function definitions and usage."""
        python_files = self.find_python_files()
        logger.info(f"Found {len(python_files)} Python files")
        
        # First pass: collect all function definitions
        for file_path in python_files:
            relative_path = file_path.relative_to(self.root_dir)
            functions = self.extract_functions_from_file(file_path)
            
            for func_name, line_num in functions:
                if func_name not in self.all_functions:
                    self.all_functions[func_name] = []
                self.all_functions[func_name].append((str(relative_path), line_num))
        
        logger.info(f"Found {len(self.all_functions)} unique function definitions")
        
        # Second pass: collect all function calls
        for file_path in python_files:
            calls = self.extract_function_calls_from_file(file_path)
            self.function_calls.update(calls)
            
            imports = self.extract_imports_from_file(file_path)
            self.imports[str(file_path.relative_to(self.root_dir))] = imports
        
        logger.info(f"Found {len(self.function_calls)} unique function calls")
    
    def find_unused_functions(self) -> Dict[str, List[Tuple[str, str]]]:
        """Find functions that are never called."""
        unused_functions = {}
        
        for func_name, locations in self.all_functions.items():
            # Skip special methods and common patterns
            if (func_name.startswith('_') or 
                func_name.startswith('__') or 
                func_name in ['main', 'if __name__', 'test_', 'setup', 'teardown']):
                continue
                
            # Check if function is called anywhere
            if func_name not in self.function_calls:
                unused_functions[func_name] = locations
        
        return unused_functions
    
    def generate_report(self) -> str:
        """Generate a comprehensive report of unused functions."""
        unused_functions = self.find_unused_functions()
        
        report = []
        report.append("=" * 80)
        report.append("UNUSED FUNCTIONS REPORT")
        report.append("=" * 80)
        report.append(f"Total functions found: {len(self.all_functions)}")
        report.append(f"Unused functions: {len(unused_functions)}")
        report.append(f"Usage rate: {((len(self.all_functions) - len(unused_functions)) / len(self.all_functions) * 100):.1f}%")
        report.append("")
        
        if not unused_functions:
            report.append("🎉 All functions are being used!")
            return "\n".join(report)
        
        # Group by file
        by_file = {}
        for func_name, locations in unused_functions.items():
            for file_path, line_num in locations:
                if file_path not in by_file:
                    by_file[file_path] = []
                by_file[file_path].append((func_name, line_num))
        
        # Sort by file path
        for file_path in sorted(by_file.keys()):
            report.append(f"📁 {file_path}")
            report.append("-" * 60)
            
            functions_in_file = by_file[file_path]
            functions_in_file.sort(key=lambda x: x[1])  # Sort by line number
            
            for func_name, line_num in functions_in_file:
                report.append(f"  Line {line_num:3d}: {func_name}")
            
            report.append("")
        
        # Summary by function name
        report.append("SUMMARY BY FUNCTION NAME:")
        report.append("-" * 60)
        for func_name in sorted(unused_functions.keys()):
            locations = unused_functions[func_name]
            report.append(f"{func_name:<30} ({len(locations)} location{'s' if len(locations) > 1 else ''})")
        
        return "\n".join(report)

def main():
    """Main function to run the analysis."""
    # Get the gateway directory for a full codebase scan
    scriptPath = Path(__file__).resolve()
    gatewayPath = scriptPath.parent.parent
    logger.info(f"Analyzing codebase in: {gatewayPath}")
    
    analyzer = FunctionUsageAnalyzer(gatewayPath)
    analyzer.analyze_codebase()
    
    report = analyzer.generate_report()
    print(report)
    
    # Save report to file
    report_file = gatewayPath / "unused_functions_report.txt"
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(report)
    
    logger.info(f"Report saved to: {report_file}")

if __name__ == "__main__":
    main()
