import importlib
import json
import os
import posixpath
import subprocess
import sys
import tarfile
import threading
import warnings
from contextlib import _GeneratorContextManager
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Dict, List, Optional, Union

import cloudpickle
import httpx
import numpy
import pandas
import yaml
from scipy.sparse import csc_matrix, csr_matrix

import mlflow
from ML_management.loader.loader import CONFIG_KEY_ARTIFACTS
from ML_management.mlmanagement import variables
from ML_management.mlmanagement.base_exceptions import *  # noqa: F403
from ML_management.mlmanagement.base_exceptions import MLMClientError, MLMServerError, PylintError
from ML_management.mlmanagement.jsonschema_inference import infer_jsonschema
from ML_management.mlmanagement.mlmanager import set_experiment, start_run, start_run_if_not_exist
from ML_management.mlmanagement.model_type import ModelType
from ML_management.mlmanagement.module_finder import ModuleFinder
from ML_management.mlmanagement.server_mlmanager_exceptions import *  # noqa: F403
from ML_management.mlmanagement.server_mlmanager_exceptions import (
    AuthError,
    InvalidExperimentNameError,
    ModelTypeIsNotFoundError,
)
from ML_management.mlmanagement.session import AuthSession
from ML_management.mlmanagement.utils import INIT_FUNCTION_NAME, is_model_name_valid, validate_predict_config
from ML_management.mlmanagement.variables import (
    EXPERIMENT_NAME_FOR_DATASET_LOADER,
    EXPERIMENT_NAME_FOR_EXECUTOR,
    FILENAME_FOR_INFERENCE_CONFIG,
    active_run_stack,
    get_log_service_url,
    get_server_ml_api,
)
from ML_management.mlmanagement.visibility_options import VisibilityOptions
from ML_management.registry.exceptions import *  # noqa: F403
from mlflow.pyfunc import DATA


def _log_model(
    artifact_path,
    description: Optional[str],
    model_version_tags: Optional[Dict[str, str]] = None,
    code_path=None,
    conda_env=None,
    python_model=None,
    artifacts: Optional[dict] = None,
    registered_model_name: str = "default_name",
    signature: mlflow.models.signature.ModelSignature = None,
    input_example: Union[
        pandas.core.frame.DataFrame, numpy.ndarray, dict, list, csr_matrix, csc_matrix, str, bytes
    ] = None,
    await_registration_for: int = 300,
    pip_requirements=None,
    extra_pip_requirements=None,
    metadata=None,
    source_model_name=None,
    source_model_version=None,
    source_executor_name=None,
    source_executor_version=None,
    source_executor_role=None,
    upload_model_mode=None,
    visibility=None,
    extra_modules_names: Optional[list] = None,
    used_modules_names: Optional[list] = None,
    root_module_name: str = "__main__",
    linter_check: bool = True,
    start_build: bool = True,
    create_venv_pack: bool = False,
):
    """
    Log a Pyfunc model with custom inference logic and optional data dependencies as an MLflow artifact.

    Current run is using.
    Parameter registered_model_name must be not empty string,
        consist of alphanumeric characters, '_'
        and must start and end with an alphanumeric character.
        Validation regexp: "(([A-Za-z0-9][A-Za-z0-9_]*)?[A-Za-z0-9])+"
    You cannot specify the parameters: loader_module, data_path and the parameters: python_model, artifacts together.
    """
    from ML_management.executor.upload_model_mode import UploadModelMode  # circular import

    if extra_modules_names and used_modules_names:
        raise RuntimeError("Parameters 'extra_modules_names' and 'used_modules_names' cannot be used at the same time.")
    if upload_model_mode == UploadModelMode.none:
        raise RuntimeError("You can't log a model using the 'upload_model_mode' parameter set to none.")
    if not is_model_name_valid(registered_model_name):
        raise RuntimeError(
            "Parameter 'registered_model_name' must be not empty string, "
            "consist of alphanumeric characters, '_' "
            "and must start and end with an alphanumeric character."
            "Validation regexp: '(([A-Za-z0-9][A-Za-z0-9_]*)?[A-Za-z0-9])+'"
        )
    del UploadModelMode  # need to delete this because it is not JSON serializable
    if create_venv_pack:
        if not isinstance(artifacts, dict):
            raise RuntimeError("You must provide config for model serving in artifacts, if create_venv_pack=True")
        validate_predict_config(path=artifacts.get(FILENAME_FOR_INFERENCE_CONFIG))
    start_run_if_not_exist()
    if used_modules_names is not None:
        submodules = set(used_modules_names)
        ModuleFinder.import_modules_by_name(submodules)
    else:
        submodules = ModuleFinder().find_root_submodules(root_name=root_module_name)
        if linter_check:
            for module_name in submodules:
                if hasattr(sys.modules[module_name], "__file__"):
                    # pylint check what would fall on any error except when abstract method is not implemented (E0110)
                    # and when pylint has been unable to import a module (E0401)
                    linter_output = subprocess.run(
                        [
                            sys.executable,
                            "-m",
                            "pylint",
                            "{}".format(sys.modules[module_name].__file__),
                            "--clear-cache-post-run=True",
                            "--disable=all",
                            "--enable=E",
                            "--disable=E0401, E1101",
                            "--fail-on=E",
                            "--reports=n",
                            "--score=n",
                            "-j 0",
                        ],
                        check=False,
                        stdout=subprocess.PIPE,
                        text=True,
                    )
                    if linter_output.stdout:  # if linter has output when something wrong with code
                        raise PylintError(linter_output.stdout)
                    del linter_output
        if extra_modules_names:
            extra_set = set(extra_modules_names)
            ModuleFinder.import_modules_by_name(extra_set)
            submodules = submodules.union(extra_set)
    try:
        for module_name in submodules:
            cloudpickle.register_pickle_by_value(sys.modules[module_name])
        kwargs = {
            "artifact_path": artifact_path,
            "description": description,
            "model_version_tags": model_version_tags,
            "code_path": code_path,
            "conda_env": conda_env,
            "python_model": python_model,
            "registered_model_name": registered_model_name,
            "signature": signature,
            "input_example": input_example,
            "await_registration_for": await_registration_for,
            "pip_requirements": pip_requirements,
            "extra_pip_requirements": extra_pip_requirements,
            "metadata": metadata,
            "source_model_name": source_model_name,
            "source_model_version": source_model_version,
            "source_executor_name": source_executor_name,
            "source_executor_version": source_executor_version,
            "source_executor_role": source_executor_role,
            "upload_model_mode": upload_model_mode,
            "visibility": visibility,
            "start_build": start_build,
            "create_venv_pack": create_venv_pack,
        }
        response = _request_log_model("log_model", kwargs)
        return _raise_error(response)
    finally:
        for module_name in submodules:
            cloudpickle.unregister_pickle_by_value(sys.modules[module_name])


def _log_object_src(
    artifact_path,
    model_path: str,
    description: str,
    model_type: ModelType = ModelType.MODEL,
    model_version_tags: Optional[Dict[str, str]] = None,
    code_path=None,
    conda_env=None,
    python_model=None,
    registered_model_name: str = "default_name",
    signature: mlflow.models.signature.ModelSignature = None,
    input_example: Union[
        pandas.core.frame.DataFrame, numpy.ndarray, dict, list, csr_matrix, csc_matrix, str, bytes
    ] = None,
    await_registration_for: int = 300,
    pip_requirements=None,
    extra_pip_requirements=None,
    metadata=None,
    source_model_name=None,
    source_model_version=None,
    source_executor_name=None,
    source_executor_version=None,
    source_executor_role=None,
    upload_model_mode=None,
    visibility: VisibilityOptions = VisibilityOptions.PRIVATE,
    start_build: bool = True,
    create_venv_pack: bool = False,
    additional_local_packages: Optional[Union[List[str], str]] = None,
):
    """
    Log a Pyfunc model with custom inference logic and optional data dependencies as an MLflow artifact.

    Current run is using.
    Parameter registered_model_name must be not empty string,
            consist of alphanumeric characters, '_'
            and must start and end with an alphanumeric character.
            Validation regexp: "(([A-Za-z0-9][A-Za-z0-9_]*)?[A-Za-z0-9])+"
    You cannot specify the parameters: loader_module, data_path and the parameters: python_model, artifacts together.
    """
    from ML_management.executor.upload_model_mode import UploadModelMode  # circular import

    if not os.path.exists(model_path):
        raise FileNotFoundError(f"Path: {model_path} does not exist.")

    if not os.path.exists(os.path.join(model_path, "conda.yaml")):
        raise FileNotFoundError("There is no conda.yaml file.")

    try:
        with open(os.path.join(model_path, "conda.yaml")) as conda_file:
            dependencies = yaml.safe_load(conda_file)["dependencies"]  # noqa: F841
    except Exception:
        raise RuntimeError("conda.yaml not valid.") from None

    if upload_model_mode == UploadModelMode.none:
        raise RuntimeError("You can't log a model using the 'upload_model_mode' parameter set to none.")
    if not is_model_name_valid(registered_model_name):
        raise RuntimeError(
            "Parameter 'registered_model_name' must be not empty string, "
            "consist of alphanumeric characters, '_' "
            "and must start and end with an alphanumeric character."
            "Validation regexp: '(([A-Za-z0-9][A-Za-z0-9_]*)?[A-Za-z0-9])+'"
        )
    del UploadModelMode  # need to delete this because it is not JSON serializable
    if create_venv_pack:
        validate_predict_config(path=os.path.join(f"{model_path}", "artifacts", f"{FILENAME_FOR_INFERENCE_CONFIG}"))
    start_run_if_not_exist()
    python_path = sys.path
    try:
        model_path = os.path.abspath(model_path)
        parts = Path(model_path).parts
        sys.path.append(str(Path(*parts[:-2])))
        get_object_func = getattr(importlib.import_module(".".join(parts[-2:])), INIT_FUNCTION_NAME)
        python_model = get_object_func()  # noqa: F841
        kwargs = {
            "artifact_path": artifact_path,
            "model_path": model_path,
            "description": description,
            "model_version_tags": model_version_tags,
            "code_path": code_path,
            "conda_env": conda_env,
            "python_model": python_model,
            "registered_model_name": registered_model_name,
            "signature": signature,
            "input_example": input_example,
            "await_registration_for": await_registration_for,
            "pip_requirements": pip_requirements,
            "extra_pip_requirements": extra_pip_requirements,
            "metadata": metadata,
            "source_model_name": source_model_name,
            "source_model_version": source_model_version,
            "source_executor_name": source_executor_name,
            "source_executor_version": source_executor_version,
            "source_executor_role": source_executor_role,
            "upload_model_mode": upload_model_mode,
            "visibility": visibility,
            "start_build": start_build,
            "create_venv_pack": create_venv_pack,
            "additional_local_packages": additional_local_packages,
            "get_object_func": get_object_func,
        }
        response = _request_log_model("log_object_src", kwargs)
        result = _raise_error(response)
    except Exception as err:
        raise err
    finally:
        sys.path = python_path

    if additional_local_packages:
        if isinstance(additional_local_packages, list):
            for package in additional_local_packages:
                response = _request_log_artifacts(package, DATA, model_type)
                _raise_error(response)
        else:
            response = _request_log_artifacts(additional_local_packages, DATA, model_type)
            _raise_error(response)
    return result


def log_executor_src(
    model_path: str,
    registered_name: str,
    description: str,
    start_build: bool = False,
    visibility: VisibilityOptions = VisibilityOptions.PRIVATE,
    additional_local_packages: Optional[Union[List[str], str]] = None,
):
    """
    Upload executor using folder with source code of the executor.

    model_path folder must contain "__init__.py" file with get_object function
    which have to return instance of the executor.
    Also model_path must contain "conda.yaml" file with dependencies of the executor
    model_path can optionally contain "artifacts" folder with files/folders,
    which would be automatically uploaded with executor
    and path to them can be accessed with self.artifacts within executor class.

    Parameters
    ----------
    model_path: str
        Path to folder with executor.
    registered_name: str
        Name of the executor.
    description: str
        Description of the executor
    start_build: bool
        Is to start build image of the executor right after it was logged. Defaults to False.
    visibility: VisibilityOptions
        Visibility of this executor to other users. Possible values VisibilityOptions.PRIVATE, PUBLIC.
        Defaults to PRIVATE.
    additional_local_packages: Optional[Union[List[str], str]] = None
        Path or list of paths to folder with local dependencies which are not within model_path folder.
        Defaults to None.

    Returns
    -------
    None

    """
    old_experiment_name = variables.active_experiment

    try:
        set_experiment(EXPERIMENT_NAME_FOR_EXECUTOR, visibility=VisibilityOptions.PUBLIC)

        with start_run(nested=True):
            _log_object_src(
                artifact_path="",
                description=description,
                registered_model_name=registered_name,
                start_build=start_build,
                model_path=model_path,
                visibility=visibility,
                additional_local_packages=additional_local_packages,
                model_type=ModelType.EXECUTOR,
            )

    except Exception as err:
        raise err
    finally:
        variables.active_experiment = old_experiment_name


def log_dataset_loader_src(
    model_path: str,
    registered_name: str,
    description: str,
    visibility: VisibilityOptions = VisibilityOptions.PRIVATE,
    additional_local_packages: Optional[Union[List[str], str]] = None,
):
    """
    Upload dataset loader using folder with source code of the dataset loader.

    model_path folder must contain "__init__.py" file with get_object function
    which have to return instance of the dataset loader.
    Also model_path must contain "conda.yaml" file with dependencies of the dataset loader
    model_path can optionally contain "artifacts" folder with files/folders,
    which would be automatically uploaded with dataset loader
    and path to them can be accessed with self.artifacts within dataset loader class.

    Parameters
    ----------
    model_path: str
        Path to folder with dataset loader.
    registered_name: str
        Name of the dataset loader.
    description: str
        Description of the dataset loader
    visibility: VisibilityOptions
        Visibility of this dataset loader to other users. Possible values VisibilityOptions.PRIVATE, PUBLIC.
        Defaults to PRIVATE.
    additional_local_packages: Optional[Union[List[str], str]] = None
        Path or list of paths to folder with local dependencies which are not within model_path folder.
        Defaults to None.

    Returns
    -------
    None

    """
    old_experiment_name = variables.active_experiment
    try:
        set_experiment(EXPERIMENT_NAME_FOR_DATASET_LOADER, visibility=VisibilityOptions.PUBLIC)
        with start_run(nested=True):
            _log_object_src(
                artifact_path="",
                description=description,
                registered_model_name=registered_name,
                model_path=model_path,
                visibility=visibility,
                additional_local_packages=additional_local_packages,
                model_type=ModelType.DATASET_LOADER,
            )
    except Exception as err:
        raise err
    finally:
        variables.active_experiment = old_experiment_name


def log_model_src(
    model_path: str,
    registered_name: str,
    description: str,
    start_build: bool = True,
    model_version_tags: Optional[Dict[str, str]] = None,
    experiment_name: Optional[str] = None,
    create_venv_pack: bool = False,
    visibility: VisibilityOptions = VisibilityOptions.PRIVATE,
    additional_local_packages: Optional[Union[List[str], str]] = None,
) -> None:
    """
    Log model to remote server.

    Parameters
    ----------
    model_path: str
        Path to the folder with the model files(*model*.py, conda.yaml, __init__.py with get object function, see docs)
    registered_name: str
        The name of the model that will be assigned to the model in the model registry.
        Parameter registered_name should match regexp ``"(([A-Za-z0-9][A-Za-z0-9_]*)?[A-Za-z0-9])+"``.
    description: str
        Desciption of the model.
    visibility: VisibilityOptions
        Visibility of this model to other users. Possible values VisibilityOptions.PRIVATE, PUBLIC.
        Defaults to PRIVATE.
    start_build: bool = True
        Whether to start building the image with the model requirements immediately after uploading.
        This can speed up the subsequent build of the task image. Default: True.
    model_version_tags: Optional[Dict[str, str]] = None
        Define model version tags. Default: None.
    create_venv_pack: bool = False
        Whether to prepare venv pack for future inference. Default: False.
    additional_local_packages: Optional[Union[List[str], str]] = None
        Path or list of paths to folder with local dependencies which are not within model_path folder.
        Defaults to None.

    Returns
    =======
    None
    """
    old_experiment_name = variables.active_experiment
    try:
        if experiment_name:
            set_experiment(experiment_name, visibility=VisibilityOptions.PUBLIC)
        with start_run(nested=True):
            _log_object_src(
                artifact_path="",
                description=description,
                registered_model_name=registered_name,
                model_path=model_path,
                start_build=start_build,
                model_version_tags=model_version_tags,
                create_venv_pack=create_venv_pack,
                visibility=visibility,
                additional_local_packages=additional_local_packages,
                model_type=ModelType.MODEL,
            )
    except Exception as err:
        raise err
    finally:
        variables.active_experiment = old_experiment_name


def log_artifact(local_path: str, artifact_path: Optional[str] = None) -> None:
    """
    Log a local file or directory as an artifact of the currently active run.

    If no run is active, this method will create a new active run.

    Parameters
    ==========
    local_path: str
        Path to the file to write.
    artifact_path: Optional[str] = None
        If provided, the directory to write to.

    Returns
    =======
    None
    """
    response = _request_log_artifacts(local_path, artifact_path)
    return _raise_error(response)


def _log_param(key: str, value: str) -> None:
    """
    Log str parameter (e.g. model hyperparameter) under the current job.

    Parameters
    ==========
    key: str
        Param name (string).
        This string may only contain alphanumerics, underscores (_), dashes (-),
        periods (.), spaces ( ), and slashes (/).
        All backend stores will support keys up to length 250, but some may support larger keys.

    value: str
        Param value (string).

    Returns
    =======
    None
    """
    if not isinstance(value, str):
        warnings.warn(f"The log_param function can log only string values. Value {value} is not log.")
        return
    return _request_log_params({key: value})


def _log_params(params: Dict[str, str]) -> None:
    """
    Log a batch of params for the current job.

    Parameters
    ==========
    params: dict
        Key parameter value pairs.
        Кey is the name of the parameter (string).
        This string may only contain alphanumerics, underscores (_), dashes (-),
        periods (.), spaces ( ), and slashes (/).
        All backend stores will support keys up to length 250, but some may support larger keys.
        Param value (string).

    Returns
    =======
    None
    """
    for key, value in params:
        if not isinstance(value, str):
            warnings.warn(f"The log_params function can log only string values. Value {value} is not log.")
        params.pop(key)
    if not params:
        return
    return _request_log_params(params)


def log_metric(key: str, value: float, step: int = 0) -> None:
    """
    Log a metric under the current job.

    Parameters
    ==========
    key: str
        Metric name (string).
        This string may only contain alphanumerics, underscores (_), dashes (-),
        periods (.), spaces ( ), and slashes (/).
        All backend stores will support keys up to length 250, but some may support larger keys.

    value: float
        Metric value (float). Note that some special values such as +/-
        Infinity may be replaced by other values depending on the store.
        For example, the SQLAlchemy store replaces +/- Infinity with max / min float values.
        All backend stores will support values up to length 5000, but some may support larger values.

    step: int
        Metric step (int). Defaults to zero if unspecified.

    Returns
    =======
    None
    """
    if not (isinstance(value, float) or isinstance(value, int)):
        warnings.warn(f"The log_metric function can log only float or integer values. Value {value} is not log.")
        return
    return _request_log_metric({key: value}, step)


def log_metrics(metrics: Dict[str, float], step: int = 0) -> None:
    """
    Log a metric under the current job.

    Parameters
    ==========
    metrics: dict[str, float]
        Key(string): metric name.
        This string may only contain alphanumerics, underscores (_), dashes (-),
        periods (.), spaces ( ), and slashes (/).
        All backend stores will support keys up to length 250, but some may support larger keys.
        Value(float): metric value. Note that some special values such as +/-
        Infinity may be replaced by other values depending on the store.
        For example, the SQLAlchemy store replaces +/- Infinity with max / min float values.
        All backend stores will support values up to length 5000, but some may support larger values.

    step: int
        Metric step (int). Defaults to zero if unspecified.

    Returns
    =======
    None
    """
    metrics_to_log = {}
    for key, value in metrics.items():
        if not (isinstance(value, float) or isinstance(value, int)):
            warnings.warn(f"The log_metrics function can log only float or integer values. {key}: {value} is not log.")
            continue
        if not isinstance(key, str):
            warnings.warn(
                f"The log_metric function can log only str name metric and float or integer values. "
                f"{key}: {value} is not log."
            )
            continue
        metrics_to_log[key] = value
    return _request_log_metric(metrics_to_log, step)


def _request_log_model(function_name: str, kwargs: dict):
    """
    Send request for log_model function.

    Steps for log model:
    0) Infer jsonschema, raise if it is invalid
    1) open temporary directory
    2) Do mlflow.save_model() locally
    3) Pack it to tar file
    4) Send it to server to log model there.
    """
    delete_args_for_save_model_func = [
        "description",
        "model_version_tags",
        "artifact_path",
        "registered_model_name",
        "await_registration_for",
        # now, extra arguments
        "upload_model_mode",
        "source_model_name",
        "source_model_version",
        "visibility",
        "source_executor_name",
        "source_executor_version",
        "source_executor_role",
        "start_build",
        "create_venv_pack",
    ]  # not need for save_model

    extra_imports_args = [
        "submodules",
        "module_name",
        "used_modules_names",
        "extra_modules_names",
        "root_module_name",
        "linter_check",
    ]

    delete_args_for_log_func = [
        "python_model",
        "artifacts",
        "conda_env",
        "pip_requirements",
        "extra_pip_requirements",
        "additional_local_packages",
        "conda_file",
        "dependencies",
        "get_object_func",
    ]  # not need for log model on server

    for delete_arg in extra_imports_args:
        kwargs.pop(delete_arg, None)
    kwargs_for_save_model = kwargs.copy()
    for delete_arg in delete_args_for_save_model_func:
        kwargs_for_save_model.pop(delete_arg, None)
    python_model = kwargs_for_save_model["python_model"]
    get_object_func = kwargs_for_save_model.get("get_object_func")
    # import some modules here because of circular import
    from ML_management.dataset_loader.dataset_loader_pattern import DatasetLoaderPattern
    from ML_management.dataset_loader.dataset_loader_pattern_to_methods_map import (
        dataset_loader_pattern_to_methods,
    )
    from ML_management.executor.base_executor import BaseExecutor
    from ML_management.executor.executor_pattern_to_methods_map import executor_pattern_to_methods
    from ML_management.model.model_type_to_methods_map import model_pattern_to_methods
    from ML_management.model.patterns.model_pattern import Model

    experiment_name = variables.active_experiment.get("experiment_name")

    if python_model is not None:
        if isinstance(python_model, Model):
            kwargs["model_type"] = ModelType.MODEL
            model_to_methods = model_pattern_to_methods
            if experiment_name in [
                EXPERIMENT_NAME_FOR_EXECUTOR,
                EXPERIMENT_NAME_FOR_DATASET_LOADER,
            ]:
                raise InvalidExperimentNameError(ModelType.MODEL.value, experiment_name)
        elif isinstance(python_model, BaseExecutor):
            kwargs["model_type"] = ModelType.EXECUTOR
            model_to_methods = executor_pattern_to_methods

            if experiment_name != EXPERIMENT_NAME_FOR_EXECUTOR:
                raise InvalidExperimentNameError(ModelType.EXECUTOR.value, experiment_name)
            # collect all needed model's methods
            kwargs["desired_model_methods"] = python_model.desired_model_methods
            kwargs["upload_model_modes"] = python_model.upload_model_modes
            kwargs["desired_dataset_loader_methods"] = python_model.desired_dataset_loader_methods
        elif isinstance(python_model, DatasetLoaderPattern):
            kwargs["model_type"] = ModelType.DATASET_LOADER
            model_to_methods = dataset_loader_pattern_to_methods
            if experiment_name != EXPERIMENT_NAME_FOR_DATASET_LOADER:
                raise InvalidExperimentNameError(kwargs["model_type"].value, experiment_name)
        else:
            raise ModelTypeIsNotFoundError()

        # now we need to infer schemas for methods.
        methods_schema = {}
        for model_type, methods_name_to_schema_map in model_to_methods.items():
            if isinstance(python_model, model_type):
                for method_name_to_schema in methods_name_to_schema_map:
                    model_method = getattr(python_model, method_name_to_schema.value, None)
                    model_method_schema = infer_jsonschema(model_method, get_object_func)
                    methods_schema[method_name_to_schema.value] = model_method_schema

        kwargs["model_method_schemas"] = methods_schema

        for delete_arg in delete_args_for_log_func:
            kwargs.pop(delete_arg, None)

        log_request = {
            "function_name": function_name,
            "active_experiment": variables.active_experiment,
            "active_run_ids": [run.info.run_id for run in active_run_stack],
        }

        if function_name == "log_model":
            kwargs["loader_module"] = mlflow.pyfunc.model.__name__
            with TemporaryDirectory() as temp_dir:
                model_folder = "model"
                path_for_model_folder = os.path.join(temp_dir, model_folder)
                mlflow.pyfunc.save_model(path=path_for_model_folder, **kwargs_for_save_model)
                model_folder = path_for_model_folder
                log_request["kwargs"] = kwargs
                return _open_pipe_send_request(model_folder, log_request, url=get_log_service_url("log_model"))
        else:
            artifacts_path = os.path.join(kwargs["model_path"], CONFIG_KEY_ARTIFACTS)
            if os.path.isfile(artifacts_path):
                raise Exception(f"The artifact file {artifacts_path} is invalid. The artifact must be a directory.")

            model_folder = kwargs["model_path"]

            del kwargs["model_path"]
            kwargs["loader_module"] = "ML_management.loader.loader"
            log_request["kwargs"] = kwargs
            return _open_pipe_send_request(model_folder, log_request, url=get_log_service_url("log_model"))

    else:
        raise Exception("python_model parameter must be specified")


def _request_log_artifacts(local_path, artifact_path, model_type: Optional[ModelType] = None):
    """Send request for log artifact."""
    log_artifact_request = {
        "kwargs": {"local_path": local_path, "artifact_path": artifact_path},
        "active_run_ids": [run.info.run_id for run in active_run_stack],
        "model_type": model_type,
    }
    if not os.path.exists(local_path):
        raise FileNotFoundError(f"Path: {local_path} does not exist.")
    if not os.path.isdir(local_path):
        basename = os.path.basename(os.path.normpath(local_path))
        with open(local_path, "rb") as file:
            url = get_log_service_url("log_artifact")

            # upload multipart
            data = {"mlflow_request": json.dumps(log_artifact_request)}
            headers = {"Transfer-Encoding": "chunked"}

            file_content_type = "application/octet-stream"

            files = {"file": (basename, file, file_content_type)}

            with AuthSession().post(
                url=url,
                stream=False,
                data=data,
                files=files,
                headers=headers,
            ) as response:
                return response

    return _open_pipe_send_request(local_path, log_artifact_request, url=get_log_service_url("log_artifact"))


def _raise_error(response: httpx.Response):
    if response.status_code == 500:
        raise MLMServerError("Internal server error.")
    if response.status_code != 200:
        detail = response.read().decode()
        if not detail:
            raise MLMServerError("Internal server error.")
        try:
            detail = json.loads(detail).get("detail")
        except Exception:
            raise MLMServerError(f"Server error '{detail}' with code {response.status_code}") from None
        if not (
            isinstance(detail, dict) and "exception_class" in detail and ("params" in detail or "message" in detail)
        ):
            raise MLMServerError(detail)
        if "params" in detail:
            error = getattr(sys.modules[__name__], detail["exception_class"])(**detail["params"])
        else:
            error = getattr(sys.modules[__name__], detail["exception_class"])(detail["message"])
        if isinstance(error, AuthError):
            error.args = (
                f"{error.args[0]}. "
                "Possible reason: you are trying to upload a version of an object owned by another user.",
            )
        raise error


def _open_pipe_send_request(folder, request_dict, url):
    r, w = os.pipe()

    try:
        thread = threading.Thread(target=_tar_folder, args=(w, folder))
        thread.start()
    except Exception as err:
        os.close(r)
        os.close(w)
        raise err

    with open(r, "rb") as buff:
        with _request(url, request_dict, buff, os.path.basename(folder), True) as response:
            return response


def _request(
    url,
    request_dict,
    file=None,
    basename=None,
    is_tar=False,
    stream=False,
) -> _GeneratorContextManager:
    """Create mlflow_request and send it to server."""
    if not file:
        return AuthSession().post(url=url, stream=stream, json=request_dict)

    # upload multipart
    data = {"mlflow_request": json.dumps(request_dict)}
    headers = {"Transfer-Encoding": "chunked"}

    file_content_type = "application/octet-stream"
    if is_tar:
        file_content_type = "application/x-tar"

    files = {"file": (basename, file, file_content_type)}

    return AuthSession().post(
        url=url,
        stream=stream,
        data=data,
        files=files,
        headers=headers,
    )


def _tar_folder(w, model_folder):
    try:
        with open(w, "wb") as buff:
            with tarfile.open(mode="w|", fileobj=buff) as tar:
                tar.add(model_folder, arcname=os.path.basename(model_folder))
    except Exception as err:
        raise MLMClientError("Some error during tar the content.") from err


def _request_log_metric(metrics: Dict[str, float], step: Optional[int] = None):
    if not active_run_stack:
        raise MLMClientError("The log_metric function must be called from the active job.")
    run_id = active_run_stack[0].info.run_id

    request_dict = {"metrics": metrics, "step": step, "run_id": run_id}

    url = posixpath.join(get_server_ml_api(), "log-metric")

    _request_with_warning(url, request_dict)


def _request_log_params(params: Dict[str, str]):
    if not active_run_stack:
        raise MLMClientError("The log_params function must be called from the active job.")
    run_id = active_run_stack[0].info.run_id

    request_dict = {"params": params, "run_id": run_id}

    url = posixpath.join(get_server_ml_api(), "log-params")

    _request_with_warning(url, request_dict)


def _request_with_warning(url, request_dict):
    with AuthSession().post(url=url, json=request_dict) as response:
        response_content = response.content
        if response.status_code != 200:
            warnings.warn(f"Server error: {response_content.decode()}, status: {response.status_code}")
            return
