from ci.util.global_interrupt_handler import handle_keyboard_interrupt


# pyright: reportUnknownMemberType=false
"""
Tools for working with build info and tool paths.
"""

import json
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from ci.util.paths import BUILD


@dataclass
class Tools:
    as_path: Path
    ld_path: Path
    objcopy_path: Path
    objdump_path: Path
    cpp_filt_path: Path
    nm_path: Path


def load_tools(build_info_path: Path) -> Tools:
    build_info: dict[str, Any] = json.loads(build_info_path.read_text())
    board_info: dict[str, Any] = build_info[next(iter(build_info))]
    aliases: dict[str, str] = board_info["aliases"]
    as_path = Path(aliases["as"])
    ld_path = Path(aliases["ld"])
    objcopy_path = Path(aliases["objcopy"])
    objdump_path = Path(aliases["objdump"])
    cpp_filt_path = Path(aliases["c++filt"])
    nm_path = Path(aliases["nm"])
    if sys.platform == "win32":
        as_path = as_path.with_suffix(".exe")
        ld_path = ld_path.with_suffix(".exe")
        objcopy_path = objcopy_path.with_suffix(".exe")
        objdump_path = objdump_path.with_suffix(".exe")
        cpp_filt_path = cpp_filt_path.with_suffix(".exe")
        nm_path = nm_path.with_suffix(".exe")
    out = Tools(as_path, ld_path, objcopy_path, objdump_path, cpp_filt_path, nm_path)
    tools = [as_path, ld_path, objcopy_path, objdump_path, cpp_filt_path, nm_path]
    for tool in tools:
        if not tool.exists():
            raise FileNotFoundError(f"Tool not found: {tool}")
    return out


def _list_builds() -> list[Path]:
    str_paths = os.listdir(BUILD)
    paths = [BUILD / p for p in str_paths]
    dirs = [p for p in paths if p.is_dir()]
    return dirs


def _check_build(build: Path) -> bool:
    # 1. should contain a build_info.json file
    # 2. should contain a .pio/build directory
    has_build_info = (build / "build_info.json").exists()
    has_pio_build = (build / ".pio" / "build").exists()
    return has_build_info and has_pio_build


def _prompt_build() -> Path:
    builds = _list_builds()
    if not builds:
        print("Error: No builds found", file=sys.stderr)
        sys.exit(1)
    print("Select a build:")
    for i, build in enumerate(builds):
        print(f"  [{i}]: {build}")
    while True:
        try:
            which = int(input("Enter the number of the build to use: "))
            if 0 <= which < len(builds):
                valid = _check_build(BUILD / builds[which])
                if valid:
                    return BUILD / builds[which]
                print("Error: Invalid build", file=sys.stderr)
            else:
                print("Error: Invalid selection", file=sys.stderr)
                continue
        except ValueError:
            print("Error: Invalid input", file=sys.stderr)
            continue


def _prompt_object_file(build: Path) -> Path:
    # Look for object files in .pio/build directory
    build_dir = build / ".pio" / "build"
    object_files: list[Path] = []

    # Walk through build directory to find .o files
    for root, _, files in os.walk(build_dir):
        for file in files:
            if file.endswith(".o") and "FrameworkArduino" not in file:
                full_path = Path(root) / file
                if "FrameworkArduino" not in full_path.parts:
                    object_files.append(full_path)

    if not object_files:
        print("Error: No object files found", file=sys.stderr)
        sys.exit(1)

    print("\nSelect an object file:")
    for i, obj_file in enumerate(object_files):
        print(f"  [{i}]: {obj_file.relative_to(build_dir)}")

    while True:
        try:
            which = int(input("Enter the number of the object file to use: "))
            if 0 <= which < len(object_files):
                return object_files[which]
            print("Error: Invalid selection", file=sys.stderr)
        except ValueError:
            print("Error: Invalid input", file=sys.stderr)
            continue


def cli() -> None:
    import argparse

    parser = argparse.ArgumentParser(
        description="Dump object file information using build tools"
    )

    parser.add_argument(
        "build_path",
        type=Path,
        nargs="?",
        help="Path to build directory containing build info JSON file",
    )

    parser.add_argument(
        "--symbols", action="store_true", help="Dump symbol table using nm"
    )
    parser.add_argument(
        "--disassemble", action="store_true", help="Dump disassembly using objdump"
    )

    args = parser.parse_args()
    build_path = args.build_path
    symbols = args.symbols
    disassemble = args.disassemble

    # Check if object file was provided and exists
    if build_path is None:
        build_path = _prompt_build()
    else:
        if not _check_build(build_path):
            print("Error: Invalid build directory", file=sys.stderr)
            sys.exit(1)

    assert build_path is not None
    assert build_path

    build_info_path = build_path / "build_info.json"
    assert build_info_path.exists(), f"File not found: {build_info_path}"

    tools = load_tools(build_info_path)

    if not symbols and not disassemble:
        while True:
            print(
                "Error: Please specify at least one action to perform", file=sys.stderr
            )
            action = input(
                "Enter 's' to dump symbols, 'd' to disassemble, or 'q' to quit: "
            )
            if action == "s":
                symbols = True
                break
            elif action == "d":
                disassemble = True
                break
            elif action == "q":
                sys.exit(0)
            else:
                print("Error: Invalid action", file=sys.stderr)

    object_file = _prompt_object_file(build_path)
    if symbols:
        import subprocess

        cmd_str = subprocess.list2cmdline(
            [str(tools.objdump_path), str(object_file), "--syms"]
        )
        print(f"Running command: {cmd_str}")
        subprocess.run([str(tools.objdump_path), str(object_file)])

    if disassemble:
        import subprocess

        cmd_str = subprocess.list2cmdline(
            [str(tools.objdump_path), "-d", str(object_file)]
        )
        print(f"Running command: {cmd_str}")
        subprocess.run([str(tools.objdump_path), "-d", str(object_file)])

    if not (symbols or disassemble):
        parser.print_help()


if __name__ == "__main__":
    try:
        cli()
    except KeyboardInterrupt as ki:
        handle_keyboard_interrupt(ki)
        raise
        print("Exiting...")
        sys.exit(1)
