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

from ci.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 = json.loads(build_info_path.read_text())
    board_info = build_info[next(iter(build_info))]
    aliases = 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 = []

    # 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:
        print("Exiting...")
        sys.exit(1)
