import json
import os
from typing import Any, Dict, Optional

from click import ClickException
import jsonschema
import ray
import yaml

import anyscale.legacy_projects
from anyscale.util import slugify


# Pathnames specific to Ray's project directory structure.
PROJECT_ID_BASENAME = "project-id"
RAY_PROJECT_DIRECTORY = "ray-project"

ANYSCALE_PROJECT_FILE = ".anyscale.yaml"
ANYSCALE_AUTOSCALER_FILE = "session-default.yaml"


CLUSTER_YAML_TEMPLATE = """
# This file was generated by `anyscale init`.

# The maximum number of workers nodes to launch in addition to the head
# node. This takes precedence over min_workers. min_workers defaults to 0.
max_workers: 1

docker:
    image: anyscale/ray:0.8.7
    container_name: ray_container

setup_commands: []

head_setup_commands: []

# Cloud-provider specific configuration.
provider:
    type: aws
    region: us-west-2
    availability_zone: us-west-2a

# How Ray will authenticate with newly launched nodes.
auth:
    ssh_user: ubuntu
"""


def validate_project_schema(project_config: Dict[str, str]) -> Any:
    """Validate a project config against the project schema.
    Args:
        project_config (dict): Parsed project yaml.
    Raises:
        jsonschema.exceptions.ValidationError: This exception is raised
            if the project file is not valid.
    """
    dir = os.path.dirname(os.path.abspath(__file__))
    with open(os.path.join(dir, "ProjectConfig.json")) as f:
        schema = json.load(f)

    jsonschema.validate(instance=project_config, schema=schema)


def find_project_root(directory: str) -> Optional[str]:
    """Find root directory of the project.

    Args:
        directory (str): Directory to start the search in.

    Returns:
        Path of the parent directory containing the project
        or None if no such project is found.
    """
    prev, directory = None, os.path.abspath(directory)
    while prev != directory:
        if os.path.exists(ANYSCALE_PROJECT_FILE):
            return directory
        prev, directory = directory, os.path.abspath(os.path.join(directory, os.pardir))
    return None


class ProjectDefinition(object):
    def __init__(self, root_dir: str):
        self.root = os.path.join(root_dir, "")
        anyscale_yaml = os.path.join(root_dir, ANYSCALE_PROJECT_FILE)
        if os.path.exists(anyscale_yaml):
            with open(anyscale_yaml) as f:
                self.config = yaml.safe_load(f)
        else:
            self.config = {}

        if "cluster" not in self.config:
            self.config["cluster"] = {
                "config": os.path.join(self.root, ANYSCALE_AUTOSCALER_FILE)
            }

    def cluster_yaml(self) -> str:
        if "config" in self.config["cluster"]:
            return str(self.config["cluster"]["config"])
        return os.path.join(self.root, ANYSCALE_AUTOSCALER_FILE)


def load_project_or_throw() -> Any:
    # First check if there is a .anyscale.yaml.
    root_dir = find_project_root(os.getcwd())
    if root_dir:
        project = ProjectDefinition(root_dir)
    else:
        # Legacy codepath for projects.
        anyscale.legacy_projects.validate_project_schema = validate_project_schema
        try:
            project = anyscale.legacy_projects.ProjectDefinition(os.getcwd())  # type: ignore
        except (jsonschema.exceptions.ValidationError, ValueError) as e:
            raise ClickException(e)  # type: ignore

        dir = os.path.dirname(os.path.abspath(__file__))
        with open(os.path.join(dir, "anyscale_schema.json")) as f:
            schema = json.load(f)

        # Validate the project file.
        try:
            jsonschema.validate(instance=project.config, schema=schema)
        except (jsonschema.exceptions.ValidationError, ValueError) as e:
            raise ClickException(e)  # type: ignore

        # Normalize project name
        project.config["name"] = slugify(project.config["name"])

    return project


def get_project_id(project_dir: str) -> int:
    """
    Args:
        project_dir: Project root directory.

    Returns:
        The ID of the associated Project in the database.

    Raises:
        ValueError: If the current project directory does
            not contain a project ID.
    """
    project_filename = os.path.join(project_dir, ANYSCALE_PROJECT_FILE)
    if os.path.isfile(project_filename):
        with open(project_filename) as f:
            config = yaml.safe_load(f)
            project_id = config["project_id"]
    else:
        project_id_filename = os.path.join(
            project_dir, RAY_PROJECT_DIRECTORY, PROJECT_ID_BASENAME
        )
        if os.path.isfile(project_id_filename):
            with open(project_id_filename, "r") as f:
                project_id = f.read()
        else:
            raise ClickException(
                "Ray project in {} not registered yet. "
                "Did you run 'anyscale init'?".format(project_dir)
            )
    try:
        result = int(project_id)
    except ValueError:
        raise ClickException(
            "{} does not contain a valid project ID".format(project_id_filename)
        )
    return result


def validate_project_name(project_name: str) -> bool:
    return " " not in project_name.strip()
