import argparse
import csv
import json
import os
import shutil
import subprocess
from pathlib import Path

import dateutil.parser  # type: ignore

HERE = Path(__file__).resolve().parent


def run_command(command):
    process = subprocess.Popen(
        command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
    )
    output, error = process.communicate()
    return output.decode("utf-8"), error.decode("utf-8")


def step_back_commits(steps):
    step_back_command = f"git reset --hard HEAD~{steps}"
    output, error = run_command(step_back_command)
    if error:
        print(f"Error stepping back {steps} commit(s): {error}")
        return False
    return True


def check_firmware_size(board: str) -> int:
    root_build_dir = Path(".build") / board
    build_info_json = root_build_dir / "build_info.json"
    build_info = json.loads(build_info_json.read_text())
    board_info = build_info.get(board)
    assert board_info, f"Board {board} not found in {build_info_json}"
    prog_path = Path(board_info["prog_path"])
    base_path = prog_path.parent
    suffixes = [".bin", ".hex", ".uf2"]
    firmware: Path
    for suffix in suffixes:
        firmware = base_path / f"firmware{suffix}"
        if firmware.exists():
            break
    else:
        msg = (
            ", ".join([f"firmware{suffix}" for suffix in suffixes])
            + f" not found in {base_path}"
        )
        raise FileNotFoundError(msg)
    size_command = f"du -b {firmware}"
    output, error = run_command(size_command)
    if error:
        print(f"Error checking firmware size: {error}")
        return -1
    size_in_bytes = output.strip().split()[0]
    return int(size_in_bytes)


def get_commit_hash():
    hash_command = "git rev-parse HEAD"
    output, error = run_command(hash_command)
    if error:
        print(f"Error getting commit hash: {error}")
        return None
    return output.strip()


def get_commit_date(commit_hash):
    date_command = f"git show -s --format=%ci {commit_hash}"
    output, error = run_command(date_command)
    if error:
        print(f"Error getting commit date: {error}")
        return None
    return dateutil.parser.parse(output.strip()).isoformat()


def main(
    board: str,
    num_commits: int,
    skip_step: int,
    start_commit: str | None = None,
    end_commit: str | None = None,
):
    # change to the script dir
    os.chdir(str(HERE))
    # Create tmp directory if it doesn't exist

    if os.path.exists("tmp"):
        shutil.rmtree("tmp")
    os.makedirs("tmp", exist_ok=True)

    # Change to the tmp directory
    os.chdir("tmp")

    # 1. Git clone FastLED repository
    print("Cloning FastLED repository...")
    clone_command = "git clone https://github.com/FastLED/FastLED.git"
    output, error = run_command(clone_command)
    # if error:
    #    print(f"Error cloning repository: {error}")
    #    os.chdir("..")
    #    return

    # Change to the FastLED directory
    os.chdir("FastLED")

    # Checkout the latest commit
    run_command("git checkout master")

    # If end_commit is specified, checkout that commit
    # if end_commit:
    #     print(f"Checking out end commit: {end_commit}")
    #     checkout_command = f"git checkout {end_commit}"
    #     output, error = run_command(checkout_command)
    #     #if error:
    #     #    print(f"Error checking out end commit: {error}")
    #     #    return

    # Prepare CSV file
    csv_filename = "../../firmware_sizes.csv"
    with open(csv_filename, "w", newline="") as csvfile:
        csvwriter = csv.writer(csvfile)
        csvwriter.writerow(["datetime", "commit_hash", "binary_size"])

    commits_checked = 0
    first_iteration = True
    while True:
        current_commit = get_commit_hash()

        if first_iteration and start_commit:
            first_iteration = False
            while True:
                if current_commit == start_commit:
                    break
                if not step_back_commits(1):
                    break
                current_commit = get_commit_hash()

        if num_commits and commits_checked >= num_commits:
            print(f"Checked {num_commits} commits")
            break

        if end_commit and current_commit == end_commit:
            print(f"Checked until end commit: {end_commit}")
            break

        # 2. Run ci-compile.py for current commit
        print(f"\nChecking commit {commits_checked + 1}")

        # remove .build/esp32dev/pio/build/esp32dev/ directory
        board_files = Path(".build") / board / ".pio" / "build" / board
        if board_files.exists():
            shutil.rmtree(str(board_files), ignore_errors=True)
        compile_command = f"python3 ci/ci-compile.py {board} --examples Blink"
        output, error = run_command(compile_command)
        if error:
            print(f"Error running ci-compile.py: {error}")
            if not step_back_commits(skip_step):
                break
            continue

        # 3. Check firmware size and get commit hash
        print("Checking firmware size...")
        try:
            size = check_firmware_size(board)
        except FileNotFoundError as e:
            print(f"Error checking firmware size: {e}")
            if not step_back_commits(skip_step):
                break
            continue
        except AssertionError as e:
            print(f"Error: {e}")
            if not step_back_commits(skip_step):
                break
            continue
        commit_hash = get_commit_hash()
        if size and commit_hash:
            commit_date = get_commit_date(commit_hash)
            print(f"Firmware size: {size} bytes")

            # Write to CSV incrementally
            with open(csv_filename, "a", newline="") as csvfile:
                csvwriter = csv.writer(csvfile)
                csvwriter.writerow([commit_date, commit_hash, size])

            print(f"Result appended to {csv_filename}")

        commits_checked += 1

        # 4. Step back one commit
        print("Stepping back 1 commit...")
        if not step_back_commits(1):
            break

    # Don't remove the tmp directory
    print("\nTemporary directory 'tmp' has been left intact for inspection.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Check FastLED firmware size for multiple commits."
    )
    parser.add_argument(
        "--num-commits", type=int, default=1, help="Number of commits to check"
    )
    parser.add_argument(
        "--skip-step",
        type=int,
        default=1,
        help="Number of commits to skip between checks",
    )
    parser.add_argument("--start-commit", type=str, help="Starting commit hash")
    parser.add_argument("--end-commit", type=str, help="Ending commit hash")
    parser.add_argument(
        "--board", type=str, required=True, help="Board to check firmware size for"
    )
    args = parser.parse_args()

    if args.start_commit or args.end_commit:
        if not (args.start_commit and not args.end_commit):
            print("Both start commit and end commit must be specified.")
            exit(1)
        # if start_commit is specified, end_commit must be specified

    num_commits = args.num_commits
    if args.start_commit and args.end_commit:
        if args.start_commit == args.end_commit:
            print("Start commit and end commit are the same.")
            exit(1)
        num_commits = 999999

    main(args.board, num_commits, args.skip_step, args.start_commit, args.end_commit)
