import argparse
import json
import logging
import os
import subprocess

from atils.common import config

logging.basicConfig(level=config.get_logging_level())  # type: ignore


def main(args: list[str]):
  parser: argparse.ArgumentParser = argparse.ArgumentParser()

  parser.add_argument(
    "action",
    help="Select which action to perform. Defaults to build",
    default="build",
    nargs="?",
  )

  parser.add_argument(
    "--build-directory",
    "-bd",
    type=str,
    help="The directory where .atils_buildconfig.json is located",
  )

  parser.add_argument(
    "--actions-only",
    "-ao",
    help="Only show actions",
    action="store_true",
  )
  parser.add_argument(
    "--action-sets-only",
    "-aso",
    help="Only show action sets",
    action="store_true",
  )

  parser.add_argument(
    "--actions",
    nargs="*",
    help="Actions defined in .atils_buildconfig.json of current directory to run",
  )

  parser.add_argument(
    "--action-set",
    help="A single action set defined in .atils_buildconfig.json of current directory to run",
  )

  arguments: argparse.Namespace = parser.parse_args(args)

  directory: str = os.getcwd()
  show_actions: bool = True
  show_action_sets: bool = True

  if arguments.actions_only:
    show_actions = False
  if arguments.action_sets_only:
    show_action_sets = False

  if not show_actions and not show_action_sets:
    raise ValueError("You need to allow showing either actions or action sets")

  if arguments.build_directory is not None:
    directory = arguments.build_directory

  if arguments.action == "list":
    run_list_action(directory, show_actions, show_action_sets)
  elif arguments.actions is not None:
    run_build_actions(arguments.actions, directory)
  elif arguments.action_set is not None:
    run_build_action_set(arguments.action_set, directory)
  else:
    default_action_set: str = _get_default_action_set(directory)
    if default_action_set:
      run_build_action_set(default_action_set, directory)
    else:
      logging.error(
        "No default action set defined. Either define one in"
        + f" {os.path.join(directory, '.atils_buildconfig.json')} or use an argument to specify what to do",
      )


def validate_listed_actions(
  available_actions: list[dict],
  listed_actions: list[str],
) -> None:
  """Take a list of valid actions, a list of actions provided by a user, and ensures that all
  actions provided by the user are valid.

  Args:
  ----
    available_actions (list[object]): A list of objects representing available actions.
    listed_actions (list[str]): A list of strings representing actions provided by the user.

  Raises:
  ------
    ValueError: If any action provided by the user is not valid.

  Returns:
  -------
    None.

  """
  available_action_names = [action["name"] for action in available_actions]
  for action in listed_actions:
    if action not in available_action_names and action != "all":
      raise ValueError(f"{action} is not a valid action")


def run_list_action(directory: str, show_actions: bool, show_action_sets: bool) -> None:
  """List all available build actions, or only actions or only action sets.

  Args:
  ----
    directory (str): The directory where our .atils_buildconfig.json file to list is located
    show_actions (bool): Whether to show actions or not
    show_action_sets (bool): Whether to show action sets or not
  Raises:
    ValueError: If both show_actions and show_action_sets are True.

  Returns:
  -------
    None

  """
  available_actions = _get_available_actions(directory)
  available_action_sets = _get_available_action_sets(directory)

  if not show_actions and not show_action_sets:
    raise ValueError("Cannot specify both actions_only and action_sets_only")
  if show_actions:
    print("Actions")
    print("=======================================")
    for action in available_actions:
      _print_action(action)
  if show_action_sets:
    print("Action Sets")
    print("=======================================")
    for action_set in available_action_sets:
      _print_action_set(action_set, available_actions)


def run_build_action_set(action_set_name: str, directory: str) -> None:
  """Runs all the actions defined in a given action_set
  Args:
    action_set (dict): A dictionary representing an action set
    directory (str): The directory where our .atils_buildconfig.json file to list is located
  """
  available_actions: list[dict] = _get_available_actions(directory)
  action_set: dict = _get_action_set(action_set_name, directory)

  validate_listed_actions(available_actions, action_set["actions"])

  if "strict" in action_set and not action_set["strict"]:
    for action in available_actions:
      if action["name"] in action_set["actions"]:
        _run_action(action, directory)
  else:
    for action in available_actions:
      if action["name"] in action_set["actions"]:
        _run_action_strict(action, directory)


def run_build_actions(listed_actions: list[str], directory: str) -> None:
  # TODO allow us to mark if all build actions must pass
  """Run user-specified build actions, in the order specified by .atils_buildconfig.json.

  Args:
  ----
    listed_actions (list[str]): The list of actions provided by the user.
    directory (str): The directory to use as the current working directory.

  """
  available_actions = _get_available_actions(directory)

  validate_listed_actions(available_actions, listed_actions)

  if "all" in listed_actions:
    for action in available_actions:
      _run_action(action, directory)
  else:
    # Since I know you might ask... we do it this way to ensure that the actions are run in order.
    # The user can enter them arbitrarily, but available_actions is sorted, so we can use that as
    # our source of truth
    for action in available_actions:
      if action["name"] in listed_actions:
        _run_action(action, directory)


def _get_available_actions(directory: str) -> list[dict]:
  """List all actions available in a .atils_buildconfig.json file in the given directory.
  The file must be in the given directory and must be a valid JSON file.
  The file must contain a list of actions, each action must be an object with the following keys:
    - name: The name of the action
    - command: The command to run
    - order: The order in which to run the action. Lower numbers are run first.
  The actions are returned sorted by their order.

  Arguments:
  ---------
    directory (str): The directory where the .atils_buildconfig.json file is located.

  Returns:
  -------
    A list of objects representing actions available in the .atils_buildconfig.json file.

  """
  filename: str = os.path.join(directory, ".atils_buildconfig.json")

  if os.path.isfile(filename):
    with open(filename) as f:
      data = json.load(f)
      if "actions" not in data:
        raise ValueError(f"No actions found in {filename}")
      return sorted(data["actions"], key=lambda x: x["order"])
  else:
    raise FileNotFoundError(f"{filename} does not exist")


def _get_available_action_sets(directory: str) -> list[dict]:
  """List all action sets available in a .atils_buildconfig.json file in the given directory.
  The file must be in the given directory and must be a valid JSON file.
  The file must contain a list of action sets, each action set must be an object with the following keys:
    - name: The name of the action set
    - actions: A list of actions in the action set
  The action sets are returned sorted by their name.

  Arguments:
  ---------
    directory (str): The directory where the .atils_buildconfig.json file is located.

  Returns:
  -------
    A list of objects representing action sets available in the .atils_buildconfig.json file. Returns an empty
    list if no such list is found

  """
  filename = os.path.join(directory, ".atils_buildconfig.json")

  if os.path.isfile(filename):
    with open(filename) as f:
      data = json.load(f)
      if "action_sets" not in data:
        return []
      return sorted(data["action_sets"], key=lambda x: x["name"])
  else:
    raise FileNotFoundError(f"{filename} does not exist")


def _get_action_set(action_set: str, directory: str) -> dict:
  """Get a given action set from an .atils_buildconfig.json file
  Args:
    action_set (str): The name of the action set to get
    directory (str): The directory where the .atils_buildconfig.json file is located
  Returns:
    A dictionary representing the action set, or an empty dictionary if the action set is not found
  """
  available_action_sets = _get_available_action_sets(directory)

  for action_set_obj in available_action_sets:
    if action_set_obj["name"] == action_set:
      return action_set_obj
  raise ValueError(
    f"Action set {action_set} not available in {os.path.join(directory, '.atils_buildconfig.json')}",
  )


def _get_default_action_set(directory: str) -> str:
  """Given a directory with an .atils_buildconfig.json file, return the name of the default action set.

  Args:
  ----
    directory (str): The directory where the .atils_buildconfig.json file is located
  Returns:
    A string representing the name of the default action set, or an empty string if no default action set is found.

  """
  available_action_sets: list[dict] = _get_available_action_sets(directory)
  for action_set in available_action_sets:
    if action_set.get("default"):
      return action_set["name"]
  return ""


def _print_action(action: dict) -> None:
  """Print the name and command of an action.

  Args:
  ----
    action (object): An object representing an action from a .atils_buildconfig.json file.

  """
  if "description" in action:
    print(f"{action['name']} | {action['description']}")
  else:
    print(f"{action['name']} | {action['command']}")
  print()


def _print_action_set(action_set: dict, actions: list[dict]) -> None:
  """Print the name and actions of an action set.

  Args:
  ----
    action_set (dict): An object representing an action set from a .atils_buildconfig.json file.
    actions (list[dict]): A list of objects, representing all available actions

  """
  print(f"{action_set['name']}:")
  if "description" in action_set:
    print(f"{action_set['description']}")
  for action in action_set["actions"]:
    for a in actions:
      if a["name"] == action:
        action_dict = a
        break
    if "description" in action:
      print(f"  {action} | {action_dict['description']}")
    else:
      print(f"  {action} | {action_dict['command']}")
  print()


def _run_action(action: dict, directory: str) -> None:
  """Run the command from an action, using subprocess.run
  Args:
    action (object): An object representing an action.
  """
  _validate_action_can_run(action)
  if action["command"] == "":
    logging.error("An atils build command can't be blank")
    exit(1)
  subprocess.run(action["command"], shell=True, cwd=directory, check=False)


def _run_action_strict(action: dict, directory: str) -> None:
  """Run the command from an action, using subprocess.run. Fails if the command fails
  Args:
    action (object): An object representing an action
  """
  _validate_action_can_run(action)
  if action["command"] == "":
    logging.error("An atils build command can't be blank")
    exit(1)
  try:
    subprocess.run(action["command"], shell=True, cwd=directory, check=True)
  except subprocess.CalledProcessError:
    logging.exception(f"Error running {action['name']}")
    exit(1)


def _validate_action_can_run(action: dict):
  """Given an action, checks if it is a CI-only action. If it is, and we are not running in a CI environment,
  exit the program.

  Args:
  ----
    action (dict): A dict representing an action from a .atils_buildconfig.json file.

  Returns:
  -------
    None.

  Raises:
  ------
    SystemExit: If we are not running in a CI environment and the action is a CI-only action.

  """  # noqa: D205
  if "ci_only" in action and action["ci_only"] and "ATILS_CI_ENV" not in os.environ:
    logging.error(
      f"Attempted to run a CI only-action ({action['name']}) in a non-CI environment. "
      + "If you know what you're doing, set ATILS_CI_ENV to true.",
    )
    exit(1)
