from ci.util.global_interrupt_handler import handle_keyboard_interrupt


#!/usr/bin/env python3
# pyright: reportUnknownMemberType=false, reportOperatorIssue=false, reportArgumentType=false
"""
Enhanced Symbol Analysis Tool with Function Call Graph Analysis
Analyzes ELF files to identify symbols and function call relationships.
Shows which functions call other functions (call graph analysis).
Works with any platform (ESP32S3, UNO, etc.) that has build_info.json.
"""

import json
import re
import subprocess
import sys
from collections import defaultdict
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Optional

from running_process import RunningProcess

# Import board mapping system
from ci.boards import create_board


@dataclass
class SymbolInfo:
    """Represents a symbol in the binary"""

    address: str
    size: int
    type: str
    name: str
    demangled_name: str
    source: str  # STRICT: NO defaults - all callers must provide explicit source


@dataclass
class TypeBreakdown:
    """Breakdown of symbols by type"""

    type: str
    count: int
    total_size: int


@dataclass
class CallStats:
    """Statistics about function calls"""

    functions_with_calls: int
    functions_called_by_others: int
    most_called: list[tuple[str, int]] = field(default_factory=lambda: [])
    most_calling: list[tuple[str, int]] = field(default_factory=lambda: [])


@dataclass
class AnalysisReport:
    """Complete symbol analysis report"""

    board: str
    total_symbols: int
    total_size: int
    largest_symbols: list[SymbolInfo] = field(default_factory=lambda: [])
    type_breakdown: list[TypeBreakdown] = field(default_factory=lambda: [])
    dependencies: dict[str, list[str]] = field(default_factory=lambda: {})
    call_graph: Optional[dict[str, list[str]]] = None
    reverse_call_graph: Optional[dict[str, list[str]]] = None
    call_stats: Optional[CallStats] = None


@dataclass
class DetailedAnalysisData:
    """Complete detailed analysis data structure for JSON output"""

    summary: AnalysisReport
    all_symbols_sorted_by_size: list[SymbolInfo]
    dependencies: dict[str, list[str]]
    call_graph: Optional[dict[str, list[str]]] = None
    reverse_call_graph: Optional[dict[str, list[str]]] = None


@dataclass
class TypeStats:
    """Statistics for symbol types with dictionary-like functionality"""

    stats: dict[str, TypeBreakdown] = field(default_factory=lambda: {})

    def add_symbol(self, symbol: SymbolInfo) -> None:
        """Add a symbol to the type statistics"""
        sym_type = symbol.type
        if sym_type not in self.stats:
            self.stats[sym_type] = TypeBreakdown(type=sym_type, count=0, total_size=0)
        self.stats[sym_type].count += 1
        self.stats[sym_type].total_size += symbol.size

    def items(self) -> list[tuple[str, TypeBreakdown]]:
        """Return items for iteration, sorted by total_size descending"""
        return sorted(self.stats.items(), key=lambda x: x[1].total_size, reverse=True)

    def values(self) -> list[TypeBreakdown]:
        """Return values for iteration"""
        return list(self.stats.values())

    def __getitem__(self, key: str) -> TypeBreakdown:
        """Allow dictionary-style access"""
        return self.stats[key]

    def __contains__(self, key: str) -> bool:
        """Allow 'in' operator"""
        return key in self.stats


def run_command(cmd: str) -> str:
    """Run a command and return stdout"""
    try:
        proc = RunningProcess(cmd, shell=True, auto_run=True, timeout=30)
        from running_process import EndOfStream

        # `get_next_line` strips the trailing newline. Re-add it so callers
        # that `output.split("\n")` actually get one entry per line rather
        # than a single mega-line that no parser can split on whitespace
        # (the cause of "nm: 1 symbols" on toolchains like xtensa-esp-elf
        # whose nm prints many hundreds of lines).
        output_lines: list[str] = []
        while line := proc.get_next_line(timeout=30):
            if isinstance(line, EndOfStream):
                break
            output_lines.append(str(line))
        output = "\n".join(output_lines)
        exit_code = proc.wait()
        if exit_code != 0:
            print(f"Error running command: {cmd}")
            print(f"Exit code: {exit_code}")
            return ""
        return output
    except KeyboardInterrupt as ki:
        from ci.util.global_interrupt_handler import (
            handle_keyboard_interrupt,
        )

        handle_keyboard_interrupt(ki)
        raise
    except Exception as e:
        print(f"Error running command: {cmd}")
        print(f"Error: {e}")
        return ""


def demangle_symbol(mangled_name: str, cppfilt_path: str) -> str:
    """Demangle a C++ symbol using c++filt"""
    try:
        cmd = f'echo "{mangled_name}" | "{cppfilt_path}"'
        proc = RunningProcess(cmd, shell=True, auto_run=True, timeout=10)
        output = ""
        from running_process import EndOfStream

        while line := proc.get_next_line(timeout=10):
            if isinstance(line, EndOfStream):
                break
            output += str(line)
        exit_code = proc.wait()
        demangled = output.strip()
        # If demangling failed, c++filt returns the original name
        return demangled if demangled and demangled != mangled_name else mangled_name
    except KeyboardInterrupt as ki:
        handle_keyboard_interrupt(ki)
        raise
    except Exception as e:
        print(f"Error demangling symbol: {mangled_name}")
        print(f"Error: {e}")
        return mangled_name


def analyze_symbols(
    elf_file: str, nm_path: str, cppfilt_path: str, readelf_path: Optional[str] = None
) -> list[SymbolInfo]:
    """Analyze ALL symbols in ELF file using both nm and readelf for comprehensive coverage"""
    print("Analyzing symbols with enhanced coverage...")

    symbols: list[SymbolInfo] = []
    symbols_dict: dict[
        str, SymbolInfo
    ] = {}  # To deduplicate by address+type (or demangled name for zero-address symbols)

    # Method 1: Use readelf to get ALL symbols (including those without size)
    if readelf_path:
        print("Getting all symbols using readelf...")
        readelf_cmd = f'"{readelf_path}" -s "{elf_file}"'
        output = run_command(readelf_cmd)

        if output:
            for line in output.strip().split("\n"):
                line = line.strip()
                # Skip header and empty lines
                if (
                    not line
                    or "Num:" in line
                    or "Symbol table" in line
                    or line.startswith("--")
                ):
                    continue

                # Parse readelf output format: Num: Value Size Type Bind Vis Ndx Name
                parts = line.split()
                if len(parts) >= 8:
                    try:
                        # Skip num (parts[0]) - not needed
                        addr = parts[1]
                        size = int(parts[2]) if parts[2].isdigit() else 0
                        symbol_type = parts[3]
                        parts[4]
                        # Skip vis and ndx (parts[5], parts[6]) - not needed
                        name = " ".join(parts[7:]) if len(parts) > 7 else ""

                        # Skip empty names and section symbols
                        if not name or name.startswith(".") or symbol_type == "SECTION":
                            continue

                        # Demangle the symbol name
                        demangled_name = demangle_symbol(name, cppfilt_path)

                        # Normalize address to int for comparison (readelf uses hex)
                        try:
                            addr_int = int(addr, 16)
                        except ValueError:
                            addr_int = 0

                        # Create key: Use address ONLY for non-zero addresses (symbol type can vary between tools)
                        # For zero-address symbols, use demangled_name+type to catch duplicates
                        sym_type_char = symbol_type[0].upper()
                        if addr_int != 0:
                            key = f"{addr_int}"
                        else:
                            key = f"0:{demangled_name}:{sym_type_char}"

                        if key not in symbols_dict:
                            symbol_info = SymbolInfo(
                                address=addr,
                                size=size,
                                type=sym_type_char,  # T, D, B, etc.
                                name=name,
                                demangled_name=demangled_name,
                                source="readelf",
                            )

                            symbols_dict[key] = symbol_info
                        else:
                            # If duplicate found, prefer the one with more accurate data
                            existing = symbols_dict[key]

                            # Prefer non-truncated mangled names (longer is usually better)
                            if len(name) > len(existing.name):
                                symbols_dict[key].name = name
                                symbols_dict[key].demangled_name = demangled_name

                            # Prefer larger non-zero size (more accurate measurement)
                            if size > existing.size:
                                symbols_dict[key].size = size
                                symbols_dict[key].source = "readelf-updated"

                    except (ValueError, IndexError):
                        continue  # Skip malformed lines

    # Method 2: Use nm with --print-size to get symbols with sizes (for accurate size info)
    print("Getting sized symbols using nm...")
    nm_cmd = f'"{nm_path}" --print-size --size-sort --radix=d "{elf_file}"'
    output = run_command(nm_cmd)

    if output:
        for line in output.strip().split("\n"):
            if not line.strip():
                continue

            parts = line.split()
            if len(parts) >= 4:
                try:
                    addr = parts[0]
                    size = int(parts[1])
                    symbol_type = parts[2]
                    name = " ".join(parts[3:])

                    # Demangle the symbol name
                    demangled_name = demangle_symbol(name, cppfilt_path)

                    # Normalize address to int for comparison (nm --radix=d uses decimal)
                    try:
                        addr_int = int(addr)
                    except ValueError:
                        addr_int = 0

                    # Create key: Use address ONLY for non-zero addresses
                    if addr_int != 0:
                        key = f"{addr_int}"
                    else:
                        key = f"0:{demangled_name}:{symbol_type}"

                    # If we already have this symbol from readelf, update with accurate size
                    if key in symbols_dict:
                        existing = symbols_dict[key]

                        # Prefer non-truncated mangled names (longer is usually better)
                        if len(name) > len(existing.name):
                            symbols_dict[key].name = name
                            symbols_dict[key].demangled_name = demangled_name

                        # Prefer larger non-zero size (more accurate measurement)
                        if size > existing.size:
                            symbols_dict[key].size = size
                            symbols_dict[key].type = symbol_type
                            symbols_dict[key].source = "nm+readelf"
                        # If same size, prefer nm over readelf (nm --print-size is more accurate)
                        elif size == existing.size and "nm" not in existing.source:
                            symbols_dict[key].source = "nm+readelf"
                    else:
                        # New symbol not found by readelf
                        symbol_info = SymbolInfo(
                            address=addr,
                            size=size,
                            type=symbol_type,
                            name=name,
                            demangled_name=demangled_name,
                            source="nm",
                        )

                        symbols_dict[key] = symbol_info

                except (ValueError, IndexError):
                    continue  # Skip malformed lines

    # Method 3: Use nm with -a to get all symbols including debugger-only
    print("Getting additional symbols using nm -a...")
    nm_all_cmd = f'"{nm_path}" -a --radix=d "{elf_file}"'
    output = run_command(nm_all_cmd)

    if output:
        for line in output.strip().split("\n"):
            if not line.strip():
                continue

            parts = line.split()
            if len(parts) >= 3:
                try:
                    addr = parts[0]
                    symbol_type = parts[1]
                    name = " ".join(parts[2:])

                    # Skip empty names
                    if not name:
                        continue

                    # Demangle the symbol name
                    demangled_name = demangle_symbol(name, cppfilt_path)

                    # Normalize address to int for comparison (nm -a uses decimal with --radix=d)
                    try:
                        addr_int = int(addr)
                    except ValueError:
                        addr_int = 0

                    # Create key: Use address ONLY for non-zero addresses
                    if addr_int != 0:
                        key = f"{addr_int}"
                    else:
                        key = f"0:{demangled_name}:{symbol_type}"

                    if key not in symbols_dict:
                        # New symbol not found by other methods
                        symbol_info = SymbolInfo(
                            address=addr,
                            size=0,  # nm -a doesn't provide size
                            type=symbol_type,
                            name=name,
                            demangled_name=demangled_name,
                            source="nm-a",
                        )

                        symbols_dict[key] = symbol_info
                    else:
                        # If duplicate found, prefer non-truncated mangled names
                        existing = symbols_dict[key]
                        if len(name) > len(existing.name):
                            symbols_dict[key].name = name
                            symbols_dict[key].demangled_name = demangled_name

                except (ValueError, IndexError):
                    continue  # Skip malformed lines

    # Convert dict to list
    symbols: list[SymbolInfo] = list(symbols_dict.values())

    print(f"Found {len(symbols)} total symbols using enhanced analysis")
    print(f"  - Symbols with size info: {len([s for s in symbols if s.size > 0])}")
    print(f"  - Symbols without size: {len([s for s in symbols if s.size == 0])}")

    return symbols


def analyze_function_calls(
    elf_file: str, objdump_path: str, cppfilt_path: str
) -> dict[str, list[str]]:
    """Analyze function calls using objdump to build call graph"""
    print("Analyzing function calls using objdump...")

    # Use objdump to disassemble the binary
    cmd = f'"{objdump_path}" -t "{elf_file}"'
    print(f"Running: {cmd}")
    symbol_output = run_command(cmd)

    # Build symbol address map for function symbols
    symbol_map: dict[str, dict[str, str]] = {}  # address -> {name, demangled}
    function_symbols: set[str] = set()  # set of function names

    for line in symbol_output.strip().split("\n"):
        if not line.strip():
            continue

        # Parse objdump symbol table output
        # Format: address flags section size name
        parts = line.split()
        if len(parts) >= 5 and ("F" in parts[1] or "f" in parts[1]):  # Function symbol
            try:
                address = parts[0]
                symbol_name = " ".join(parts[4:])

                # Demangle the symbol name
                demangled_name = demangle_symbol(symbol_name, cppfilt_path)

                entry: dict[str, str] = {
                    "name": symbol_name,
                    "demangled": demangled_name,
                }
                symbol_map[address] = entry
                function_symbols.add(demangled_name)
            except (ValueError, IndexError):
                continue

    print(f"Found {len(function_symbols)} function symbols")

    # Now disassemble text sections to find function calls
    cmd = f'"{objdump_path}" -d "{elf_file}"'
    print(f"Running: {cmd}")
    disasm_output = run_command(cmd)

    if not disasm_output:
        print("Warning: No disassembly output received")
        return {}

    # Parse disassembly to find function calls
    call_graph: defaultdict[str, set[str]] = defaultdict(
        set
    )  # caller -> set of callees
    current_function = None

    # Common call instruction patterns for different architectures
    call_patterns = [
        r"call\s+(\w+)",  # x86/x64 call
        r"bl\s+(\w+)",  # ARM branch with link
        r"jal\s+(\w+)",  # RISC-V jump and link
        r"callx?\d*\s+(\w+)",  # Xtensa call variations
    ]

    call_regex = re.compile(
        "|".join(f"(?:{pattern})" for pattern in call_patterns), re.IGNORECASE
    )

    function_start_pattern = re.compile(r"^([0-9a-f]+)\s+<([^>]+)>:")

    lines = disasm_output.split("\n")
    for i, line in enumerate(lines):
        line = line.strip()

        # Check for function start
        func_match = function_start_pattern.match(line)
        if func_match:
            func_name = func_match.group(2)

            # Demangle function name
            current_function = demangle_symbol(func_name, cppfilt_path)
            continue

        # Look for call instructions
        if current_function and (
            "call" in line.lower() or "bl " in line.lower() or "jal" in line.lower()
        ):
            call_match = call_regex.search(line)
            if call_match:
                # Extract the target function name
                for group in call_match.groups():
                    if group:
                        target_func = demangle_symbol(group, cppfilt_path)
                        call_graph[current_function].add(target_func)
                        break

    print(f"Built call graph with {len(call_graph)} calling functions")

    # Convert sets to lists for JSON serialization
    return {caller: list(callees) for caller, callees in call_graph.items()}


def build_reverse_call_graph(call_graph: dict[str, list[str]]) -> dict[str, list[str]]:
    """Build reverse call graph: function -> list of functions that call it"""
    reverse_graph: defaultdict[str, list[str]] = defaultdict(list)

    for caller, callees in call_graph.items():
        for callee in callees:
            reverse_graph[callee].append(caller)

    return dict(reverse_graph)


def analyze_map_file(map_file: Path) -> dict[str, list[str]]:
    """Analyze the map file to understand module dependencies"""
    print(f"Analyzing map file: {map_file}")

    dependencies: dict[str, list[str]] = {}
    current_archive: Optional[str] = None

    if not map_file.exists():
        print(f"Map file not found: {map_file}")
        return {}

    try:
        with open(map_file, "r") as f:
            for line in f:
                line = line.strip()

                # Look for archive member includes - handle both ESP32 and UNO formats
                if ".a(" in line and ")" in line:
                    # Extract module name
                    start = line.find("(") + 1
                    end = line.find(")")
                    if start > 0 and end > start:
                        current_archive = line[start:end]
                        dependencies[current_archive] = []

                elif current_archive and line and not line.startswith((".", "/", "*")):
                    # This line shows what pulled in the module
                    if "(" in line and ")" in line:
                        # Extract the symbol that caused the inclusion
                        symbol_start = line.find("(") + 1
                        symbol_end = line.find(")")
                        if symbol_start > 0 and symbol_end > symbol_start:
                            symbol = line[symbol_start:symbol_end]
                            dependencies[current_archive].append(symbol)
                    current_archive = None
    except KeyboardInterrupt as ki:
        handle_keyboard_interrupt(ki)
        raise
    except Exception as e:
        print(f"Error reading map file: {e}")
        return {}

    return dependencies


def generate_report(
    board_name: str,
    symbols: list[SymbolInfo],
    dependencies: dict[str, list[str]],
    call_graph: Optional[dict[str, list[str]]] = None,
    reverse_call_graph: Optional[dict[str, list[str]]] = None,
    enhanced_mode: bool = False,
) -> AnalysisReport:
    """Generate a comprehensive report with optional call graph analysis"""
    print("\n" + "=" * 80)
    if enhanced_mode and call_graph and reverse_call_graph:
        print(f"{board_name.upper()} ENHANCED SYMBOL ANALYSIS REPORT")
    else:
        print(f"{board_name.upper()} SYMBOL ANALYSIS REPORT")
    print("=" * 80)

    # Summary statistics
    total_symbols = len(symbols)
    symbols_with_size = [s for s in symbols if s.size > 0]
    symbols_without_size = [s for s in symbols if s.size == 0]

    # Calculate total size from ONLY sized symbols
    total_size = sum(s.size for s in symbols_with_size)

    print("\nSUMMARY:")
    print(f"  Total symbols: {total_symbols}")
    print(f"  Symbols with size info: {len(symbols_with_size)}")
    print(f"  Symbols without size (elided by linker): {len(symbols_without_size)}")
    print(f"  Total symbol size: {total_size} bytes ({total_size / 1024:.1f} KB)")

    if enhanced_mode and call_graph and reverse_call_graph:
        print(f"  Functions with calls: {len(call_graph)}")
        print(f"  Functions called by others: {len(reverse_call_graph)}")

    # Show source breakdown
    source_stats: dict[str, int] = {}
    for sym in symbols:
        source = sym.source
        if source not in source_stats:
            source_stats[source] = 0
        source_stats[source] += 1

    print("\nSYMBOL SOURCES:")
    for source, count in sorted(source_stats.items()):
        print(f"  {source}: {count} symbols")

    # Largest symbols overall (FILTER OUT ZERO-SIZE SYMBOLS)
    print("\nLARGEST SYMBOLS (all symbols, sorted by size):")

    # Filter to only symbols with size > 0 (exclude elided/unused symbols)
    sized_symbols = [s for s in symbols if s.size > 0]
    symbols_sorted = sorted(sized_symbols, key=lambda x: x.size, reverse=True)

    zero_size_count = len(symbols) - len(sized_symbols)
    if zero_size_count > 0:
        print(
            f"  (Filtered out {zero_size_count} zero-size symbols that were elided by linker)"
        )

    display_count = 30 if enhanced_mode else 50
    for i, sym in enumerate(symbols_sorted[:display_count]):
        display_name = sym.demangled_name
        print(f"  {i + 1:2d}. {sym.size:6d} bytes - {display_name}")

        # Show what functions call this symbol (if enhanced mode and it's a function)
        if enhanced_mode and reverse_call_graph and display_name in reverse_call_graph:
            callers = reverse_call_graph[display_name]
            if callers:
                caller_names = [
                    name[:40] + "..." if len(name) > 40 else name
                    for name in callers[:3]
                ]
                print(f"      Called by: {', '.join(caller_names)}")
                if len(callers) > 3:
                    print(f"      ... and {len(callers) - 3} more")

        # Show mangled name if different (non-enhanced mode)
        elif (
            not enhanced_mode
            and hasattr(sym, "demangled_name")
            and sym.demangled_name
            and sym.demangled_name != sym.name
        ):
            print(
                f"      (mangled: {sym.name[:80]}{'...' if len(sym.name) > 80 else ''})"
            )

    # Initialize variables for enhanced mode data
    most_called_stats: list[tuple[str, int]] = []
    most_calling_stats: list[tuple[str, int]] = []

    # Enhanced function call analysis
    if enhanced_mode and call_graph and reverse_call_graph:
        print("\n" + "=" * 80)
        print("FUNCTION CALL ANALYSIS")
        print("=" * 80)

        # Most called functions
        most_called = sorted(
            reverse_call_graph.items(), key=lambda x: len(x[1]), reverse=True
        )
        most_called_stats = [(name, len(callers)) for name, callers in most_called]
        print("\nMOST CALLED FUNCTIONS (functions called by many others):")
        for i, (func_name, callers) in enumerate(most_called[:15]):
            short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name
            print(f"  {i + 1:2d}. {short_name}")
            print(f"      Called by {len(callers)} functions")
            if len(callers) <= 5:
                for caller in callers:
                    caller_short = caller[:50] + "..." if len(caller) > 50 else caller
                    print(f"        - {caller_short}")
            else:
                for caller in callers[:3]:
                    caller_short = caller[:50] + "..." if len(caller) > 50 else caller
                    print(f"        - {caller_short}")
                print(f"        ... and {len(callers) - 3} more")
            print()

        # Functions that call many others
        most_calling = sorted(call_graph.items(), key=lambda x: len(x[1]), reverse=True)
        most_calling_stats = [(name, len(callees)) for name, callees in most_calling]
        print("\nFUNCTIONS THAT CALL MANY OTHERS:")
        for i, (func_name, callees) in enumerate(most_calling[:10]):
            short_name = func_name[:60] + "..." if len(func_name) > 60 else func_name
            print(f"  {i + 1:2d}. {short_name}")
            print(f"      Calls {len(callees)} functions")
            if len(callees) <= 5:
                for callee in callees:
                    callee_short = callee[:50] + "..." if len(callee) > 50 else callee
                    print(f"        -> {callee_short}")
            else:
                for callee in callees[:3]:
                    callee_short = callee[:50] + "..." if len(callee) > 50 else callee
                    print(f"        -> {callee_short}")
                print(f"        ... and {len(callees) - 3} more")
            print()

    # Symbol type breakdown
    section_title = "\n" + "=" * 80 + "\n" if enhanced_mode else "\n"
    print(section_title + "SYMBOL TYPE BREAKDOWN:")
    type_stats = TypeStats()
    for sym in symbols:
        type_stats.add_symbol(sym)

    for sym_type, stats in type_stats.items():
        print(
            f"  {sym_type}: {stats.count} symbols, {stats.total_size} bytes ({stats.total_size / 1024:.1f} KB)"
        )

    # Dependencies analysis
    if dependencies:
        print("\nMODULE DEPENDENCIES:")
        for module in sorted(dependencies.keys()):
            if dependencies[module]:  # Only show modules with dependencies
                print(f"  {module}:")
                for symbol in dependencies[module][:5]:  # Show first 5 symbols
                    print(f"    - {symbol}")
                if len(dependencies[module]) > 5:
                    print(f"    ... and {len(dependencies[module]) - 5} more")

    # Build return data
    report_data = AnalysisReport(
        board=board_name,
        total_symbols=total_symbols,
        total_size=total_size,
        largest_symbols=symbols_sorted[:20],
        type_breakdown=list(type_stats.values()),
        dependencies=dependencies,
    )

    # Add enhanced data if available
    if enhanced_mode and call_graph and reverse_call_graph:
        report_data.call_graph = call_graph
        report_data.reverse_call_graph = reverse_call_graph
        report_data.call_stats = CallStats(
            functions_with_calls=len(call_graph),
            functions_called_by_others=len(reverse_call_graph),
            most_called=most_called_stats[:10],
            most_calling=most_calling_stats[:10],
        )

    return report_data


def find_board_build_info(
    board_name: Optional[str] = None, example: str = "Blink"
) -> tuple[Path, str]:
    """Find build info for a specific board or detect available boards

    Args:
        board_name: Board name to search for (optional)
        example: Example name for finding example-specific build_info_{example}.json (default: "Blink")

    Returns:
        Tuple of (build_info_path, board_name)
    """
    # Detect build directory
    current = Path.cwd()
    build_dir: Optional[Path] = None
    while current != current.parent:
        candidate = current / ".build"
        if candidate.exists():
            build_dir = candidate
            break
        current = current.parent

    if not build_dir:
        print("Error: Could not find build directory (.build)")
        sys.exit(1)

    assert build_dir is not None

    # If specific board requested, look for it
    if board_name:
        # Try example-specific build_info_{example}.json first
        build_info_filename = f"build_info_{example}.json"

        # 1) Direct board directory: .build/<board>/build_info_{example}.json
        board_dir = build_dir / board_name
        if board_dir.exists() and (board_dir / build_info_filename).exists():
            return board_dir / build_info_filename, board_name

        # 2) PlatformIO nested directory: .build/pio/<board>/build_info_{example}.json
        pio_board_dir = build_dir / "pio" / board_name
        if pio_board_dir.exists() and (pio_board_dir / build_info_filename).exists():
            return pio_board_dir / build_info_filename, board_name

        # Fallback to legacy build_info.json (for backward compatibility)
        if board_dir.exists() and (board_dir / "build_info.json").exists():
            print(
                f"Warning: Using legacy build_info.json (consider migrating to {build_info_filename})"
            )
            return board_dir / "build_info.json", board_name

        if pio_board_dir.exists() and (pio_board_dir / "build_info.json").exists():
            print(
                f"Warning: Using legacy build_info.json (consider migrating to {build_info_filename})"
            )
            return pio_board_dir / "build_info.json", board_name

        print(
            f"Error: Board '{board_name}' not found or missing {build_info_filename} (or build_info.json)"
        )
        sys.exit(1)

    # Otherwise, find any available board (prefer example-specific files)
    available_boards: list[tuple[Path, str]] = []
    build_info_filename = f"build_info_{example}.json"

    # 1) Direct children of .build - try example-specific first
    for item in build_dir.iterdir():
        if item.is_dir():
            build_info_file = item / build_info_filename
            if build_info_file.exists():
                available_boards.append((build_info_file, item.name))

    # 2) Nested PlatformIO structure .build/pio/* - try example-specific first
    pio_dir = build_dir / "pio"
    if pio_dir.exists() and pio_dir.is_dir():
        for item in pio_dir.iterdir():
            if item.is_dir():
                build_info_file = item / build_info_filename
                if build_info_file.exists():
                    available_boards.append((build_info_file, item.name))

    # Fallback to legacy build_info.json if no example-specific files found
    if not available_boards:
        for item in build_dir.iterdir():
            if item.is_dir():
                build_info_file = item / "build_info.json"
                if build_info_file.exists():
                    print(
                        f"Warning: Using legacy build_info.json (consider migrating to {build_info_filename})"
                    )
                    available_boards.append((build_info_file, item.name))

        if pio_dir.exists() and pio_dir.is_dir():
            for item in pio_dir.iterdir():
                if item.is_dir():
                    build_info_file = item / "build_info.json"
                    if build_info_file.exists():
                        print(
                            f"Warning: Using legacy build_info.json (consider migrating to {build_info_filename})"
                        )
                        available_boards.append((build_info_file, item.name))

    if not available_boards:
        print(
            f"Error: No boards with {build_info_filename} or build_info.json found in {build_dir}"
        )
        sys.exit(1)

    # Return the first available board
    return available_boards[0]


def main():
    import argparse

    parser = argparse.ArgumentParser(
        description="Enhanced symbol analysis with optional function call analysis for any platform"
    )
    parser.add_argument(
        "--board", help="Board name to analyze (e.g., uno, esp32dev, esp32s3)"
    )
    parser.add_argument(
        "--example",
        default="Blink",
        help="Example name for finding example-specific build_info_{example}.json",
    )
    parser.add_argument(
        "--no-enhanced",
        action="store_false",
        dest="enhanced",
        default=True,
        help="Disable enhanced analysis with function call graph (enhanced is default)",
    )
    parser.add_argument(
        "--show-calls-to",
        help="Show what functions call a specific function (enables enhanced mode)",
    )
    parser.add_argument(
        "--basic",
        action="store_true",
        help="Use basic nm-only symbol analysis (for backward compatibility). Default is comprehensive analysis with readelf + nm",
    )
    args = parser.parse_args()

    # Enable enhanced mode if show-calls-to is specified
    enhanced_mode = args.enhanced or args.show_calls_to

    # Use comprehensive symbol analysis by default, basic only if requested
    comprehensive_symbols = not args.basic

    # Find build info
    build_info_path, board_name = find_board_build_info(args.board, args.example)
    print(f"Found build info for {board_name}: {build_info_path}")

    with open(build_info_path) as f:
        build_info = json.load(f)

    # Get board info using proper board mapping
    board = create_board(board_name)
    real_board_name = board.get_real_board_name()

    # Try the real board name first, then fall back to directory name
    if real_board_name in build_info:
        board_info = build_info[real_board_name]
        actual_board_key = real_board_name
        if real_board_name != board_name:
            print(
                f"Note: Using board key '{real_board_name}' from board mapping (directory was '{board_name}')"
            )
    elif board_name in build_info:
        board_info = build_info[board_name]
        actual_board_key = board_name
    else:
        # Try to find the actual board key in the JSON as fallback
        board_keys = list(build_info.keys())
        if len(board_keys) == 1:
            actual_board_key = board_keys[0]
            board_info = build_info[actual_board_key]
            print(
                f"Note: Using only available board key '{actual_board_key}' from build_info.json (expected '{real_board_name}' or '{board_name}')"
            )
        else:
            print(
                f"Error: Could not find board '{real_board_name}' or '{board_name}' in build_info.json"
            )
            print(f"Available board keys: {board_keys}")
            sys.exit(1)

    nm_path = board_info["aliases"]["nm"]
    cppfilt_path = board_info["aliases"]["c++filt"]
    elf_file = board_info["prog_path"]

    # Get readelf path (derive from nm path if not in aliases)
    if "readelf" in board_info["aliases"]:
        readelf_path = board_info["aliases"]["readelf"]
    else:
        # Derive readelf path from nm path (replace 'nm' with 'readelf')
        readelf_path = nm_path.replace("-nm", "-readelf")

    # Find map file
    map_file = Path(elf_file).with_suffix(".map")

    print(f"Analyzing ELF file: {elf_file}")
    print(f"Using nm tool: {nm_path}")
    print(f"Using c++filt tool: {cppfilt_path}")
    print(f"Using readelf tool: {readelf_path}")
    if enhanced_mode:
        objdump_path = board_info["aliases"]["objdump"]
        print(f"Using objdump tool: {objdump_path}")
    print(f"Map file: {map_file}")

    # Analyze symbols
    if comprehensive_symbols:
        symbols = analyze_symbols(elf_file, nm_path, cppfilt_path, readelf_path)
    else:
        symbols = analyze_symbols(elf_file, nm_path, cppfilt_path)

    # Analyze function calls if enhanced mode
    call_graph = {}
    reverse_call_graph = {}
    if enhanced_mode:
        objdump_path = board_info["aliases"]["objdump"]
        call_graph = analyze_function_calls(elf_file, objdump_path, cppfilt_path)
        reverse_call_graph = build_reverse_call_graph(call_graph)

    # Analyze dependencies
    dependencies = analyze_map_file(map_file)

    # Handle specific function query
    if args.show_calls_to:
        target_function = args.show_calls_to
        print("\n" + "=" * 80)
        print(f"FUNCTIONS THAT CALL: {target_function}")
        print("=" * 80)

        # Find functions that call the target (exact match first)
        exact_callers = reverse_call_graph.get(target_function, [])

        # Also search for partial matches
        partial_matches: dict[str, list[str]] = {}
        for func_name, callers in reverse_call_graph.items():
            if (
                target_function.lower() in func_name.lower()
                and func_name != target_function
            ):
                partial_matches[func_name] = callers

        if exact_callers:
            print(f"\nExact match - Functions calling '{target_function}':")
            for i, caller in enumerate(exact_callers, 1):
                print(f"  {i}. {caller}")

        if partial_matches:
            print(f"\nPartial matches - Functions containing '{target_function}':")
            for func_name, callers in partial_matches.items():
                print(f"\n  Function: {func_name}")
                print(f"  Called by {len(callers)} functions:")
                for caller in callers[:5]:
                    print(f"    - {caller}")
                if len(callers) > 5:
                    print(f"    ... and {len(callers) - 5} more")

        if not exact_callers and not partial_matches:
            print(f"No functions found that call '{target_function}'")
            print("Available functions (first 20):")
            available_funcs = sorted(reverse_call_graph.keys())[:20]
            for func in available_funcs:
                print(f"  - {func}")

        return  # Exit early for specific query

    # Generate report using user-friendly board name
    report = generate_report(
        board_name.upper(),
        symbols,
        dependencies,
        call_graph,
        reverse_call_graph,
        enhanced_mode,
    )

    # Save detailed data to JSON (sorted by size, largest first)
    # Find the build directory (go up from wherever we are to find .build)
    current = Path.cwd()
    while current != current.parent:
        build_dir = current / ".build"
        if build_dir.exists():
            filename_suffix = (
                "_enhanced_symbol_analysis.json"
                if enhanced_mode
                else "_symbol_analysis.json"
            )
            output_file = build_dir / f"{board_name}{filename_suffix}"
            break
        current = current.parent
    else:
        # Fallback to current directory if .build not found
        filename_suffix = (
            "_enhanced_symbol_analysis.json"
            if enhanced_mode
            else "_symbol_analysis.json"
        )
        output_file = Path(f"{board_name}{filename_suffix}")

    # Create detailed analysis data structure
    detailed_data = DetailedAnalysisData(
        summary=report,
        all_symbols_sorted_by_size=sorted(symbols, key=lambda x: x.size, reverse=True),
        dependencies=dependencies,
        call_graph=call_graph if enhanced_mode else None,
        reverse_call_graph=reverse_call_graph if enhanced_mode else None,
    )

    with open(output_file, "w") as f:
        json.dump(asdict(detailed_data), f, indent=2)

    description = (
        "enhanced analysis with complete call graph"
        if enhanced_mode
        else "basic symbol analysis"
    )
    print(f"\nDetailed {description} saved to: {output_file}")

    if not enhanced_mode:
        print("This file contains ALL symbols without any filtering or classification.")
        print(
            "Enhanced mode with function call graph analysis is enabled by default. Use --no-enhanced to disable."
        )
    else:
        print("This file contains ALL symbols and complete call graph analysis.")


if __name__ == "__main__":
    main()
