#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2022 Stéphane Caron
# SPDX-License-Identifier: Apache-2.0

"""Run Bazel targets on systems where Bazel is not installed."""

import argparse
import logging
import os
import sys
from os import path
from typing import List, Optional

__version__ = "0.3.0"


def find_file(name: str, required: bool) -> Optional[str]:
    """Search for a file or directory in the script's parent folders.

    Args:
        name: Name of the file or directory to search.
        required: If True, raise an exception if the file is not found.

    Returns:
        File path, if found, None otherwise.

    Raises:
        FileNotFoundError: if the file was not found.
    """
    cur_path = os.getcwd()
    while cur_path:
        bin_path = path.join(cur_path, name)
        if path.exists(bin_path):
            return bin_path
        old_path = cur_path
        cur_path = path.dirname(cur_path)
        if cur_path == old_path:
            break
    if required:
        raise FileNotFoundError(f"Cannot find {name} in parent folders")
    return None


class Workspace:
    """Bazel workspace information.

    Attributes:
        bazel_bin: Path to bazel-bin directory.
        name: Name of the workspace, defined in WORKSPACE file.
        root: Path to the workspace root directory.
    """

    bazel_bin: str
    name: str
    root: str

    def __init__(self):
        """Initialize Bazel workspace information."""
        bazel_bin = find_file("bazel-bin", required=True)
        workspace_file = find_file("WORKSPACE", required=True)
        name = self.get_workspace_name(workspace_file)
        logging.info(f"Found bazel-bin at {bazel_bin}")
        logging.info(f"Found workspace file at {workspace_file}")
        logging.info(f"Read workspace name as \"{name}\"")
        self.bazel_bin = bazel_bin
        self.name = name
        self.root = path.dirname(workspace_file)

    def get_workspace_name(self, workspace_file: str) -> str:
        """Read workspace name from WORKSPACE file.

        Args:
            workspace_file: Path to WORKSPACE file.

        Returns:
            Workspace name, if found.

        Raises:
            ValueError: if workspace name could not be found.
        """
        for line in open(workspace_file, encoding="utf-8").readlines():
            if line.startswith('workspace(name = "'):
                return line.split('"')[1]

        raise ValueError(
            "Could not find name in WORKSPACE. "
            "Note that we don't parse Starlark beyond "
            '``workspace(name = "something")``.'
        )


def read_arch(bazel_bin, target, name):
    """Read system architecture."""
    suffix = "-2.params"
    if path.exists(f"{bazel_bin}/{target}/{name}_spine-2.params"):
        suffix = "_spine-2.params"  # for C++ agents
    with open(f"{bazel_bin}/{target}/{name}{suffix}", "r") as params:
        for line in params:
            if line.startswith("bazel-out"):
                return line.split("/")[1]


def log_run(target_name: str, arch: str) -> None:
    """Log target name and build configuration.

    Args:
        target_name: Name of the Bazel target.
        arch: Build configuration found.
    """
    RED: str = "\033[31m"
    GREEN: str = "\033[32m"
    YELLOW: str = "\033[33m"
    RESET: str = "\033[0m"

    target_message = f"Found target {YELLOW}{target_name}{RESET} "
    target_message += "for build configuration "
    color = RED
    if "opt" in arch:
        color = GREEN
    elif "fastbuild" in arch:
        color = YELLOW
    elif "unknown" in arch:
        color = RED
    target_message += color + arch + RESET
    logging.info(target_message)


def run(workspace: Workspace, target: str, subargs: List[str]) -> None:
    """Run target from a Bazel workspace.

    Args:
        workspace: Bazel workspace information.
        target: Label of the Bazel target to run.
        subargs: Command-line arguments for the target.
    """
    try:
        if ":" in target:
            target_dir, target_name = target.split(":")
        else:  # target name is directory name
            target_dir = target
            target_name = target.split("/")[-1]
    except ValueError as e:
        raise ValueError(
            f"{target} does not appear to be a valid Bazel label"
        ) from e
    target_dir = target_dir.lstrip("/")
    if target_dir[0] == "@":
        external_name, target_dir = target_dir[1:].split("//")
        target_dir = f"external/{external_name}/{target_dir}"

    try:
        arch = read_arch(workspace.bazel_bin, target_dir, target_name)
    except FileNotFoundError:
        logging.info(
            "Couldn't read arch from "
            f"'{workspace.bazel_bin}/{target_dir}/{target_name}-2.params', "
            "maybe the target is not a Python script?"
        )
        arch = "unknown"

    log_run(target_name, arch)

    execution_path = (
        f"{workspace.bazel_bin}/{target_dir}/"
        f"{target_name}.runfiles/{workspace.name}/{target_dir}"
    )

    os.chdir(execution_path)
    os.execv(target_name, [target_name] + subargs)


def get_argument_parser() -> argparse.ArgumentParser:
    """Get command-line argument parser."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "-s",
        "--sudo",
        default=False,
        action="store_true",
        help="run as administrator (sudo -E)",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help="verbose mode",
    )
    parser.add_argument("command", help="Bazel command")
    parser.add_argument("target", help="Bazel target")
    parser.add_argument(
        "subargs",
        nargs=argparse.REMAINDER,
        help="arguments forwarded to the target",
    )
    return parser


def main():
    """Entry point for the command-line tool."""
    parser = get_argument_parser()
    args = parser.parse_args()
    if args.verbose:
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)
    if args.sudo and os.geteuid() != 0:
        args = ["sudo", "-E", sys.executable] + sys.argv + [os.environ]
        os.execlpe("sudo", *args)
    if args.command == "run":
        run(Workspace(), args.target, args.subargs)
    else:  # unknown command
        logging.error(f'Command "{args.command}" not available with raspunzel')


if __name__ == "__main__":
    main()
