#!/usr/bin/env python3
# pyright: reportUnknownMemberType=false
"""
Build Info Analyzer - Tool for extracting platform information from build_info.json files.

This tool provides easy access to platform-specific defines, compiler flags, toolchain paths,
and other build configuration information generated by PlatformIO.

Usage:
    python build_info_analyzer.py --board uno --show-defines
    python build_info_analyzer.py --board esp32dev --show-all
    python build_info_analyzer.py --list-boards
"""

import argparse
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Optional


@dataclass(slots=True)
class PlatformDefinesResult:
    """Result of get_platform_defines."""

    ok: bool
    defines: list[str]
    error: str


@dataclass(slots=True)
class CompilerInfoResult:
    """Result of get_compiler_info."""

    ok: bool
    compiler_info: "CompilerInfo"
    error: str


@dataclass(slots=True)
class ToolchainAliasesResult:
    """Result of get_toolchain_aliases."""

    ok: bool
    aliases: dict[str, str]
    error: str


@dataclass(slots=True)
class AllInfoResult:
    """Result of get_all_info."""

    ok: bool
    info: dict[str, Any]
    error: str


@dataclass(slots=True)
class CompareDefinesResult:
    """Result of compare_defines."""

    ok: bool
    comparison: dict[str, Any]
    error: str


@dataclass
class CompilerInfo:
    """Information about compilers and toolchain"""

    cc_path: str = ""
    cxx_path: str = ""
    cc_flags: list[str] = field(default_factory=lambda: list())
    cxx_flags: list[str] = field(default_factory=lambda: list())
    compiler_type: str = ""
    build_type: str = ""


@dataclass
class BuildInfo:
    """Complete build information for a platform"""

    board_name: str
    defines: dict[str, str] = field(default_factory=lambda: dict())
    compiler_info: CompilerInfo = field(default_factory=CompilerInfo)
    aliases: dict[str, Optional[str]] = field(default_factory=lambda: dict())


class BuildInfoAnalyzer:
    """Analyzer for build_info.json files generated by PlatformIO builds."""

    def __init__(self, build_dir: str):
        """
        Initialize the analyzer.

        Args:
            build_dir: Directory containing platform build directories
        """
        self.build_dir = Path(build_dir)

    def list_available_boards(self) -> list[str]:
        """
        List all boards that have build_info.json files.

        Returns:
            List of board names that have been built
        """
        boards: list[str] = []
        if not self.build_dir.exists():
            return boards

        for item in self.build_dir.iterdir():
            if item.is_dir() and (item / "build_info.json").exists():
                boards.append(item.name)

        return sorted(boards)

    def get_build_info_path(self, board_name: str) -> Optional[Path]:
        """
        Get the path to build_info.json for a specific board.

        Args:
            board_name: Name of the board (e.g., 'uno', 'esp32dev')

        Returns:
            Path to build_info.json file or None if not found
        """
        build_info_path = self.build_dir / board_name / "build_info.json"
        if build_info_path.exists():
            return build_info_path
        return None

    def load_build_info(self, board_name: str) -> Optional[dict[str, Any]]:
        """
        Load and parse build_info.json for a board.

        Args:
            board_name: Name of the board

        Returns:
            Parsed JSON data or None if file not found
        """
        build_info_path = self.get_build_info_path(board_name)
        if not build_info_path:
            return None

        try:
            with open(build_info_path, "r") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError) as e:
            print(f"Error loading build_info.json for {board_name}: {e}")
            return None

    def create_board_key_from_build_info(
        self, data: dict[str, Any], board_name: str
    ) -> Optional[str]:
        """
        Get the actual board key used in build_info.json.

        Sometimes the board name differs from the directory name.

        Args:
            data: Parsed build_info.json data
            board_name: Directory name of the board

        Returns:
            Actual board key used in JSON or None if not found
        """
        # Try exact match first
        if board_name in data:
            return board_name

        # If only one key, use it
        keys = list(data.keys())
        if len(keys) == 1:
            return keys[0]

        # Try to find a close match
        for key in keys:
            if board_name.lower() in key.lower() or key.lower() in board_name.lower():
                return key

        return None

    def get_platform_defines(self, board_name: str) -> PlatformDefinesResult:
        """
        Get platform-specific preprocessor defines for a board.

        Args:
            board_name: Name of the board

        Returns:
            PlatformDefinesResult with ok, defines, and error fields
        """
        data = self.load_build_info(board_name)
        if not data:
            return PlatformDefinesResult(
                ok=False, defines=[], error=f"Build info not found for {board_name}"
            )

        board_key = self.create_board_key_from_build_info(data, board_name)
        if not board_key:
            available_keys = list(data.keys())
            return PlatformDefinesResult(
                ok=False,
                defines=[],
                error=f"Board key not found. Available keys: {available_keys}",
            )

        board_data = data[board_key]
        defines = board_data.get("defines", [])

        return PlatformDefinesResult(ok=True, defines=defines, error="")

    def get_compiler_info(self, board_name: str) -> CompilerInfoResult:
        """
        Get compiler information for a board.

        Args:
            board_name: Name of the board

        Returns:
            CompilerInfoResult with ok, compiler_info, and error fields
        """
        data = self.load_build_info(board_name)
        if not data:
            return CompilerInfoResult(
                ok=False,
                compiler_info=CompilerInfo(),
                error=f"Build info not found for {board_name}",
            )

        board_key = self.create_board_key_from_build_info(data, board_name)
        if not board_key:
            return CompilerInfoResult(
                ok=False,
                compiler_info=CompilerInfo(),
                error="Board key not found in build_info.json",
            )

        board_data = data[board_key]

        compiler_info = CompilerInfo(
            cc_path=board_data.get("cc_path", ""),
            cxx_path=board_data.get("cxx_path", ""),
            cc_flags=board_data.get("cc_flags", []),
            cxx_flags=board_data.get("cxx_flags", []),
            compiler_type=board_data.get("compiler_type", ""),
            build_type=board_data.get("build_type", ""),
        )

        return CompilerInfoResult(ok=True, compiler_info=compiler_info, error="")

    def get_toolchain_aliases(self, board_name: str) -> ToolchainAliasesResult:
        """
        Get toolchain tool aliases for a board.

        Args:
            board_name: Name of the board

        Returns:
            ToolchainAliasesResult with ok, aliases, and error fields
        """
        data = self.load_build_info(board_name)
        if not data:
            return ToolchainAliasesResult(
                ok=False, aliases={}, error=f"Build info not found for {board_name}"
            )

        board_key = self.create_board_key_from_build_info(data, board_name)
        if not board_key:
            return ToolchainAliasesResult(
                ok=False, aliases={}, error="Board key not found in build_info.json"
            )

        board_data = data[board_key]
        aliases = board_data.get("aliases", {})

        return ToolchainAliasesResult(ok=True, aliases=aliases, error="")

    def get_all_info(self, board_name: str) -> AllInfoResult:
        """
        Get all available information for a board.

        Args:
            board_name: Name of the board

        Returns:
            AllInfoResult with ok, info, and error fields
        """
        data = self.load_build_info(board_name)
        if not data:
            return AllInfoResult(
                ok=False, info={}, error=f"Build info not found for {board_name}"
            )

        board_key = self.create_board_key_from_build_info(data, board_name)
        if not board_key:
            return AllInfoResult(
                ok=False, info={}, error="Board key not found in build_info.json"
            )

        return AllInfoResult(ok=True, info=data[board_key], error="")

    def compare_defines(self, board1: str, board2: str) -> CompareDefinesResult:
        """
        Compare platform defines between two boards.

        Args:
            board1: First board name
            board2: Second board name

        Returns:
            CompareDefinesResult with ok, comparison, and error fields
        """
        result1 = self.get_platform_defines(board1)
        result2 = self.get_platform_defines(board2)

        if not result1.ok:
            return CompareDefinesResult(
                ok=False,
                comparison={},
                error=f"Error getting defines for {board1}: {result1.error}",
            )
        if not result2.ok:
            return CompareDefinesResult(
                ok=False,
                comparison={},
                error=f"Error getting defines for {board2}: {result2.error}",
            )

        set1 = set(result1.defines)
        set2 = set(result2.defines)

        comparison = {
            "board1": board1,
            "board2": board2,
            "board1_only": sorted(list(set1 - set2)),
            "board2_only": sorted(list(set2 - set1)),
            "common": sorted(list(set1 & set2)),
            "board1_total": len(result1.defines),
            "board2_total": len(result2.defines),
            "common_count": len(set1 & set2),
        }

        return CompareDefinesResult(ok=True, comparison=comparison, error="")


def print_defines(defines: list[str], board_name: str):
    """Print platform defines in a formatted way."""
    print(f"\n📋 Platform Defines for {board_name.upper()}:")
    print("=" * 50)
    for define in defines:
        print(f"  {define}")
    print(f"\nTotal: {len(defines)} defines")


def print_compiler_info(compiler_info: CompilerInfo, board_name: str):
    """Print compiler information in a formatted way."""
    print(f"\n🔧 Compiler Information for {board_name.upper()}:")
    print("=" * 50)
    print(f"Compiler Type: {compiler_info.compiler_type or 'Unknown'}")
    print(f"Build Type: {compiler_info.build_type or 'Unknown'}")
    print(f"C Compiler: {compiler_info.cc_path or 'Unknown'}")
    print(f"C++ Compiler: {compiler_info.cxx_path or 'Unknown'}")

    if compiler_info.cc_flags:
        print(f"\nC Flags ({len(compiler_info.cc_flags)}):")
        for flag in compiler_info.cc_flags:
            print(f"  {flag}")

    if compiler_info.cxx_flags:
        print(f"\nC++ Flags ({len(compiler_info.cxx_flags)}):")
        for flag in compiler_info.cxx_flags:
            print(f"  {flag}")


def print_toolchain_aliases(aliases: dict[str, str], board_name: str):
    """Print toolchain aliases in a formatted way."""
    print(f"\n⚙️  Toolchain Aliases for {board_name.upper()}:")
    print("=" * 50)
    for tool, path in aliases.items():
        if path:
            # Show just the tool name from the path for readability
            tool_name = Path(path).name if path else "Not available"
            print(f"  {tool:10}: {tool_name}")
        else:
            print(f"  {tool:10}: Not available")


def print_comparison(comparison: dict[str, Any]):
    """Print a comparison between two boards."""
    board1 = comparison["board1"]
    board2 = comparison["board2"]

    print("\n🔍 Platform Defines Comparison:")
    print("=" * 60)
    print(f"📊 {board1.upper()} vs {board2.upper()}")
    print(f"   {board1}: {comparison['board1_total']} defines")
    print(f"   {board2}: {comparison['board2_total']} defines")
    print(f"   Common: {comparison['common_count']} defines")

    if comparison["board1_only"]:
        print(f"\n🔴 Only in {board1.upper()} ({len(comparison['board1_only'])}):")
        for define in comparison["board1_only"]:
            print(f"  {define}")

    if comparison["board2_only"]:
        print(f"\n🔵 Only in {board2.upper()} ({len(comparison['board2_only'])}):")
        for define in comparison["board2_only"]:
            print(f"  {define}")

    if comparison["common"]:
        print(f"\n🟢 Common Defines ({len(comparison['common'])}):")
        for define in comparison["common"]:
            print(f"  {define}")


def main():
    """Main function for command line usage."""
    parser = argparse.ArgumentParser(
        description="Analyze build_info.json files to extract platform information",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --list-boards                     # List all available boards
  %(prog)s --board uno --show-defines       # Show UNO platform defines
  %(prog)s --board esp32dev --show-compiler  # Show ESP32 compiler info
  %(prog)s --board teensy31 --show-all      # Show all info for Teensy 3.1
  %(prog)s --compare uno esp32dev            # Compare defines between boards
        """,
    )

    parser.add_argument(
        "--build-dir",
        default=".build",
        help="Build directory containing platform subdirectories (default: .build)",
    )

    parser.add_argument(
        "--list-boards",
        action="store_true",
        help="List all boards with build_info.json files",
    )

    parser.add_argument(
        "--board", help="Board name to analyze (e.g., uno, esp32dev, teensy31)"
    )

    parser.add_argument(
        "--show-defines", action="store_true", help="Show platform preprocessor defines"
    )

    parser.add_argument(
        "--show-compiler", action="store_true", help="Show compiler information"
    )

    parser.add_argument(
        "--show-toolchain", action="store_true", help="Show toolchain aliases"
    )

    parser.add_argument(
        "--show-all", action="store_true", help="Show all available information"
    )

    parser.add_argument(
        "--compare",
        nargs=2,
        metavar=("BOARD1", "BOARD2"),
        help="Compare platform defines between two boards",
    )

    parser.add_argument(
        "--json", action="store_true", help="Output results in JSON format"
    )

    args = parser.parse_args()

    analyzer = BuildInfoAnalyzer(args.build_dir)

    if args.list_boards:
        boards = analyzer.list_available_boards()
        if not boards:
            print("❌ No boards with build_info.json found in build directory")
            print(f"   Directory: {analyzer.build_dir}")
            print(
                "   Try running a compilation first: uv run python -m ci.ci-compile uno --examples Blink"
            )
            return 1

        print(f"📋 Available boards ({len(boards)}):")
        for board in boards:
            print(f"  ✅ {board}")
        return 0

    if args.compare:
        board1, board2 = args.compare
        compare_result = analyzer.compare_defines(board1, board2)
        if not compare_result.ok:
            print(f"❌ Error: {compare_result.error}")
            return 1

        if args.json:
            print(json.dumps(compare_result.comparison, indent=2))
        else:
            print_comparison(compare_result.comparison)
        return 0

    if not args.board:
        print("❌ Error: --board is required (or use --list-boards or --compare)")
        return 1

    # Handle single board analysis
    if args.show_defines or args.show_all:
        defines_result = analyzer.get_platform_defines(args.board)
        if not defines_result.ok:
            print(f"❌ Error getting defines: {defines_result.error}")
            return 1

        if args.json:
            print(json.dumps({"defines": defines_result.defines}, indent=2))
        else:
            print_defines(defines_result.defines, args.board)

    if args.show_compiler or args.show_all:
        compiler_result = analyzer.get_compiler_info(args.board)
        if not compiler_result.ok:
            print(f"❌ Error getting compiler info: {compiler_result.error}")
            return 1

        if args.json:
            print(
                json.dumps(
                    {"compiler": asdict(compiler_result.compiler_info)}, indent=2
                )
            )
        else:
            print_compiler_info(compiler_result.compiler_info, args.board)

    if args.show_toolchain or args.show_all:
        aliases_result = analyzer.get_toolchain_aliases(args.board)
        if not aliases_result.ok:
            print(f"❌ Error getting toolchain aliases: {aliases_result.error}")
            return 1

        if args.json:
            print(json.dumps({"toolchain": aliases_result.aliases}, indent=2))
        else:
            print_toolchain_aliases(aliases_result.aliases, args.board)

    return 0


if __name__ == "__main__":
    exit(main())
