"""SDK for client library."""
import json
import posixpath
import sys
from typing import Any, Dict, List, Optional, Union

import pandas as pd
from jsf import JSF
from ML_management.executor_template.executor_pattern import JobExecutorPattern
from ML_management.executor_template.executor_pattern_to_methods_map import ExecutorMethodName
from ML_management.mlmanagement import get_server_url, get_server_websocket_url
from ML_management.mlmanagement.session import AuthSession
from ML_management.models.model_type_to_methods_map import ModelMethodName
from ML_management.registry.registry_manager import RegistryManager
from ML_management.sdk import schema
from ML_management.sdk.schema import ExecutionJob, ModelVersion
from sgqlc.operation import Operation


def _to_datetime(df: pd.DataFrame, column_names: List[str]) -> pd.DataFrame:
    """
    Convert df's columns to datetime.

    Parameters
    ----------
    df: pd.DataFrame
        pd.DataFrame in which the columns will be converted.
    column_names: List[str]
        Сolumn names to be converted.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with converted columns.
    """
    for column_name in column_names:
        df[column_name] = pd.to_datetime(df[column_name], unit="s")

    return df


def send_graphql_request(op: Operation, json_response: bool = True) -> Any:
    """Send request to server and process the response."""
    json_data = AuthSession().sgqlc_request(op)

    if "data" not in json_data or json_data["data"] is None:
        error_message = json_data["errors"][0]["message"]
        if (
            ("extensions" in json_data["errors"][0])
            and ("params" in json_data["errors"][0]["extensions"])
            and ("message" in json_data["errors"][0]["extensions"]["params"])
        ):
            error_extensions_message = json_data["errors"][0]["extensions"]["params"]["message"]
            raise Exception(f"{error_message}\nMessage: {error_extensions_message}")
        else:
            raise Exception(error_message)

    if json_response:
        return json_data["data"]
    else:
        return op + json_data


def list_model() -> pd.DataFrame:
    """
    List available models.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with list of available models.
    """
    op = Operation(schema.Query)
    op.list_model.name()
    op.list_model.description()
    op.list_model.creation_timestamp()
    op.list_model.last_updated_timestamp()
    json_data = send_graphql_request(op)
    df = pd.DataFrame.from_dict(json_data["listModel"])
    if not df.empty:
        df = _to_datetime(df, ["creationTimestamp", "lastUpdatedTimestamp"])
    return df


def list_dataset_loader() -> pd.DataFrame:
    """
    List available dataset_loaders.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with list of available dataset_loaders.
    """
    op = Operation(schema.Query)
    op.list_dataset_loader.name()
    op.list_dataset_loader.description()
    op.list_dataset_loader.creation_timestamp()
    op.list_dataset_loader.last_updated_timestamp()
    json_data = send_graphql_request(op)
    df = pd.DataFrame.from_dict(json_data["listDatasetLoader"])
    if not df.empty:
        df = _to_datetime(df, ["creationTimestamp", "lastUpdatedTimestamp"])
    return df


def list_executor() -> pd.DataFrame:
    """
    List available executors.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with list of available executors.
    """
    op = Operation(schema.Query)
    op.list_executor.name()
    op.list_executor.description()
    op.list_executor.creation_timestamp()
    op.list_executor.last_updated_timestamp()
    json_data = send_graphql_request(op)
    df = pd.DataFrame.from_dict(json_data["listExecutor"])
    if not df.empty:
        df = _to_datetime(df, ["creationTimestamp", "lastUpdatedTimestamp"])
    return df


def add_ml_job_single_model(
    job_executor_name: str,
    executor_params: dict,
    dataset_loader_name: str,
    dataset_params: dict,
    model_name: str,
    model_params: List[Dict[str, dict]],
    collector_params: dict,
    gpu: bool = False,
    job_executor_version: Optional[int] = None,
    dataset_loader_version: Optional[int] = None,
    model_version: Optional[int] = None,
    new_model_name: Optional[str] = None,
    experiment_name: str = "Default",
    collector_name: str = "s3",
    choice_criteria: str = "latest",
    cron_expression: Optional[str] = None,
    periodic_type: str = "ONCE",
    metric_name: Optional[str] = None,
    optimal_min: bool = False,
) -> str:
    """
    Create execution job.

    Parameters
    ----------
    job_executor_name: str
        Name of the executor that will execute the job.
    executor_params: Dict[str, ...]
        Dictionary of executor parameters. Example: {'executor_param1': list,
                                                     'executor_param2': int,
                                                     'executor_param3': dict,
                                                     ...}
    dataset_loader_name: str
        Name of the DatasetLoader that the model will use.
    dataset_params: dict
        Dictionary of DatasetLoader parameters.
    model_name: str
        Name of the model to interact with.
    model_params: List[Dict[str, dict]]
        List of dictionaries with parameters of model methods. Example: [{"method_one": {key1: value1, key2: value2}},
                                                                         {"method_two": {key1: value1, key2: value2}}]
    collector_params: dict
        Dictionary of collector parameters. Example: {'collector_param1': list,
                                                      'collector_param2': int,
                                                      'collector_param3': dict,
                                                     ...}
    gpu: bool = False
        Whether to use GPU for this job or not. Default: False
    job_executor_version: Optional[int] = None
        Version of the executor that will execute the job. Default: None, "latest" version is used.
    dataset_loader_version: Optional[int] = None
        Version of the DatasetLoader that the model will interact with. Default: None, "latest" version is used.
    model_version: Optional[int] = None
        Version of the model to interact with. Default: None, "latest" version is used.
    new_model_name: Optional[str] = None
        Name of the model to save in case new model is to be created as a result of job execution (regulated by executor.upload_model_mode).
        Default: None.
    experiment_name: str = "Default"
        Name of the experiment. Default: "Default"
    collector_name: {"s3", }
        Name of the collector to interact with. Default: "s3"
    choice_criteria: {"latest", "initial", "best"}
        Criteria to choose the model to interact with. Required if model_version is not specified. Default: "latest"
        If "best": according to the optimal_min value, minimum (or maximum) value for the selected metric_name is taken
    cron_expression: str = None
        Cron expression for periodic or deferred jobs. Default: None
    periodic_type: {"ONCE", "PERIODIC"}
        Frequency of the task. Default: "ONCE"
    metric_name: str = None
        Name of the metric(must be logged in) for choice_criteria. Default: None
    optimal_min: bool = False
        Whether to take minimum or maximum value for the selected choice_criteria and metric_name. Default: False


    Returns
    -------
    str
        Name of the Job.
    """
    models = [
        {
            "role": JobExecutorPattern.DEFAULT_ROLE,
            "model": {
                "name": model_name,
                "version": model_version,
                "choice_criteria": choice_criteria,
                "metric_name": metric_name,
                "optimal_min": optimal_min,
            },
            "params": model_params,
            "new_model_name": new_model_name,
        }
    ]

    return add_ml_job(
        job_executor_name=job_executor_name,
        executor_params=executor_params,
        dataset_loader_name=dataset_loader_name,
        dataset_params=dataset_params,
        models=models,
        collector_params=collector_params,
        gpu=gpu,
        job_executor_version=job_executor_version,
        dataset_loader_version=dataset_loader_version,
        experiment_name=experiment_name,
        collector_name=collector_name,
        cron_expression=cron_expression,
        periodic_type=periodic_type,
    )


def add_ml_job(
    job_executor_name: str,
    executor_params: dict,
    dataset_loader_name: str,
    dataset_params: dict,
    models: List[dict],
    collector_params: dict,
    gpu: bool = False,
    job_executor_version: Optional[int] = None,
    dataset_loader_version: Optional[int] = None,
    experiment_name: str = "Default",
    collector_name: str = "s3",
    cron_expression: Optional[str] = None,
    periodic_type: str = "ONCE",
) -> str:
    """
    Create execution job.

    Parameters
    ----------
    job_executor_name: str
        Name of the executor that will execute the job.
    executor_params: Dict[str, ...]
        Dictionary of executor parameters. Example: {'executor_param1': list,
                                                     'executor_param2': int,
                                                     'executor_param3': dict,
                                                     ...}
    dataset_loader_name: str
        Name of the DatasetLoader that the model will use.
    dataset_params: dict
        Dictionary of DatasetLoader parameters.
    models = List[dict]
        Necessary information for using the model. Example: [
                                                                {
                                                                    "role": "role_one",
                                                                    "model": {
                                                                        "name": "model_1_name",
                                                                        "version": 1, # Optional. Default: latest version
                                                                    },
                                                                    "params": [{"method_one": {key1: value1, key2: value2}},
                                                                               {"method_two": {key1: value1, key2: value2}}],
                                                                    "new_model_name": "some_name" # Optional. Default: None

                                                                },
                                                                {
                                                                    "role": "role_two",
                                                                    "model": {
                                                                        "name": "model_2_name",
                                                                    },
                                                                    "params": [{"method_one": {key1: value1, key2: value2}},
                                                                               {"method_two": {key1: value1, key2: value2}}]
                                                                },
                                                            ]
    collector_params: dict
        Dictionary of collector parameters. Example: {'collector_param1': list,
                                                      'collector_param2': int,
                                                      'collector_param3': dict,
                                                     ...}
    gpu: bool = False
        Whether to use GPU for this job or not. Default: False
    job_executor_version: Optional[int] = None
        Version of the executor that will execute the job. Default: None, "latest" version is used.
    dataset_loader_version: Optional[int] = None
        Version of the DatasetLoader that the model will interact with. Default: None, "latest" version is used.
    new_model_name: Optional[str] = None
        Name of the model to save in case new model is to be created as a result of job execution (regulated by executor.upload_model_mode).
        Default: None.
    experiment_name: str = "Default"
        Name of the experiment. Default: "Default"
    collector_name: {"s3", }
        Name of the collector to interact with. Default: "s3"
    choice_criteria: {"latest", "initial", "best"}
        Criteria to choose the model to interact with. Required if model_version is not specified. Default: "latest"
        If "best": according to the optimal_min value, minimum (or maximum) value for the selected metric_name is taken
    cron_expression: str = None
        Cron expression for periodic or deferred jobs. Default: None
    periodic_type: {"ONCE", "PERIODIC"}
        Frequency of the task. Default: "ONCE"
    metric_name: str = None
        Name of the metric(must be logged in) for choice_criteria. Default: None
    optimal_min: bool = False
        Whether to take minimum or maximum value for the selected choice_criteria and metric_name. Default: False

    Returns
    -------
    str
        Name of the Job.
    """
    op = Operation(schema.Query)

    if dataset_loader_version is None:
        dataset_loader_version = int(RegistryManager().get_latest_dataset_loader_version(name=dataset_loader_name).version)

    dataset_loader_version_choice = schema.ObjectVersionOptionalInput(name=dataset_loader_name, version=dataset_loader_version)

    _dataset_loader_version = schema.ObjectVersionInput(name=dataset_loader_name, version=dataset_loader_version)
    dataset_collector_schema = op.dataset_loader_version_from_name_version(dataset_loader_version=_dataset_loader_version).data_json_schema(
        collector_name=collector_name
    )
    dataset_collector_schema.dataset_loader_method_schema.schema_name()
    dataset_collector_schema.collector_method_schema.schema_name()

    dataset_loader_version_obj = send_graphql_request(op, json_response=False)

    dataset_loader_method_schema = (
        dataset_loader_version_obj.dataset_loader_version_from_name_version.data_json_schema.dataset_loader_method_schema.schema_name
    )
    collector_method_schema = (
        dataset_loader_version_obj.dataset_loader_version_from_name_version.data_json_schema.collector_method_schema.schema_name
    )

    dataset_loader_method_params = schema.MethodParamsInput(
        method_name=dataset_loader_method_schema,
        method_params=json.dumps(dataset_params),
    )

    collector_method_params = schema.MethodParamsInput(method_name=collector_method_schema, method_params=json.dumps(collector_params))

    data_params = schema.DataParamsInput(
        dataset_loader_version_choice=dataset_loader_version_choice,
        collector_name=collector_name,
        dataset_loader_method_params=dataset_loader_method_params,
        collector_method_params=collector_method_params,
    )

    if job_executor_version is None:
        job_executor_version = int(RegistryManager().get_latest_executor_version(name=job_executor_name).version)

    op = Operation(schema.Query)
    _executor_version = schema.ObjectVersionInput(name=job_executor_name, version=job_executor_version)

    executor_version_choice = schema.ObjectVersionOptionalInput(name=job_executor_name, version=job_executor_version)

    executor_model_schema = op.executor_version_from_name_version(executor_version=_executor_version)
    executor_model_schema.executor_method_schema_name()

    executor_version_obj = send_graphql_request(op, json_response=False)
    executor_method_schema = ExecutorMethodName[executor_version_obj.executor_version_from_name_version.executor_method_schema_name].value
    executor_method_params = schema.MethodParamsInput(method_name=executor_method_schema, method_params=json.dumps(executor_params))

    list_role_model_params = []

    for model in models:
        version = model["model"].get("version", int(RegistryManager().get_latest_model_version(name=model["model"]["name"]).version))

        choice_criteria = model["model"].get("choice_criteria", "latest")

        metric_name = model["model"].get("metric_name")

        optimal_min = model["model"].get("optimal_min", False)

        model_version_choice = schema.ModelVersionChoice(
            name=model["model"]["name"],
            version=version,
            choice_criteria=choice_criteria,
            metric_name=metric_name,
            optimal_min=optimal_min,
        )

        model_methods_params = []

        for item in model["params"]:
            for key in item:
                model_methods_params.append(schema.MethodParamsInput(method_name=key, method_params=json.dumps(item[key])))

        new_model_name = model.get("new_model_name")

        current_model_params = schema.ModelParamsInput(
            model_version_choice=model_version_choice, list_model_method_params=model_methods_params, new_model_name=new_model_name
        )

        model_role = schema.RoleModelParamsInput(role=model["role"], model_params=current_model_params)

        list_role_model_params.append(model_role)

    executor_params = schema.ExecutorParamsInput(
        executor_method_params=executor_method_params,
        executor_version_choice=executor_version_choice,
    )

    op = Operation(schema.Mutation)
    mutation = op.add_ml_job(
        form=schema.JobParameters(
            executor_params=executor_params,
            list_role_model_params=list_role_model_params,
            data_params=data_params,
            experiment_name=experiment_name,
            cron_expression=cron_expression,
            periodic_type=periodic_type,
            gpu=gpu,
        )
    )

    mutation.name()

    job = send_graphql_request(op, json_response=False)

    return job.add_ml_job.name


def job_by_name(name: str) -> ExecutionJob:
    """
    Return Job object by name.

    Parameters
    ----------
    name: str
        Name of the job.

    Returns
    -------
    Job
        Instance of the Job class.
    """
    op = Operation(schema.Query)

    base_query = op.job_from_name(name=name)
    base_query.name()
    base_query.periodic_type()
    base_query.status()
    base_query.registration_timestamp()
    base_query.start_timestamp()
    base_query.end_timestamp()
    base_query.build_job()
    base_query.exception()

    job = send_graphql_request(op, json_response=False)

    return job.job_from_name


def job_metric_by_name(name: str) -> pd.DataFrame:
    """
    Job's most recent logged metrics.

    Parameters
    ----------
    name: str
        Name of the job.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with latest metrics.
    """
    op = Operation(schema.Query)

    op.job_from_name(name=name).run.latest_metrics()
    json_data = send_graphql_request(op)

    json_data = json_data["jobFromName"]["run"]["latestMetrics"] if json_data["jobFromName"]["run"] else None

    return pd.DataFrame([json_data])


def list_model_version(name: str) -> pd.DataFrame:
    """
    List available versions of the model with such name.

    Parameters
    ----------
    name: str
        Name of the model.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with a list of available model versions.
    """
    op = Operation(schema.Query)
    base_query = op.model_from_name(name=name).list_model_version
    base_query.version()
    base_query.creation_timestamp()
    base_query.status()
    json_data = send_graphql_request(op)

    df = pd.DataFrame.from_dict(json_data["modelFromName"]["listModelVersion"])
    df = _to_datetime(df, ["creationTimestamp"])

    return df.sort_values(by=["version"], ignore_index=True)


def list_dataset_loader_version(name: str) -> pd.DataFrame:
    """
    List available versions of the dataset_loader with such name.

    Parameters
    ----------
    name: str
        Name of the DatasetLoader.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with a list of available dataset_loader versions.
    """
    op = Operation(schema.Query)
    base_query = op.dataset_loader_from_name(name=name).list_dataset_loader_version
    base_query.version()
    base_query.creation_timestamp()
    base_query.status()
    json_data = send_graphql_request(op)

    df = pd.DataFrame.from_dict(json_data["datasetLoaderFromName"]["listDatasetLoaderVersion"])
    df = _to_datetime(df, ["creationTimestamp"])

    return df.sort_values(by=["version"], ignore_index=True)


def list_executor_version(name: str) -> pd.DataFrame:
    """
    List available versions of the executor with such name.

    Parameters
    ----------
    name: str
        Name of the executor.

    Returns
    -------
    pd.DataFrame
        Pandas dataframe with a list of available executor versions.
    """
    op = Operation(schema.Query)
    base_query = op.executor_from_name(name=name).list_executor_version
    base_query.version()
    base_query.creation_timestamp()
    base_query.status()
    json_data = send_graphql_request(op)

    df = pd.DataFrame.from_dict(json_data["executorFromName"]["listExecutorVersion"])
    df = _to_datetime(df, ["creationTimestamp"])

    return df.sort_values(by=["version"], ignore_index=True)


def model_version_metainfo(model_name: str, model_version: Optional[int] = None) -> ModelVersion:
    """
    Meta information about the model version by the model name and version.

    Parameters
    ----------
    model_name: str
        Name of the model.
    model_version: Optional[int] = None
        Version of the model. Default: None, "latest" version is used.

    Returns
    -------
    ModelVersion
        ModelVersion instance with meta information.
    """
    if model_version is None:
        model_version = int(RegistryManager().get_latest_model_version(name=model_name).version)

    op = Operation(schema.Query)
    _model_version = schema.ObjectVersionInput(name=model_name, version=model_version)
    base_query = op.model_version_from_name_version(model_version=_model_version)
    base_query.name()
    base_query.version()
    base_query.status()
    base_query.build_job().status()
    base_query.build_job().docker_image_tag()
    base_query.available_executor_versions.name()
    base_query.available_executor_versions.version()
    model_version = send_graphql_request(op, json_response=False)
    return model_version.model_version_from_name_version


def rebuild_model_version_image(model_name: str, model_version: int) -> str:
    """
    Start building new docker image for specified model version.

    Parameters
    ----------
    model_name: str
        Name of the model.
    model_version: int
        Version of the model

    Returns
    -------
    str
        name of new docker image for specified model version.
    """
    op = Operation(schema.Mutation)
    _model_version = schema.ObjectVersionInput(name=model_name, version=model_version)
    op.rebuild_model_version_image(model_version=_model_version)
    result = send_graphql_request(op=op)
    return result["rebuildModelVersionImage"]


def _generate_fake_schema(json_schema: dict) -> dict:
    if "required" not in json_schema.keys():
        return {}

    required_properties = {key: json_schema["properties"][key] for key in json_schema["required"]}
    json_schema["properties"] = required_properties

    faker = JSF(json_schema)
    fake_json = faker.generate()
    return fake_json


def _print_params_by_schema(json_schema: Dict, schema_type: str) -> None:
    """Print entity JSON Schema and example with required params."""
    properties_and_required_dict = {key: json_schema[key] for key in ("properties", "required") if key in json_schema}

    json_formatted_str = json.dumps(properties_and_required_dict, indent=2)

    print(f"{schema_type} json-schema:")

    print(json_formatted_str)

    print(f"{schema_type} parameters example:")

    fake_json = _generate_fake_schema(json_schema)

    print(fake_json)


def _get_model_schema_for_executor(executor_name: str, executor_version: Optional[int], models: list) -> dict:
    if executor_version is None:
        executor_version = int(RegistryManager().get_latest_executor_version(name=executor_name).version)

    role_models = []

    for model in models:
        model_name = model["name"]
        model_version = model.get("version", int(RegistryManager().get_latest_model_version(name=model_name).version))
        model_role = model.get("role", JobExecutorPattern.DEFAULT_ROLE)

        current_model = schema.ObjectVersionInput(name=model_name, version=model_version)
        current_role = schema.RoleModelVersionInput(role=model_role, model=current_model)

        role_models.append(current_role)

    op = Operation(schema.Query)

    _executor_version = schema.ObjectVersionInput(name=executor_name, version=executor_version)
    base_query = (
        op.executor_version_from_name_version(executor_version=_executor_version)
        .job_json_schema(models=role_models)
        .list_role_model_method_schemas
    )

    base_query.role()
    base_query.list_model_method_schemas.schema_name()
    base_query.list_model_method_schemas.json_schema()

    json_data = send_graphql_request(op)
    return json_data["executorVersionFromNameVersion"]["jobJsonSchema"]["listRoleModelMethodSchemas"]


def print_model_schema_for_executor(executor_name: str, models: List[dict], executor_version: Optional[int] = None) -> None:
    """
    Print model schema for particular executor.

    Parameters
    ----------
    executor_name: str
        Name of the executor.
    models: List[dict]
        Necessary information about the model. Example1: [{"name": str = "simple_model"}];

                                               Example2: [{"name": str = "model_name1",
                                                           "role": Optional[str] = "role1",
                                                           "version": Optional[int] = version},
                                                          {"name": str = "model_name2",
                                                           "role": Optional[str] = "role2",
                                                           "version": Optional[int] = version}]
    executor_version: Optional[int] = None
        Version of the executor. Default: None, "latest" version is used.
    """
    models_methods_schemas = _get_model_schema_for_executor(
        executor_name=executor_name,
        executor_version=executor_version,
        models=models,
    )

    for model_methods_schemas in models_methods_schemas:
        role = model_methods_schemas["role"]
        for model in models:
            if ("role" in model and model["role"] == role) or "role" not in model:
                model_name = model["name"]

        print(f"Model name: {model_name}, model role: {role}")
        for model_methods_schema in model_methods_schemas["listModelMethodSchemas"]:
            _print_params_by_schema(
                json.loads(model_methods_schema["jsonSchema"]), ModelMethodName(model_methods_schema["schemaName"]).name
            )
        print()


def generate_model_params_for_executor(
    executor_name: str, models: List[dict], executor_version: Optional[int] = None
) -> Union[List[Dict[str, dict]], List[dict]]:
    """
    Return example of model's methods parameters for executor.

    Parameters
    ----------
    executor_name: str
        Name of the executor.
    models: List[dict]
        Necessary information about the model. Example1: [{"name": str = "simple_model"}];

                                               Example2: [{"name": str = "model_name1",
                                                           "role": Optional[str] = "role1",
                                                           "version": Optional[int] = version},
                                                          {"name": str = "model_name2",
                                                           "role": Optional[str] = "role2",
                                                           "version": Optional[int] = version}]
    executor_version: Optional[int] = None
        Version of the executor. Default: None, "latest" version is used.

    Returns
    -------
    List[Dict[str, Dict]]:
        Example of model's methods parameters in case simple executor. Example output: [{"method_one": {key1: value1, key2: value2}},
                                                                                        {"method_two": {key1: value1, key2: value2}}]
    List[dict]:
        Example of models parameter for add_ml_job function. Example:  [
                                                                            {
                                                                                "role": "role_one",
                                                                                "model": {
                                                                                    "name": "model_1_name",
                                                                                    "version": 1, # Optional. Default: latest version
                                                                                },
                                                                                "params": [{"method_one": {key1: value1, key2: value2}},
                                                                                        {"method_two": {key1: value1, key2: value2}}]
                                                                            },
                                                                            {
                                                                                "role": "role_two",
                                                                                "model": {
                                                                                    "name": "model_2_name",
                                                                                },
                                                                                "params": [{"method_one": {key1: value1, key2: value2}},
                                                                                        {"method_two": {key1: value1, key2: value2}}]
                                                                            },
                                                                        ]

    """
    models_methods_schemas = _get_model_schema_for_executor(
        executor_name=executor_name,
        executor_version=executor_version,
        models=models,
    )

    model_parameters = []

    for model_methods_schemas in models_methods_schemas:
        list_model_params = []
        for model_methods_schema in model_methods_schemas["listModelMethodSchemas"]:
            list_model_params.append(
                {model_methods_schema["schemaName"]: _generate_fake_schema(json.loads(model_methods_schema["jsonSchema"]))}
            )
        if len(models_methods_schemas) == 1:
            return list_model_params
        role = model_methods_schemas["role"]
        current_dict = {}
        for model in models:
            if model["role"] == role:
                model_name = model["name"]
                model_version = model.get("version", int(RegistryManager().get_latest_model_version(name=model_name).version))
                break
        current_dict["role"] = role
        current_dict["model"] = {}
        current_dict["model"]["name"] = model_name
        current_dict["model"]["version"] = model_version
        current_dict["params"] = list_model_params

        model_parameters.append(current_dict)

    return model_parameters


def print_datasetloader_schema(name: str, version: Optional[int] = None) -> None:
    """
    Print DatasetLoader schema.

    Parameters
    ----------
    name: str
        Name of the DatasetLoader.
    version: Optional[int] = None
        Version of the DatasetLoader. Default: None, "latest" version is used.
    """
    if version is None:
        version = int(RegistryManager().get_latest_dataset_loader_version(name=name).version)

    op = Operation(schema.Query)
    _datasetloader_version = schema.ObjectVersionInput(name=name, version=version)
    base_query = op.dataset_loader_version_from_name_version(dataset_loader_version=_datasetloader_version)
    base_query.dataset_loader_method_schema()
    json_data = send_graphql_request(op)
    json_data = json_data["datasetLoaderVersionFromNameVersion"]["datasetLoaderMethodSchema"]
    _print_params_by_schema(json_schema=json_data, schema_type="DatasetLoader")


def print_executor_schema(name: str, version: Optional[int] = None) -> None:
    """
    Print executor schema.

    Parameters
    ----------
    name: str
        Name of the executor.
    version: Optional[int] = None
        Version of the executor. Default: None, "latest" version is used.
    """
    if version is None:
        version = int(RegistryManager().get_latest_executor_version(name=name).version)
    op = Operation(schema.Query)
    executor_version = schema.ObjectVersionInput(name=name, version=version)
    base_query = op.executor_version_from_name_version(executor_version=executor_version)
    base_query.executor_method_schema()
    json_data = send_graphql_request(op)

    json_data = json_data["executorVersionFromNameVersion"]["executorMethodSchema"]
    _print_params_by_schema(json_schema=json_data, schema_type="Executor")


def print_executor_roles(name: str, version: Optional[int] = None) -> None:
    """
    Print the roles required by the executor.

    Parameters
    ----------
    name: str
        Name of the executor.
    version: Optional[int] = None
        Version of the executor. Default: None, "latest" version is used.
    """
    if version is None:
        version = int(RegistryManager().get_latest_executor_version(name=name).version)
    op = Operation(schema.Query)
    executor_version = schema.ObjectVersionInput(name=name, version=version)
    base_query = op.executor_version_from_name_version(executor_version=executor_version)
    base_query.desired_model_methods()
    json_data = send_graphql_request(op)
    print(json_data["executorVersionFromNameVersion"]["desiredModelMethods"])


def _get_logs_url(job_identity: str, is_execution: bool = True, stream: bool = True) -> str:
    server_url = get_server_websocket_url() if stream else get_server_url()
    url_base = posixpath.join(server_url, "logs-api")
    job_type = "execution" if is_execution else "build"
    local_path = f"{f'stream/{job_type}' if stream else f'filedump/{job_type}'}?identity={job_identity}"
    return posixpath.join(url_base, local_path)


def _get_logs(job_identity: str, is_execution: bool, stream: bool = True, file_name: Optional[str] = None) -> None:
    url = _get_logs_url(job_identity=job_identity, stream=stream, is_execution=is_execution)
    if stream:
        ws = None
        file = sys.stdout
        try:
            ws = AuthSession().instantiate_websocket_connection(url)
            if file_name:
                file = open(file_name, "w")
            while True:
                data = json.loads(ws.recv())
                if "status" not in data or data["status"] != "OK":
                    raise RuntimeError("Internal Server Error")
                print("\n".join(data["logs"]), file=file)
        except KeyboardInterrupt:
            pass
        except Exception as err:
            print(err)
        finally:
            if ws is not None:
                ws.close()
            if file_name:
                file.close()
        return
    with AuthSession().get(url, stream=True) as resp:
        if file_name:
            with open(file_name, "wb") as f:
                for line in resp.iter_lines():
                    if line:
                        f.writelines([line, b"\n\n"])
        else:
            for line in resp.iter_lines():
                if line:
                    print(line.decode())


def get_logs(job_name: str, stream: bool = True, file_name: Optional[str] = None) -> None:
    """
    Stream logs of the execution job by job name.

    Parameters
    ----------
    job_name: str
        Name of the execution job whose logs we want to view.
    stream: bool
        Stream logs or dump all available at the moment.
    file_name: Optional[str] = None
        Name of the file where to save logs. Default: None. If None prints logs to the output.
    """
    _get_logs(is_execution=True, job_identity=job_name, stream=stream, file_name=file_name)


def get_build_logs(docker_image_tag: str, stream: bool = True, file_name: Optional[str] = None) -> None:
    """
    Stream logs of the build job by docker image tag.

    Parameters
    ----------
    docker_image_tag: str
        The tag of the docker image whose creation logs you want to view.
    stream: bool
        Stream logs or dump all available at the moment.
    file_name: Optional[str] = None
        Name of the file where to save logs. Default: None. If None prints logs to the output.
    """
    _get_logs(is_execution=False, job_identity=docker_image_tag, stream=stream, file_name=file_name)


def get_required_classes_by_executor(name: str, version: Optional[int] = None) -> List[str]:
    """
    Return the names of classes for the model to be inherited from, by the name of the executor.

    Parameters
    ----------
    name: str
        Name of the executor.
    executor_version: Optional[int] = None
        Version of the executor. Default: None, "latest" version is used.

    Returns
    -------
    List[str]:
        List of model class names to be inherited from.
    """
    if version is None:
        version = int(RegistryManager().get_latest_executor_version(name=name).version)
    op = Operation(schema.Query)
    executor_version = schema.ObjectVersionInput(name=name, version=version)
    base_query = op.executor_version_from_name_version(executor_version=executor_version)
    base_query.desired_model_patterns()
    json_data = send_graphql_request(op)
    return json_data["executorVersionFromNameVersion"]["desiredModelPatterns"]
