def gridsearch(
        model_class,
        parameters: dict,
        series: TimeSeries,
        past_covariates: Optional[TimeSeries] = None,
        future_covariates: Optional[TimeSeries] = None,
        forecast_horizon: Optional[int] = None,
        stride: int = 1,
        start: Optional[Union[pd.Timestamp, float, int]] = None,
        start_format: Literal["position", "value"] = "value",
        last_points_only: bool = False,
        show_warnings: bool = True,
        val_series: Optional[TimeSeries] = None,
        use_fitted_values: bool = False,
        metric: Callable[[TimeSeries, TimeSeries], float] = metrics.mape,
        reduction: Callable[[np.ndarray], float] = np.mean,
        verbose=False,
        n_jobs: int = 1,
        n_random_samples: Optional[Union[int, float]] = None,
        data_transformers: Optional[
            dict[str, Union[BaseDataTransformer, Pipeline]]
        ] = None,
        fit_kwargs: Optional[dict[str, Any]] = None,
        predict_kwargs: Optional[dict[str, Any]] = None,
        sample_weight: Optional[Union[TimeSeries, str]] = None,
        random_state: Optional[int] = None,
    ) -> tuple["ForecastingModel", dict[str, Any], float]:
        """
        Find the best hyper-parameters among a given set using a grid search.

        This function has 3 modes of operation: Expanding window mode, split mode and fitted value mode.
        The three modes of operation evaluate every possible combination of hyper-parameter values
        provided in the `parameters` dictionary by instantiating the `model_class` subclass
        of ForecastingModel with each combination, and returning the best-performing model with regard
        to the `metric` function. The `metric` function is expected to return an error value,
        thus the model resulting in the smallest `metric` output will be chosen.

        The relationship of the training data and test data depends on the mode of operation.

        Expanding window mode (activated when `forecast_horizon` is passed):
        For every hyperparameter combination, the model is repeatedly trained and evaluated on different
        splits of `series`. This process is accomplished by using
        the :func:`backtest` function as a subroutine to produce historic forecasts starting from `start`
        that are compared against the ground truth values of `series`.
        Note that the model is retrained for every single prediction, thus this mode is slower.

        Split window mode (activated when `val_series` is passed):
        This mode will be used when the `val_series` argument is passed.
        For every hyper-parameter combination, the model is trained on `series` and
        evaluated on `val_series`.

        Fitted value mode (activated when `use_fitted_values` is set to `True`):
        For every hyper-parameter combination, the model is trained on `series`
        and evaluated on the resulting fitted values.
        Not all models have fitted values, and this method raises an error if the model doesn't have a `fitted_values`
        member. The fitted values are the result of the fit of the model on `series`. Comparing with the
        fitted values can be a quick way to assess the model, but one cannot see if the model is overfitting the series.

        Derived classes must ensure that a single instance of a model will not share parameters with the other
        instances, e.g., saving models in the same path. Otherwise, an unexpected behavior can arise while running
        several models in parallel (when ``n_jobs != 1``). If this cannot be avoided, then gridsearch
        should be redefined, forcing ``n_jobs = 1``.

        Currently this method only supports deterministic predictions (i.e. when models' predictions
        have only 1 sample).

        Parameters
        ----------
        model_class
            The ForecastingModel subclass to be tuned for 'series'.
        parameters
            A dictionary containing as keys hyperparameter names, and as values lists of values for the
            respective hyperparameter.
        series
            The target series used as input and target for training.
        past_covariates
            Optionally, a past-observed covariate series. This applies only if the model supports past covariates.
        future_covariates
            Optionally, a future-known covariate series. This applies only if the model supports future covariates.
        forecast_horizon
            The integer value of the forecasting horizon. Activates expanding window mode.
        stride
            Only used in expanding window mode. The number of time steps between two consecutive predictions.
        start
            Only used in expanding window mode. Optionally, the first point in time at which a prediction is computed.
            This parameter supports: ``float``, ``int``, ``pandas.Timestamp``, and ``None``.
            If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
            If an ``int``, it is either the index position of the first prediction point for `series` with a
            `pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
            the index position with `start_format="position"`.
            If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
            If ``None``, the first prediction point will automatically be set to:

            - the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
              predictable point is earlier than the first trainable point.
            - the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
              or `retrain` is a Callable and the first trainable point is earlier than the first predictable point.
            - the first trainable point (given `train_length`) otherwise

            Note: If `start` is not within the trainable / forecastable points, uses the closest valid start point that
              is a round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
            Note: If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is also
              shifted by `output_chunk_shift` points into the future.
            Note: If `start` is outside the possible historical forecasting times, will ignore the parameter
              (default behavior with ``None``) and start at the first trainable/predictable point.
        start_format
            Only used in expanding window mode. Defines the `start` format. Only effective when `start` is an integer
            and `series` is indexed with a `pd.RangeIndex`.
            If set to 'position', `start` corresponds to the index position of the first predicted point and can range
            from `(-len(series), len(series) - 1)`.
            If set to 'value', `start` corresponds to the index value/label of the first predicted point. Will raise
            an error if the value is not in `series`' index. Default: ``'value'``
        last_points_only
            Only used in expanding window mode. Whether to use the whole forecasts or only the last point of each
            forecast to compute the error.
        show_warnings
            Only used in expanding window mode. Whether to show warnings related to the `start` parameter.
        val_series
            The TimeSeries instance used for validation in split mode. If provided, this series must start right after
            the end of `series`; so that a proper comparison of the forecast can be made.
        use_fitted_values
            If `True`, uses the comparison with the fitted values.
            Raises an error if ``fitted_values`` is not an attribute of `model_class`.
        metric
            A metric function that returns the error between two `TimeSeries` as a float value . Must either be one of
            Darts' "aggregated over time" metrics (see `here
            <https://unit8co.github.io/darts/generated_api/darts.metrics.html>`_), or a custom metric that as input two
            `TimeSeries` and returns the error
        reduction
            A reduction function (mapping array to float) describing how to aggregate the errors obtained
            on the different validation series when backtesting. By default it'll compute the mean of errors.
        verbose
            Whether to print the progress.
        n_jobs
            The number of jobs to run in parallel. Parallel jobs are created only when there are two or more parameters
            combinations to evaluate. Each job will instantiate, train, and evaluate a different instance of the model.
            Defaults to `1` (sequential). Setting the parameter to `-1` means using all the available cores.
        n_random_samples
            The number/ratio of hyperparameter combinations to select from the full parameter grid. This will perform
            a random search instead of using the full grid.
            If an integer, `n_random_samples` is the number of parameter combinations selected from the full grid and
            must be between `0` and the total number of parameter combinations.
            If a float, `n_random_samples` is the ratio of parameter combinations selected from the full grid and must
            be between `0` and `1`. Defaults to `None`, for which random selection will be ignored.
        data_transformers
            Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
            (possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
            in the un-transformed space. For fittable transformer / pipeline:

            - if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
            - if `retrain=False`, the data transformer transforms the series once before all the forecasts.

            The fitted transformer is used to transform the input during both training and prediction.
            If the transformation is invertible, the forecasts will be inverse-transformed.
        fit_kwargs
            Additional arguments passed to the model `fit()` method.
        predict_kwargs
            Additional arguments passed to the model `predict()` method.
        sample_weight
            Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
            `retrain` is not ``False``. They are applied per observation, per label (each step in
            `output_chunk_length`), and per component.
            If a series, then those weights are used. If the weight series only have a single component / column, then
            the weights are applied globally to all components in `series`. Otherwise, for component-specific weights,
            the number of components must match those of `series`.
            If a string, then the weights are generated using built-in weighting functions. The available options are
            `"linear"` or `"exponential"` decay - the further in the past, the lower the weight.
        random_state
            Controls the randomness of probabilistic predictions.

        Returns
        -------
        ForecastingModel, Dict, float
            A tuple containing an untrained `model_class` instance created from the best-performing hyper-parameters,
            along with a dictionary containing these best hyper-parameters,
            and metric score for the best hyper-parameters.
        """
        raise_if_not(
            (forecast_horizon is not None)
            + (val_series is not None)
            + use_fitted_values
            == 1,
            "Please pass exactly one of the arguments 'forecast_horizon', "
            "'val_series' or 'use_fitted_values'.",
            logger,
        )

        if not isinstance(parameters, dict):
            raise_log(
                ValueError(
                    f"`parameters` should be a dictionary, received a: {type(parameters)}."
                )
            )

        if not all(
            isinstance(params, (list, np.ndarray)) for params in parameters.values()
        ):
            raise_log(
                ValueError(
                    "Every value in the `parameters` dictionary should be a list or a np.ndarray."
                ),
                logger,
            )

        if use_fitted_values:
            raise_if_not(
                hasattr(
                    model_class(**{k: v[0] for k, v in parameters.items()}),
                    "fitted_values",
                ),
                "The model must have a fitted_values attribute to compare with the train TimeSeries (local models)",
                logger,
            )

        elif val_series is not None:
            raise_if_not(
                series.width == val_series.width,
                "Training and validation series require the same number of components.",
                logger,
            )

        data_transformers = _convert_data_transformers(
            data_transformers=data_transformers, copy=True
        )

        if fit_kwargs is None:
            fit_kwargs = dict()
        if predict_kwargs is None:
            predict_kwargs = dict()

        # compute all hyperparameter combinations from selection
        params_cross_product = list(product(*parameters.values()))

        # If n_random_samples has been set, randomly select a subset of the full parameter cross product to search with
        if n_random_samples is not None:
            params_cross_product = model_class._sample_params(
                params_cross_product, n_random_samples
            )

        # iterate through all combinations of the provided parameters and choose the best one
        iterator = _build_tqdm_iterator(
            zip(params_cross_product),
            verbose,
            total=len(params_cross_product),
            desc="gridsearch",
        )

        def _evaluate_combination(param_combination) -> float:
            param_combination_dict = dict(
                list(zip(parameters.keys(), param_combination))
            )
            if param_combination_dict.get("model_name", None):
                current_time = time.strftime("%Y-%m-%d_%H.%M.%S.%f", time.localtime())
                param_combination_dict["model_name"] = (
                    f"{current_time}_{param_combination_dict['model_name']}"
                )

            model = model_class(**param_combination_dict)
            if use_fitted_values:  # fitted value mode
                if data_transformers:
                    series_, past_covariates_, future_covariates_ = (
                        _apply_data_transformers(
                            series=series,
                            past_covariates=past_covariates,
                            future_covariates=future_covariates,
                            data_transformers=data_transformers,
                            max_future_cov_lag=model.extreme_lags[5],
                            fit_transformers=True,
                        )
                    )
                else:
                    series_ = series
                    past_covariates_ = past_covariates
                    future_covariates_ = future_covariates

                model._fit_wrapper(
                    series=series_,
                    past_covariates=past_covariates_,
                    future_covariates=future_covariates_,
                    sample_weight=sample_weight,
                    **fit_kwargs,
                )
                fitted_values = TimeSeries.from_times_and_values(
                    series.time_index, model.fitted_values
                )
                if data_transformers and "series" in data_transformers:
                    fitted_values = _apply_inverse_data_transformers(
                        series=series_,
                        forecasts=fitted_values,
                        data_transformers=data_transformers,
                        series_idx=None,
                    )
                error = metric(series, fitted_values)
            elif val_series is None:  # expanding window mode
                error = model.backtest(
                    series=series,
                    past_covariates=past_covariates,
                    future_covariates=future_covariates,
                    num_samples=1,
                    start=start,
                    start_format=start_format,
                    forecast_horizon=forecast_horizon,
                    stride=stride,
                    metric=metric,
                    reduction=reduction,
                    last_points_only=last_points_only,
                    verbose=verbose,
                    show_warnings=show_warnings,
                    data_transformers=data_transformers,
                    fit_kwargs=fit_kwargs,
                    predict_kwargs=predict_kwargs,
                    sample_weight=sample_weight,
                    random_state=random_state,
                )
            else:  # split mode
                if data_transformers:
                    series_, past_covariates_, future_covariates_ = (
                        _apply_data_transformers(
                            series=series,
                            past_covariates=past_covariates,
                            future_covariates=future_covariates,
                            data_transformers=data_transformers,
                            max_future_cov_lag=model.extreme_lags[5],
                            fit_transformers=True,
                        )
                    )
                else:
                    series_ = series
                    past_covariates_ = past_covariates
                    future_covariates_ = future_covariates

                model._fit_wrapper(
                    series=series_,
                    past_covariates=past_covariates_,
                    future_covariates=future_covariates_,
                    sample_weight=sample_weight,
                    **fit_kwargs,
                )
                pred = model._predict_wrapper(
                    n=len(val_series),
                    series=series_,
                    past_covariates=past_covariates_,
                    future_covariates=future_covariates_,
                    num_samples=1,
                    verbose=verbose,
                    random_state=random_state,
                    **predict_kwargs,
                )
                pred = _apply_inverse_data_transformers(
                    series=series_,
                    forecasts=pred,
                    data_transformers=data_transformers,
                )
                error = metric(val_series, pred)

            return float(error)

        errors: list[float] = _parallel_apply(
            iterator, _evaluate_combination, n_jobs, {}, {}
        )

        min_error = min(errors)

        best_param_combination = dict(
            list(zip(parameters.keys(), params_cross_product[errors.index(min_error)]))
        )

        logger.info("Chosen parameters: " + str(best_param_combination))

        return model_class(**best_param_combination), best_param_combination, min_error

        Find the best hyper-parameters among a given set using a grid search.

        This function has 3 modes of operation: Expanding window mode, split mode and fitted value mode.
        The three modes of operation evaluate every possible combination of hyper-parameter values
        provided in the `parameters` dictionary by instantiating the `model_class` subclass
        of ForecastingModel with each combination, and returning the best-performing model with regard
        to the `metric` function. The `metric` function is expected to return an error value,
        thus the model resulting in the smallest `metric` output will be chosen.

        The relationship of the training data and test data depends on the mode of operation.

        Expanding window mode (activated when `forecast_horizon` is passed):
        For every hyperparameter combination, the model is repeatedly trained and evaluated on different
        splits of `series`. This process is accomplished by using
        the :func:`backtest` function as a subroutine to produce historic forecasts starting from `start`
        that are compared against the ground truth values of `series`.
        Note that the model is retrained for every single prediction, thus this mode is slower.

        Split window mode (activated when `val_series` is passed):
        This mode will be used when the `val_series` argument is passed.
        For every hyper-parameter combination, the model is trained on `series` and
        evaluated on `val_series`.

        Fitted value mode (activated when `use_fitted_values` is set to `True`):
        For every hyper-parameter combination, the model is trained on `series`
        and evaluated on the resulting fitted values.
        Not all models have fitted values, and this method raises an error if the model doesn't have a `fitted_values`
        member. The fitted values are the result of the fit of the model on `series`. Comparing with the
        fitted values can be a quick way to assess the model, but one cannot see if the model is overfitting the series.

        Derived classes must ensure that a single instance of a model will not share parameters with the other
        instances, e.g., saving models in the same path. Otherwise, an unexpected behavior can arise while running
        several models in parallel (when ``n_jobs != 1``). If this cannot be avoided, then gridsearch
        should be redefined, forcing ``n_jobs = 1``.

        Currently this method only supports deterministic predictions (i.e. when models' predictions
        have only 1 sample).

        Parameters
        ----------
        model_class
            The ForecastingModel subclass to be tuned for 'series'.
        parameters
            A dictionary containing as keys hyperparameter names, and as values lists of values for the
            respective hyperparameter.
        series
            The target series used as input and target for training.
        past_covariates
            Optionally, a past-observed covariate series. This applies only if the model supports past covariates.
        future_covariates
            Optionally, a future-known covariate series. This applies only if the model supports future covariates.
        forecast_horizon
            The integer value of the forecasting horizon. Activates expanding window mode.
        stride
            Only used in expanding window mode. The number of time steps between two consecutive predictions.
        start
            Only used in expanding window mode. Optionally, the first point in time at which a prediction is computed.
            This parameter supports: ``float``, ``int``, ``pandas.Timestamp``, and ``None``.
            If a ``float``, it is the proportion of the time series that should lie before the first prediction point.
            If an ``int``, it is either the index position of the first prediction point for `series` with a
            `pd.DatetimeIndex`, or the index value for `series` with a `pd.RangeIndex`. The latter can be changed to
            the index position with `start_format="position"`.
            If a ``pandas.Timestamp``, it is the time stamp of the first prediction point.
            If ``None``, the first prediction point will automatically be set to:

            - the first predictable point if `retrain` is ``False``, or `retrain` is a Callable and the first
              predictable point is earlier than the first trainable point.
            - the first trainable point if `retrain` is ``True`` or ``int`` (given `train_length`),
              or `retrain` is a Callable and the first trainable point is earlier than the first predictable point.
            - the first trainable point (given `train_length`) otherwise

            Note: If `start` is not within the trainable / forecastable points, uses the closest valid start point that
              is a round multiple of `stride` ahead of `start`. Raises a `ValueError`, if no valid start point exists.
            Note: If the model uses a shifted output (`output_chunk_shift > 0`), then the first predicted point is also
              shifted by `output_chunk_shift` points into the future.
            Note: If `start` is outside the possible historical forecasting times, will ignore the parameter
              (default behavior with ``None``) and start at the first trainable/predictable point.
        start_format
            Only used in expanding window mode. Defines the `start` format. Only effective when `start` is an integer
            and `series` is indexed with a `pd.RangeIndex`.
            If set to 'position', `start` corresponds to the index position of the first predicted point and can range
            from `(-len(series), len(series) - 1)`.
            If set to 'value', `start` corresponds to the index value/label of the first predicted point. Will raise
            an error if the value is not in `series`' index. Default: ``'value'``
        last_points_only
            Only used in expanding window mode. Whether to use the whole forecasts or only the last point of each
            forecast to compute the error.
        show_warnings
            Only used in expanding window mode. Whether to show warnings related to the `start` parameter.
        val_series
            The TimeSeries instance used for validation in split mode. If provided, this series must start right after
            the end of `series`; so that a proper comparison of the forecast can be made.
        use_fitted_values
            If `True`, uses the comparison with the fitted values.
            Raises an error if ``fitted_values`` is not an attribute of `model_class`.
        metric
            A metric function that returns the error between two `TimeSeries` as a float value . Must either be one of
            Darts' "aggregated over time" metrics (see `here
            <https://unit8co.github.io/darts/generated_api/darts.metrics.html>`_), or a custom metric that as input two
            `TimeSeries` and returns the error
        reduction
            A reduction function (mapping array to float) describing how to aggregate the errors obtained
            on the different validation series when backtesting. By default it'll compute the mean of errors.
        verbose
            Whether to print the progress.
        n_jobs
            The number of jobs to run in parallel. Parallel jobs are created only when there are two or more parameters
            combinations to evaluate. Each job will instantiate, train, and evaluate a different instance of the model.
            Defaults to `1` (sequential). Setting the parameter to `-1` means using all the available cores.
        n_random_samples
            The number/ratio of hyperparameter combinations to select from the full parameter grid. This will perform
            a random search instead of using the full grid.
            If an integer, `n_random_samples` is the number of parameter combinations selected from the full grid and
            must be between `0` and the total number of parameter combinations.
            If a float, `n_random_samples` is the ratio of parameter combinations selected from the full grid and must
            be between `0` and `1`. Defaults to `None`, for which random selection will be ignored.
        data_transformers
            Optionally, a dictionary of `BaseDataTransformer` or `Pipeline` to apply to the corresponding series
            (possibles keys; "series", "past_covariates", "future_covariates"). If provided, all input series must be
            in the un-transformed space. For fittable transformer / pipeline:

            - if `retrain=True`, the data transformer re-fit on the training data at each historical forecast step.
            - if `retrain=False`, the data transformer transforms the series once before all the forecasts.

            The fitted transformer is used to transform the input during both training and prediction.
            If the transformation is invertible, the forecasts will be inverse-transformed.
        fit_kwargs
            Additional arguments passed to the model `fit()` method.
        predict_kwargs
            Additional arguments passed to the model `predict()` method.
        sample_weight
            Optionally, some sample weights to apply to the target `series` labels for training. Only effective when
            `retrain` is not ``False``. They are applied per observation, per label (each step in
            `output_chunk_length`), and per component.
            If a series, then those weights are used. If the weight series only have a single component / column, then
            the weights are applied globally to all components in `series`. Otherwise, for component-specific weights,
            the number of components must match those of `series`.
            If a string, then the weights are generated using built-in weighting functions. The available options are
            `"linear"` or `"exponential"` decay - the further in the past, the lower the weight.
        random_state
            Controls the randomness of probabilistic predictions.

        Returns
        -------
        ForecastingModel, Dict, float
            A tuple containing an untrained `model_class` instance created from the best-performing hyper-parameters,
            along with a dictionary containing these best hyper-parameters,
            and metric score for the best hyper-parameters.
        
darts/models/forecasting/forecasting_model.py