# Copyright 2015-2026 Earth Sciences Department, BSC-CNS
#
# This file is part of Autosubmit.
#
# Autosubmit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Autosubmit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Autosubmit.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import copy
import datetime
import json
import locale
import os
import platform
import pwd
import re
import shutil
import signal
import subprocess
import sys
import tarfile
import time
import warnings
from collections import defaultdict
from configparser import ConfigParser
from contextlib import suppress
from importlib.metadata import version
from importlib.resources import files as read_files
from pathlib import Path
from time import sleep
from typing import cast, Generator, Optional, Union

from bscearth.utils.date import date2str
from portalocker import Lock
from portalocker.exceptions import BaseLockException
from pyparsing import nestedExpr
from ruamel.yaml import YAML

import autosubmit.helpers.autosubmit_helper as AutosubmitHelper
import autosubmit.statistics.utils as StatisticsUtils
from autosubmit.config.basicconfig import BasicConfig
from autosubmit.config.configcommon import AutosubmitConfig
from autosubmit.config.yamlparser import YAMLParserFactory
from autosubmit.database.db_common import (
    create_db, get_experiment_description, get_autosubmit_version, check_experiment_exists,
    update_experiment_description_version
)
from autosubmit.database.db_structure import get_structure
from autosubmit.experiment.detail_updater import ExperimentDetails
from autosubmit.experiment.experiment_common import (
    check_ownership, copy_experiment, create_required_folders, delete_experiment, new_experiment
)
from autosubmit.git.autosubmit_git import AutosubmitGit
from autosubmit.git.autosubmit_git import check_unpushed_changes, clean_git
from autosubmit.helpers.utils import check_jobs_file_exists, get_rc_path, user_yes_no_query
from autosubmit.history.experiment_history import ExperimentHistory
from autosubmit.history.experiment_status import ExperimentStatus
from autosubmit.job.job import Job
from autosubmit.job.job_common import Status
from autosubmit.job.job_grouping import JobGrouping
from autosubmit.job.job_list import JobList
from autosubmit.job.job_list_persistence import JobListPersistence, JobListPersistenceDb, JobListPersistencePkl
from autosubmit.job.job_package_persistence import JobPackagePersistence
from autosubmit.job.job_packager import JobPackager
from autosubmit.job.job_utils import SubJob, SubJobManager
from autosubmit.log.log import Log, AutosubmitError, AutosubmitCritical
from autosubmit.notifications.mail_notifier import MailNotifier
from autosubmit.notifications.notifier import Notifier
from autosubmit.platforms.paramiko_platform import ParamikoPlatform
from autosubmit.platforms.paramiko_submitter import ParamikoSubmitter
from autosubmit.platforms.platform import Platform
from autosubmit.utils import as_conf_default_values

dialog = None

"""Main module for autosubmit. Only contains an interface class to all functionality implemented on autosubmit."""

sys.path.insert(0, os.path.abspath('.'))


def signal_handler(signal_received, frame):  # noqa: F841
    # Disable all the no-member violations in this function
    # pylint: disable=W0613
    """
    Used to handle interrupt signals, allowing autosubmit to clean before exit

    :param signal_received:
    :param frame:
    :return:
    """
    Log.info('Autosubmit will interrupt at the next safe occasion')
    Autosubmit.exit = True


def signal_handler_create(signal_received, frame):  # noqa: F841
    # Disable all the no-member violations in this function
    # pylint: disable=W0613
    """
    Used to handle KeyboardInterrupt signals while the create method is being executed

    :param signal_received:
    :param frame:
    :return:
    """
    raise AutosubmitCritical(
        'Autosubmit has been closed in an unexpected way. Killed or control + c.', 7010)


class MyParser(argparse.ArgumentParser):
    def error(self, message):
        sys.stderr.write(f'error: {message}\n')
        self.print_help()
        sys.exit(2)


class CancelAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):  # noqa: F841
        setattr(namespace, self.dest, True)
        if namespace.filter_status.upper() == "SUBMITTED, QUEUING, RUNNING " or namespace.target.upper() == "FAILED":
            pass
        else:
            parser.error("-fs and -t can only be used when --cancel is provided")


class Autosubmit:
    """
    Interface class for autosubmit.
    """

    @property
    def experiment_data(self):
        """Get the current voltage. """
        return self.experiment_data

    # Get the version number from the relevant file. If not, from autosubmit package
    script_dir = os.path.abspath(os.path.dirname(__file__))

    if not os.path.exists(os.path.join(script_dir, 'VERSION')):
        script_dir = os.path.join(script_dir, os.path.pardir)

    version_path = os.path.join(script_dir, 'VERSION')
    readme_path = os.path.join(script_dir, 'README.md')
    changes_path = os.path.join(script_dir, 'CHANGELOG.md')
    if os.path.isfile(version_path):
        with open(version_path) as f:
            autosubmit_version = f.read().strip()
    else:
        autosubmit_version = version("autosubmit")  # from importlib.metadata

    exit = False

    @staticmethod
    def environ_init():
        """Initialise AS environment. """
        # Python output buffering delays the appearance of stdout and stderr
        # when output is not directed to a terminal
        os.environ['PYTHONUNBUFFERED'] = 'true'

    @staticmethod
    def parse_args() -> tuple[int, Optional[argparse.Namespace]]:
        """Parse arguments given to an executable and start execution of command given.

        Returns a tuple with the exit code (``status``), and an optional list of
        arguments for ``argparse``. The list of arguments is only ever returned
        when the arguments are valid for the execution of a subcommand. Otherwise,
        they will be ``None``.
        """
        Autosubmit.environ_init()
        try:
            BasicConfig.read()
            parser = MyParser(
                description='Main executable for autosubmit. ')
            parser.add_argument('-v', '--version', dest='version', action='store_true')

            log_levels = ('NO_LOG', 'INFO', 'WARNING', 'DEBUG', 'ERROR')
            parser.add_argument('-lf', '--logfile', choices=log_levels, default='DEBUG', type=str,
                                help="sets file's log level.")
            parser.add_argument('-lc', '--logconsole', choices=log_levels, default='WARNING', type=str,
                                help="sets console's log level")

            subparsers = parser.add_subparsers(dest='command')
            # Run
            subparser = subparsers.add_parser(
                'run', description="runs specified experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument('-st', '--start_time', required=False,
                                   help='Sets the starting time for this experiment')
            subparser.add_argument('-sa', '--start_after', required=False,
                                   help='Sets a experiment expid which completion will trigger the start of this experiment.')
            subparser.add_argument('-rom', '--run_only_members', required=False,
                                   help='Sets members allowed on this run.')
            subparser.add_argument('-p', '--profile', action='store_true', default=False, required=False,
                                   help='Prints performance parameters of the execution of this command.')

            # Expid
            subparser = subparsers.add_parser(
                'expid', description="Creates a new experiment")
            group_experiment_types = subparser.add_mutually_exclusive_group()
            group_experiment_types.add_argument('-op', '--operational', action='store_true',
                                                help='creates a new experiment with operational experiment id')
            group_experiment_types.add_argument('-ev', '--evaluation', action='store_true',
                                                help='creates a new experiment with evaluation experiment id')
            group_experiment_types.add_argument('-t', '--testcase', action='store_true',
                                                help='creates a new experiment with testcase experiment id')
            group = subparser.add_mutually_exclusive_group()
            group.add_argument('-dm', '--dummy', action='store_true',
                               help='creates a new experiment with default values, usually for testing')
            group.add_argument('-min', '--minimal_configuration', action='store_true',
                               help='creates a new experiment with minimal configuration, usually combined with -repo')
            group.add_argument('-fs', '--filter_status', type=str,
                               choices=('Any', 'READY', 'COMPLETED',
                                        'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN'),
                               help='Select the original status to filter the list of jobs')
            subparser.add_argument(
                '-y', '--copy', help='makes a copy of the specified experiment')
            subparser.add_argument('-repo', '--git_repo', type=str, default="", required=False,
                                   help='sets a git repository for the experiment')
            subparser.add_argument('-b', '--git_branch', type=str, default="", required=False,
                                   help='sets a git branch for the experiment')
            subparser.add_argument('-conf', '--git_as_conf', type=str, default="", required=False,
                                   help='sets the git path to as_conf')
            subparser.add_argument('-local', '--use_local_minimal', required=False, action="store_true",
                                   help='uses local minimal file instead of git')

            subparser.add_argument('-H', '--HPC', required=False, default="local",
                                   help='specifies the HPC to use for the experiment')
            subparser.add_argument('-d', '--description', type=str, required=True,
                                   help='sets a description for the experiment to store in the database.')

            # Delete
            subparser = subparsers.add_parser(
                'delete', description="delete specified experiment")
            subparser.add_argument('expid', help='experiment identifiers separated by commas',
                               nargs='?')
            subparser.add_argument(
                '-f', '--force', action='store_true', help='deletes experiment without confirmation')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Monitor
            subparser = subparsers.add_parser(
                'monitor', description="plots specified experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-o', '--output', choices=('pdf', 'png', 'ps', 'svg', 'txt'),
                                   help='chooses type of output for generated plot')  # Default -o value comes from .yml
            subparser.add_argument('-group_by', choices=('date', 'member', 'chunk', 'split', 'automatic'), default=None,
                                   help='Groups the jobs automatically or by date, member, chunk or split')
            subparser.add_argument('-expand', type=str,
                                   help='Supply the list of dates/members/chunks to filter the list of jobs. Default = "Any". '
                                        'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            subparser.add_argument(
                '-expand_status', type=str, help='Select the stat uses to be expanded')
            subparser.add_argument('--hide_groups', action='store_true',
                                   default=False, help='Hides the groups from the plot')
            subparser.add_argument('-cw', '--check_wrapper', action='store_true',
                                   default=False, help='Generate possible wrapper in the current workflow')
            group2 = subparser.add_mutually_exclusive_group(required=False)

            group = subparser.add_mutually_exclusive_group(required=False)
            group.add_argument('-fl', '--list', type=str,
                               help='Supply the list of job names to be filtered. Default = "Any". '
                                    'LIST = "b037_20101101_fc3_21_sim b037_20111101_fc4_26_sim"')
            group.add_argument('-fc', '--filter_chunks', type=str,
                               help='Supply the list of chunks to filter the list of jobs. Default = "Any". '
                                    'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            group.add_argument('-fs', '--filter_status', type=str,
                               choices=('Any', 'READY', 'COMPLETED',
                                        'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN'),
                               help='Select the original status to filter the list of jobs')
            group.add_argument('-ft', '--filter_type', type=str,
                               help='Select the job type to filter the list of jobs')
            subparser.add_argument('--hide', action='store_true', default=False,
                                   help='hides plot window')
            group2.add_argument('-txt', '--text', action='store_true', default=False,
                                help='Generates only txt status file')

            group2.add_argument('-txtlog', '--txt_logfiles', action='store_true', default=False,
                                help='Generates only txt status file(AS < 3.12b behaviour)')

            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            # subparser.add_argument('-d', '--detail', action='store_true',
            #                        default=False, help='Shows Job List view in terminal')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument('-p', '--profile', action='store_true', default=False, required=False,
                                   help='Prints performance parameters of the execution of this command.')

            # Stats
            subparser = subparsers.add_parser(
                'stats', description="plots statistics for specified experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-ft', '--filter_type', type=str, help='Select the job type to filter '
                                                                          'the list of jobs')
            subparser.add_argument('-fp', '--filter_period', type=int,
                                   help='Select the period to filter jobs from current time to the past in'
                                        'number of hours back (must be greater than 0)')
            subparser.add_argument('-o', '--output', choices=('pdf', 'png', 'ps', 'svg'), default='pdf',
                                   help='type of output for generated plot')
            subparser.add_argument('--section_summary', action='store_true', default=False,
                                   help='Includes section summary in the plot')
            subparser.add_argument('--jobs_summary', action='store_true', default=False,
                                   help='Includes jobs summary in the plot')
            subparser.add_argument('--hide', action='store_true', default=False,
                                   help='hides plot window')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Clean
            subparser = subparsers.add_parser(
                'clean', description="clean specified experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                '-pr', '--project', action="store_true", help='clean project')
            subparser.add_argument('-p', '--plot', action="store_true",
                                   help='clean plot, only 2 last will remain')
            subparser.add_argument('-s', '--stats', action="store_true",
                                   help='clean stats, only last will remain')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Recovery
            subparser = subparsers.add_parser('recovery', description="recover specified experiment")
            subparser.add_argument('expid', type=str, help='experiment identifier')
            subparser.add_argument('-np', '--noplot', action='store_true', default=False, help='omit plot')
            subparser.add_argument(
                '--all', action="store_true", default=False, help='Get completed files to synchronize pkl')
            subparser.add_argument(
                '-s', '--save', action="store_true", default=False, help='Save changes to disk')
            subparser.add_argument(
                '--hide', action='store_true', default=False, help='hides plot window')
            subparser.add_argument(
                '-group_by', choices=('date', 'member', 'chunk', 'split', 'automatic'), default=None,
                help='Groups the jobs automatically or by date, member, chunk or split'
            )
            subparser.add_argument(
                '-expand', type=str,
                help='Supply the list of dates/members/chunks to filter the list of jobs. Default = "Any". '
                     'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            subparser.add_argument('-expand_status', type=str, help='Select the statuses to be expanded')
            subparser.add_argument(
                '-nt', '--notransitive', action='store_true', default=False,
                help='Disable transitive reduction (deprecated)'
            )
            subparser.add_argument(
                '-nl', '--no_recover_logs', action='store_true', default=False,
                help='Disable logs recovery (deprecated)'
            )
            subparser.add_argument(
                '-d', '--detail', action='store_true', default=False, help='Show Job List view in terminal')
            subparser.add_argument(
                '-f', '--force', action='store_true', default=False, help='Cancel active jobs ')
            subparser.add_argument(
                '-v', '--update_version', action='store_true', default=False, help='Update experiment version')
            subparser.add_argument('-off', '--offline', action='store_true',
                                   default=False, help='Offline recovery')

            # Migrate
            subparser = subparsers.add_parser(
                'migrate', description="Migrate experiments from current user to another")
            subparser.add_argument('expid', help='experiment identifier')
            group = subparser.add_mutually_exclusive_group(required=True)
            group.add_argument('-o', '--offer', action="store_true",
                               default=False, help='Offer experiment')
            group.add_argument('-p', '--pickup', action="store_true",
                               default=False, help='Pick-up released experiment')
            subparser.add_argument('-r', '--onlyremote', action="store_true",
                                   default=False, help='Only moves remote files')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Inspect
            subparser = subparsers.add_parser(
                'inspect', description="Generate all .cmd files")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument(
                '-f', '--force', action="store_true", help='Overwrite all cmd')
            subparser.add_argument('-cw', '--check_wrapper', action='store_true',
                                   default=False, help='Generate possible wrapper in the current workflow')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument(
                '-q', '--quick', action="store_true", help='Only checks one job per each section')

            group.add_argument('-fs', '--filter_status', type=str,
                               choices=('Any', 'READY', 'COMPLETED',
                                        'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN'),
                               help='Select the original status to filter the list of jobs')
            group = subparser.add_mutually_exclusive_group(required=False)
            group.add_argument('-fl', '--list', type=str,
                               help='Supply the list of job names to be filtered. Default = "Any". '
                                    'LIST = "b037_20101101_fc3_21_sim b037_20111101_fc4_26_sim"')
            group.add_argument('-fc', '--filter_chunks', type=str,
                               help='Supply the list of chunks to filter the list of jobs. Default = "Any". '
                                    'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            group.add_argument('-fs', '--filter_status', type=str,
                               choices=('Any', 'READY', 'COMPLETED',
                                        'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN'),
                               help='Select the original status to filter the list of jobs')
            group.add_argument('-ft', '--filter_type', type=str,
                               help='Select the job type to filter the list of jobs')

            # Check
            subparser = subparsers.add_parser(
                'check', description="check configuration for specified experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Describe
            subparser = subparsers.add_parser(
                'describe', description="Show details for specified experiment")
            subparser.add_argument('expid',
                                   help='experiment identifier, can be a list of expid separated by comma or spaces',
                                   default="*", nargs="?")
            subparser.add_argument('-u', '--user', help='username, default is current user or listed expid',
                                   default=""),
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')

            # Report
            subparser = subparsers.add_parser(
                'report', description="Show metrics.. ")  # TODO
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                '-t', '--template', type=str, help='Supply the metric template.')
            subparser.add_argument('-all', '--show_all_parameters', action='store_true',
                                   default=False, help='Writes a file containing all parameters')
            subparser.add_argument(
                '-fp', '--folder_path', type=str, help='Allows to select a non-default folder.')
            subparser.add_argument(
                '-p', '--placeholders', default=False, action='store_true',
                help='disables the substitution of placeholders by -')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Create
            subparser = subparsers.add_parser(
                'create', description="create specified experiment joblist")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                '-np', '--noplot', action='store_true', default=False, help='omit plot')
            subparser.add_argument('--hide', action='store_true', default=False,
                                   help='hides plot window')
            subparser.add_argument('-d', '--detail', action='store_true',
                                   default=False, help='Show Job List view in terminal')
            subparser.add_argument('-o', '--output', choices=('pdf', 'png', 'ps', 'svg'),
                                   help='chooses type of output for generated plot')  # Default -o value comes from .conf
            subparser.add_argument('-group_by', choices=('date', 'member', 'chunk', 'split', 'automatic'), default=None,
                                   help='Groups the jobs automatically or by date, member, chunk or split')
            subparser.add_argument('-expand', type=str,
                                   help='Supply the list of dates/members/chunks to filter the list of jobs. Default = "Any". '
                                        'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            subparser.add_argument(
                '-expand_status', type=str, help='Select the statuses to be expanded')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument('-cw', '--check_wrapper', action='store_true',
                                   default=False, help='Generate possible wrapper in the current workflow')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument('-p', '--profile', action='store_true', default=False, required=False,
                                   help='Prints performance parameters of the execution of this command.')
            subparser.add_argument(
                '-f', '--force', action='store_true', default=False, help='force regenerate job_list')
            # Configure
            subparser = subparsers.add_parser('configure', description="configure database and path for autosubmit. It "
                                                                       "can be done at machine, user or local level."
                                                                       "If no arguments specified configure will "
                                                                       "display dialog boxes (if installed)")
            subparser.add_argument(
                '--advanced', action="store_true", help="Open advanced configuration of autosubmit")
            subparser.add_argument(
                "--database-backend",
                choices=("sqlite", "postgres"),
                default="sqlite",
                help="Select the database backend to use. Default is sqlite.",
            )
            subparser.add_argument(
                "--database-conn-url", default=None,
                help="Database connection URL string. Required for postgres backend."
            )
            subparser.add_argument(
                "-db",
                "--databasepath",
                default=None,
                help="path to SQLite database. If not supplied, "
                     "it will prompt for it. Required for SQLite backend.",
            )
            subparser.add_argument(
                '-dbf', '--databasefilename', default=None, help='database filename')
            subparser.add_argument('-lr', '--localrootpath', default=None, help='path to store experiments. If not '
                                                                                'supplied, it will prompt for it')
            subparser.add_argument('-pc', '--platformsconfpath', default=None, help='path to platforms.yml file to '
                                                                                    'use by default. Optional')
            subparser.add_argument('-jc', '--jobsconfpath', default=None, help='path to jobs.yml file to use by '
                                                                               'default. Optional')
            subparser.add_argument(
                '-sm', '--smtphostname', default=None, help='SMTP server hostname. Optional')
            subparser.add_argument(
                '-mf', '--mailfrom', default=None, help='Notifications sender address. Optional')
            group = subparser.add_mutually_exclusive_group()
            group.add_argument('--all', action="store_true",
                               help='configure for all users')
            group.add_argument('--local', action="store_true", help='configure only for using Autosubmit from this '
                                                                    'path')

            # Install
            subparsers.add_parser(
                'install', description='install database for autosubmit on the configured folder')

            # Set status
            subparser = subparsers.add_parser(
                'setstatus', description="sets job status for an experiment")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                '-np', '--noplot', action='store_true', default=False, help='omit plot')
            subparser.add_argument(
                '-s', '--save', action="store_true", default=False, help='Save changes to disk')
            subparser.add_argument('-t', '--status_final',
                                   choices=('READY', 'COMPLETED', 'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN',
                                            'QUEUING', 'RUNNING', 'HELD'),
                                   required=True,
                                   help='Supply the target status')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            group = subparser.add_mutually_exclusive_group(required=True)
            group.add_argument('-fl', '--list', type=str,
                               help='Supply the list of job names to be changed. Default = "Any". '
                                    'LIST = "b037_20101101_fc3_21_sim b037_20111101_fc4_26_sim"')
            group.add_argument('-fc', '--filter_chunks', type=str,
                               help='Supply the list of chunks to change the status. Default = "Any". '
                                    'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            group.add_argument('-fs', '--filter_status', type=str,
                               help='Select the status (one or more) to filter the list of jobs.'
                                    "Valid values = ['Any', 'READY', 'COMPLETED', 'WAITING', 'SUSPENDED', 'FAILED', 'UNKNOWN']")
            group.add_argument('-ft', '--filter_type', type=str,
                               help='Select the job type to filter the list of jobs')
            group.add_argument('-ftc', '--filter_type_chunk', type=str,
                               help='Supply the list of chunks to change the status. Default = "Any". When the member name "all" is set, all the chunks \
                               selected from for that member will be updated for all the members. Example: all [1], will have as a result that the \
                                   chunks 1 for all the members will be updated. Follow the format: '
                                    '"[ 19601101 [ fc0 [1 2 3 4] Any [1] ] 19651101 [ fc0 [16-30] ] ],SIM,SIM2,SIM3"')
            group.add_argument('-ftcs', '--filter_type_chunk_split', type=str,
                               help='Supply the list of chunks & splits to change the status. Default = "Any". When the member name "all" is set, all the chunks \
                                           selected from for that member will be updated for all the members. Example: all [1], will have as a result that the \
                                               chunks 1 for all the members will be updated. Follow the format: '
                                    '"[ 19601101 [ fc0 [1 [1 2] 2 3 4] Any [1] ] 19651101 [ fc0 [16-30] ] ],SIM,SIM2,SIM3"')

            subparser.add_argument('--hide', action='store_true', default=False,
                                   help='hides plot window')
            subparser.add_argument('-group_by', choices=('date', 'member', 'chunk', 'split', 'automatic'), default=None,
                                   help='Groups the jobs automatically or by date, member, chunk or split')
            subparser.add_argument('-expand', type=str,
                                   help='Supply the list of dates/members/chunks to filter the list of jobs. Default = "Any". '
                                        'LIST = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 19651101 [ fc0 [16-30] ] ]"')
            subparser.add_argument(
                '-expand_status', type=str, help='Select the statuses to be expanded')
            subparser.add_argument('-nt', '--notransitive', action='store_true',
                                   default=False, help='Disable transitive reduction')
            subparser.add_argument('-cw', '--check_wrapper', action='store_true',
                                   default=False, help='Generate possible wrapper in the current workflow')
            subparser.add_argument('-d', '--detail', action='store_true',
                                   default=False, help='Generate detailed view of changes')

            # Test Case
            subparser = subparsers.add_parser(
                'testcase', description='create test case experiment')
            group = subparser.add_mutually_exclusive_group()
            group.add_argument(
                '-y', '--copy', help='makes a copy of the specified experiment')
            group.add_argument('-min', '--minimal_configuration', action='store_true',
                               help='creates a new experiment with minimal configuration, usually combined with -repo')
            subparser.add_argument(
                '-d', '--description', required=True, help='description of the test case')
            subparser.add_argument('-c', '--chunks', help='chunks to run')
            subparser.add_argument('-m', '--member', help='member to run')
            subparser.add_argument('-s', '--stardate', help='stardate to run')
            subparser.add_argument(
                '-H', '--HPC', required=True, help='HPC to run experiment on it')

            subparser.add_argument('-repo', '--git_repo', type=str, default="", required=False,
                                   help='sets a git repository for the experiment')
            subparser.add_argument('-b', '--git_branch', type=str, default="", required=False,
                                   help='sets a git branch for the experiment')
            subparser.add_argument('-conf', '--git_as_conf', type=str, default="", required=False,
                                   help='sets the git path to as_conf')
            subparser.add_argument('-local', '--use_local_minimal', required=False, action="store_true",
                                   help='uses local minimal file instead of git')

            # Database Fix
            subparser = subparsers.add_parser(
                'dbfix', description='tries to fix a corrupted jobs database')
            subparser.add_argument('expid', help='experiment identifier')

            # Pkl Fix
            subparser = subparsers.add_parser(
                'pklfix', description='restore the backup of your pkl')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                "-f",
                "--force",
                action="store_true",
                help="force restore without confirmation",
            )

            # Update Description
            subparser = subparsers.add_parser(
                'updatedescrip', description="Updates the experiment's description.")
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('description', help='New description.')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Test
            subparser = subparsers.add_parser(
                'test', description='test experiment')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument(
                '-c', '--chunks', required=True, help='chunks to run')
            subparser.add_argument('-m', '--member', help='member to run')
            subparser.add_argument('-s', '--stardate', help='stardate to run')
            subparser.add_argument(
                '-H', '--HPC', help='HPC to run experiment on it')
            subparser.add_argument(
                '-b', '--branch', help='branch of git to run (or revision from subversion)')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Refresh
            subparser = subparsers.add_parser(
                'refresh', description='refresh project directory for an experiment')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-mc', '--model_conf', default=False, action='store_true',
                                   help='overwrite model conf file')
            subparser.add_argument('-jc', '--jobs_conf', default=False, action='store_true',
                                   help='overwrite jobs conf file')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Update Version
            subparser = subparsers.add_parser(
                'updateversion', description='refresh experiment version')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            # Provenance
            subparser = subparsers.add_parser(
                'provenance', description='Produce provenance for autosubmit')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('--rocrate', action='store_true', default=False,
                                   help='Produce an RO-Crate file')
            # Archive
            subparser = subparsers.add_parser(
                'archive', description='archives an experiment')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-nclean', '--noclean', default=False, action='store_true',
                                   help='Avoid Cleaning of experiment folder')
            subparser.add_argument('-uc', '--uncompress', default=False, action='store_true',
                                   help='Only does a container without compress')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument('--rocrate', action='store_true', default=False,
                                   help='Produce an RO-Crate file')
            # Unarchive
            subparser = subparsers.add_parser(
                'unarchive', description='unarchive an experiment')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-nclean', '--noclean', default=False, action='store_true',
                                   help='Avoid Cleaning of experiment folder')
            subparser.add_argument('-uc', '--uncompressed', default=False, action='store_true',
                                   help='Extract files of the tar file without gzip compression')
            subparser.add_argument('-v', '--update_version', action='store_true',
                                   default=False, help='Update experiment version')
            subparser.add_argument('--rocrate', action='store_true', default=False,
                                   help='Unarchive an RO-Crate file')
            # update proj files
            subparser = subparsers.add_parser('upgrade', description='Updates autosubmit 3 proj files to autosubmit 4')
            subparser.add_argument('expid', help='experiment identifier')
            subparser.add_argument('-f', '--files', default='', type=str, help='list of files')
            # Readme
            subparsers.add_parser('readme', description='show readme')

            # Changelog
            subparsers.add_parser('changelog', description='show changelog')

            # Cat-log
            subparser = subparsers.add_parser(
                'cat-log', description='View workflow and job logs.')
            subparser.add_argument('-f', '--file', default=None, action='store', metavar='FILE',
                                   help='Workflow or Job log file. Options are o(output), j(job), e(error), s(status). Default o(output).')
            subparser.add_argument('-m', '--mode', default=None, action='store', metavar='MODE',
                                   help='Mode. Options are c(cat), t(tail). Default is c(cat).')
            subparser.add_argument('-i', '--inspect', default=False, action='store_true',
                                   help='Read job files generated by the inspect subcommand.')
            subparser.add_argument('ID', metavar='ID',
                                   help='An ID of a Workflow (eg a000) or a Job (eg a000_20220401_fc0_1_1_APPLICATION).')

            # stop
            subparser = subparsers.add_parser(
                'stop', description='Completely stops an autosubmit run process')
            group = subparser.add_mutually_exclusive_group(required=True)
            group.add_argument('expid', help='experiment identifiers separated by commas',
                               nargs='?')
            subparser.add_argument('-f', '--force', default=False, action='store_true',
                                   help='Forces to stop autosubmit process, equivalent to kill -9')
            group.add_argument('-a', '--all', default=False, action='store_true',
                               help='Stop all current running autosubmit processes, will ask for confirmation unless -y is used')
            group.add_argument('-fa', '--force_all', default=False, action='store_true',
                               help='Stop all current running autosubmit processes')
            subparser.add_argument(
                "-y",
                "--yes",
                default=False,
                action="store_true",
                help="Automatically answer yes to prompts",
            )
            subparser.add_argument('-c', '--cancel', action=CancelAction, default=False, nargs=0,
                                   help='Orders to the schedulers to stop active jobs.')
            subparser.add_argument('-fs', '--filter_status', type=str, default="SUBMITTED, QUEUING, RUNNING",
                                   help='Select the status (one or more) to filter the list of jobs.')
            subparser.add_argument('-t', '--target', type=str, default="FAILED", metavar='STATUS',
                                   help='Final status of killed jobs. Default is FAILED.')
            args, unknown = parser.parse_known_args()
            if args.version:
                Log.info(Autosubmit.autosubmit_version)
                return 0, None
            elif unknown or args.command is None:
                parser.print_help()
                return 1, None

            return 0, args
        except SystemExit:
            return 1, None
        except BaseException as e:
            raise AutosubmitCritical(f"Incorrect arguments for this command: {str(e)}", 7011)

    @staticmethod
    def run_command(args: argparse.Namespace):
        expid = "None"

        if hasattr(args, 'notransitive') and args.notransitive:
            warnings.warn('notransitive is deprecated and will be removed in a future major release!')

        if hasattr(args, 'expid'):
            expid = args.expid
        if args.command != "configure" and args.command != "install":
            Autosubmit._init_logs(args, args.logconsole, args.logfile, expid)
        if args.command == 'run':
            return Autosubmit.run_experiment(args.expid, args.start_time, args.start_after, args.run_only_members,
                                             args.profile)
        elif args.command == 'expid':
            return Autosubmit.expid(args.description, args.HPC, args.copy, args.dummy, args.minimal_configuration,
                                    args.git_repo, args.git_branch, args.git_as_conf, args.operational, args.testcase,
                                    args.evaluation, args.use_local_minimal) != ''
        elif args.command == 'delete':
            return delete_experiment(args.expid, args.force)
        elif args.command == 'monitor':
            return Autosubmit.monitor(args.expid, args.output, args.list, args.filter_chunks, args.filter_status,
                                      args.filter_type, args.hide, args.text, args.group_by, args.expand,
                                      args.expand_status, args.hide_groups, args.check_wrapper,
                                      args.txt_logfiles, args.profile)
        elif args.command == 'stats':
            return Autosubmit.statistics(args.expid, args.filter_type, args.filter_period, args.output,
                                         args.section_summary, args.jobs_summary, args.hide)
        elif args.command == 'clean':
            return Autosubmit.clean(args.expid, args.project, args.plot, args.stats)
        elif args.command == 'recovery':
            if args.no_recover_logs:
                warnings.warn('no_recover_logs is deprecated and will be removed in a future major release!')
            return Autosubmit.recovery(args.expid, args.noplot, args.save, args.all, args.hide, args.group_by,
                                       args.expand, args.expand_status, args.detail, args.force, args.offline)
        elif args.command == 'check':
            return Autosubmit.check(args.expid)
        elif args.command == 'inspect':
            return Autosubmit.inspect(args.expid, args.list, args.filter_chunks, args.filter_status,
                                      args.filter_type, args.force, args.check_wrapper, args.quick)
        elif args.command == 'report':
            return Autosubmit.report(args.expid, args.template, args.show_all_parameters, args.folder_path,
                                     args.placeholders)
        elif args.command == 'describe':
            return Autosubmit.describe(args.expid, args.user)
        elif args.command == 'migrate':
            raise AutosubmitCritical('The command migrate was removed and will be added again in a future release')
        elif args.command == 'create':
            return Autosubmit.create(args.expid, args.noplot, args.hide, args.output, args.group_by, args.expand,
                                     args.expand_status, args.check_wrapper, args.detail, args.profile, args.force)
        elif args.command == 'configure':
            if not args.advanced or (args.advanced and dialog is None):
                return Autosubmit.configure(
                    args.advanced,
                    args.databasepath,
                    args.databasefilename,
                    args.localrootpath,
                    args.platformsconfpath,
                    args.jobsconfpath,
                    args.smtphostname,
                    args.mailfrom,
                    args.all,
                    args.local,
                    args.database_backend,
                    args.database_conn_url,
                )
            else:
                return Autosubmit.configure_dialog()
        elif args.command == 'install':
            return Autosubmit.install()
        elif args.command == 'setstatus':
            return Autosubmit.set_status(args.expid, args.noplot, args.save, args.status_final, args.list,
                                         args.filter_chunks, args.filter_status, args.filter_type,
                                         args.filter_type_chunk, args.filter_type_chunk_split, args.hide,
                                         args.group_by, args.expand, args.expand_status,
                                         args.check_wrapper, args.detail)
        elif args.command == 'testcase':
            if args.chunks:
                warnings.warn('chunks is deprecated and will be removed in a future major release!')
            if args.member:
                warnings.warn('member is deprecated and will be removed in a future major release!')
            if args.stardate:
                warnings.warn('stardate is deprecated and will be removed in a future major release!')
            return Autosubmit.testcase(args.description, args.HPC, args.copy, args.minimal_configuration, args.git_repo,
                                       args.git_branch, args.git_as_conf, args.use_local_minimal)
        elif args.command == 'refresh':
            return Autosubmit.refresh(args.expid, args.model_conf, args.jobs_conf)
        elif args.command == 'updateversion':
            return Autosubmit.update_version(args.expid)
        elif args.command == 'upgrade':
            return Autosubmit.upgrade_scripts(args.expid, files=args.files)
        elif args.command == 'provenance':
            return Autosubmit.provenance(args.expid, rocrate=args.rocrate)
        elif args.command == 'archive':
            return Autosubmit.archive(args.expid, noclean=args.noclean, uncompress=args.uncompress,
                                      rocrate=args.rocrate)
        elif args.command == 'unarchive':
            return Autosubmit.unarchive(args.expid, uncompressed=args.uncompressed, rocrate=args.rocrate)
        elif args.command == 'readme':
            if os.path.isfile(Autosubmit.readme_path):
                with open(Autosubmit.readme_path) as f:
                    print(f.read())
                    return True
            return False
        elif args.command == 'changelog':
            if os.path.isfile(Autosubmit.changes_path):
                with open(Autosubmit.changes_path) as f:
                    print(f.read())
                    return True
            return False
        elif args.command == 'dbfix':
            return Autosubmit.database_fix(args.expid)
        elif args.command == 'pklfix':
            return Autosubmit.pkl_fix(args.expid, args.force)
        elif args.command == 'updatedescrip':
            return Autosubmit.update_description(args.expid, args.description)
        elif args.command == 'cat-log':
            return Autosubmit.cat_log(args.ID, args.file, args.mode, args.inspect)
        elif args.command == 'stop':
            return Autosubmit.stop(args.expid, args.force, args.all, args.force_all, args.cancel, args.filter_status,
                                   args.target, args.yes)

    @staticmethod
    def _init_logs(args, console_level='INFO', log_level='DEBUG', expid='None'):
        Log.set_console_level(console_level)
        if args.command != "configure":
            if not BasicConfig.CONFIG_FILE_FOUND:
                raise AutosubmitCritical(
                    'No configuration file(autosubmitrc) found in this filesystem. Please run "autosubmit configure" first.',
                    7006)
            if args.command != "install":
                # Only check for database file if database backend is set to sqlite
                if BasicConfig.DATABASE_BACKEND == 'sqlite':
                    if not os.path.exists(BasicConfig.DB_PATH):
                        raise AutosubmitCritical(
                            'Experiments database not found in this filesystem. Please run "autosubmit install" first.',
                            7072)
                    else:
                        permissions = os.access(BasicConfig.DB_PATH, os.R_OK)  # Check for read access
                        if not permissions:
                            raise AutosubmitCritical(
                                f'Experiments database {BasicConfig.DB_PATH} not readable. Please check permissions.'
                                , 7007)
                        permissions = os.access(BasicConfig.DB_PATH, os.W_OK)  # Check for write access
                        if not permissions:
                            raise AutosubmitCritical(
                                f'Experiments database {BasicConfig.DB_PATH} not writable. Please check permissions.'
                                , 7007)

        expid_less = ["expid", "describe", "testcase", "install", "-v",
                      "readme", "changelog", "configure", "unarchive",
                      "cat-log"]
        if args.command == "stop":
            if args.all or args.force_all:
                expid_less.append("stop")
        global_log_command = ["archive", "upgrade"]
        import platform
        fullhost = platform.node()
        if "." in fullhost:
            host = fullhost.split(".")[0]
        elif "," in fullhost:
            host = fullhost.split(",")[0]
        else:
            host = fullhost
        forbidden = BasicConfig.DENIED_HOSTS
        authorized = BasicConfig.ALLOWED_HOSTS
        message = f"Command: {args.command.upper()} is not allowed to run in host: {host}.\n"
        message += "List of permissions as follows:Command | hosts \nAllowed hosts\n"
        for command in authorized:
            message += f"   {command}:{authorized[command]} \n"
        message += "Denied hosts\n"
        for command in forbidden:
            message += f"   {command}:{forbidden[command]} \n"
        message += f"[Command: autosubmit {args.command.upper()}] is not allowed to run in [host: {host}]."
        if args.command in BasicConfig.DENIED_HOSTS:
            if 'all' in BasicConfig.DENIED_HOSTS[args.command] or host in BasicConfig.DENIED_HOSTS[
                args.command] or fullhost in BasicConfig.DENIED_HOSTS[args.command]:
                raise AutosubmitCritical(message, 7071)
        if args.command in BasicConfig.ALLOWED_HOSTS:
            if 'all' not in BasicConfig.ALLOWED_HOSTS[args.command] and not (
                    host in BasicConfig.ALLOWED_HOSTS[args.command] or fullhost in BasicConfig.ALLOWED_HOSTS[
                args.command]):
                raise AutosubmitCritical(message, 7071)
        if (expid != 'None' and expid) and args.command not in expid_less and args.command not in global_log_command:
            if isinstance(expid, list):
                expids = cast(list[str], expid)
            elif "," in expid:
                expids = expid.split(",")
            else:
                expids = expid.split(" ")
            expids = [x.strip() for x in expids]
            for expid in expids:
                exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
                tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
                aslogs_path = os.path.join(tmp_path, BasicConfig.LOCAL_ASLOG_DIR)
                if not os.path.exists(exp_path):
                    if BasicConfig.DATABASE_BACKEND == 'sqlite':
                        raise AutosubmitCritical("Experiment does not exist", 7012)
                    else:
                        # TODO: This needs #1352 issue, to have the workflow configuration fully
                        #       loaded from version control -- we have that ``main.yml`` in ClimateDT
                        #       and other quirks to fix... we should document for now so users are
                        #       at least aware of this. But replicating ``autosubmit expid`` here is
                        #       as good as we can do for now.
                        create_required_folders(expid, Path(exp_path))

                as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
                as_conf.reload(force_load=True)

                if args.command not in ["expid", "upgrade"] and len(as_conf.experiment_data) == 0:
                    raise AutosubmitCritical(
                        f"Experiment {expid} has no yml data. Please, if you really wish to use "
                        f"AS 4 prompt:\nautosubmit upgrade {expid}", 7012)

                # delete is treated differently
                owner, eadmin, current_owner = Autosubmit._check_ownership_and_set_last_command(as_conf, expid,
                                                                                                args.command)
            if not os.path.exists(tmp_path):
                os.mkdir(tmp_path)
            if not os.path.exists(aslogs_path):
                os.mkdir(aslogs_path)
            if args.command in ['stop', 'delete']:
                exp_id = "_".join(expids)
                Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                          args.command + '_' + exp_id + '.log'), "out", log_level)
                Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                          args.command + '_' + exp_id + '_err.log'), "err")
            else:
                if owner:
                    os.chmod(tmp_path, 0o775)
                    with suppress(PermissionError, FileNotFoundError, Exception):  # for -txt option
                        os.chmod(f'{exp_path}/status', 0o775)

                    Log.set_file(os.path.join(aslogs_path, args.command + '.log'), "out", log_level)
                    Log.set_file(os.path.join(aslogs_path, args.command + '_err.log'), "err")
                    if args.command in ["run"]:
                        if os.path.exists(os.path.join(aslogs_path, 'jobs_active_status.log')):
                            os.remove(os.path.join(aslogs_path, 'jobs_active_status.log'))
                        if os.path.exists(os.path.join(aslogs_path, 'jobs_failed_status.log')):
                            os.remove(os.path.join(aslogs_path, 'jobs_failed_status.log'))
                            Log.set_file(os.path.join(aslogs_path, 'jobs_active_status.log'), "status")
                            Log.set_file(os.path.join(aslogs_path, 'jobs_failed_status.log'), "status_failed")
                else:
                    st = os.stat(tmp_path)
                    oct_perm = str(oct(st.st_mode))[-3:]
                    if int(oct_perm[1]) in [6, 7] or int(oct_perm[2]) in [6, 7]:
                        Log.set_file(os.path.join(tmp_path, args.command + '.log'), "out", log_level)
                        Log.set_file(os.path.join(tmp_path, args.command + '_err.log'), "err")
                    else:
                        Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                                  args.command + expid + '.log'), "out", log_level)
                        Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                                  args.command + expid + '_err.log'), "err")
                        Log.printlog(f"Permissions of {tmp_path} are {oct_perm}. The log is being written in the "
                                     f"{BasicConfig.GLOBAL_LOG_DIR} path instead of {oct_perm}. "
                                     f"Please tell to the owner to fix the permissions")
            Log.file_path = tmp_path
            if owner:
                if "update_version" in args:
                    force_update_version = args.update_version
                else:
                    force_update_version = False
                if args.command not in ["upgrade", "updateversion"]:
                    if force_update_version:
                        if as_conf.get_version() != Autosubmit.autosubmit_version:
                            Log.info(
                                f"The {expid} experiment {as_conf.get_version()} version is being updated to {Autosubmit.autosubmit_version} for match autosubmit version")
                            as_conf.set_version(Autosubmit.autosubmit_version)
                            update_experiment_description_version(expid, version=Autosubmit.autosubmit_version)

                    else:
                        if as_conf.get_version() is not None and as_conf.get_version() != Autosubmit.autosubmit_version:
                            raise AutosubmitCritical(f"Current experiment uses ({as_conf.get_version()}) which is not"
                                                     f" the running Autosubmit version"
                                                     f"\nPlease, update the experiment version if you wish to continue"
                                                     f" using AutoSubmit {Autosubmit.autosubmit_version}"
                                                     f"\nYou can achieve this using the command autosubmit"
                                                     f" updateversion {expid} "
                                                     f"\nOr with the -v parameter: autosubmit {args.command} {expid} "
                                                     f"-v ", 7014)
        else:
            if isinstance(expid, list):
                exp_id = '_'.join(expid)
            elif expid == 'None' or not expid:
                exp_id = ""
            else:
                exp_id = "_" + expid
            if args.command not in expid_less:
                exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
                if not os.path.exists(exp_path):
                    if BasicConfig.DATABASE_BACKEND == 'sqlite':
                        raise AutosubmitCritical("Experiment does not exist", 7012)
                    else:
                        os.mkdir(exp_path)
            Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                      args.command + exp_id + '.log'), "out", log_level)
            Log.set_file(os.path.join(BasicConfig.GLOBAL_LOG_DIR,
                                      args.command + exp_id + '_err.log'), "err")
        # Enforce LANG=UTF-8
        try:
            try:
                locale.setlocale(locale.LC_ALL, 'C.UTF-8')
            except Exception:
                try:
                    locale.setlocale(locale.LC_ALL, 'C.utf8')
                except Exception:
                    try:
                        locale.setlocale(locale.LC_ALL, 'en_GB')
                    except Exception:
                        locale.setlocale(locale.LC_ALL, 'es_ES')
        except Exception:
            Log.info("Locale C.utf8 is not found, using 'C' as fallback")
            locale.setlocale(locale.LC_ALL, 'C')

        Log.info(f"Autosubmit is running with {Autosubmit.autosubmit_version}")

    @staticmethod
    def _check_ownership_and_set_last_command(as_conf, expid, command):
        if command not in ["monitor", "describe", "delete", "report", "stats", "dbfix"]:
            owner, eadmin, current_owner = check_ownership(expid, raise_error=True)
        else:
            owner, eadmin, current_owner = check_ownership(expid, raise_error=False)
        if owner:
            as_conf.set_last_as_command(command)
        return owner, eadmin, current_owner

    @staticmethod
    def copy_as_config(exp_id, copy_id):
        for conf_file in os.listdir(Path(BasicConfig.LOCAL_ROOT_DIR, copy_id, "conf")):
            # Copy only relevant files
            if conf_file.endswith((".conf", ".yml", ".yaml")):
                shutil.copy(Path(BasicConfig.LOCAL_ROOT_DIR, copy_id, "conf", conf_file),
                            Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, "conf", conf_file.replace(copy_id, exp_id)))
            # if ends with .conf convert it to AS4 yaml file
            if conf_file.endswith(".conf"):
                try:
                    AutosubmitConfig.ini_to_yaml(Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, 'conf'),
                                                 Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, "conf",
                                                      conf_file.replace(copy_id, exp_id)))
                except Exception as e:
                    Log.warning(f"Error converting {conf_file.replace(copy_id, exp_id)} to yml: {str(e)}")

    @staticmethod
    def generate_as_config(
            exp_id: str,
            dummy: bool=False,
            minimal_configuration: bool=False,
            local: bool=False,
            parameters: Optional[dict[str, Union[dict, list, str]]] = None
    ) -> None:
        """Retrieve the configuration from autosubmit.config package.

        :param exp_id: Experiment ID
        :param dummy: Whether the experiment is a dummy one or not.
        :param minimal_configuration: Whether the experiment is configured with minimal configuration or not.
        :param local: Whether the experiment project type is local or not.
        :param parameters: Optional list of parameters to be used when processing the configuration files.
        :return: None
        """

        def _add_comments_to_yaml(yaml_data, parameters, keys=None):
            """A recursive generator that visits every leaf node and yields the flatten parameter. """
            if keys is None:
                keys = []
            if isinstance(yaml_data, dict):
                for key, value in yaml_data.items():
                    if isinstance(value, dict):
                        _add_comments_to_yaml(value, parameters, [*keys, key])
                    else:
                        parameter_key = '.'.join([*keys, key]).upper()
                        if parameter_key in parameters:
                            comment = parameters[parameter_key]
                            yaml_data.yaml_set_comment_before_after_key(key, before=comment, indent=yaml_data.lc.col)

        def _recurse_into_parameters(parameters: dict[str, Union[dict, list, str]], keys=None) -> Generator:
            """Recurse into the ``PARAMETERS`` dictionary, and emits a dictionary.

            The key in the dictionary is the flattened parameter key/ID, and the value
            is the parameter documentation.

            :param parameters: Global parameters dictionary.
            :param keys: For recursion, the accumulated keys.
            :return: A dictionary with the
            """
            if keys is None:
                keys = []
            if isinstance(parameters, dict):
                for key, value in parameters.items():
                    if isinstance(value, dict):
                        yield from _recurse_into_parameters(value, [*keys, key])
                    else:
                        key = key.upper()
                        # The parameters have some keys that contain ``${PARENT}.key`` as that is
                        # how they are displayed in the Sphinx docs. So we need to detect it and
                        # handle it. p.s. We also know the max-length of the parameters dict is 2!
                        # See the ``autosubmit.helpers.parameters`` module for more.
                        if not key.startswith(f'{keys[0]}.'):
                            yield '.'.join([*keys, key]).upper(), value
                        else:
                            yield key, value

        template_files = [file.name for file in (read_files('autosubmit.config') / 'files').iterdir() if file.is_file()]
        if parameters is None:
            parameters = {}
        parameter_comments: dict = dict(_recurse_into_parameters(parameters))

        for as_conf_file in template_files:
            origin = str(read_files('autosubmit.config') / f'files/{as_conf_file}')
            target = None

            if dummy:
                # Create a ``dummy.yml`` file.
                if as_conf_file.endswith('dummy.yml'):
                    file_name = f'{as_conf_file.split("-")[0]}_{exp_id}.yml'
                    target = Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, 'conf', file_name)
            elif minimal_configuration:
                # Create a ``minimal.yml`` file.
                #
                # Here we have two minimal configuration files that we can copy, the local or the git files.
                # The function knows whether it is a local through the ``local`` argument, and that defines
                # which files we will copy (``local-minimal.yml`` if ``local``, ``git-minimal.yml`` otherwise.)
                if (local and as_conf_file.endswith("local-minimal.yml")) or (
                        not local and as_conf_file.endswith('git-minimal.yml')):
                    target = Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, 'conf/minimal.yml')
            elif not as_conf_file.endswith('dummy.yml') and not as_conf_file.endswith('minimal.yml'):
                # Create any other file that is not ``dummy.yml`` nor ``minimal.yml``.
                file_name = f'{Path(as_conf_file).stem}_{exp_id}.yml'
                target = Path(BasicConfig.LOCAL_ROOT_DIR, exp_id, 'conf', file_name)

            # Here we annotate the copied configuration with comments from the Python source code.
            # This means the YAML configuration files contain the exact same comments from our
            # Python code, which is also displayed in our Sphinx documentation (be careful with what
            # you write!)
            #
            # The previous code was simply doing a shutil(origin, target). This does not modify
            # much that logic, except we add comments before writing the copy...
            if origin and target:
                with open(origin, 'r') as input, open(target, 'w+') as output:
                    yaml = YAML(typ='rt')
                    yaml_data = yaml.load(input)
                    _add_comments_to_yaml(yaml_data, parameter_comments)
                    yaml.dump(yaml_data, output)

    @staticmethod
    def replace_parameter_inside_section(content, parameter, new_value, section):
        # same but for any section any parameter, not only EXPID case insensitive
        # Find the any section
        if section:
            section_match = re.search(rf'({section}:[\s\S]*?{parameter}:.*?)(?=\n|$)', content, re.IGNORECASE)
            if section_match:
                section = section_match.group(1)
                # Replace parameter in the section
                new_section = re.sub(rf'({parameter}:).*', rf'\1 "{new_value}"', section)
                # Replace the old section
                content = content.replace(section, new_section)
        else:
            # replace only the parameter
            content = re.sub(rf'({parameter}:).*', rf'\1 "{new_value}"', content)
        return content

    @staticmethod
    def as_conf_default_values(exp_id:str, hpc:str = "local", minimal_configuration:bool = False, git_repo:str = "",
                               git_branch:str = "main", git_as_conf:str = "") -> None:
        """Replace default values in as_conf files.

        :param exp_id: experiment id
        :param hpc: platform
        :param minimal_configuration: minimal configuration
        :param git_repo: path to project git repository
        :param git_branch: main branch
        :param git_as_conf: path to as_conf file in git repository
        :return: None
        """
        # the var hpc was hardcoded in the header of the function

        # open and replace values
        for as_conf_file in Path(BasicConfig.LOCAL_ROOT_DIR, f"{exp_id}/conf").iterdir():
            if as_conf_file.name.endswith(".yml") or as_conf_file.name.endswith(".yaml"):
                with open(as_conf_file, 'r+') as f:
                    # Copied files could not have default names.
                    content = f.read()
                    search = re.search('AUTOSUBMIT_VERSION: .*', content, re.MULTILINE)
                    if search is not None:
                        content = content.replace(search.group(0), f"AUTOSUBMIT_VERSION: \""
                                                                   f"{Autosubmit.autosubmit_version}\"")
                    search = re.search('NOTIFICATIONS: .*', content, re.MULTILINE)
                    if search is not None:
                        content = content.replace(search.group(0), "NOTIFICATIONS: False")
                    search = re.search('TO: .*', content, re.MULTILINE)
                    if search is not None:
                        content = content.replace(search.group(0), "TO: \"\"")
                    content = Autosubmit.replace_parameter_inside_section(content, "EXPID", exp_id, "DEFAULT")
                    search = re.search('HPCARCH: .*', content, re.MULTILINE)
                    if search is not None:
                        x = search.group(0).split(":")
                        # clean blank space, quotes and double quote
                        aux = x[1].strip(' "\'')
                        # hpc in config is empty && -H has a value-> write down hpc value
                        if hpc != "":
                            content = content.replace(search.group(0), f"HPCARCH: \"{hpc}\"")
                        elif len(aux) > 0:
                            content = content.replace(search.group(0), f"HPCARCH: \"{aux}\"")
                        else:
                            content = content.replace(search.group(0), "HPCARCH: \"local\"")
                        # the other case is aux!=0 that we dont care about val(hpc) because its a copyExpId
                    if minimal_configuration:
                        search = re.search('CUSTOM_CONFIG: .*', content, re.MULTILINE)
                        if search is not None:
                            content = content.replace(search.group(0),
                                                      "CUSTOM_CONFIG: \"%PROJDIR%/" + git_as_conf + "\"")
                        search = re.search('PROJECT_ORIGIN: .*', content, re.MULTILINE)
                        if search is not None:
                            content = content.replace(search.group(0), f"PROJECT_ORIGIN: \"{git_repo}\"")
                        search = re.search('PROJECT_PATH: .*', content, re.MULTILINE)
                        if search is not None:
                            content = content.replace(search.group(0), f"PROJECT_PATH: \"{git_repo}\"")
                        search = re.search('PROJECT_BRANCH: .*', content, re.MULTILINE)
                        if search is not None:
                            content = content.replace(search.group(0), f"PROJECT_BRANCH: \"{git_branch}\"")

                    f.seek(0)
                    f.write(content)
                    f.truncate()

    @staticmethod
    def expid(description, hpc="", copy_id='', dummy=False, minimal_configuration=False,
              git_repo="", git_branch="", git_as_conf="", operational=False, testcase=False,
              evaluation=False, use_local_minimal=False) -> str:
        """Creates a new experiment for given HPC.

        :param description: description of the experiment
        :type description: str
        :param hpc: HPC where the experiment will be executed
        :type hpc: str
        :param copy_id: if specified, experiment id to copy
        :type copy_id: str
        :param dummy: if true, creates a dummy experiment
        :type dummy: bool
        :param minimal_configuration: if true, creates a minimal configuration
        :type minimal_configuration: bool
        :param git_repo: git repository to clone
        :type git_repo: str
        :param git_branch: git branch to clone
        :type git_branch: str
        :param git_as_conf: path to as_conf file in git repository
        :type git_as_conf: str
        :param operational: if true, creates an operational experiment
        :type operational: bool
        :param testcase:
        :type testcase: bool
        :param evaluation: if true, creates an evaluation experiment
        :type evaluation: bool
        :param use_local_minimal: Gets local minimal instead of git minimal
        :type use_local_minimal: bool
        :returns: return the expid of the experiment.
        :rtype: str
        """
        if use_local_minimal:
            git_branch = ""
            if AutosubmitGit.is_git_repo(git_repo):
                git_repo = ""

        root_folder = Path(BasicConfig.LOCAL_ROOT_DIR)
        if description is None:
            raise AutosubmitCritical(
                "Check that the parameters are defined (-d) ", 7011)
        if hpc is None and not minimal_configuration:
            raise AutosubmitCritical(
                "Check that the parameters are defined (-H) ", 7011)
        # Register the experiment in the database
        # Copy another experiment from the database
        if copy_id != '' and copy_id is not None:
            copy_id_folder = root_folder / copy_id
            if not copy_id_folder.exists():
                raise AutosubmitCritical(f"Experiment {copy_id} doesn't exists", 7011)
            if minimal_configuration:
                conf_dir = Path(copy_id_folder) / "conf"
                if not Path(conf_dir / 'minimal.yml').is_file() and not Path(conf_dir / 'minimal.yaml').is_file():
                    raise AutosubmitCritical(
                        "Cannot copy an experiment that does not have a minimal.yml file", 7011)
            exp_id = copy_experiment(copy_id, description, Autosubmit.autosubmit_version, testcase, operational,
                                     evaluation)
        else:
            # Create a new experiment from scratch
            exp_id = new_experiment(description, Autosubmit.autosubmit_version, testcase, operational, evaluation)

        if exp_id == '':
            raise AutosubmitCritical("No expid", 7011)

        # Create the experiment structure
        Log.info("Generating folder structure...")

        exp_folder = root_folder / Path(exp_id)
        try:
            Log.info(f"Experiment folder: {exp_folder}")
            create_required_folders(exp_id, exp_folder)
        except OSError as e:
            with suppress(Exception):
                Autosubmit._delete_expid(exp_id, True)
            raise AutosubmitCritical(f"Error while creating the experiment structure: {str(e)}", 7011)

        # Create the experiment configuration
        Log.info("Generating config files...")
        try:
            if copy_id != '' and copy_id is not None:
                # Copy the configuration from selected experiment
                Autosubmit.copy_as_config(exp_id, copy_id)
            else:
                # Create a new configuration
                Autosubmit.generate_as_config(exp_id, dummy, minimal_configuration, use_local_minimal)
        except Exception as e:
            try:
                Autosubmit._delete_expid(exp_id, True)
            except Exception:
                pass
            raise AutosubmitCritical(f"Error while creating the experiment configuration: {str(e)}", 7011)
        # Change template values by default values specified from the commandline
        try:
            as_conf_default_values(Autosubmit.autosubmit_version, exp_id, hpc, minimal_configuration, git_repo, git_branch, git_as_conf)
        except Exception as e:
            try:
                Autosubmit._delete_expid(exp_id, True)
            except Exception:
                pass
            raise AutosubmitCritical(f"Error while setting the default values: {str(e)}", 7011)

        # Try to update the experiment details
        try:
            ExperimentDetails(exp_id).save_update_details()
        except Exception:
            Log.warning(f"Could not update experiment details for {exp_id}. Omitting this step.")

        Log.result(f"Experiment {exp_id} created")
        return exp_id

    @staticmethod
    def _load_parameters(as_conf, job_list, platforms):
        """Add parameters from configuration files into platform objects, and into the job_list object.

        :param as_conf: Basic configuration handler.\n
        :type as_conf: AutosubmitConfig object\n
        :param job_list: Handles the list as a unique entity.\n
        :type job_list: JobList() object\n
        :param platforms: List of platforms related to the experiment.\n
        :type platforms: List() of Platform Objects. e.g EcPlatform(), SlurmPlatform().
        :return: Nothing, modifies input.
        """

        Log.debug("Loading HPC parameters...")
        # Platform = from DEFAULT.HPCARCH, e.g. marenostrum4
        if as_conf.get_platform() not in platforms.keys():
            Log.warning("Main platform is not defined in platforms.yml")
        else:
            platform = platforms[as_conf.get_platform()]
            platform.add_parameters(as_conf)
        # Attach parameters to JobList
        as_conf.experiment_data['STARTDATES'] = []
        for date in job_list._date_list:
            as_conf.experiment_data['STARTDATES'].append(date2str(date, job_list.get_date_format()))

    @staticmethod
    def inspect(expid: str, lst: str, filter_chunks: str, filter_status: str, filter_section: str, force=False,
                check_wrapper=False, quick=False) -> bool:
        """Generates cmd files experiment.

         :param expid: identifier of experiment to be run
         :param lst:
         :param filter_chunks:
         :param filter_status:
         :param filter_section:
         :param force:
         :param check_wrapper:
         :param quick:
         :return: True if run to the end, False otherwise
         """
        try:
            Log.info(f"Inspecting experiment {expid}")
            check_ownership(expid, raise_error=True)
            exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
            tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
            if os.path.exists(os.path.join(tmp_path, 'autosubmit.lock')):
                locked = True
            else:
                locked = False
            Log.info("Starting inspect command")
            os.system('clear')
            signal.signal(signal.SIGINT, signal_handler)
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            as_conf.check_conf_files(True)
            as_conf.get_project_type()
            safetysleeptime = as_conf.get_safetysleeptime()
            Log.debug(f"The Experiment name is: {expid}")
            Log.debug(f"Sleep: {safetysleeptime}")
            packages_persistence = JobPackagePersistence(expid)

            if BasicConfig.DATABASE_BACKEND == 'sqlite':
                os.chmod(os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid,
                                      "pkl", "job_packages_" + expid + ".db"), 0o644)

            packages_persistence.reset_table(True)
            job_list = Autosubmit.load_job_list(expid, as_conf)
            job_list.packages_dict = {}

            Log.debug(f"Length of the jobs list: {len(job_list)}")

            # variables to be updated on the fly
            safetysleeptime = as_conf.get_safetysleeptime()
            Log.debug(f"Sleep: {safetysleeptime}")
            # Generate
            Log.info("Starting to generate cmd scripts")
            jobs = []
            jobs_cw = []

            if not isinstance(job_list, type([])):
                if check_wrapper and (not locked or (force and locked)):
                    Log.info("Generating all cmd script adapted for wrappers")
                    jobs = job_list.get_uncompleted()
                    if force:
                        jobs_cw = job_list.get_completed()
                else:
                    if locked:
                        Log.warning("There is a .lock file and not -f, generating only all unsubmitted cmd scripts")
                        jobs = job_list.get_unsubmitted()
                    elif force:
                        Log.info("Overwriting all cmd scripts")
                        jobs = job_list.get_job_list()
                    else:
                        Log.info("Generating cmd scripts only for selected jobs")
                        if filter_chunks:
                            fc = filter_chunks
                            Log.debug(fc)
                            if fc == 'Any':
                                jobs = job_list.get_job_list()
                            else:
                                # noinspection PyTypeChecker
                                data = json.loads(Autosubmit._create_json(fc))
                                for date_json in data['sds']:
                                    date = date_json['sd']
                                    jobs_date = [j for j in job_list.get_job_list() if date2str(
                                        j.date) == date]

                                    for member_json in date_json['ms']:
                                        member = member_json['m']
                                        jobs_member = [j for j in jobs_date if j.member == member]

                                        for chunk_json in member_json['cs']:
                                            chunk = int(chunk_json)
                                            jobs = jobs + [job for job in [j for j in jobs_member if j.chunk == chunk]]

                        elif filter_status:
                            Log.debug(
                                f"Filtering jobs with status {filter_status}")
                            if filter_status == 'Any':
                                jobs = job_list.get_job_list()
                            else:
                                fs = Autosubmit._get_status(filter_status)
                                jobs = [job for job in [j for j in job_list.get_job_list() if j.status == fs]]

                        elif filter_section:
                            ft = filter_section
                            Log.debug(ft)

                            if ft == 'Any':
                                jobs = job_list.get_job_list()
                            else:
                                for job in job_list.get_job_list():
                                    if job.section == ft:
                                        jobs.append(job)
                        elif lst:
                            jobs_lst = lst.split()

                            if jobs == 'Any':
                                jobs = job_list.get_job_list()
                            else:
                                for job in job_list.get_job_list():
                                    if job.name in jobs_lst:
                                        jobs.append(job)
                        else:
                            jobs = job_list.get_job_list()
            if quick:
                wrapped_sections = list()
                if check_wrapper:
                    for wrapper_data in as_conf.experiment_data.get("WRAPPERS", {}).values():
                        if isinstance(wrapper_data, dict):
                            jobs_in_wrapper = wrapper_data.get("JOBS_IN_WRAPPER", [])
                            wrapped_sections.extend(jobs_in_wrapper)
                            wrapped_sections = list(set(wrapped_sections))
                jobs_aux = list()
                sections_added = set()
                for job in jobs:
                    if job.section not in sections_added or job.section in wrapped_sections:
                        sections_added.add(job.section)
                        jobs_aux.append(job)
                jobs = jobs_aux
                del jobs_aux
                sections_added = set()
                jobs_aux = list()
                for job in jobs_cw:
                    if job.section not in sections_added or job.section in wrapped_sections:
                        sections_added.add(job.section)
                        jobs_aux.append(job)
                    jobs_cw = jobs_aux
                del jobs_aux
            file_paths = ""

            if isinstance(jobs, type([])):
                for job in jobs:
                    file_paths += f"{BasicConfig.LOCAL_ROOT_DIR}/{expid}/tmp/{job.name}.cmd | {job.file}\n"
                    job.status = Status.WAITING
                Autosubmit.generate_scripts_andor_wrappers(
                    as_conf, job_list, jobs, packages_persistence, False)
            if len(jobs_cw) > 0:
                for job in jobs_cw:
                    file_paths += f"{BasicConfig.LOCAL_ROOT_DIR}/{expid}/tmp/{job.name}.cmd\n"
                    job.status = Status.WAITING
                Autosubmit.generate_scripts_andor_wrappers(
                    as_conf, job_list, jobs_cw, packages_persistence, False)

            # obtain base script

            Log.info("No more scripts to generate, you can proceed to check them manually")
            Log.result(file_paths)

        except AutosubmitCritical:
            raise
        except AutosubmitError:
            raise
        except BaseException:
            raise
        return True

    @staticmethod
    def generate_scripts_andor_wrappers(as_conf, job_list, jobs_filtered, packages_persistence, only_wrappers=False):
        """
        :param as_conf: Class that handles basic configuration parameters of Autosubmit. \n
        :type as_conf: AutosubmitConfig() Object \n
        :param job_list: Representation of the jobs of the experiment, keeps the list of jobs inside. \n
        :type job_list: JobList() Object \n
        :param jobs_filtered: list of jobs that are relevant to the process. \n
        :type jobs_filtered: List() of Job Objects \n
        :param packages_persistence: Object that handles local db persistence.  \n
        :type packages_persistence: JobPackagePersistence() Object \n
        :param only_wrappers: True when coming from Autosubmit.create(). False when coming from Autosubmit.inspect(), \n
        :type only_wrappers: Boolean \n
        :return: Nothing\n
        :rtype: \n
        """
        Log.warning("Generating the auxiliary job_list used for the -cw flag.")
        job_list._job_list = jobs_filtered
        job_list._persistence_file = job_list._persistence_file + "_cw_flag"
        as_conf.load_parameters()
        date_list = as_conf.get_date_list()
        if len(date_list) != len(set(date_list)):
            raise AutosubmitCritical('There are repeated start dates!', 7014)
        num_chunks = as_conf.get_num_chunks()
        chunk_ini = as_conf.get_chunk_ini()
        member_list = as_conf.get_member_list()
        as_conf.get_member_list(run_only=True)
        date_format = ''
        if as_conf.get_chunk_size_unit() == 'hour':
            date_format = 'H'
        for date in date_list:
            if date.hour > 1:
                date_format = 'H'
            if date.minute > 1:
                date_format = 'M'
        wrapper_jobs = dict()
        for wrapper_section, wrapper_data in as_conf.experiment_data.get("WRAPPERS", {}).items():
            if type(wrapper_data) is not dict:
                continue
            wrapper_jobs[wrapper_section] = as_conf.get_wrapper_jobs(wrapper_data)
        Log.warning("Aux Job_list was generated successfully")

        # Load platforms.
        submitter = ParamikoSubmitter(as_conf=as_conf)
        hpcarch = as_conf.get_platform()

        Autosubmit._load_parameters(as_conf, job_list, submitter.platforms)
        platforms_to_test = set()
        for job in job_list.get_job_list():
            if job.platform_name == "" or job.platform_name is None:
                job.platform_name = hpcarch
            job.platform = submitter.platforms[job.platform_name]
            if job.platform is not None and job.platform != "":
                platforms_to_test.add(job.platform)
        job_list.update_list(as_conf, False)
        # Loading parameters again
        Autosubmit._load_parameters(as_conf, job_list, submitter.platforms)
        # Related to TWO_STEP_START new variable defined in expdef
        unparsed_two_step_start = as_conf.get_parse_two_step_start()
        if unparsed_two_step_start != "":
            job_list.parse_jobs_by_filter(unparsed_two_step_start)
        job_list.create_dictionary(date_list, member_list, num_chunks, chunk_ini, date_format, as_conf.get_retrials(),
                                   wrapper_jobs, as_conf)
        for job in job_list.get_active():
            if job.status != Status.WAITING:
                job.status = Status.READY
        while job_list.get_active():
            Autosubmit.submit_ready_jobs(as_conf, job_list, platforms_to_test, packages_persistence, True,
                                         only_wrappers, hold=False)
            job_list.update_list(as_conf, False)
        for job in job_list.get_job_list():
            job.status = Status.WAITING

    @staticmethod
    def manage_wrapper_job(as_conf, job_list, platform, wrapper_id, save=False):
        check_wrapper_jobs_sleeptime = as_conf.get_wrapper_check_time()
        Log.debug(f'WRAPPER CHECK TIME = {check_wrapper_jobs_sleeptime}')
        # Setting prev_status as an easy way to check status change for inner jobs
        wrapper_job = job_list.job_package_map[wrapper_id]
        if as_conf.get_notifications() == "true":
            for inner_job in wrapper_job.job_list:
                inner_job.prev_status = inner_job.status
        check_wrapper = True
        if wrapper_job.status == Status.RUNNING:
            check_wrapper = True if datetime.timedelta.total_seconds(datetime.datetime.now(
            ) - wrapper_job.checked_time) >= check_wrapper_jobs_sleeptime else False
        if check_wrapper:
            Log.debug(f'Checking Wrapper {str(wrapper_id)}')
            wrapper_job.checked_time = datetime.datetime.now()
            platform.check_job(wrapper_job)
            try:
                if wrapper_job.status != wrapper_job.new_status:
                    Log.info('Wrapper job ' + wrapper_job.name + ' changed from ' + str(
                        Status.VALUE_TO_KEY[wrapper_job.status]) + ' to status ' + str(
                        Status.VALUE_TO_KEY[wrapper_job.new_status]))
                    save = True
            except Exception:
                raise AutosubmitCritical(
                    "Wrapper is in Unknown Status couldn't get wrapper parameters", 7050)

            # New status will be saved and inner_jobs will be checked.
            wrapper_job.check_status(
                wrapper_job.new_status)
            # Erase from packages if the wrapper failed to be queued ( Hold Admin bug )
            if wrapper_job.status == Status.WAITING:
                job_list.job_package_map.pop(
                    wrapper_id, None)
                job_list.packages_dict.pop(
                    wrapper_id, None)
            save = True
        return wrapper_job, save

    @staticmethod
    def wrapper_notify(as_conf, expid, wrapper_job):
        if as_conf.get_notifications() == "true":
            for inner_job in wrapper_job.job_list:
                Autosubmit.job_notify(as_conf, expid, inner_job, inner_job.prev_status, {})

    @staticmethod
    def job_notify(as_conf, expid, job, job_prev_status, job_changes_tracker):
        job_changes_tracker[job.name] = (job_prev_status, job.status)
        if as_conf.get_notifications() == "true":
            if Status.VALUE_TO_KEY[job.status] in job.notify_on:
                Notifier.notify_status_change(MailNotifier(BasicConfig), expid, job.name,
                                              Status.VALUE_TO_KEY[job_prev_status],
                                              Status.VALUE_TO_KEY[job.status],
                                              as_conf.experiment_data["MAIL"]["TO"])
        return job_changes_tracker

    @staticmethod
    def check_wrappers(
            as_conf: AutosubmitConfig,
            job_list: JobList,
            platforms_to_test: set[Platform],
            expid: str,
    ) -> tuple[dict[str, list[list[Job]]], dict[str, tuple[Status, Status]]]:
        """Check wrappers and inner jobs status also order the non-wrapped jobs to be submitted by active platforms.

        :param as_conf: a AutosubmitConfig object
        :param job_list: a JobList object
        :param platforms_to_test: a list of Platform
        :param expid: a string with the experiment id
        :return: non-wrapped jobs to check and a dictionary with the changes in the jobs status
        """
        jobs_to_check: dict[str, list[list[Job]]] = defaultdict(list)
        job_changes_tracker: dict[str, tuple[Status, Status]] = dict()
        for platform_to_test in platforms_to_test:
            queuing_jobs = job_list.get_in_queue_grouped_id(platform_to_test)
            platform_name = platform_to_test.name
            Log.debug(f'Checking jobs for platform={platform_name}')
            for job_id, jobs in queuing_jobs.items():
                # Check Wrappers one-by-one
                if job_list.job_package_map and job_id in job_list.job_package_map:
                    wrapper_job, _ = Autosubmit.manage_wrapper_job(as_conf, job_list, platform_to_test, job_id)
                    # Notifications e-mail
                    Autosubmit.wrapper_notify(as_conf, expid, wrapper_job)
                    # Detect and store changes for the GUI
                    job_changes_tracker = {job.name: (
                        job.prev_status, job.status) for job in wrapper_job.job_list if
                        job.prev_status != job.status}
                else:
                    # TODO: ``JobList.get_in_queue_grouped_id`` appears to return only jobs that are submitted,
                    #       running, queuing, unknown, and held. Is it possible to have failed jobs too?
                    # Adds to a list all running jobs to be checked. Failed jobs are ignored.
                    for job in [j for j in jobs if j.status != Status.FAILED]:
                        job_prev_status = job.status
                        # If exist key has been pressed and previous status was running, do not check
                        if not Autosubmit.exit:
                            jobs_to_check[platform_name].append([job, job_prev_status])
        return jobs_to_check, job_changes_tracker

    @staticmethod
    def check_wrapper_stored_status(as_conf: AutosubmitConfig, job_list: JobList, wrapper_wallclock: str) -> JobList:
        """Check if the wrapper job has been submitted and the inner jobs are in the queue after a load.

        :param as_conf: A BasicConfig object.
        :type as_conf: BasicConfig
        :param job_list: A JobList object.
        :type job_list: JobList
        :param wrapper_wallclock: The wallclock of the wrapper.
        :type wrapper_wallclock: str
        :return: Updated JobList object.
        :rtype: JobList
        """
        # if packages_dict attr is in job_list
        if hasattr(job_list, "packages_dict"):
            wrapper_status = Status.SUBMITTED
            for package_name, jobs in job_list.packages_dict.items():
                from .job.job import WrapperJob
                # Ordered by higher priority status
                if all(job.status == Status.COMPLETED for job in jobs):
                    wrapper_status = Status.COMPLETED
                elif any(job.status == Status.RUNNING for job in jobs):
                    wrapper_status = Status.RUNNING
                elif any(job.status == Status.FAILED for job in
                         jobs):  # No more inner jobs running but inner job in failed
                    wrapper_status = Status.FAILED
                elif any(job.status == Status.QUEUING for job in jobs):
                    wrapper_status = Status.QUEUING
                elif any(job.status == Status.HELD for job in jobs):
                    wrapper_status = Status.HELD
                elif any(job.status == Status.SUBMITTED for job in jobs):
                    wrapper_status = Status.SUBMITTED

                wrapper_job = WrapperJob(package_name, jobs[0].id, wrapper_status, 0, jobs,
                                         wrapper_wallclock, jobs[0].platform, as_conf, jobs[0].hold)
                job_list.job_package_map[jobs[0].id] = wrapper_job
        return job_list

    @staticmethod
    def get_historical_database(expid, job_list, as_conf):
        """Get the historical database for the experiment.

        :param expid: a string with the experiment id
        :param job_list: a JobList object
        :param as_conf: a AutosubmitConfig object
        :return: an experiment history object
        """
        exp_history = None
        try:
            # Historical Database: Can create a new run if there is a difference in the number of jobs or if the current run does not exist.
            exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                            historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
            exp_history.initialize_database()
            exp_history.process_status_changes(job_list.get_job_list(), as_conf.get_chunk_size_unit(),
                                               as_conf.get_chunk_size(),
                                               current_config=as_conf.get_full_config_as_json())
            Autosubmit.database_backup(expid)
        except Exception:
            try:
                Autosubmit.database_fix(expid)
                # This error is important
            except Exception:
                pass
        try:
            ExperimentStatus(expid).set_as_running()
        except Exception as e:
            # Connection to status database ec_earth.db can fail.
            # API worker will fix the status.
            Log.debug(f"Autosubmit couldn't set your experiment as running on the autosubmit times database: "
                      f"{os.path.join(BasicConfig.DB_DIR, BasicConfig.AS_TIMES_DB)}. Exception: {str(e)}", 7003)
        return exp_history

    @staticmethod
    def process_historical_data_iteration(job_list, job_changes_tracker, expid):
        """Process the historical data for the current iteration.

        :param job_list: a JobList object.
        :param job_changes_tracker: a dictionary with the changes in the job status.
        :param expid: a string with the experiment id.
        :return: an ExperimentHistory object.
        """

        exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                        historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
        if len(job_changes_tracker) > 0:
            exp_history.process_job_list_changes_to_experiment_totals(job_list.get_job_list())
            Autosubmit.database_backup(expid)
        return exp_history

    @staticmethod
    def prepare_run(
            expid: str,
            start_time: Optional[str] = None,
            start_after: Optional[str] = None,
            run_only_members: Optional[str] = None,
            recover: bool = False,
            check_scripts: bool = False,
            submitter: Optional[ParamikoSubmitter] = None
    ) -> tuple[
        JobList,
        ParamikoSubmitter,
        Optional[ExperimentHistory],
        Optional[str],
        AutosubmitConfig,
        set[Platform],
        JobPackagePersistence,
        bool,
    ]:
        """Prepare the run of the experiment.

        :param expid: a string with the experiment id.
        :param start_time: a string with the starting time of the experiment.
        :param start_after: a string with the experiment id to start after.
        :param run_only_members: a string with the members to run.
        :param recover: a boolean to indicate if the experiment is recovering from a failure.
        :param check_scripts: Whether to check the scripts before submitting.
        :param submitter: the actual loaded platforms if any
        :return: a Union
        """
        host = platform.node()
        # Init the AutosubmitConfig and check that every file exists, and it is a valid configuration.
        as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
        as_conf.check_conf_files(running_time=True, force_load=True)
        if not recover:
            # Database stuff, to check if the experiment is active or not.
            try:
                # Handling starting time
                AutosubmitHelper.handle_start_time(start_time)
                # Start after completion trigger block
                AutosubmitHelper.handle_start_after(start_after, expid)
                # Handling run_only_members
            except AutosubmitCritical:
                raise
            except BaseException as e:
                raise AutosubmitCritical("Failure during setting the start time check trace for details", 7014, str(e))
            os.system('clear')
            signal.signal(signal.SIGINT, signal_handler)
            # The time between running iterations, default to 10 seconds. Can be changed by the user
            safetysleeptime = as_conf.get_safetysleeptime()
            retrials = as_conf.get_retrials()
            Log.debug(f"The Experiment name is: {expid}")
            Log.debug(f"Sleep: {safetysleeptime}")
            Log.debug(f"Default retrials: {retrials}")
            # Is where Autosubmit stores the job_list and wrapper packages to the disc.
            pkl_dir = os.path.join(
                BasicConfig.LOCAL_ROOT_DIR, expid, 'pkl')
            Log.debug(
                f"Starting from job list restored from {pkl_dir} files")

        if not submitter:
            submitter = ParamikoSubmitter(as_conf=as_conf)
        # Tries to load the job_list from disk, discarding any changes in running time ( if recovery ).
        # Could also load a backup from previous iteration.
        # The submit ready functions will cancel all job submitted if one submitted in that iteration had issues,
        # so it should be safe to recover from a backup without losing job ids
        if recover:
            Log.info("Recovering job_list")
        try:
            job_list = Autosubmit.load_job_list(expid, as_conf, new=False)
        except IOError as e:
            raise AutosubmitError(
                "Job_list not found", 6016, str(e))
        except AutosubmitCritical as e:
            raise AutosubmitCritical(
                "Corrupted job_list, backup couldn't be restored", 7040, e.message)
        except BaseException as e:
            Log.debug(f"Error while loading job_list: {str(e)}")
            raise AutosubmitCritical(
                "Corrupted job_list, backup couldn't be restored", 7040, str(e))
        Log.debug(f"Length of the jobs list: {len(job_list)}")
        if recover:
            Log.info("Recovering parameters info")
        # This function name is not clear after the transformation it received across years.
        # What it does, is to load and transform all as_conf.experiment_data into a 1D dict stored in job_list object.
        Autosubmit._load_parameters(
            as_conf, job_list, submitter.platforms)
        Log.debug("Checking experiment templates...")
        platforms_to_test = set()
        hpcarch = as_conf.get_platform()
        # Load only platforms used by the experiment, by looking at JOBS.$JOB.PLATFORM.
        # So Autosubmit only establishes connections to the machines that are used.
        # Also, it ignores platforms used by "COMPLETED/FAILED" jobs as they are no need any more.
        # ( in case of recovery or run a workflow that were already running )
        for job in job_list.get_job_list():
            if job.platform_name is None or job.platform_name == "":
                job.platform_name = hpcarch
            # noinspection PyTypeChecker
            try:
                job.platform = submitter.platforms[job.platform_name.upper()]
            except Exception:
                raise AutosubmitCritical(f"hpcarch={job.platform_name} not found in the platforms configuration file",
                                         7014)
            # noinspection PyTypeChecker
            if job.status not in (Status.COMPLETED, Status.SUSPENDED):
                platforms_to_test.add(job.platform)
        # This function, looks at %JOBS.$JOB.FILE% ( mandatory ) and %JOBS.$JOB.CHECK% ( default True ).
        # Checks the contents of the .sh/.py/r files and looks for AS placeholders.
        try:
            if check_scripts:
                job_list.check_scripts(as_conf)
        except Exception as e:
            raise AutosubmitCritical("Error while checking job templates", 7014, str(e))
        Log.debug("Loading job packages")
        # Packages == wrappers and jobs inside wrappers. Name is also misleading.
        try:
            packages_persistence = JobPackagePersistence(expid)
        except IOError as e:
            raise AutosubmitError("job_packages not found", 6016, str(e))
        # Check if the user wants to continue using wrappers and loads the appropriate info.
        if as_conf.experiment_data.get("WRAPPERS", None) is not None:
            if BasicConfig.DATABASE_BACKEND == 'sqlite':
                os.chmod(os.path.join(BasicConfig.LOCAL_ROOT_DIR,
                                      expid, "pkl", "job_packages_" + expid + ".db"), 0o644)
            try:
                packages = packages_persistence.load()
            except IOError as e:
                raise AutosubmitError("job_packages not found", 6016, str(e))
            Log.debug("Processing job packages")
            try:
                # fallback value, only affects to is_overclock
                wrapper_wallclock = as_conf.experiment_data.get("CONFIG", {}).get("WRAPPERS_WALLCLOCK", "48:00")
                for (exp_id, package_name, job_name, wrapper_wallclock) in packages:
                    if package_name not in job_list.packages_dict:
                        job_list.packages_dict[package_name] = []
                    job_list.packages_dict[package_name].append(job_list.get_job_by_name(job_name))
                # This function, checks the stored STATUS of jobs inside wrappers.
                # Since "wrapper status" is a memory variable.
                job_list = Autosubmit.check_wrapper_stored_status(as_conf, job_list, wrapper_wallclock)
            except Exception as e:
                raise AutosubmitCritical(
                    "Autosubmit failed while processing job packages. This might be due to a change in "
                    "your experiment configuration files after 'autosubmit create' was performed.",
                    7014, str(e))
        if recover:
            Log.info("Recovering wrappers... Done")

        Log.debug("Checking job_list current status")
        job_list.update_list(as_conf, first_time=True)
        job_list.clear_generate()
        job_list.save()
        as_conf.save()
        if not recover:
            Log.info(f"Autosubmit is running with v{Autosubmit.autosubmit_version}")
            # Before starting main loop, setup historical database tables and main information
        # Check if the user has launch autosubmit run with -rom option ( previously named -rm )
        allowed_members = AutosubmitHelper.get_allowed_members(run_only_members, as_conf)
        if allowed_members:
            # Set allowed members after checks have been performed.
            # This triggers the setter and main logic of the -rm feature.
            job_list.run_members = allowed_members
            Log.result(f"Only jobs with member value in {str(allowed_members)} or no member will be allowed in this "
                       f"run. Also, those jobs already SUBMITTED, QUEUING, or RUNNING will be allowed to complete and"
                       f" will be tracked.")
        if not recover:
            # This function, looks at the "TWO_STEP_START" variable in the experiment configuration file.
            # This may not be necessary anymore as the same can be achieved by using the new DEPENDENCIES dict.
            # I replicated the same functionality in the new DEPENDENCIES dict using crossdate wrappers of
            # auto-monarch da ( documented in rst .)
            # We can look at it when auto-monarch starts to use AS 4.0, now it is maintained for compatibility.
            unparsed_two_step_start = as_conf.get_parse_two_step_start()
            if unparsed_two_step_start != "":
                job_list.parse_jobs_by_filter(unparsed_two_step_start)
            Log.debug("Running job data structure")
            exp_history = Autosubmit.get_historical_database(expid, job_list, as_conf)
            # establish the connection to all platforms
            # Restore is misleading, it is actually a "connect" function when the recover flag is not set.
            Autosubmit.restore_platforms(platforms_to_test, as_conf=as_conf)
            return job_list, submitter , exp_history, host , as_conf, list(platforms_to_test), packages_persistence, False
        else:
            return job_list, submitter, None, None, as_conf, list(platforms_to_test), packages_persistence, True

    @staticmethod
    def get_iteration_info(as_conf, job_list):
        """Prints the current iteration information.

        :param as_conf: autosubmit configuration object
        :param job_list: job list object
        :return: common parameters for the iteration
        """
        total_jobs = len(job_list.get_job_list())
        Log.info(f"\n\n{(total_jobs - len(job_list.get_completed()))} of {total_jobs} jobs remaining "
                 f"({time.strftime('%H:%M')})")
        if len(job_list.get_failed()) > 0:
            Log.info(f"{len(job_list.get_failed())} jobs has been  failed ({time.strftime('%H:%M')})")
        safetysleeptime = as_conf.get_safetysleeptime()
        default_retrials = as_conf.get_retrials()
        check_wrapper_jobs_sleeptime = as_conf.get_wrapper_check_time()
        Log.debug(f"Sleep: {safetysleeptime}")
        Log.debug(f"Number of retrials: {default_retrials}")
        return total_jobs, safetysleeptime, default_retrials, check_wrapper_jobs_sleeptime

    @staticmethod
    def check_logs_status(job_list: "JobList", as_conf, new_run):
        for job in job_list.get_completed_failed_without_logs():
            if new_run:
                job.platform.spawn_log_retrieval_process(as_conf)

            job_list.update_log_status(job, as_conf, new_run)

    @staticmethod
    def refresh_log_recovery_process(platforms: list[Platform], as_conf: AutosubmitConfig) -> None:
        """Relaunch the log recovery processes for each platform if necessary."""
        for p in platforms:  # Send keep_alive signal
            if not p.log_recovery_process or not p.log_recovery_process.is_alive():
                p.clean_log_recovery_process()
                p.spawn_log_retrieval_process(as_conf)
            p.work_event.set()

    @staticmethod
    def run_experiment(expid: str, start_time: Optional[str] = None, start_after: Optional[str] = None,
                       run_only_members: Optional[str] = None, profile=False) -> int:
        """Runs and experiment (submitting all the jobs properly and repeating its execution in case of failure).

        :param expid: the experiment id
        :param start_time: the time at which the experiment should start
        :param start_after: the expid after which the experiment should start
        :param run_only_members: the members to run
        :param profile: if True, the function will be profiled
        :return: exit status

        """
        # Start profiling if the flag has been used
        if profile:
            from .profiler.profiler import Profiler
            profiler = Profiler(expid)
            profiler.start()

        # Initialize common folders'
        try:
            exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
            tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
        except BaseException as e:
            raise AutosubmitCritical("Failure during the loading of the experiment configuration, check file paths",
                                     7014, str(e))

        try:
            with Lock(os.path.join(tmp_path, 'autosubmit.lock'), timeout=1):
                try:
                    Log.debug("Preparing run")
                    # This function is called only once, when the experiment is started. It is used to initialize the experiment and to check the correctness of the configuration files.
                    # If there are issues while running, this function will be called again to reinitialize the experiment.
                    job_list, submitter, exp_history, host, as_conf, platforms_to_test, packages_persistence, _ = Autosubmit.prepare_run(
                        expid, start_time, start_after, run_only_members)
                except AutosubmitCritical:
                    # e.message += " HINT: check the CUSTOM_DIRECTIVE syntax in your jobs configuration files."
                    raise
                except Exception as e:
                    raise AutosubmitCritical("Error in run initialization", 7014, str(e))  # Changing default to 7014

                as_conf_config = as_conf.experiment_data.get('CONFIG', {})
                git_operational_check_enabled = as_conf_config.get('GIT_OPERATIONAL_CHECK_ENABLED', True)

                if git_operational_check_enabled:
                    Log.debug('Checking for dirty local Git repository')
                    check_unpushed_changes(expid, as_conf)
                else:
                    Log.warning('Git operational check disabled by user')

                Log.debug("Running main running loop")
                Log.warning("Known issue: Due to recent changes in Autosubmit's script generation, error line numbers in "
                            "`script.cmd.err` files may be offset by ~5 lines. Please adjust accordingly when debugging.")
                did_run = False
                #########################
                # AUTOSUBMIT - MAIN LOOP
                #########################
                # Main loop
                # Recovery retrials, when platforms have issues. Hard limit is set just in case is an Autosubmit bug or bad config and the minium duration is the weekend (72 h).
                # Run experiment steps:
                # 0. Prepare the experiment to start running it.
                # 1. Check if there are jobs in the workflow that has to run (get_active)
                # For each platform:
                #  2. Check the status of all jobs in the current workflow that are queuing or running. Also updates all workflow jobs status by checking the status in the platform machines and job parent status.
                #  3. Submit jobs that are on ready status.
                # 4. When there are no more active jobs, wait until all log recovery threads finishes and exit Autosubmit.
                # In case of issues, the experiment is reinitialized and the process starts with the last non-corrupted workflow status.
                # User can always stop the run, and unless force killed, Autosubmit will exit in a clean way.
                # Experiment run will always start from the last known workflow status.

                max_recovery_retrials = as_conf.experiment_data.get("CONFIG", {}).get("RECOVERY_RETRIALS",
                                                                                      3650)  # (72h - 122h )
                recovery_retrials = 0
                Autosubmit.check_logs_status(job_list, as_conf, new_run=True)
                while job_list.get_active():
                    try:
                        if Autosubmit.exit:
                            Autosubmit.check_logs_status(job_list, as_conf, new_run=False)
                            if job_list.get_failed():
                                return 1
                            return 0
                        Autosubmit.refresh_log_recovery_process(platforms_to_test, as_conf)
                        for job in job_list.get_ready():
                            job.update_parameters(as_conf, set_attributes=True)
                        did_run = True
                        # reload parameters changes
                        Log.debug("Reloading parameters...")
                        try:
                            # This function name is not clear after the transformation it received across years.
                            # What it does, is to load and transform all as_conf.experiment_data into a 1D dict stored in job_list object.
                            Autosubmit._load_parameters(as_conf, job_list, submitter.platforms)
                        except BaseException as e:
                            raise AutosubmitError("Config files seems to not be accessible", 6040, str(e))
                        total_jobs, safetysleeptime, default_retrials, check_wrapper_jobs_sleeptime = Autosubmit.get_iteration_info(
                            as_conf, job_list)

                        # This function name is totally misleading, yes it check the status of the wrappers,
                        # but also orders jobs the jobs that  are not wrapped by platform.
                        jobs_to_check, job_changes_tracker = Autosubmit.check_wrappers(as_conf, job_list,
                                                                                       platforms_to_test, expid)
                        # Jobs to check are grouped by platform.
                        # platforms_to_test could be renamed to active_platforms or something like that.
                        for platform in platforms_to_test:
                            platform_jobs = jobs_to_check.get(platform.name, [])
                            if len(platform_jobs) == 0:
                                Log.info(f"No jobs to check for platform {platform.name}")
                                continue

                            Log.info(f"Checking {len(platform_jobs)} jobs for platform {platform.name}")
                            # Check all non-wrapped jobs status for the current platform
                            platform.check_all_jobs(platform_jobs, as_conf)
                            # mail notification ( in case of changes )
                            for job, job_prev_status in jobs_to_check[platform.name]:
                                if job_prev_status != job.update_status(as_conf):
                                    Autosubmit.job_notify(as_conf, expid, job, job_prev_status, job_changes_tracker)
                        # Updates all workflow status with the new information.
                        job_list.update_list(as_conf, submitter=submitter)
                        job_list.save()
                        # Submit jobs that are ready to run
                        if len(job_list.get_ready()) > 0:
                            Autosubmit.submit_ready_jobs(as_conf, job_list, platforms_to_test, packages_persistence,
                                                         hold=False)
                            job_list.update_list(as_conf, submitter=submitter)
                            job_list.save()
                            as_conf.save()

                        # Safe spot to store changes
                        try:
                            exp_history = Autosubmit.process_historical_data_iteration(job_list, job_changes_tracker,
                                                                                       expid)
                        except BaseException:
                            Log.printlog("Historic database seems corrupted, AS will repair it and resume the run",
                                         Log.INFO)
                            try:
                                Autosubmit.database_fix(expid)
                                exp_history = Autosubmit.process_historical_data_iteration(job_list,
                                                                                           job_changes_tracker, expid)
                            except Exception:
                                Log.warning(
                                    "Couldn't recover the Historical database, AS will continue without it, GUI may be affected")
                        if Autosubmit.exit:
                            Autosubmit.check_logs_status(job_list, as_conf, new_run=False)
                            job_list.save()
                            as_conf.save()
                        time.sleep(safetysleeptime)
                    except AutosubmitError as e:  # If an error is detected, restore all connections and job_list
                        Log.error(f"Trace: {e.trace}")
                        Log.error(f"{e.message} [eCode={e.code}]")
                        # No need to wait until the remote platform reconnection
                        recovery = False
                        as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
                        consecutive_retrials = 1
                        failed_names = {}
                        Log.info("Storing failed job count...")
                        try:
                            for job in job_list.get_job_list():
                                if job.fail_count > 0:
                                    failed_names[job.name] = job.fail_count
                        except BaseException:
                            Log.printlog("Error trying to store failed job count", Log.WARNING)
                        Log.result("Storing failed job count...done")
                        while not recovery and (
                                recovery_retrials < max_recovery_retrials or max_recovery_retrials <= 0):
                            delay = min(15 * consecutive_retrials, 120)
                            recovery_retrials += 1
                            sleep(delay)
                            consecutive_retrials = consecutive_retrials + 1
                            Log.info(f"Waiting {delay} seconds before continue")
                            try:
                                job_list, submitter, _, _, as_conf, platforms_to_test, packages_persistence, recovery = Autosubmit.prepare_run(
                                    expid,
                                    start_time,
                                    start_after,
                                    run_only_members,
                                    recover=True, submitter=submitter)
                            except AutosubmitError as e:
                                recovery = False
                                Log.result(f"Recover of job_list has fail {e.message}")
                            except IOError as e:
                                recovery = False
                                Log.result(f"Recover of job_list has fail {str(e)}")
                            except BaseException as e:
                                recovery = False
                                Log.result(f"Recover of job_list has fail {str(e)}")
                        # Restore platforms and try again, to avoid endless loop with failed configuration, a hard limit is set.
                        reconnected = False
                        times = 0
                        max_times = 10
                        Log.info("Restoring the connection to all experiment platforms")
                        consecutive_retrials = 1
                        delay = min(15 * consecutive_retrials, 120)
                        while not reconnected and (
                                recovery_retrials < max_recovery_retrials or max_recovery_retrials <= 0):
                            recovery_retrials += 1
                            Log.info("Recovering the remote platform connection")
                            Log.info(f"Waiting {delay} seconds before continue")
                            sleep(delay)
                            consecutive_retrials = consecutive_retrials + 1
                            try:
                                if times % max_times == 0:
                                    mail_notify = True
                                    max_times = max_times + max_times
                                    times = 0
                                else:
                                    mail_notify = False
                                times = times + 1
                                Autosubmit.restore_platforms(platforms_to_test, mail_notify=mail_notify,
                                                             as_conf=as_conf, expid=expid)
                                reconnected = True
                            except AutosubmitCritical as e:
                                # Message prompt by restore_platforms.
                                Log.info(f"{e.message}\nCouldn't recover the platforms, retrying in 15seconds...")
                                reconnected = False
                            except IOError:
                                reconnected = False
                            except BaseException:
                                reconnected = False
                        if recovery_retrials == max_recovery_retrials and max_recovery_retrials > 0:
                            raise AutosubmitCritical(f"Autosubmit Encounter too much errors during running time, limit of {max_recovery_retrials*120} reached",
                                7051, e.message)
                    except AutosubmitCritical as e:  # Critical errors can't be recovered. Failed configuration or autosubmit error
                        raise AutosubmitCritical(e.message, e.code, e.trace)
                    except BaseException:
                        raise  # If this happens, there is a bug in the code or an exception not-well caught
                Log.result("No more jobs to run.")
                # search hint - finished run
                job_list.save()
                if not did_run and len(
                        job_list.get_completed_failed_without_logs()) > 0:  # Revise if there is any log unrecovered from previous run
                    Log.info("Connecting to the platforms, to recover missing logs")
                    submitter = ParamikoSubmitter(as_conf=as_conf)
                    if submitter.platforms is None:
                        raise AutosubmitCritical("No platforms configured!!!", 7014)
                    platforms_to_test = [value for value in submitter.platforms.values()]
                    Autosubmit.restore_platforms(platforms_to_test, as_conf=as_conf, expid=expid)
                Log.info("Waiting for all logs to be updated")
                for p in platforms_to_test:
                    if p.log_recovery_process:
                        p.cleanup_event.set()  # Send cleanup event
                        p.log_recovery_process.join()
                Autosubmit.check_logs_status(job_list, as_conf, new_run=False)
                job_list.save()
                if len(job_list.get_completed_failed_without_logs()) == 0:
                    Log.result("Autosubmit recovered all job logs.")
                else:
                    Log.warning(
                        f"Autosubmit couldn't recover the following job logs: {[job.name for job in job_list.get_completed_failed_without_logs()]}")
                try:
                    exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                                    historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
                    exp_history.process_job_list_changes_to_experiment_totals(job_list.get_job_list())
                    Autosubmit.database_backup(expid)
                except Exception:
                    try:
                        Autosubmit.database_fix(expid)
                    except Exception:
                        pass
                for p in platforms_to_test:
                    p.close_connection()
                if len(job_list.get_failed()) > 0:
                    Log.info("Some jobs have failed and reached maximum retrials")
                else:
                    Log.result("Run successful")
                    # Updating finish time for job data header
                    # Database is locked, may be related to my local db todo 4.1.1
                    try:
                        if exp_history is not None:
                            exp_history.finish_current_experiment_run()
                    except Exception:
                        Log.warning("Database is locked")
                rocrate_data = as_conf.experiment_data.get("ROCRATE", None)
                if rocrate_data:
                    Autosubmit.provenance(expid, rocrate=True)
                else:
                    Log.info("ROCRATE not present in experiment YAML configuration. No RO-Crate archive created.")
        except BaseLockException:
            raise
        except AutosubmitCritical:
            raise
        except BaseException:
            raise
        finally:
            if profile:
                profiler.stop()

        # Suppress in case ``job_list`` was not defined yet...
        with suppress(NameError):
            if job_list.get_failed():
                return 1
        return 0

    # TODO: move to utils
    @staticmethod
    def restore_platforms(platforms_to_test, mail_notify=False, as_conf=None, expid=None):
        Log.info("Checking the connection to all platforms in use")
        issues = ""
        ssh_config_issues = ""
        private_key_error = ("Please, add your private key to the ssh-agent ( ssh-add <path_to_key> ) or use "
                             "a non-encrypted key\nIf ssh agent is not initialized, prompt first eval `ssh-agent -s`")

        for platform_to_test in platforms_to_test:
            platform_issues = ""
            try:
                message = platform_to_test.test_connection(as_conf)
                if message is None:
                    message = "OK"
                if message != "OK":
                    if message.find("doesn't accept remote connections") != -1:
                        ssh_config_issues += message
                    elif message.find("Authentication failed") != -1:
                        ssh_config_issues += message + (". Please, check the user and project of this platform\n"
                                                        "If it is correct, try another host")
                    elif message.find("private key file is encrypted") != -1:
                        if private_key_error not in ssh_config_issues:
                            ssh_config_issues += private_key_error
                    elif message.find("Invalid certificate") != -1:
                        ssh_config_issues += message + ".Please, the eccert expiration date"
                    else:
                        ssh_config_issues += message + (f" this is an PARAMIKO SSHEXCEPTION: indicates that there is "
                                                        f"something incompatible in the ssh_config for host:{platform_to_test.host}\n maybe"
                                                        f" you need to contact your sysadmin")
            except Exception:
                try:
                    if mail_notify:
                        email = as_conf.get_mails_to()
                        if "@" in email[0]:
                            Notifier.notify_experiment_status(MailNotifier(BasicConfig), expid, email, platform)
                except Exception as e2:
                    Log.debug(f'Unexpected exception sending email notification: {str(e2)}')
                platform_issues += f"\n[{platform.name}] Connection Unsuccessful to host {platform.host} "
                issues += platform_issues
                continue
            if platform_to_test.check_remote_permissions():
                Log.result(f"[{platform_to_test.name}] Correct user privileges for host {platform_to_test.host}")
            else:
                platform_issues += (
                    f"\n[{platform_to_test.name}] has configuration issues.\n Check that the connection is"
                    f" passwd-less.(ssh {platform_to_test.user}@{platform_to_test.host})\n Check the parameters that"
                    f" build the root_path are correct:{{scratch_dir/project/user}} ="
                    f" {{{platform_to_test.scratch}/{platform_to_test.project}/{platform_to_test.user}}}")
                issues += platform_issues
            if platform_issues == "":

                Log.printlog(f"[{platform_to_test.name}] Connection successful to host {platform_to_test.host}",
                             Log.RESULT)
            else:
                if platform_to_test.connected:
                    platform_to_test.connected = False
                    Log.printlog(f"[{platform_to_test.name}] Connection successful to host {platform_to_test.host}, "
                                 f"however there are issues with %HPCROOT%", Log.WARNING)
                else:
                    Log.printlog(f"[{platform_to_test.name}] Connection failed to host {platform_to_test.host}",
                                 Log.WARNING)
        if issues != "":
            if ssh_config_issues.find(private_key_error[:-2]) != -1:
                raise AutosubmitCritical("Private key is encrypted, Autosubmit does not run in "
                                         "interactive mode.\nPlease, add the key to the ssh agent(ssh-add "
                                         "<path_to_key>).\nIt will remain open as long as session is active, "
                                         "for force clean you can prompt ssh-add -D",
                                         7073, issues + "\n" + ssh_config_issues)
            else:
                raise AutosubmitCritical("Issues while checking the connectivity of platforms.",
                                         7010, issues + "\n" + ssh_config_issues)

    @staticmethod
    def submit_ready_jobs(as_conf: AutosubmitConfig, job_list: JobList, platforms_to_test: list[Platform],
                          packages_persistence: JobPackagePersistence, inspect=False,
                          only_wrappers=False, hold=False) -> bool:
        """Gets READY jobs and send them to the platforms if there is available space on the queues.

        :param hold:
        :param as_conf: autosubmit config object
        :type as_conf: AutosubmitConfig object
        :param job_list: job list to check
        :type job_list: JobList object
        :param platforms_to_test: platforms used
        :type platforms_to_test: set of Platform Objects, e.g. SgePlatform(), SlurmPlatform().
        :param packages_persistence: Handles database per experiment.
        :type packages_persistence: JobPackagePersistence object
        :param inspect: True if coming from generate_scripts_andor_wrappers().
        :type inspect: Boolean
        :param only_wrappers: True if it comes from create -cw, False if it comes from inspect -cw.
        :type only_wrappers: Boolean
        :return: True if at least one job was submitted, False otherwise
        """
        save_1 = False
        save_2 = False
        wrapper_errors = {}
        any_job_submitted = False
        # Check section jobs
        if not only_wrappers and not inspect:
            jobs_section = set([job.section for job in job_list.get_ready()])
            for section in jobs_section:
                if check_jobs_file_exists(as_conf, section):
                    raise AutosubmitCritical(f"Job {section} does not have a correct template// template not found",
                                             7014)
        try:
            for platform in platforms_to_test:
                packager = JobPackager(as_conf, platform, job_list)
                packages_to_submit = packager.build_packages()
                save_1, failed_packages, error_message, valid_packages_to_submit, any_job_submitted = (
                    platform.submit_ready_jobs(as_conf, job_list, packages_persistence, packages_to_submit,
                                               inspect=inspect, only_wrappers=only_wrappers)
                )
                wrapper_errors.update(packager.wrappers_with_error)
                save_2 = False
                # Jobs that are being retrieved in batch. Right now, only available for slurm platforms.
                if platform.type.lower() in ["slurm", "pjm"] and not inspect and not only_wrappers:
                    # Process the script generated in submit_ready_jobs
                    save_2, valid_packages_to_submit = platform.process_batch_ready_jobs(valid_packages_to_submit,
                                                                                         failed_packages,
                                                                                         error_message="")
                if not inspect and len(valid_packages_to_submit) > 0:
                    job_list.save()
                # Save wrappers(jobs that has the same id) to be visualized and checked in other parts of the code
                job_list.save_wrappers(valid_packages_to_submit, failed_packages, as_conf, packages_persistence,
                                       hold=hold, inspect=inspect)
                if error_message != "":
                    raise AutosubmitCritical(f"Submission Failed due wrong configuration:{error_message}", 7014)
            Autosubmit.check_deadlock(wrapper_errors, any_job_submitted, job_list)
            return save_1 or save_2
        except Exception:
            raise

    @staticmethod
    def check_deadlock(wrapper_errors: dict, any_job_submitted: bool, job_list: JobList) -> None:
        """Check for deadlock situations and raise an exception if detected.
        :param wrapper_errors: Dictionary of wrapper errors.
        :param any_job_submitted: Boolean indicating if any job was submitted.
        :param job_list: JobList object containing the jobs.
        """
        if wrapper_errors and not any_job_submitted and len(job_list.get_in_queue()) == 0:
            # Deadlock situation
            err_msg = ""
            for wrapper in wrapper_errors:
                err_msg += f"wrapped_jobs:{wrapper} in {wrapper_errors[wrapper]}\n"
            raise AutosubmitCritical(err_msg, 7014)

    @staticmethod
    def monitor(expid: str, file_format: str, lst: str, filter_chunks: str, filter_status: str, filter_section: str,
                hide: bool, txt_only=False, group_by: Optional[str] = None, expand="", expand_status="",
                hide_groups=False, check_wrapper=False, txt_logfiles=False, profile=False) -> bool:
        """Plots workflow graph for a given experiment with status of each job coded by node color.

        Plot is created in experiment's plot folder with name <expid>_<date>_<time>.<file_format>

        :param txt_logfiles:
        :param expid: Identifier of the experiment to plot.
        :param file_format: Plot file format. It can be pdf, png, ps or svg
        :param lst: list of jobs to change status
        :param filter_chunks: chunks to change status
        :param filter_status: current status of the jobs to change status
        :param filter_section: sections to change status
        :param hide: hides plot window
        :param txt_only: workflow will only be written as text
        :param group_by: workflow will only be written as text
        :param expand: Filtering of jobs for its visualization
        :param expand_status: Filtering of jobs for its visualization
        :param hide_groups: Simplified workflow illustration by encapsulating the jobs.
        :param check_wrapper: Shows a preview of how the wrappers will look
        :param profile: Whether to enable a profiler during the command execution or not.
        """
        from .monitor.monitor import Monitor

        # Start profiling if the flag has been used
        if profile:
            from .profiler.profiler import Profiler
            profiler = Profiler(expid)
            profiler.start()

        try:
            exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
            Log.info("Getting job list...")
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            as_conf.check_conf_files(False)
            # Getting output type from configuration
            output_type = as_conf.get_output_type()
            pkl_dir = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid, 'pkl')
            job_list = Autosubmit.load_job_list(expid, as_conf, monitor=True, new=False)
            Log.debug(f"Job list restored from {pkl_dir} files")
        except AutosubmitError as e:
            if profile:
                profiler.stop()
            raise AutosubmitCritical(e.message, e.code, e.trace)
        except AutosubmitCritical:
            if profile:
                profiler.stop()
            raise
        except BaseException:
            if profile:
                profiler.stop()
            raise

        try:
            jobs = []
            if not isinstance(job_list, type([])):
                if filter_chunks:
                    fc = filter_chunks
                    Log.debug(fc)

                    if fc == 'Any':
                        jobs = job_list.get_job_list()
                    else:
                        # noinspection PyTypeChecker
                        data = json.loads(Autosubmit._create_json(fc))
                        for date_json in data['sds']:
                            date = date_json['sd']
                            jobs_date = [j for j in job_list.get_job_list() if date2str(
                                j.date) == date]

                            for member_json in date_json['ms']:
                                member = member_json['m']
                                jobs_member = [j for j in jobs_date if j.member == member]

                                for chunk_json in member_json['cs']:
                                    chunk = int(chunk_json)
                                    jobs = jobs + \
                                           [job for job in [j for j in jobs_member if j.chunk == chunk]]

                elif filter_status:
                    Log.debug(f"Filtering jobs with status {filter_status}")
                    if filter_status == 'Any':
                        jobs = job_list.get_job_list()
                    else:
                        fs = Autosubmit._get_status(filter_status)
                        jobs = [job for job in [j for j in job_list.get_job_list() if j.status == fs]]

                elif filter_section:
                    ft = filter_section
                    Log.debug(ft)

                    if ft == 'Any':
                        jobs = job_list.get_job_list()
                    else:
                        for job in job_list.get_job_list():
                            if job.section == ft:
                                jobs.append(job)

                elif lst:
                    jobs_lst = lst.split()

                    if jobs == 'Any':
                        jobs = job_list.get_job_list()
                    else:
                        for job in job_list.get_job_list():
                            if job.name in jobs_lst:
                                jobs.append(job)
                else:
                    jobs = job_list.get_job_list()
        except BaseException as e:
            if profile:
                profiler.stop()
            raise AutosubmitCritical("Issues during the job_list generation. Maybe due I/O error", 7040, str(e))

        # WRAPPERS
        try:
            if len(as_conf.experiment_data.get("WRAPPERS", {})) > 0 and check_wrapper:
                # Class constructor creates table if it does not exist
                packages_persistence = JobPackagePersistence(expid)
                # Permissions
                if BasicConfig.DATABASE_BACKEND == 'sqlite':
                    os.chmod(os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid, "pkl", "job_packages_" + expid + ".db"),
                             0o644)
                # Database modification
                packages_persistence.reset_table(True)
                # Load another job_list to go through that goes through the jobs, but we want to monitor the other one
                job_list_wr = Autosubmit.load_job_list(expid, as_conf, monitor=True, new=False)
                Autosubmit.generate_scripts_andor_wrappers(as_conf, job_list_wr, job_list_wr.get_job_list(),
                                                           packages_persistence, True)

                packages = packages_persistence.load(True)
                packages += JobPackagePersistence(expid).load()
            else:
                packages = JobPackagePersistence(expid).load()
        except BaseException as e:
            if profile:
                profiler.stop()
            raise AutosubmitCritical("Issues during the wrapper loading, may be related to IO issues", 7040, str(e))

        groups_dict = dict()
        try:
            if group_by:
                status = list()
                if expand_status:
                    for s in expand_status.split():
                        status.append(Autosubmit._get_status(s.upper()))

                job_grouping = JobGrouping(group_by, copy.deepcopy(
                    jobs), job_list, expand_list=expand, expanded_status=status)
                groups_dict = job_grouping.group_jobs()
        except BaseException as e:
            if profile:
                profiler.stop()
            raise AutosubmitCritical(
                "Jobs can't be grouped, perhaps you're using an invalid format. Take a look into readthedocs", 7011,
                str(e))

        monitor_exp = Monitor()
        try:
            if txt_only or txt_logfiles or file_format == "txt":
                monitor_exp.generate_output_txt(expid, jobs, os.path.join(
                    exp_path, "/tmp/LOG_" + expid), txt_logfiles, job_list_object=job_list)
                if txt_only:
                    current_length = len(job_list.get_job_list())
                    if current_length > 1000:
                        Log.info(
                            "Experiment has too many jobs to be printed in the terminal. Maximum job quantity is 1000, your experiment has " + str(
                                current_length) + " jobs.")
                    else:
                        Log.info(job_list.print_with_status())
            else:
                # if file_format is set, use file_format, otherwise use conf value
                monitor_exp.generate_output(expid,
                                            jobs,
                                            os.path.join(
                                                exp_path, "/tmp/LOG_", expid),
                                            output_format=file_format if file_format is not None and len(
                                                str(file_format)) > 0 else output_type,
                                            packages=packages,
                                            show=not hide,
                                            groups=groups_dict,
                                            hide_groups=hide_groups,
                                            job_list_object=job_list)
        except BaseException as e:
            raise AutosubmitCritical(
                "An error has occurred while printing the workflow status. Check if you have X11 redirection and an img viewer correctly set",
                7014, str(e))
        finally:
            if profile:
                profiler.stop()

        return True

    @staticmethod
    def statistics(expid: str, filter_type: str, filter_period: int, file_format: str, section_summary: bool,
                   jobs_summary: bool, hide: bool) -> bool:
        """Plots statistics graph for a given experiment.
        Plot is created in experiment's plot folder with name <expid>_<date>_<time>.<file_format>

        :param expid: Identifier of the experiment to plot.
        :param filter_type: type of the jobs to plot
        :param filter_period: period to plot
        :param file_format: plot's file format. It can be pdf, png, ps or svg
        :param section_summary: shows summary statistics
        :param jobs_summary: shows jobs statistics
        :param hide: hides plot window
        """
        from .monitor.monitor import Monitor

        try:
            Log.info("Loading jobs...")
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            as_conf.check_conf_files(False)

            pkl_dir = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid, 'pkl')
            job_list = Autosubmit.load_job_list(expid, as_conf, new=False)
            for job in job_list.get_job_list():
                job._init_runtime_parameters()
                job.update_dict_parameters(as_conf)
            Log.debug(f"Job list restored from {pkl_dir} files")
            jobs = StatisticsUtils.filter_by_section(job_list.get_job_list(), filter_type)
            jobs, period_ini, period_fi = StatisticsUtils.filter_by_time_period(jobs, filter_period)
            # Package information
            job_to_package, package_to_jobs, _, _ = JobList.retrieve_packages(BasicConfig, expid, [job.name for job in
                                                                                                   job_list.get_job_list()])
            queue_time_fixes = {}
            if job_to_package:
                current_table_structure = get_structure(expid, Path(BasicConfig.STRUCTURES_DIR))
                subjobs = []
                for job in job_list.get_job_list():
                    job_info = JobList.retrieve_times(job.status, job.name, job._tmp_path, make_exception=True,
                                                      job_times=None, seconds=True, job_data_collection=None)
                    time_total = (job_info.queue_time + job_info.run_time) if job_info else 0
                    subjobs.append(
                        SubJob(job.name,
                               job_to_package.get(job.name, None),
                               job_info.queue_time if job_info else 0,
                               job_info.run_time if job_info else 0,
                               time_total,
                               job_info.status if job_info else Status.UNKNOWN)
                    )
                queue_time_fixes = SubJobManager(subjobs, job_to_package, package_to_jobs,
                                                 current_table_structure).get_collection_of_fixes_applied()

            if len(jobs) > 0:
                try:
                    Log.info("Plotting stats...")
                    monitor_exp = Monitor()
                    # noinspection PyTypeChecker
                    report_created = monitor_exp.generate_output_stats(expid, jobs, file_format, hide, section_summary,
                                                                       jobs_summary, period_ini, period_fi,
                                                                       queue_time_fixes)
                    report_message = "Statistics plot ready" if report_created else "No statistics plot produced."
                    Log.result(report_message)
                except Exception as e:
                    raise AutosubmitCritical(
                        "Stats couldn't be shown", 7061, str(e))
            else:
                Log.info(f"There are no {filter_type} jobs in the period from {period_ini} to {period_fi}...")
        except BaseException as e:
            raise AutosubmitCritical("Stats couldn't be generated. Check trace for more details", 7061, str(e))
        return True

    @staticmethod
    def clean(expid: str, project: bool, plot: bool, stats: bool) -> bool:
        """Clean the experiment directory to save storage space.

        It removes project directory and outdated plots or stats.

        When it deletes plots or stats, it keeps the two newest files
        (i.e. two newest statistics files, and two newest plot files).

        :param expid: The experiment ID.
        :type: str
        :param project: Whether to clean the experiment project folder or not.
        :type project: bool
        :param plot: Whether to delete plot files (keeping two newest) or not.
        :type plot: bool
        :param stats: Whether to delete statistics files (keeping two newest) or not.
        :type stats: bool
        :return: ``True`` is the command ran successfully and ``False`` otherwise.
        :rtype: bool
        :raises: AutosubmitCritical if anything goes wrong cleaning the experiment folders.
        """
        from .monitor.monitor import clean_plot, clean_stats

        try:
            if project:
                autosubmit_config = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
                autosubmit_config.check_conf_files(False)

                project_type = autosubmit_config.get_project_type()
                if project_type == "git":
                    Log.info("Cleaning GIT directory...")
                    if not clean_git(autosubmit_config):
                        return False
                    Log.result("Git project cleaned!\n")
                else:
                    Log.info("No project to clean...\n")
            if plot:
                Log.info("Cleaning plots...")
                clean_plot(expid)
                Log.result("Plots cleaned!\n")
            if stats:
                Log.info("Cleaning stats directory...")
                clean_stats(expid)
                Log.result("Plots cleaned!\n")
        except BaseException as e:
            raise AutosubmitCritical("Couldn't clean this experiment, check if you have the correct permissions", 7012,
                                     str(e))
        return True

    @staticmethod
    def online_recovery(as_conf: AutosubmitConfig, platforms: list[ParamikoPlatform], job_list: JobList, offline: bool = False) -> list[str]:
        """Return a list of completed job names recovered from the given platforms.

        Test each platform connection and collect completed job names. If a platform
        is not reachable and ``offline`` is True, recover completed jobs from the
        experiment history for that platform. On unrecoverable connection errors
        raise ``AutosubmitCritical``.

        :param as_conf: Autosubmit configuration object.
        :param platforms: Sequence of Platform objects to query.
        :param job_list: JobList used to recover completed jobs when offline.
        :param offline: If True, proceed with offline recovery when a platform is not reachable.
        :return: List of completed job names (may be empty).
        :raises AutosubmitCritical: If a platform is unreachable and offline is False,
                                    or if fetching completed job names fails.
        """
        completed_jobnames = set()
        for p in platforms:
            message = p.test_connection(as_conf)
            if not p.connected:
                if offline:
                    Log.warning(f"Platform {p.name} is not reachable, proceeding with offline recovery for this platform")
                    completed_jobnames.update(job_list.recover_all_completed_jobs_from_exp_history(p))
                else:
                    raise AutosubmitCritical(f"Couldn't connect to platform {p.name} during recovery: {message}", 7050)
            else:
                # Fetch completed jobs from platform
                completed_jobnames.update(p.get_completed_job_names())

        return list(completed_jobnames)

    @staticmethod
    def recovery(
            expid: str,
            noplot: bool,
            save: bool,
            all_jobs: bool,
            hide: bool,
            group_by: Optional[str] = None,
            expand: Optional[list[str]] = None,
            expand_status: Optional[list[str]] = None,
            detail: bool = False,
            force: bool = False,
            offline: bool = False,
    ) -> bool:
        """Recover job statuses for an experiment and update the job list.

        Return True when the recovery completed successfully.

        :param expid: Experiment identifier.
        :param noplot: If True, do not generate a plot.
        :param save: If True, persist changes to the job list.
        :param all_jobs: If True, recover all jobs; otherwise only active jobs.
        :param hide: If True, hide GUI/windows when generating plots.
        :param group_by: Optional grouping key for display.
        :param expand: Optional list of job names/sections to expand in the view.
        :param expand_status: Optional list of statuses to expand in the view.
        :param detail: If True, produce a more detailed (and more expensive) textual representation.
        :param force: If True, cancel active jobs before recovery.
        :param offline: If True, avoid connecting to remote platforms and use offline recovery.
        :return: True if recovery ran successfully, False otherwise.
        :raises AutosubmitCritical: On configuration/IO failures.
        """
        if not save:
            Log.warning("Changes will be NOT saved to the jobList. Use -s option to save")

        check_ownership(expid, raise_error=True)
        exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
        as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
        as_conf.check_conf_files(True)
        Log.info(f'Recovering experiment {expid}')
        job_list = Autosubmit.load_job_list(expid, as_conf, new=False, monitor=True)
        as_conf.check_conf_files(False)

        # Getting output type provided by the user in config, 'pdf' as default
        hpcarch = as_conf.get_platform()

        submitter = Autosubmit._get_submitter(as_conf)
        submitter.load_platforms(as_conf)
        platforms = submitter.platforms

        platforms_to_test = set()
        for job in job_list.get_job_list():
            job.submitter = submitter
            if not job.platform_name:
                job.platform_name = hpcarch
            job.platform = platforms[job.platform_name]
            platforms_to_test.add(job.platform)

        completed_jobnames = Autosubmit.online_recovery(as_conf, platforms_to_test, job_list, offline)
        current_active_jobs = job_list.get_in_queue()
        if current_active_jobs and not (force and save):
            raise AutosubmitCritical(f"Experiment can't be recovered due being {len(current_active_jobs)} "
                                     f"active jobs in your experiment, If you want to recover the experiment,"
                                     f" please use the flag -f and all active jobs will be cancelled. "
                                     f"Be warned that -f and --offline won't cancel jobs if the connection can't be established", 7053)
        elif current_active_jobs and force and save and not offline:
            all_connected = True
            for p in platforms_to_test:
                if not p.connected:
                    all_connected = False
                    Log.warning(f"Platform {p.name} is not reachable")
            if not all_connected:
                raise AutosubmitCritical("Some platforms are not reachable, cannot proceed with forced recovery. You can add --offline with -f to don't cancel jobs", 7050)
        # TODO: https://github.com/BSC-ES/autosubmit/issues/1251 don't need force flag
        if save:
            offline_jobs = []
            for job in current_active_jobs:
                if offline or not job.platform.connected:
                    offline_jobs.append(job.name)
                else:
                    job.platform_name = as_conf.jobs_data.get(job.section, {}).get("PLATFORM", hpcarch).upper()
                    # noinspection PyTypeChecker
                    job.platform = submitter.platforms[job.platform_name]
                    try:
                        job.platform.send_command(f"{job.platform.cancel_cmd} {job.id}", ignore_log=True)
                    except AutosubmitError as e:
                        # Not sure if this is the best way to check for invalid job id error for non-slurm
                        if "invalid job id" in e.message.lower():
                            Log.warning(f"Job {job.name} could not be cancelled because it was not found on the platform")
                        else:
                            Log.warning(f"Job {job.name} could not be cancelled: {e.message}")
            if offline_jobs:
                Log.warning(f"Jobs {''.join(offline_jobs)} could not be cancelled due to offline mode.")

        output_type = as_conf.get_output_type()
        Log.info(f'Recovering experiment {expid}')

        if all_jobs:
            jobs_to_recover = job_list.get_job_list()
        else:
            jobs_to_recover = job_list.get_active()

        try:
            for job in jobs_to_recover:
                if job.name in completed_jobnames:
                    job.status = Status.COMPLETED
                    Log.info(f"CHANGED job '{job.name}' status to COMPLETED")

                elif job.status != Status.SUSPENDED:
                    job.status = Status.WAITING
                    job._fail_count = 0
                    Log.info(f"CHANGED job '{job.name}' status to WAITING")

            job_list.check_completed_jobs_after_recovery()

            Log.info("Updating the jobs list")
            job_list.update_list(as_conf)

            if save:
                job_list.recover_last_data()
                job_list.save()
            else:
                Log.warning('Changes NOT saved to the jobList. Use -s option to save')

            Log.result("Recovery finalized")

        except Exception as e:
            raise AutosubmitCritical("Couldn't restore the experiment workflow", 7040, str(e))

        # The expand/group was not covered before, in this PR it was just moved from be mandatory to optional
        # Added TRY EXCEPT for plotting and detail to avoid recovery failure (as the jobs were recovered)
        try:
            if not noplot:
                packages = JobPackagePersistence(expid).load()
                from .monitor.monitor import Monitor
                status = list()
                if group_by:
                    if expand_status:
                        if isinstance(expand_status, str):
                            for s in expand_status.split():
                                status.append(Autosubmit._get_status(s.upper()))
                        elif isinstance(expand_status, list):
                            for s in expand_status:
                                status.append(Autosubmit._get_status(s.upper()))
                        else:
                            Log.warning("Grouping status has an invalid format, it should be a string or a list of strings")
                job_grouping = JobGrouping(group_by, copy.deepcopy(job_list.get_job_list()), job_list,
                                           expand_list=expand,
                                           expanded_status=status)
                groups_dict = job_grouping.group_jobs()
                Log.info("\nPlotting the jobs list...")
                monitor_exp = Monitor()
                monitor_exp.generate_output(
                    expid, job_list.get_job_list(), os.path.join(exp_path, "/tmp/LOG_", expid),
                    output_format=output_type, packages=packages, show=not hide, groups=groups_dict,
                    job_list_object=job_list)
        except Exception:
            Log.warning("An error has occurred while plotting the jobs list after recovery. "
                        "Check if you have X11 redirection and an img viewer correctly set. Trace: {str(e)}")
        try:
            if detail:
                Autosubmit.detail(job_list)
        except Exception as e:
            Log.warning(f"An error has occurred while generating the detailed view of the jobs after recovery. Trace: {str(e)}")

        return True

    @staticmethod
    def check(experiment_id: str) -> bool:
        """Checks experiment configuration and warns about any detected error or inconsistency.

        :param experiment_id: experiment identifier:
        """
        try:
            os.path.join(BasicConfig.LOCAL_ROOT_DIR, experiment_id)
            as_conf = AutosubmitConfig(
                experiment_id, BasicConfig, YAMLParserFactory())
            as_conf.check_conf_files(False)
            as_conf.get_project_type()
            submitter = ParamikoSubmitter(as_conf=as_conf)
            if not submitter.platforms:
                return False

            pkl_dir = os.path.join(
                BasicConfig.LOCAL_ROOT_DIR, experiment_id, 'pkl')
            job_list = Autosubmit.load_job_list(experiment_id, as_conf)
            Log.debug(f"Job list restored from {pkl_dir} files")

            Autosubmit._load_parameters(as_conf, job_list, submitter.platforms)

            hpc_architecture = as_conf.get_platform()
            for job in job_list.get_job_list():
                if job.platform_name is None:
                    job.platform_name = hpc_architecture
                job.platform = submitter.platforms[job.platform_name]

        except AutosubmitError:
            raise
        except BaseException as e:
            raise AutosubmitCritical("Checking incomplete due an unknown error. Please check the trace", 7070, str(e))

        return job_list.check_scripts(as_conf)

    @staticmethod
    def report(expid: str, template_file_path="", show_all_parameters=False, folder_path="",
               placeholders=False) -> bool:
        """Show report for specified experiment.

        :param expid: experiment identifier
        :param template_file_path: path to template file
        :param show_all_parameters: show all parameters
        :param folder_path: path to folder
        :param placeholders: show placeholders
        """
        try:
            ignore_performance_keys = ["error_message",
                                       "warnings_job_data", "considered"]
            exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
            tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
            if folder_path is not None and len(str(folder_path)) > 0:
                tmp_path = folder_path
            # Gather experiment info
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            try:
                as_conf.reload(True)
                parameters = as_conf.load_parameters()
            except Exception:
                raise AutosubmitCritical(
                    "Unable to gather the parameters from config files, check permissions.", 7012)
            # Performance Metrics call
            try:
                import requests
                BasicConfig.read()
                request = requests.get(f"{BasicConfig.AUTOSUBMIT_API_URL}/performance/{expid}")
                performance_metrics = json.loads(request.text)
                # If error, then None
                performance_metrics = None if performance_metrics and performance_metrics[
                    "error"] is True else performance_metrics
                if performance_metrics:
                    for key in ignore_performance_keys:
                        performance_metrics.pop(key, None)
            except Exception:
                Log.printlog("Autosubmit couldn't retrieve performance metrics.")
                performance_metrics = None
            # Preparation for section parameters
            try:
                submitter = ParamikoSubmitter(as_conf=as_conf)
                hpcarch = submitter.platforms[as_conf.get_platform()]
            except Exception as e:
                Log.warning(f'Failed creating Paramiko submitter, will try loading only the local platform: {str(e)}')
                submitter = ParamikoSubmitter(as_conf=as_conf)
                hpcarch = submitter.platforms[as_conf.get_platform()]

            job_list = Autosubmit.load_job_list(expid, as_conf)
            for job in job_list.get_job_list():
                if job.platform_name is None or job.platform_name == "":
                    job.platform_name = hpcarch.name
                job.platform = submitter.platforms[job.platform_name]

            if show_all_parameters:
                Log.info("Gathering all parameters (all keys are on upper_case)")
                parameter_output = f'{expid}_parameter_list_{datetime.datetime.today().strftime("%Y%m%d-%H%M%S")}.txt'
                parameter_file = open(os.path.join(
                    tmp_path, parameter_output), 'w')
                # Common parameters
                jobs_parameters = {}
                try:
                    for job in job_list.get_job_list():
                        job_parameters = job.update_parameters(as_conf, set_attributes=True)
                        for key, value in job_parameters.items():
                            jobs_parameters["JOBS" + "." + job.section + "." + key] = value
                except Exception:
                    pass
                if len(jobs_parameters) > 0:
                    del as_conf.experiment_data["JOBS"]
                parameters = as_conf.load_parameters()
                parameters.update(jobs_parameters)
                for key, value in parameters.items():
                    if value is not None and len(str(value)) > 0:
                        full_value = key + "=" + str(value) + "\n"
                        parameter_file.write(full_value)
                    else:
                        if placeholders:
                            parameter_file.write(
                                key + "=" + "%" + key + "%" + "\n")
                        else:
                            parameter_file.write(key + "=" + "-" + "\n")

                if performance_metrics is not None and len(str(performance_metrics)) > 0:
                    for key in performance_metrics:
                        parameter_file.write(f"{key} = {performance_metrics.get(key, '-')}\n")
                parameter_file.close()

                os.chmod(os.path.join(tmp_path, parameter_output), 0o755)
                Log.result(f"A list of all parameters has been written on {os.path.join(tmp_path, parameter_output)}")

            if template_file_path is not None:
                if os.path.exists(template_file_path):
                    Log.info(
                        "Gathering the selected parameters (all keys are on upper_case)")
                    template_file = open(template_file_path, 'r')
                    template_content = template_file.read()
                    for key, value in parameters.items():
                        template_content = re.sub(
                            '%(?<!%%)' + key + '%(?!%%)', str(parameters[key]), template_content, flags=re.I)
                    # Performance metrics
                    if performance_metrics is not None and len(str(performance_metrics)) > 0:
                        for key in performance_metrics:
                            template_content = re.sub(
                                '%(?<!%%)' + key + '%(?!%%)', str(performance_metrics[key]), template_content,
                                flags=re.I)
                    template_content = template_content.replace("%%", "%")
                    if not placeholders:
                        template_content = re.sub(
                            r"%[^% \n\t]+%", "-", template_content, flags=re.I)
                    report = f'{expid}_report_{datetime.datetime.today().strftime("%Y%m%d-%H%M%S")}.txt'
                    open(os.path.join(tmp_path, report),
                         'w').write(template_content)
                    os.chmod(os.path.join(tmp_path, report), 0o755)
                    template_file.close()
                    Log.result(f"Report {report} has been created on {os.path.join(tmp_path, report)}")
                    return True
                else:
                    raise AutosubmitCritical(
                        f"Template {template_file_path} doesn't exists ", 7014)
            return True
        except AutosubmitError:
            raise
        except AutosubmitCritical:
            raise
        except BaseException as e:
            raise AutosubmitCritical("Unknown error while reporting the parameters list, likely it is due IO issues",
                                     7040, str(e))

    @staticmethod
    def describe(input_experiment_list="*", get_from_user=""):
        """Show details for specified experiment

        :param input_experiment_list: experiments identifier:
        :type input_experiment_list: str
        :param get_from_user: user to get the experiments from
        :type get_from_user: str
        :return: tuple with user, created time, model, branch, and HPC
        :rtype: bool | (str, str, str, str, str)
        """
        experiments_ids = input_experiment_list
        not_described_experiments = []
        if get_from_user == "*" or get_from_user == "":
            get_from_user = pwd.getpwuid(os.getuid())[0]
        user = ""
        created = ""
        model = ""
        branch = ""
        hpc = ""
        if ',' in experiments_ids:
            experiments_ids = experiments_ids.split(',')
        elif '*' in experiments_ids:
            experiments_ids = []
            basic_conf = BasicConfig()
            for f in Path(basic_conf.LOCAL_ROOT_DIR).glob("????"):
                # If it reaches there it means that f.owner() doesn't exist
                # anymore( owner is an id) so we just skip it and continue.
                with suppress(Exception):
                    if f.is_dir() and f.owner() == get_from_user:
                        experiments_ids.append(f.name)
        else:
            experiments_ids = experiments_ids.split(' ')
        for experiment_id in experiments_ids:
            try:
                experiment_id = experiment_id.strip(" ")
                exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, experiment_id)

                as_conf = AutosubmitConfig(experiment_id, BasicConfig, YAMLParserFactory())
                as_conf.check_conf_files(False, no_log=True)
                user = os.stat(as_conf.conf_folder_yaml).st_uid
                try:
                    user = pwd.getpwuid(user).pw_name
                except Exception:
                    Log.warning(
                        "The user does not exist anymore in the system, using id instead")
                    continue

                created = datetime.datetime.fromtimestamp(os.path.getmtime(as_conf.conf_folder_yaml))

                if as_conf.get_svn_project_url():
                    model = as_conf.get_svn_project_url()
                    branch = as_conf.get_svn_project_url()
                else:
                    model = as_conf.get_git_project_origin()
                    branch = as_conf.get_git_project_branch()
                if model == "":
                    model = "Not Found"
                if branch == "":
                    branch = "Not Found"

                submitter = ParamikoSubmitter(as_conf=as_conf)
                if not submitter.platforms:
                    return False
                hpc = as_conf.get_platform()
                description = get_experiment_description(experiment_id)
                Log.result("Describing {0}", experiment_id)
                Log.result("Owner: {0}", user)
                Log.result("Location: {0}", exp_path)
                Log.result("Created: {0}", created)
                Log.result("Model: {0}", model)
                Log.result("Branch: {0}", branch)
                Log.result("HPC: {0}", hpc)
                Log.result("Description: {0}", description[0][0])
            except Exception:
                not_described_experiments.append(experiment_id)
        if len(not_described_experiments) > 0:
            Log.printlog(f"Could not describe the following experiments:\n"
                         f"{not_described_experiments}", Log.WARNING)
        if len(experiments_ids) == 1:
            # for backward compatibility or GUI
            return user, created, model, branch, hpc
        elif len(experiments_ids) == 0:
            Log.result(f"No experiments found for expid={input_experiment_list} and user {get_from_user}")

    @staticmethod
    def configure(
            advanced,
            database_path,
            database_filename,
            local_root_path,
            platforms_conf_path,
            jobs_conf_path,
            smtp_hostname,
            mail_from,
            machine: bool,
            local: bool,
            database_backend: str = "sqlite",
            database_conn_url: str = "",
    ):
        """Configure several paths for autosubmit: database, local root and others. Can be configured at system,
        user or local levels. Local level configuration precedes user level and user level precedes system
        configuration.

        :param advanced:
        :param database_path: path to autosubmit database
        :type database_path: str
        :param database_filename: database filename
        :type database_filename: str
        :param local_root_path: path to autosubmit's experiments' directory
        :type local_root_path: str
        :param platforms_conf_path: path to platforms conf file to be used as model for new experiments
        :type platforms_conf_path: str
        :param jobs_conf_path: path to jobs conf file to be used as model for new experiments
        :type jobs_conf_path: str
        :param machine: True if this configuration has to be stored for all the machine users
        :type machine: bool
        :param local: True if this configuration has to be stored in the local path
        :type local: bool
        :param mail_from:
        :type mail_from: str
        :param smtp_hostname:
        :type smtp_hostname: str
        """
        try:
            home_path = Path.home()
            autosubmitapi_url = "http://192.168.11.91:8081" + " # Replace me?"
            # Setting default values
            if not advanced and database_path is None and local_root_path is None:
                database_path = home_path / 'autosubmit'
                local_root_path = home_path / 'autosubmit'
                global_logs_path = home_path / 'autosubmit/logs'
                structures_path = home_path / 'autosubmit/metadata/structures'
                historicdb_path = home_path / 'autosubmit/metadata/data'
                historiclog_path = home_path / 'autosubmit/metadata/logs'
                database_filename = "autosubmit.db"

            if database_backend == "sqlite":
                while database_path is None:
                    database_path = input("Introduce Database path: ")
                    if database_path.find("~/") < 0:
                        database_path = None
                        Log.error("Not a valid path. You must include '~/' at the beginning.")
                database_path = Path(database_path).expanduser().resolve()
                # if not os.path.exists(database_path):
                Path(database_path).mkdir(parents=True, exist_ok=True)
                # Log.error("Database path does not exist.")
                # return False
                while database_filename is None:
                    database_filename = input("Introduce Database name: ")

            while local_root_path is None:
                local_root_path = input("Introduce path to experiments: ")
                if local_root_path.find("~/") < 0:
                    local_root_path = None
                    Log.error("Not a valid path. You must include '~/' at the beginning.")
            local_root_path = Path(local_root_path).expanduser().resolve()

            Path(local_root_path).mkdir(parents=True, exist_ok=True)
            global_logs_path = local_root_path / 'logs'
            structures_path = local_root_path / 'metadata/structures'
            historicdb_path = local_root_path / 'metadata/data'
            historiclog_path = local_root_path / 'metadata/logs'

            if platforms_conf_path is not None and len(str(platforms_conf_path)) > 0:
                platforms_conf_path = Path(platforms_conf_path).expanduser().resolve()
                if not platforms_conf_path.exists():
                    Log.error("platforms.yml path does not exist.")
                    return False
            if jobs_conf_path is not None and len(str(jobs_conf_path)) > 0:
                jobs_conf_path = Path(jobs_conf_path).expanduser().resolve()
                if not os.path.exists(jobs_conf_path):
                    Log.error("jobs.yml path does not exist.")
                    return False

            rc_path: Path = get_rc_path(machine, local)

            with open(rc_path, 'w') as config_file:
                Log.info("Writing configuration file...")
                try:
                    parser = ConfigParser()
                    parser.add_section('database')
                    parser.set('database', 'backend', database_backend)
                    if database_backend == "postgres":
                        if database_conn_url is None:
                            raise AutosubmitCritical(
                                "You must provide a connection URL for the PostgreSQL database. "
                                "Try adding --database-conn-url=[YOUR_URL] to your configure command.", 7014)
                        parser.set('database', 'connection_url', str(database_conn_url))
                    else:
                        parser.set('database', 'path', str(database_path))
                        if database_filename is not None and len(str(database_filename)) > 0:
                            parser.set('database', 'filename', str(database_filename))
                    parser.add_section('local')
                    parser.set('local', 'path', str(local_root_path))
                    if (jobs_conf_path is not None and len(str(jobs_conf_path)) > 0) or (
                            platforms_conf_path is not None and len(str(platforms_conf_path)) > 0):
                        parser.add_section('conf')
                        if jobs_conf_path is not None:
                            parser.set('conf', 'jobs', str(jobs_conf_path))
                        if platforms_conf_path is not None:
                            parser.set('conf', 'platforms', str(platforms_conf_path))
                    if smtp_hostname is not None or mail_from is not None:
                        parser.add_section('mail')
                        parser.set('mail', 'smtp_server', smtp_hostname)
                        parser.set('mail', 'mail_from', mail_from)
                    parser.add_section("globallogs")
                    parser.set("globallogs", "path", str(global_logs_path))
                    parser.add_section("structures")
                    parser.set("structures", "path", str(structures_path))
                    parser.add_section("historicdb")
                    parser.set("historicdb", "path", str(historicdb_path))
                    parser.add_section("historiclog")
                    parser.set("historiclog", "path", str(historiclog_path))
                    parser.add_section("autosubmitapi")
                    parser.set("autosubmitapi", "url", autosubmitapi_url)
                    parser.write(config_file)
                    Log.result(f"Configuration file written successfully: \n\t{rc_path}")

                    dirs = [local_root_path, global_logs_path, structures_path, historicdb_path, historiclog_path]

                    for dir in dirs:
                        create_me = Path(dir)
                        create_me.mkdir(parents=True, exist_ok=True)
                        create_me.chmod(0o775)

                    sep = '\n\t'
                    Log.result(sep.join(['Directories configured successfully:'] + [str(d) for d in dirs]))
                except (IOError, OSError) as e:
                    raise AutosubmitCritical(f"Can not write config file: {e.message}", 7012)
        except (AutosubmitCritical, AutosubmitError):
            raise
        except BaseException as e:
            raise AutosubmitCritical(str(e), 7014)
        return True

    @staticmethod
    def configure_dialog():
        """Configure several paths for autosubmit interactively: database, local root and others.
        Can be configured at system, user or local levels. Local level configuration precedes user level and user level
        precedes system configuration. """
        home_path = Path("~").expanduser().resolve()

        try:
            d = dialog.Dialog(
                dialog="dialog", autowidgetsize=True, screen_color='GREEN')
        except dialog.DialogError:
            raise AutosubmitCritical(
                "Graphical visualization failed, not enough screen size", 7060)
        except Exception:
            raise AutosubmitCritical(
                "Dialog libs aren't found in your Operational system", 7060)

        d.set_background_title("Autosubmit configure utility")
        if os.geteuid() == 0:
            text = ''
            choice = [
                ("All", "All users on this machine (may require root privileges)")]
        else:
            text = "If you want to configure Autosubmit for all users, you will need to provide root privileges"
            choice = []

        choice.append(("User", "Current user"))
        choice.append(
            ("Local", "Only when launching Autosubmit from this path"))

        try:
            code, level = d.menu(text, choices=choice, width=60,
                                 title="Choose when to apply the configuration")
            if code != dialog.Dialog.OK:
                os.system('clear')
                return False
        except dialog.DialogError:
            raise AutosubmitCritical(
                "Graphical visualization failed, not enough screen size", 7060)

        filename = '.autosubmitrc'
        if level == 'All':
            path = '/etc'
            filename = 'autosubmitrc'
        elif level == 'User':
            path = home_path
        else:
            path = '.'
        path = os.path.join(path, filename)

        # Setting default values
        database_path = home_path
        local_root_path = home_path
        database_filename = 'autosubmit.db'
        jobs_conf_path = ''
        platforms_conf_path = ''

        d.infobox("Reading configuration file...", width=50, height=5)
        try:
            if os.path.isfile(path):
                parser = ConfigParser()
                parser.optionxform = str
                parser.load(path)
                if parser.has_option('database', 'path'):
                    database_path = parser.get('database', 'path')
                if parser.has_option('database', 'filename'):
                    database_filename = parser.get('database', 'filename')
                if parser.has_option('local', 'path'):
                    local_root_path = parser.get('local', 'path')
                if parser.has_option('conf', 'platforms'):
                    platforms_conf_path = parser.get('conf', 'platforms')
                if parser.has_option('conf', 'jobs'):
                    jobs_conf_path = parser.get('conf', 'jobs')

        except (IOError, OSError) as e:
            raise AutosubmitCritical(
                "Can not read config file", 7014, e.message)

        while True:
            try:
                code, database_path = d.dselect(database_path, width=80, height=20,
                                                title='\\Zb\\Z1Select path to database\\Zn', colors='enable')
            except dialog.DialogError:
                raise AutosubmitCritical(
                    "Graphical visualization failed, not enough screen size", 7060)
            if Autosubmit._requested_exit(code, d):
                raise AutosubmitCritical(
                    "Graphical visualization failed, requested exit", 7060)
            elif code == dialog.Dialog.OK:
                database_path = database_path.replace('~', home_path)
                if not os.path.exists(database_path):
                    d.msgbox(
                        "Database path does not exist.\nPlease, insert the right path", width=50, height=6)
                else:
                    break

        while True:
            try:
                code, local_root_path = d.dselect(local_root_path, width=80, height=20,
                                                  title='\\Zb\\Z1Select path to experiments repository\\Zn',
                                                  colors='enable')
            except dialog.DialogError:
                raise AutosubmitCritical(
                    "Graphical visualization failed, not enough screen size", 7060)

            if Autosubmit._requested_exit(code, d):
                raise AutosubmitCritical(
                    "Graphical visualization failed,requested exit", 7060)
            elif code == dialog.Dialog.OK:
                database_path = database_path.replace('~', home_path)
                if not os.path.exists(database_path):
                    d.msgbox(
                        "Local root path does not exist.\nPlease, insert the right path", width=50, height=6)
                else:
                    break
        while True:
            try:
                (code, tag) = d.form(text="",
                                     elements=[("Database filename", 1, 1, database_filename, 1, 40, 20, 20),
                                               (
                                                   "Default platform.yml path", 2, 1, platforms_conf_path, 2, 40, 40,
                                                   200),
                                               ("Default jobs.yml path", 3, 1, jobs_conf_path, 3, 40, 40, 200)],
                                     height=20,
                                     width=80,
                                     form_height=10,
                                     title='\\Zb\\Z1Just a few more options:\\Zn', colors='enable')
            except dialog.DialogError:
                raise AutosubmitCritical(
                    "Graphical visualization failed, not enough screen size", 7060)

            if Autosubmit._requested_exit(code, d):
                raise AutosubmitCritical(
                    "Graphical visualization failed, _requested_exit", 7060)
            elif code == dialog.Dialog.OK:
                database_filename = tag[0]
                platforms_conf_path = tag[1]
                jobs_conf_path = tag[2]

                platforms_conf_path = platforms_conf_path.replace(
                    '~', home_path).strip()
                jobs_conf_path = jobs_conf_path.replace('~', home_path).strip()

                if platforms_conf_path and not os.path.exists(platforms_conf_path):
                    d.msgbox(
                        "Platforms conf path does not exist.\nPlease, insert the right path", width=50, height=6)
                elif jobs_conf_path and not os.path.exists(jobs_conf_path):
                    d.msgbox(
                        "Jobs conf path does not exist.\nPlease, insert the right path", width=50, height=6)
                else:
                    break

        smtp_hostname = "mail.bsc.es"
        mail_from = "automail@bsc.es"
        while True:
            try:
                (code, tag) = d.form(text="",
                                     elements=[("SMTP server hostname", 1, 1, smtp_hostname, 1, 40, 20, 20),
                                               ("Notifications sender address", 2, 1, mail_from, 2, 40, 40, 200)],
                                     height=20,
                                     width=80,
                                     form_height=10,
                                     title='\\Zb\\Z1Mail notifications configuration:\\Zn', colors='enable')
            except dialog.DialogError:
                raise AutosubmitCritical(
                    "Graphical visualization failed, not enough screen size", 7060)

            if Autosubmit._requested_exit(code, d):
                raise AutosubmitCritical(
                    "Graphical visualization failed, requested exit", 7060)
            elif code == dialog.Dialog.OK:
                smtp_hostname = tag[0]
                mail_from = tag[1]
                break
                # TODO: Check that is a valid config?

        config_file = open(path, 'w')
        d.infobox("Writing configuration file...", width=50, height=5)
        try:
            parser = ConfigParser()
            parser.add_section('database')
            parser.set('database', 'path', database_path)
            if database_filename:
                parser.set('database', 'filename', database_filename)
            parser.add_section('local')
            parser.set('local', 'path', local_root_path)
            if jobs_conf_path or platforms_conf_path:
                parser.add_section('conf')
                if jobs_conf_path:
                    parser.set('conf', 'jobs', jobs_conf_path)
                if platforms_conf_path:
                    parser.set('conf', 'platforms', platforms_conf_path)
            parser.add_section('mail')
            parser.set('mail', 'smtp_server', smtp_hostname)
            parser.set('mail', 'mail_from', mail_from)
            parser.write(config_file)
            config_file.close()
            d.msgbox("Configuration file written successfully",
                     width=50, height=5)
            os.system('clear')
        except (IOError, OSError) as e:
            raise AutosubmitCritical(
                "Can not write config file", 7012, e.message)
        return True

    @staticmethod
    def _requested_exit(code, d):
        if code != dialog.Dialog.OK:
            code = d.yesno(
                'Exit configure utility without saving?', width=50, height=5)
            if code == dialog.Dialog.OK:
                os.system('clear')
                return True
        return False

    @staticmethod
    def install():
        """Creates a new database instance for autosubmit at the configured path."""
        if BasicConfig.DATABASE_BACKEND == 'sqlite':
            if not os.path.exists(BasicConfig.DB_PATH):
                Log.info("Creating autosubmit database...")
                query_file = read_files('autosubmit.database') / 'data/autosubmit.sql'
                query = query_file.read_text()
                if not create_db(query):
                    raise AutosubmitCritical("Can not write database file", 7004)
                Log.result("Autosubmit database created successfully")
            else:
                raise AutosubmitCritical("Database already exists.", 7004)
        else:
            Log.info("Creating autosubmit Postgres database...")
            if not create_db(''):
                raise AutosubmitCritical("Failed to create Postgres database", 7004)
        return True

    @staticmethod
    def refresh(expid, model_conf, jobs_conf):
        """Refresh project folder for given experiment

        :param model_conf:
        :type model_conf: bool
        :param jobs_conf:
        :type jobs_conf: bool
        :param expid: experiment identifier
        :type expid: str
        """
        try:
            check_ownership(expid, raise_error=True)
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            as_conf.reload(force_load=True)
            # as_conf.check_conf_files(False)
        except (AutosubmitError, AutosubmitCritical):
            raise
        except BaseException as e:
            raise AutosubmitCritical("Error while reading the configuration files", 7064, str(e))
        try:
            if "Expdef" in as_conf.wrong_config:
                as_conf.show_messages()
            project_type = as_conf.get_project_type()
            if Autosubmit._copy_code(as_conf, expid, project_type, True):
                Log.result("Project folder updated")
            Autosubmit._create_project_associated_conf(
                as_conf, model_conf, jobs_conf)
        except (AutosubmitError, AutosubmitCritical):
            raise
        except BaseException as e:
            raise AutosubmitCritical("  Download failed", 7064, str(e))
        return True

    @staticmethod
    def update_version(expid):
        """Refresh experiment version with the current autosubmit version.

        :param expid: experiment identifier
        :type expid: str
        """
        check_ownership(expid, raise_error=True)

        as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
        as_conf.reload(force_load=True)
        as_conf.check_expdef_conf()

        Log.info(f"Changing {expid} experiment version from {as_conf.get_version()} to {Autosubmit.autosubmit_version}")
        as_conf.set_version(Autosubmit.autosubmit_version)
        update_experiment_description_version(expid, version=Autosubmit.autosubmit_version)

        return True

    @staticmethod
    def update_description(expid, new_description):
        Log.info("Checking if experiment exists...")
        check_experiment_exists(expid)
        Log.info("Experiment found.")
        Log.info(f"Setting {expid} description to '{new_description}'")
        return update_experiment_description_version(expid, description=new_description)

    # fastlook
    @staticmethod
    def update_old_script(root_dir, template_path, as_conf):
        # Do a backup and tries to update
        warn = ""
        substituted = ""
        Log.info(f"Checking {template_path}")
        if template_path.exists():
            backup_path = root_dir / Path(template_path.name + "_AS_v3_backup_placeholders")
            if not backup_path.exists():
                Log.info(f"Backup stored at {backup_path}")
                shutil.copyfile(template_path, backup_path)
            template_content = open(template_path, 'r', encoding=locale.getlocale()[1]).read()
            # Look for %_%
            variables = re.findall('%(?<!%%)[a-zA-Z0-9_.-]+%(?!%%)', template_content, flags=re.IGNORECASE)
            variables = [variable[1:-1].upper() for variable in variables]
            results = {}
            # Change format
            for old_format_key in variables:
                for key in as_conf.load_parameters().keys():
                    key_affix = key.split(".")[-1]
                    if key_affix == old_format_key:
                        if old_format_key not in results:
                            results[old_format_key] = set()

                        results[old_format_key].add("%" + key.strip("'") + "%")
            for key, new_key in results.items():
                if len(new_key) > 1:
                    if list(new_key)[0].find("JOBS") > -1 or list(new_key)[0].find("PLATFORMS") > -1:
                        pass
                    else:
                        warn += (f"{key} couldn't translate to {new_key} since it is a duplicate variable. "
                                 f"Please chose one of the keys value.\n")
                else:
                    new_key = new_key.pop().upper()
                    substituted += f"{key.upper()} translated to {new_key}\n"
                    template_content = re.sub('%(?<!%%)' + key + '%(?!%%)', new_key, template_content, flags=re.I)
            # write_it
            # Deletes unused keys from confs
            if template_path.name.lower().find("autosubmit") > -1:
                template_content = re.sub('(?m)^( )*(EXPID:)( )*[a-zA-Z0-9._-]*(\n)*', "", template_content, flags=re.I)
            # Write final result
            open(template_path, "w").write(template_content)

        if warn == "" and substituted == "":
            Log.result(f"Completed check for {template_path}.\nNo %_% variables found.")
        else:
            Log.result(f"Completed check for {template_path}")

        return warn, substituted

    @staticmethod
    def upgrade_scripts(expid: str, files="") -> bool:
        def get_files(root_dir_, extensions, files=""):
            all_files = []
            if len(files) > 0:
                for ext in extensions:
                    all_files.extend(root_dir_.rglob(ext))
            else:
                if ',' in files:
                    files = files.split(',')
                elif ' ' in files:
                    files = files.split(' ')
                for file in files:
                    all_files.append(file)
            return all_files

        Log.info("Checking if experiment exists...")
        try:
            # Check that the user is the owner and the configuration is well configured
            check_ownership(expid, raise_error=True)
            folder = Path(BasicConfig.LOCAL_ROOT_DIR) / expid / "conf"
            factory = YAMLParserFactory()
            # update scripts to yml format
            for f in folder.rglob("*.yml"):
                # Tries to convert an invalid yml to correct one
                try:
                    parser = factory.create_parser()
                    parser.load(Path(f))
                except BaseException:
                    try:
                        AutosubmitConfig.ini_to_yaml(f.parent, Path(f))
                    except BaseException:
                        Log.warning(f"Couldn't convert conf file to yml: {f.parent}")
                        return False

            # Converts all ini into yaml
            Log.info("Converting all .conf files into .yml.")
            for f in folder.rglob("*.conf"):
                if not Path(f.stem + ".yml").exists():
                    try:
                        AutosubmitConfig.ini_to_yaml(Path(f).parent, Path(f))
                    except Exception:
                        Log.warning(f"Couldn't convert conf file to yml: {Path(f).parent}")
                        return False
            as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
            as_conf.reload(force_load=True)
            # Load current variables
            as_conf.check_conf_files()
            # Load current parameters ( this doesn't read job parameters)
            as_conf.load_parameters()

        except (AutosubmitError, AutosubmitCritical):
            raise
        # Update configuration files
        warn = ""
        substituted = ""
        root_dir = Path(as_conf.basic_config.LOCAL_ROOT_DIR) / expid / "conf"
        Log.info("Looking for %_% variables inside conf files")
        for f in get_files(root_dir, ('*.yml', '*.yaml', '*.conf')):
            template_path = root_dir / Path(f).name
            try:
                w, s = Autosubmit.update_old_script(root_dir, template_path, as_conf)
                if w != "":
                    warn += f"Warnings for: {template_path.name}\n{w}\n"
                if s != "":
                    substituted += f"Variables changed for: {template_path.name}\n{s}\n"
            except BaseException as e:
                Log.printlog(f"Couldn't read {template_path} template.\ntrace:{str(e)}")
        if substituted != "" and warn != "":
            Log.result(substituted)
            Log.result(warn)
        # Update templates
        root_dir = Path(as_conf.get_project_dir())
        template_path = Path()
        warn = ""
        substituted = ""
        Log.info("Looking for %_% variables inside templates")
        for section, value in as_conf.jobs_data.items():
            try:
                template_path = root_dir / Path(value.get("FILE", ""))
                w, s = Autosubmit.update_old_script(template_path.parent, template_path, as_conf)
                if w != "":
                    warn += f"Warnings for: {template_path.name}\n{w}\n"
                if s != "":
                    substituted += f"Variables changed for: {template_path.name}\n{s}\n"
            except BaseException as e:
                Log.printlog(f"Couldn't read {template_path} template.\ntrace:{str(e)}")
        if substituted != "":
            Log.printlog(substituted, Log.RESULT)
        if warn != "":
            Log.printlog(warn, Log.ERROR)
        Log.info(f"Changing {expid} experiment version from {as_conf.get_version()} to "
                 f"{Autosubmit.autosubmit_version}")
        as_conf.set_version(Autosubmit.autosubmit_version)
        update_experiment_description_version(expid, version=Autosubmit.autosubmit_version)
        return True

    @staticmethod
    def pkl_fix(expid: str, force: bool = False) -> int:
        """Tries to find a backup of the pkl file and restores it. Verifies that autosubmit is not running on
        this experiment.

        :param expid: experiment identifier
        :type expid: str
        :param force: force the operation without confirmation
        :type force: bool
        :return:
        :rtype:
        """
        if BasicConfig.DATABASE_BACKEND != 'sqlite':
            raise AutosubmitCritical(
                "This operation is only available when Autosubmit database backend is set to sqlite", 7000)
        exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
        tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
        pkl_folder_path = os.path.join(exp_path, "pkl")
        current_pkl_path = os.path.join(
            pkl_folder_path, f"job_list_{expid}.pkl")
        backup_pkl_path = os.path.join(
            pkl_folder_path, f"job_list_{expid}_backup.pkl")
        try:
            with Lock(os.path.join(tmp_path, 'autosubmit.lock'), timeout=1):
                # Not locked
                Log.info(f"Looking for backup file {backup_pkl_path}")
                if os.path.exists(backup_pkl_path):
                    # Backup file exists
                    Log.info("Backup file found.")
                    # Make sure backup file is not empty
                    _stat_b = os.stat(backup_pkl_path)
                    if _stat_b.st_size <= 6:
                        # It is empty -> Return
                        Log.info(
                            f"The backup file {backup_pkl_path} is empty. Pkl restore operation stopped."
                            f" No changes have been made.")
                    if os.path.exists(current_pkl_path):
                        # Pkl file exists
                        Log.info(f"Current pkl file {current_pkl_path} found.")
                        _stat = os.stat(current_pkl_path)
                        if _stat.st_size > 6:
                            # Greater than 6 bytes -> Not empty
                            if not force and not user_yes_no_query(
                                    f"The current pkl file {current_pkl_path} is not empty. Do you want to continue?"
                            ):
                                # The user chooses not to continue. Operation stopped.
                                Log.info("Pkl restore operation stopped. No changes have been made.")
                                return 0
                            # File not empty: Archive
                            archive_pkl_name = os.path.join(pkl_folder_path,
                                                            f"{datetime.datetime.today().strftime('%d%m%Y%H%M%S')}"
                                                            f"_job_list_{expid}.pkl")
                            # Waiting for completion
                            subprocess.call(
                                ["cp", current_pkl_path, archive_pkl_name])

                            if os.path.exists(archive_pkl_name):
                                Log.result(f"File {current_pkl_path} archived as {archive_pkl_name}.")
                        else:
                            # File empty: Delete
                            result = os.popen(f"rm {current_pkl_path}")
                            if result is not None:
                                Log.info(f"File {current_pkl_path} deleted.")
                    # Restore backup file
                    Log.info(f"Restoring {backup_pkl_path} into {current_pkl_path}")
                    subprocess.call(["mv", backup_pkl_path, current_pkl_path])

                    if os.path.exists(current_pkl_path):
                        Log.result("Pkl restored.")
                else:
                    Log.info(
                        "Backup file not found. Pkl restore operation stopped. No changes have been made.")
            return 0
        except AutosubmitCritical as e:
            raise AutosubmitCritical(e.message, e.code, e.trace)

    @staticmethod
    def database_backup(expid):
        if BasicConfig.DATABASE_BACKEND == 'sqlite':
            try:
                database_path = os.path.join(BasicConfig.JOBDATA_DIR, f"job_data_{expid}.db")
                backup_path = os.path.join(BasicConfig.JOBDATA_DIR, f"job_data_{expid}.sql")
                command = f"sqlite3 {database_path} .dump > {backup_path} "
                Log.debug("Backing up jobs_data...")
                subprocess.call(command, shell=True)
                Log.debug("Jobs_data database backup completed.")
            except BaseException:
                Log.debug("Jobs_data database backup failed.")
        elif BasicConfig.DATABASE_BACKEND == 'postgres':
            # TODO: Implement Postgres backup
            Log.debug("Postgres database backup not implemented yet.")

    @staticmethod
    def database_fix(expid: str) -> bool:
        """Database methods. Performs a sql dump of the database and restores it.

        :param expid: experiment identifier
        :type expid: str
        :return:
        :rtype:
        """
        os.umask(0)  # Overrides user permissions
        corrupted_db_path = os.path.join(BasicConfig.JOBDATA_DIR, f"job_data_{expid}_corrupted.db")

        database_path = os.path.join(BasicConfig.JOBDATA_DIR, f"job_data_{expid}.db")
        dump_file_name = f'job_data_{expid}.sql'
        dump_file_path = os.path.join(BasicConfig.JOBDATA_DIR, dump_file_name)
        bash_command = f'cat {dump_file_path} | sqlite3 {database_path}'
        try:
            if os.path.exists(database_path):
                os.popen(f"mv {database_path} {corrupted_db_path}").read()
                time.sleep(1)
                Log.info("Original database moved.")
            try:
                exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                                historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
                Log.info("Restoring from sql")
                os.popen(bash_command).read()
                exp_history.initialize_database()
                return True

            except Exception:
                Log.warning("It was not possible to restore the jobs_data.db file... , a new blank db will be created")

                exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                                historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
                exp_history.initialize_database()
                return False
        except Exception as exp:
            Log.critical(str(exp))
            return False

    @staticmethod
    def rocrate(expid: str, path: Path) -> bool:
        """Produces an RO-Crate archive for an Autosubmit experiment.

        Skips other crate ZIP archive files in ``tmp/ASLOGS``. It ignores
        files that start with ``<EXPID>-crate`` and end with ``.zip``.

        :param expid: experiment ID
        :param path: path to save the RO-Crate in
        :return: ``True`` if successful, ``False`` otherwise
        """
        from autosubmit.statistics.statistics import Statistics
        from textwrap import dedent

        as_conf = AutosubmitConfig(expid)
        # ``.reload`` will call the function to unify the YAML configuration.
        as_conf.reload(True)

        workflow_configuration = as_conf.experiment_data

        # Load the rocrate prepopulated file, or raise an error and write the template.
        # Similar to what COMPSs does.
        # See: https://github.com/bsc-wdc/compss/blob/9e79542eef60afa9e288e7246e697bd7ac42db08/compss/runtime/scripts/
        #      system/provenance/generate_COMPSs_RO-Crate.py
        rocrate_json = workflow_configuration.get('ROCRATE', None)
        if not rocrate_json:
            Log.error(dedent('''\
                No ROCRATE configuration value provided! Use it to create your
                JSON-LD schema, using @id, @type, and other schema.org attributes,
                and it will be merged with the values retrieved from the workflow
                configuration. Some values are not present in Autosubmit, such as
                license, so you must provide it if you want to include in your
                RO-Crate data, e.g. create a file $expid/conf/rocrate.yml (or use
                an existing one) with a top level ROCRATE key, containing your
                JSON-LD data:

                ROCRATE:
                  INPUTS:
                    # Add the extra keys to be exported.
                    - "MHM"
                  OUTPUTS:
                    # Relative to the Autosubmit project folder.
                    - "*/*.gif"
                  PATCH: |
                    {
                      "@graph": [
                        {
                          "@id": "./",
                          "license": "Apache-2.0",
                          "creator": {
                            "@id": "https://orcid.org/0000-0001-8250-4074"
                          }
                        },
                        {
                          "@id": "https://orcid.org/0000-0001-8250-4074",
                          "@type": "Person",
                          "affiliation": {
                              "@id": "https://ror.org/05sd8tv96"
                          }
                        },
                        ...
                      ]
                    }
                '''))
            raise AutosubmitCritical("You must provide an ROCRATE configuration key when using RO-Crate...", 7014)

        # Read job list (from pickles) to retrieve start and end time.
        # Code adapted from ``autosubmit stats``.
        job_list = Autosubmit.load_job_list(expid, as_conf)
        jobs = job_list.get_job_list()
        exp_stats = Statistics(jobs=jobs, start=None, end=None, queue_time_fix={})
        exp_stats.calculate_statistics()
        start_time = None
        end_time = None
        # N.B.: ``exp_stats.jobs_stat`` is sorted in reverse order.
        number_of_jobs = len(exp_stats.jobs_stat)
        if number_of_jobs > 0:
            start_time = exp_stats.jobs_stat[-1].start_time.replace(microsecond=0).isoformat()
        if number_of_jobs > 1:
            end_time = exp_stats.jobs_stat[0].finish_time.replace(microsecond=0).isoformat()

        from autosubmit.provenance.rocrate import create_rocrate_archive
        crate = create_rocrate_archive(as_conf, rocrate_json, jobs, start_time, end_time, path)
        return crate is not None

    @staticmethod
    def provenance(expid: str, rocrate: bool = False) -> None:
        """Create the experiment provenance archive.

        :param expid: experiment identifier
        :type expid: str
        :param rocrate: flag to enable RO-Crate
        :type rocrate: bool
        """

        if not rocrate:
            msg = "Can not create RO-Crate ZIP file. Argument '--rocrate' required"
            raise AutosubmitCritical(msg, 7012)

        aslogs_folder = Path(
            BasicConfig.LOCAL_ROOT_DIR,
            expid,
            BasicConfig.LOCAL_TMP_DIR,
            BasicConfig.LOCAL_ASLOG_DIR
        )

        try:
            Autosubmit.rocrate(expid, Path(aslogs_folder))
            Log.info('RO-Crate ZIP file created!')
        except Exception as e:
            raise AutosubmitCritical(f"Error creating RO-Crate ZIP file: {str(e)}", 7012)

    @staticmethod
    def archive(expid, noclean=True, uncompress=True, rocrate=False):
        """Archives an experiment: call clean (if experiment is of version 3 or later), compress folder
        to tar.gz and moves to year's folder

        :param expid: experiment identifier
        :type expid: str
        :param noclean: flag telling it whether to clean the experiment or not.
        :type noclean: bool
        :param uncompress: flag telling it whether to decompress or not.
        :type uncompress: bool
        :param rocrate: flag to enable RO-Crate
        :type rocrate: bool
        :return: ``True`` if the experiment has been successfully archived. ``False`` otherwise.
        :rtype: bool
        """
        exp_folder = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)

        if not noclean:
            # Cleaning to reduce file size.
            version = get_autosubmit_version(expid)
            if version is not None and version.startswith('3') and not Autosubmit.clean(expid, True, True, True):
                raise AutosubmitCritical(
                    "Can not archive project. Clean not successful", 7012)

        # Getting year of last completed. If not, year of expid folder
        year = None
        tmp_folder = os.path.join(exp_folder, BasicConfig.LOCAL_TMP_DIR)
        if os.path.isdir(tmp_folder):
            for filename in os.listdir(tmp_folder):
                if filename.endswith("COMPLETED"):
                    file_year = time.localtime(os.path.getmtime(
                        os.path.join(tmp_folder, filename))).tm_year
                    if year is None or year < file_year:
                        year = file_year

        if year is None:
            year = time.localtime(os.path.getmtime(exp_folder)).tm_year
        try:
            year_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, str(year))
            if not os.path.exists(year_path):
                os.mkdir(year_path)
                os.chmod(year_path, 0o775)
        except Exception as e:
            raise AutosubmitCritical(f"Failed to create year-directory {str(year)} for experiment {expid}", 7012,
                                     str(e))
        Log.info(f"Archiving in year {str(year)}")

        if rocrate:
            Autosubmit.rocrate(expid, Path(year_path))
            Log.info('RO-Crate ZIP file created!')
        else:
            # Creating tar file
            Log.info("Creating tar file ... ")
            try:
                if not uncompress:
                    compress_type = "w:gz"
                    output_filepath = f'{expid}.tar.gz'
                else:
                    compress_type = "w"
                    output_filepath = f'{expid}.tar'
                with tarfile.open(os.path.join(year_path, output_filepath), compress_type) as tar:
                    tar.add(exp_folder, arcname='')
                    tar.close()
                    os.chmod(os.path.join(year_path, output_filepath), 0o775)
            except Exception as e:
                raise AutosubmitCritical("Can not write tar file", 7012, str(e))

            Log.info("Tar file created!")

        try:
            shutil.rmtree(exp_folder)
        except Exception as e:
            Log.warning(f"Can not fully remove experiments folder: {str(e)}")
            if os.stat(exp_folder):
                try:
                    tmp_folder = os.path.join(
                        BasicConfig.LOCAL_ROOT_DIR, "tmp")
                    tmp_expid = os.path.join(tmp_folder, expid + "_to_delete")
                    os.rename(exp_folder, tmp_expid)
                    Log.warning(f"Experiment folder renamed to: {exp_folder}_to_delete")
                except Exception as e:
                    Autosubmit.unarchive(expid, uncompressed=False, rocrate=rocrate)
                    raise AutosubmitCritical(
                        "Can not remove or rename experiments folder", 7012, str(e))

        Log.result("Experiment archived successfully")
        return True

    @staticmethod
    def unarchive(experiment_id, uncompressed=True, rocrate=False):
        """Unarchives an experiment: uncompress folder from tar.gz and moves to experiment root folder.

        :param experiment_id: experiment identifier
        :type experiment_id: str
        :param uncompressed: if True, the tar file is uncompressed
        :type uncompressed: bool
        :param rocrate: flag to enable RO-Crate
        :type rocrate: bool
        """
        exp_folder = os.path.join(BasicConfig.LOCAL_ROOT_DIR, experiment_id)

        # Searching by year. We will store it on database
        year = datetime.datetime.today().year
        archive_path = None
        if rocrate:
            compress_type = None
            output_pathfile = f'{experiment_id}.zip'
        elif not uncompressed:
            compress_type = "r:gz"
            output_pathfile = f'{experiment_id}.tar.gz'
        else:
            compress_type = "r:"
            output_pathfile = f'{experiment_id}.tar'
        while year > 2000:
            archive_path = os.path.join(
                BasicConfig.LOCAL_ROOT_DIR, str(year), output_pathfile)
            if os.path.exists(archive_path):
                break
            year -= 1

        if year == 2000:
            Log.error(f"Experiment {experiment_id} is not archived")
            return False
        Log.info(f"Experiment located in {year} archive")

        # Creating tar file
        Log.info("Unpacking tar file ... ")
        if not os.path.isdir(exp_folder):
            os.mkdir(exp_folder)
        try:
            if rocrate:
                import zipfile
                with zipfile.ZipFile(archive_path, 'r') as zip:
                    zip.extractall(exp_folder)
            else:
                with tarfile.open(os.path.join(archive_path), compress_type) as tar:
                    tar.extractall(exp_folder)
                    tar.close()
        except Exception as e:
            shutil.rmtree(exp_folder, ignore_errors=True)
            Log.printlog(f"Can not extract file: {str(e)}", 6012)
            return False

        Log.info("Unpacking finished")

        try:
            os.remove(archive_path)
        except Exception as e:
            Log.printlog(f"Can not remove archived file folder: {str(e)}", 7012)
            Log.result(f"Experiment {experiment_id} unarchived successfully")
            return True

        Log.result(f"Experiment {experiment_id} unarchived successfully")
        return True

    @staticmethod
    def _create_project_associated_conf(as_conf: AutosubmitConfig, force_model_conf: bool,
                                        force_jobs_conf: bool) -> None:
        project_destiny = as_conf.get_file_project_conf()
        jobs_destiny = as_conf.get_file_jobs_conf()

        if as_conf.get_project_type() != 'none':
            if as_conf.get_file_project_conf():
                copy = True
                if os.path.exists(os.path.join(as_conf.get_project_dir(), as_conf.get_file_project_conf())):
                    if os.path.exists(project_destiny):
                        if force_model_conf:
                            os.rename(project_destiny, str(project_destiny) + "_backup")
                        else:
                            copy = False
                    if copy:
                        shutil.copyfile(os.path.join(as_conf.get_project_dir(), as_conf.get_file_project_conf()),
                                        project_destiny)

            if as_conf.get_file_jobs_conf():
                copy = True
                if os.path.exists(os.path.join(as_conf.get_project_dir(), as_conf.get_file_jobs_conf())):
                    if os.path.exists(jobs_destiny):
                        if force_jobs_conf:
                            os.rename(jobs_destiny, str(jobs_destiny) + "_backup")
                        else:
                            copy = False
                    if copy:
                        shutil.copyfile(os.path.join(as_conf.get_project_dir(), as_conf.get_file_jobs_conf()),
                                        jobs_destiny)

    @staticmethod
    def create(expid: str, noplot: bool, hide: bool, output='pdf', group_by: Optional[str] = None,
               expand: Optional[list] = list(), expand_status: Optional[str] = list(), check_wrappers=False,
               detail=False, profile=False, force=False) -> int:
        """Creates job list for given experiment. Configuration files must be valid before executing this process.

        :param detail: Show Job List view in terminal
        :param check_wrappers: Generate possible wrapper in the current workflow
        :param expand_status: Select the statuses to be expanded
        :param expand: Supply the list of dates/members/chunks to filter the list of jobs.
        :param group_by: Groups the jobs automatically by date, member, chunk or split
        :param expid: Experiment identifier
        :param noplot: if True, method omits final plotting of the jobs list. Only needed on large experiments when
            plotting time can be much larger than creation time.
        :return: True if successful, False if not
        :param hide: hides plot window
        :param hide: hides plot window
        :param output: plot's file format. It can be pdf, png, ps or svg
        :param profile: Whether to use a profiler during the execution of the command or not.
        :param force: Whether to force the creation of a new job object or not.
        """
        # Start profiling if the flag has been used
        if profile:
            from .profiler.profiler import Profiler
            profiler = Profiler(expid)
            profiler.start()

        # checking if there is a lock file to avoid multiple running on the same expid
        try:
            check_ownership(expid, raise_error=True)
            exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
            tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
            with Lock(os.path.join(tmp_path, 'autosubmit.lock'), timeout=1) as fh:
                try:
                    Log.info(
                        "Preparing .lock file to avoid multiple instances with same expid.")

                    as_conf = AutosubmitConfig(expid, BasicConfig, YAMLParserFactory())
                    # Get original configuration
                    as_conf.reload(force_load=True, only_experiment_data=True)
                    # Getting output type provided by the user in config, 'pdf' as default
                    try:
                        if not Autosubmit._copy_code(as_conf, expid, as_conf.get_project_type(), False):
                            return False
                    except AutosubmitCritical:
                        raise
                    except BaseException as e:
                        raise AutosubmitCritical(
                            "Error obtaining the project data, check the parameters related to PROJECT and GIT/SVN or LOCAL sections",
                            code=7014, trace=str(e))
                    # Update configuration with the new config in the dist ( if any )
                    as_conf.check_conf_files(running_time=False, force_load=True, no_log=False)
                    if len(as_conf.experiment_data.get("JOBS",
                                                       {})) == 0 and "CUSTOM_CONFIG" in as_conf.experiment_data.get(
                        "DEFAULT", {}):
                        raise AutosubmitCritical(
                            f'Job list is empty\nCheck if there are YML files in {as_conf.experiment_data.get("DEFAULT", "").get("CUSTOM_CONFIG", "")}',
                            code=7015)
                    output_type = as_conf.get_output_type()

                    if not os.path.exists(os.path.join(exp_path, "pkl")):
                        raise AutosubmitCritical(f"The pkl folder doesn't exists. Make sure that the 'pkl'"
                                                 f" folder exists in the following path: {exp_path}", code=6013)
                    if not os.path.exists(os.path.join(exp_path, "plot")):
                        raise AutosubmitCritical(f"The plot folder doesn't exists. Make sure that the 'plot'"
                                                 f" folder exists in the following path: {exp_path}", code=6013)

                    persistence_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid, "pkl")
                    persistence_file = f'job_list_{expid}.pkl'
                    job_list_persistence: JobListPersistence = Autosubmit._get_job_list_persistence(expid, as_conf)
                    update_job = not job_list_persistence.pkl_exists(persistence_path, persistence_file)
                    Autosubmit._create_project_associated_conf(
                        as_conf, False, update_job)

                    # Load parameters
                    Log.info("Loading parameters...")
                    parameters = as_conf.load_parameters()

                    date_list = as_conf.get_date_list()
                    if len(date_list) != len(set(date_list)):
                        raise AutosubmitCritical('There are repeated start dates!', 7014)
                    num_chunks = as_conf.get_num_chunks()
                    chunk_ini = as_conf.get_chunk_ini()
                    member_list = as_conf.get_member_list()
                    run_only_members = as_conf.get_member_list(run_only=True)
                    # print("Run only members {0}".format(run_only_members))
                    if len(member_list) != len(set(member_list)):
                        raise AutosubmitCritical(
                            "There are repeated member names!")
                    rerun = as_conf.get_rerun()

                    Log.info("\nCreating the jobs list...")
                    job_list = JobList(expid, as_conf, YAMLParserFactory(),
                                       Autosubmit._get_job_list_persistence(expid, as_conf))
                    date_format = ''
                    if as_conf.get_chunk_size_unit() == 'hour':
                        date_format = 'H'
                    for date in date_list:
                        if date.hour > 1:
                            date_format = 'H'
                        if date.minute > 1:
                            date_format = 'M'
                    wrapper_jobs = dict()

                    for wrapper_name, wrapper_parameters in as_conf.get_wrappers().items():
                        # continue if it is a global option (non-dict)
                        if type(wrapper_parameters) is not dict:
                            continue
                        wrapper_jobs[wrapper_name] = as_conf.get_wrapper_jobs(wrapper_parameters)

                    job_list.generate(as_conf, date_list, member_list, num_chunks, chunk_ini, parameters, date_format,
                                      as_conf.get_retrials(),
                                      as_conf.get_default_job_type(),
                                      wrapper_jobs, run_only_members=run_only_members, force=force, create=True)

                    if str(rerun).lower() == "true":
                        job_list.rerun(as_conf.get_rerun_jobs(), as_conf)
                    else:
                        job_list.remove_rerun_only_jobs()
                    Log.info("\nSaving the jobs list...")
                    job_list.clear_generate()
                    job_list.save()
                    as_conf.save()
                    try:
                        packages_persistence = JobPackagePersistence(expid)
                        packages_persistence.reset_table()
                        packages_persistence.reset_table(True)
                    except Exception:
                        pass

                    groups_dict = dict()

                    # Setting up job historical database header. Must create a new run.
                    # Historical Database: Setup new run
                    try:
                        exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                                        historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
                        exp_history.initialize_database()

                        # exp_history.create_new_experiment_run(as_conf.get_chunk_size_unit(), as_conf.get_chunk_size(), as_conf.get_full_config_as_json(), job_list.get_job_list())
                        exp_history.process_status_changes(job_list.get_job_list(),
                                                           chunk_unit=as_conf.get_chunk_size_unit(),
                                                           chunk_size=as_conf.get_chunk_size(),
                                                           current_config=as_conf.get_full_config_as_json(),
                                                           create=True)
                        Autosubmit.database_backup(expid)
                    except BaseException:
                        Log.printlog("Historic database seems corrupted, AS will repair it and resume the run",
                                     Log.INFO)
                        try:
                            Autosubmit.database_fix(expid)
                        except Exception:
                            Log.warning(
                                "Couldn't recover the Historical database, AS will continue without it, GUI may be affected")
                    if not noplot:
                        from .monitor.monitor import Monitor
                        if group_by:
                            status = list()
                            if expand_status:
                                for s in expand_status.split():
                                    status.append(
                                        Autosubmit._get_status(s.upper()))

                            job_grouping = JobGrouping(group_by, copy.deepcopy(job_list.get_job_list()), job_list,
                                                       expand_list=expand, expanded_status=status)
                            groups_dict = job_grouping.group_jobs()
                        # WRAPPERS
                        if len(as_conf.experiment_data.get("WRAPPERS", {})) > 0 and check_wrappers:
                            job_list_wr = Autosubmit.load_job_list(expid, as_conf, monitor=True, new=False)
                            Autosubmit.generate_scripts_andor_wrappers(
                                as_conf, job_list_wr, job_list_wr.get_job_list(), packages_persistence, True)
                            packages = packages_persistence.load(True)
                        else:
                            packages = None

                        Log.info("\nPlotting the jobs list...")
                        monitor_exp = Monitor()
                        # if output is set, use output
                        monitor_exp.generate_output(expid, job_list.get_job_list(),
                                                    os.path.join(
                                                        exp_path, "/tmp/LOG_", expid),
                                                    output if output is not None else output_type,
                                                    packages,
                                                    not hide,
                                                    groups=groups_dict,
                                                    job_list_object=job_list)
                    Log.result("\nJob list created successfully")
                    Log.warning(
                        "Remember to MODIFY the MODEL config files!")
                    fh.flush()
                    os.fsync(fh.fileno())
                    if detail:
                        Autosubmit.detail(job_list)
                    return 0
                # catching Exception
                except KeyboardInterrupt:
                    # Setting signal handler to handle subsequent CTRL-C
                    signal.signal(signal.SIGINT, signal_handler_create)
                    fh.flush()
                    os.fsync(fh.fileno())
                    raise AutosubmitCritical("Stopped by user input", 7010)
                except BaseException:
                    raise
        finally:
            if profile:
                profiler.stop()

    @staticmethod
    def detail(job_list):
        current_length = len(job_list.get_job_list())
        if current_length > 1000:
            Log.warning(
                "-d option: Experiment has too many jobs to be printed in the terminal. Maximum job quantity is 1000, your experiment has " + str(
                    current_length) + " jobs.")
        else:
            Log.info(job_list.print_with_status())
            Log.status(job_list.print_with_status(nocolor=True))

    @staticmethod
    def _copy_code(as_conf: AutosubmitConfig, expid: str, project_type: str, force: bool) -> bool:
        """Method to copy code from experiment repository to project directory.

        :param as_conf: experiment configuration class
        :type as_conf: AutosubmitConfig
        :param expid: experiment identifier
        :type expid: str
        :param project_type: project type (git, svn, local)
        :type project_type: str
        :param force: if True, overwrites current data
        :return: True if successful, False if not
        :rtype: bool
        """
        project_destination = as_conf.get_project_destination()
        if project_destination is None or len(project_destination) == 0:
            if project_type.lower() != "none":
                raise AutosubmitCritical("Autosubmit couldn't identify the project destination.", 7014)

        if project_type == "git":
            try:
                submitter = ParamikoSubmitter(as_conf=as_conf)
                hpcarch = submitter.platforms[as_conf.get_platform()]
            except AutosubmitCritical as e:
                Log.warning(f"{e.message}\nRemote git cloning is disabled")
                hpcarch = "local"
            except KeyError:
                Log.warning(f"Platform {as_conf.get_platform()} not found in configuration file")
                hpcarch = "local"
            return AutosubmitGit.clone_repository(as_conf, force, hpcarch)
        elif project_type == "svn":
            svn_project_url = as_conf.get_svn_project_url()
            svn_project_revision = as_conf.get_svn_project_revision()
            local_proj_dir = os.path.join(
                BasicConfig.LOCAL_ROOT_DIR, expid, BasicConfig.LOCAL_PROJ_DIR)
            if os.path.exists(local_proj_dir):
                Log.info(f"Using project folder: {local_proj_dir}")
                if not force:
                    Log.debug("The project folder exists. SKIPPING...")
                    return True
                else:
                    shutil.rmtree(local_proj_dir, ignore_errors=True)
            try:
                os.mkdir(local_proj_dir)
            except BaseException as e:
                raise AutosubmitCritical(f"Project path:{local_proj_dir} can't be created. Revise that the path"
                                         f" is the correct one.", 7014, str(e))

            Log.debug(f"The project folder {local_proj_dir} has been created.")
            Log.info(f"Checking out revision {svn_project_revision + ' ' + svn_project_url} into {local_proj_dir}")
            try:
                output = subprocess.check_output("cd " + local_proj_dir + "; svn --force-interactive checkout -r " +
                                                 svn_project_revision + " " + svn_project_url + " " +
                                                 project_destination, shell=True)
            except subprocess.CalledProcessError:
                try:
                    shutil.rmtree(local_proj_dir, ignore_errors=True)
                except Exception:
                    pass
                raise AutosubmitCritical(f"Can not check out revision {svn_project_revision} {svn_project_url} "
                                         f"into {local_proj_dir}", 7062)
            Log.debug(f"{output}")
        elif project_type == "local":
            local_project: str = as_conf.get_local_project_path()
            if not local_project:
                raise AutosubmitCritical(
                    "Empty project path! Please change this parameter to a valid one.", 7014)
            # check if local_project_path is a valid path
            local_project_path: Path = Path(local_project)
            if not local_project_path.is_dir():
                msg = f'Local project path is not a valid path and/or it does not exist: {str(local_project_path)}'
                raise AutosubmitCritical(msg, 7014)

            local_proj_dir_path: Path = Path(BasicConfig.LOCAL_ROOT_DIR, expid, BasicConfig.LOCAL_PROJ_DIR)
            project_destination = local_proj_dir_path / project_destination

            # TODO: Move to a new package/file once we simplify ``autosubmit.py``.
            def copy_contents(from_: Path, to: Path):
                try:
                    # TODO: Do it in pure-python?
                    Log.info(f"Copying {str(from_)} into {str(to)}")
                    cmd_output = subprocess.check_output(f"cp -R {str(from_)}/* {str(to)}/", shell=True)
                    Log.debug(str(cmd_output))
                except subprocess.CalledProcessError:
                    with suppress(Exception):
                        Log.debug(f'Deleting {str(to.parent)}')
                        shutil.rmtree(to.parent)
                    raise AutosubmitCritical(f"Cannot copy {str(from_)} into {str(to.parent)}. Exiting...", 7063)

            if not local_proj_dir_path.exists():
                Path(local_proj_dir_path).mkdir(parents=True)
                Path(project_destination).mkdir(parents=True)
                Log.debug(f"The project folder {local_proj_dir_path} has been created.")
                copy_contents(local_project_path, project_destination)
            else:
                Log.info(f"Using project folder: {str(local_proj_dir_path)}")

                # We use ``rsync`` if the directory already exists, syncing existing files.
                # If the file does not exist, we create the directory and issue an ``cp``
                # command to copy the files. Otherwise, we inform the user of no action.
                if not project_destination.exists():
                    Path(project_destination).mkdir(parents=True)
                    copy_contents(local_project_path, project_destination)
                elif force:
                    try:
                        cmd = f"rsync -ach --info=progress2 {str(local_project_path)}/* {str(project_destination)}"
                        subprocess.call([cmd], shell=True)
                    except (subprocess.CalledProcessError, IOError):
                        raise AutosubmitCritical(f"Cannot rsync {str(local_project_path)} into "
                                                 f"{str(project_destination.parent)}. Exiting...", 7063)
                else:
                    # Previously we did not inform users when nothing was copied.
                    Log.info("Local project destination already exists, will not sync project files.")

        return True

    @staticmethod
    def change_status(final, final_status, job, save):
        """Set job status to final.

        :param save:
        :param final:
        :param final_status:
        :param job:
        """
        if save:
            if job.status in [Status.SUBMITTED, Status.QUEUING, Status.HELD] and final_status not in [Status.QUEUING,
                                                                                                      Status.HELD,
                                                                                                      Status.SUSPENDED]:
                job.hold = False
                if job.platform_name and job.platform_name.upper() != "LOCAL":
                    job.platform.send_command(job.platform.cancel_cmd + " " + str(job.id), ignore_log=True)
            elif job.status in [Status.QUEUING, Status.RUNNING, Status.SUBMITTED] and final_status == Status.SUSPENDED:
                if job.platform_name and job.platform_name.upper() != "LOCAL":
                    job.platform.send_command("scontrol hold " + f"{job.id}", ignore_log=True)
            elif final_status in [Status.QUEUING, Status.RUNNING] and (job.status == Status.SUSPENDED):
                if job.platform_name and job.platform_name.upper() != "LOCAL":
                    job.platform.send_command("scontrol release " + f"{job.id}", ignore_log=True)
        if job.status == Status.FAILED and job.status != final_status:
            job._fail_count = 0
        job.status = final_status
        Log.info("CHANGED: job: " + job.name + " status to: " + final)
        Log.status("CHANGED: job: " + job.name + " status to: " + final)

    @staticmethod
    def _validate_section(as_conf, filter_section):
        section_validation_error = False
        section_error = False
        section_not_foundList = list()
        section_validation_message = "\n## Section Validation Message ##"
        countStart = filter_section.count('[')
        countEnd = filter_section.count(']')
        if countStart > 1 or countEnd > 1:
            section_validation_error = True
            section_validation_message += "\n\tList of sections has a format error. Perhaps you were trying to use -fc instead."
        if section_validation_error is False:
            if len(str(filter_section).strip()) > 0:
                if len(filter_section.split()) > 0:
                    jobSections = as_conf.jobs_data
                    for section in filter_section.split():
                        # print(section)
                        # Provided section is not an existing section, or it is not the keyword 'Any'
                        if section not in jobSections and (section != "Any"):
                            section_error = True
                            section_not_foundList.append(section)
            else:
                section_validation_error = True
                section_validation_message += "\n\tEmpty input. No changes performed."
        if section_validation_error is True or section_error is True:
            if section_error is True:
                section_validation_message += "\n\tSpecified section(s) : [" + str(section_not_foundList) + " not found" \
                                                                                                            ".\n\tProcess stopped. Review the format of the provided input. Comparison is case sensitive." + \
                                              "\n\tRemember that this option expects section names separated by a blank space as input."

            raise AutosubmitCritical("Error in the supplied input for -ft.", 7011, section_validation_message)

    @staticmethod
    def _validate_list(as_conf, job_list, filter_list):
        job_validation_error = False
        job_error = False
        job_not_foundList = list()
        job_validation_message = "\n## Job Validation Message ##"
        jobs = list()
        countStart = filter_list.count('[')
        countEnd = filter_list.count(']')
        if countStart > 1 or countEnd > 1:
            job_validation_error = True
            job_validation_message += "\n\tList of jobs has a format error. Perhaps you were trying to use -fc instead."

        if job_validation_error is False:
            for job in job_list.get_job_list():
                jobs.append(job.name)
            if len(str(filter_list).strip()) > 0:
                if len(filter_list.split()) > 0:
                    for sentJob in filter_list.split():
                        # Provided job does not exist, or it is not the keyword 'Any'
                        if sentJob not in jobs and (sentJob != "Any"):
                            job_error = True
                            job_not_foundList.append(sentJob)
            else:
                job_validation_error = True
                job_validation_message += "\n\tEmpty input. No changes performed."

        if job_validation_error is True or job_error is True:
            if job_error is True:
                job_validation_message += "\n\tSpecified job(s) : [" + str(
                    job_not_foundList) + "] not found in the experiment " + \
                                          str(as_conf.expid) + ". \n\tProcess stopped. Review the format of the provided input. Comparison is case sensitive." + \
                                          "\n\tRemember that this option expects job names separated by a blank space as input."
            raise AutosubmitCritical(
                "Error in the supplied input for -ft.", 7011, job_validation_message)

    @staticmethod
    def _validate_status(job_list, filter_status):
        status_validation_error = False
        status_validation_message = "\n## Status Validation Message ##"
        # Trying to identify chunk formula
        countStart = filter_status.count('[')
        countEnd = filter_status.count(']')
        if countStart > 1 or countEnd > 1:
            status_validation_error = True
            status_validation_message += "\n\tList of status provided has a format error. Perhaps you were trying to use -fc instead."
        # If everything is fine until this point
        if status_validation_error is False:
            status_filter = filter_status.split()
            status_reference = Status()
            status_list = list()
            for job in job_list.get_job_list():
                reference = status_reference.VALUE_TO_KEY[job.status]
                if reference not in status_list:
                    status_list.append(reference)
            for status in status_filter:
                if status not in status_list:
                    status_validation_error = True
                    status_validation_message += "\n\t There are no jobs with status " + \
                                                 status + " in this experiment."
        if status_validation_error is True:
            raise AutosubmitCritical("Error in the supplied input for -fs.", 7011, status_validation_message)

    @staticmethod
    def _validate_chunk_formula(chunk_formula: str, validation_message: str) -> str:
        """Validate chunk formula syntax.

        [ 19900101 [ fc0 [ Any ] fc1 [1 2] ] 19950101 [ fc0 [1-10] ] ]


        :param chunk_formula: Chunk formula string.
        :param validation_message: Message to append validation errors to.
        """

        if not chunk_formula:
            validation_message += "\n\tMissing chunk formula before the first comma."
            if "[" not in chunk_formula or "]" not in chunk_formula:
                validation_message += "\n\tMissing chunk formula brackets."
            return validation_message

        brackets_left = chunk_formula.count('[')
        brackets_right = chunk_formula.count(']')
        if brackets_left != brackets_right:
            validation_message += "\n\tUnbalanced brackets in chunk formula."

        try:
            json_data = json.loads(Autosubmit._create_json(chunk_formula))
        except Exception as e:
            validation_message += "\n\tMust follow chunk formula structure: [ DATE [ MEMBER [ CHUNKS ] ... ] ... ]"
            validation_message += f"\n\tJSON Error: {str(e)}"
            return validation_message

        dates = 'sds'
        members = 'ms'
        chunks = 'cs'
        date = 'sd'
        member = 'm'

        sections = []
        json_validation_message = ""
        for date_entry in json_data[dates]:
            try:
                date_str = date_entry[date]
            except KeyError:
                json_validation_message += "\n\tMissing DATE in chunk formula."
                continue
            for member_entry in date_entry[members]:
                try:
                    member_str = member_entry[member]
                except KeyError:
                    json_validation_message += "\n\tMissing MEMBER in chunk formula."
                    continue
                try:
                    chunks_str = str(len(member_entry[chunks])) if "ANY" not in member_entry[chunks] else "ANY"
                except KeyError:
                    json_validation_message += "\n\tMissing CHUNKS in chunk formula."
                    continue
                section_str = f"{date_str} [ {member_str} [ {chunks_str} ] ]"
                sections.append(section_str)

        if json_validation_message:
            validation_message += "\n\tMust follow chunk formula structure: [ DATE [ MEMBER [ CHUNKS ] ... ] ... ]"
            validation_message += json_validation_message

        return validation_message

    @staticmethod
    def _validate_section_split_formula(section_split_formula: str, validation_message: str) -> str:
        """Validate section/split formula syntax.

        Expects to receive the second part of the -ftcs filter. ex: SIM [ Any ], SIM2 [1 2], SIM3.

        :param section_split_formula: section_split_formula string.
        :param validation_message: Message to append validation errors to.
        """

        if not section_split_formula:
            return validation_message

        for section in [section.strip() for section in section_split_formula.split(",") if section.strip()]:
            if '[' not in section and ']' not in section:
                if len(section.split()) > 1:
                    validation_message += f"\n\tMalformed section/split entry: {section}. "
                    continue
            else:
                brackets_left = section.count('[')
                brackets_right = section.count(']')
                if brackets_left != brackets_right:
                    validation_message += "\n\tUnbalanced brackets in section/split formula."

                if brackets_left > 1:
                    validation_message += f"\n\tToo many opening brackets '[' in section/split entry: {section}."
                if brackets_right > 1:
                    validation_message += f"\n\tToo many closing brackets ']' in section/split entry: {section}."
                if brackets_left == 1 and brackets_right == 1:
                    splits = section.split('[')[-1].split(']')[0].strip().upper()
                    if any(char in splits for char in (":", "-")):
                        start_end = re.split(r'[:\-]', splits)
                        if len(start_end) < 2:
                            validation_message += f"\n\tIncomplete split range in section/split entry: {splits}."
                        elif not (start_end[0].strip().isdigit() and start_end[-1].strip().isdigit()):
                            validation_message += f"\n\tNon-integer split range in section/split entry: {splits}."
                    elif not all(p.strip().isdigit() or p.strip().upper() == "ANY" for p in splits.split()):
                        validation_message += f"\n\tNon-integer split in section/split entry: {splits}."

        return validation_message

    @staticmethod
    def _validate_chunk_section_split(filter_string: str) -> None:
        """Validate a chunk/section/split filter string for commands using -fc/-ftc/-ftcs.

        Validate that the filter string contains a chunk formula and, optionally,
        a comma-separated list of section names. Section names are checked
        case-insensitively against the keys in ``as_conf.jobs_data``.

        Splits are also optional and must be included in the Section part of the formula.

        [ chunk_splits_formula, section1 [splits], section2 [splits], ... ]

        Example:

        [ 19900101 [ fc0 [ Any ] fc1 [1 2] ] 19950101 [ fc0 [1-10] ], SIM [ Any ], SIM2 [1 2] ]

        :param filter_string: Filter string with form '<chunk_split_formula>[,<SECTION>[<splits>],...]'.
        :raises AutosubmitCritical: If the input is malformed or references unknown sections.
        """

        validation_message = "## -fc // -ftc // -ftcs Validation Message ##"
        filter_string = filter_string.upper().strip()

        level = 0
        for i, ch in enumerate(filter_string):
            if ch == '[':
                level += 1
            elif ch == ']':
                level -= 1
                if level < 0:
                    validation_message += f"\n\tUnexpected ']' at position {i}."
        if level != 0:
            validation_message += "\n\tUnbalanced brackets in filter string."

        filter_string_parts = filter_string.split(',')
        chunk_formula = filter_string_parts[0].strip()
        section_split_formula = ",".join(filter_string_parts[1:]) if len(filter_string_parts) > 1 else ''

        validation_message = Autosubmit._validate_chunk_formula(chunk_formula, validation_message)
        validation_message = Autosubmit._validate_section_split_formula(section_split_formula, validation_message)

        if validation_message != "## -fc // -ftc // -ftcs Validation Message ##":
            raise AutosubmitCritical("Error in the supplied input for -fc // -ftc // -ftcs.", 7011, validation_message)

    @staticmethod
    def _validate_set_status_filters(as_conf: AutosubmitConfig, job_list: JobList,
                                     filter_list: Optional[str],
                                     filter_chunk_section_split: Optional[str],
                                     filter_status: Optional[str],
                                     filter_section: Optional[str]) -> None:
        """Validate filters provided to the setstatus command.

        Each non-empty filter is validated by its corresponding helper. Raises
        AutosubmitCritical (code 7014) if all filters are empty or whitespace-only.

        :param as_conf: Autosubmit configuration object.
        :param job_list: JobList object containing jobs to validate against.
        :param filter_list: Job name list filter (``-fl``).
        :param filter_chunk_section_split: Chunk/section/split filter (``-fc``, ``-ftc``, ``-ftcs``).
        :param filter_status: Status filter (``-fs``).
        :param filter_section: Section filter (``-ft``).
        :raises AutosubmitCritical: If no non-empty filter is provided or if any validator fails.
        """
        all_empty = True
        if filter_section:
            Autosubmit._validate_section(as_conf, filter_section)
            all_empty = False

        if filter_list:
            Autosubmit._validate_list(as_conf, job_list, filter_list)
            all_empty = False

        if filter_status:
            Autosubmit._validate_status(job_list, filter_status)
            all_empty = False

        if filter_chunk_section_split:
            Autosubmit._validate_chunk_section_split(filter_chunk_section_split)
            all_empty = False

        if all_empty:
            raise AutosubmitCritical("At least one filter must be provided and must be not empty when using -fs, -ft, -fc, -ftc or -ftcs.", 7014)


    @staticmethod
    def _split_match(j: Job, split_list: list[str]) -> bool:
        """Check if job split matches"""
        return "ANY" in split_list or not j.splits or int(j.splits) < 2 or not split_list or str(j.split) in split_list

    @staticmethod
    def _filter_sections_splits(filter_section_splits: str, jobs: list[Job]) -> list[Job]:
        """Filter jobs by sections and splits.
        :param filter_section_splits: filter sections and splits
        :param jobs: list of jobs
        :return: list of jobs matching the filter
        """
        section_matching_jobs: list[Job] = []
        for section in filter_section_splits:
            section_name = section.strip().split("[")[0].strip()
            job_splits = []
            if "[" in section and "]" in section:
                job_splits_str = section.strip().split("[")[1].strip(" ]")
                # splits: can be: [ 1:15 ] [ 1-15 ] [ 1 2 3 4 5 6 ] [ Any ]
                if "ANY" in job_splits_str.upper() or not job_splits_str.strip():
                    job_splits = ["ANY"]
                else:
                    for job_split in job_splits_str.split():
                        job_split = job_split.strip()
                        start = None
                        end = None
                        if job_split.find("-") != -1:
                            start = job_split.split("-")[0]
                            end = job_split.split("-")[1]
                        elif job_split.find(":") != -1:
                            start = job_split.split(":")[0]
                            end = job_split.split(":")[1]
                        if start and end:
                            job_splits += [str(i) for i in range(int(start), int(end) + 1)]
                        else:
                            job_splits.append(str(job_split))
            if not job_splits:
                job_splits = ["ANY"]

            if section_name == "ANY":
                section_matching_jobs.extend([j for j in jobs if Autosubmit._split_match(j, job_splits)])
            else:
                section_matching_jobs.extend([j for j in jobs if j.section == section_name and Autosubmit._split_match(j, job_splits)])

            section_matching_jobs = list(set(section_matching_jobs))

            # All jobs matched, no need to continue
            if len(section_matching_jobs) == len(jobs):
                break

        return section_matching_jobs

    @staticmethod
    def _filter_chunks(filter_chunk_str: str, job_list: "JobList", matching_jobs: list[Job]) -> list[Job]:
        """Filter jobs by chunks.

        :param filter_chunk_str: filter chunks
        :param job_list: JobList object
        :param matching_jobs: list of jobs to filter

        :return: list of jobs matching the filter
        """
        final_list = []
        data = json.loads(Autosubmit._create_json(filter_chunk_str))
        # Mapping so it is easier to understand
        dates = 'sds'
        members = 'ms'
        chunks = 'cs'
        date = 'sd'
        member = 'm'

        selected_dates: set = set()
        selected_members: set = set()

        # Prune first to reduce the amount of jobs that the chunk filter (last one) has to interate with
        # Here we want a reduced list of jobs that matches any date or member selected and remove the rest.
        for date_json in data[dates]:
            if "ANY" == str(date_json[date]).upper():
                selected_dates = set(date2str(d).upper() for d in job_list._date_list)
            else:
                selected_dates.add(date_json[date].upper())
            for member_json in date_json[members]:
                if "ANY" == str(member_json[member]).upper():
                    selected_members = set(m.upper() for m in job_list._member_list)
                else:
                    selected_members.add(member_json[member].upper())

        matching_jobs = [job for job in matching_jobs if
                         (not job.date or date2str(job.date).upper() in selected_dates) and
                         (not job.member or job.member.upper() in selected_members)]

        # Now, build final list according to the structure in data
        for date_json in data[dates]:
            if "ANY" == str(date_json[date]).upper():
                selected_dates = set(date2str(d).upper() for d in job_list._date_list)
            else:
                selected_dates = {date_json[date].upper()}
            for member_json in date_json[members]:
                if "ANY" == str(member_json[member]).upper():
                    selected_members = set([m.upper() for m in job_list._member_list])
                else:
                    selected_members = {member_json[member].upper()}

                selected_chunks = member_json[chunks] if "ANY" != str(member_json[chunks][-1]).upper() else [str(chunks) for chunks in job_list._chunk_list]
                final_list.extend([job for job in matching_jobs if
                                   (not job.date or date2str(job.date).upper() in selected_dates) and
                                   (not job.member or job.member.upper() in selected_members) and
                                   (not job.chunk or job.synchronize or str(job.chunk) in selected_chunks)])
        return final_list

    @staticmethod
    def filter_jobs_by_chunks_splits(job_list: "JobList", filter_chunks: str) -> list[Job]:
        """Select jobs from *job_list* according to *filter_chunks* specification.

        Expected format:
            - "[ DATE|Any [ MEMBER|Any [ CHUNKS|Any ] ... ] ... ], SECTION1|Any [SPLITS|Any], ..."

        :param job_list: JobList object
        :param filter_chunks: filter chunks
        :return: list of jobs matching the filter
        """

        filter_chunks = filter_chunks.upper()
        if "," in filter_chunks:
            splitted_filters = filter_chunks.split(",")
            fc = splitted_filters[0]
            matching_jobs = Autosubmit._filter_sections_splits(splitted_filters[1:], job_list.get_job_list())
        else:
            fc = filter_chunks
            matching_jobs = job_list.get_job_list()

        final_list = Autosubmit._filter_chunks(fc, job_list, matching_jobs)

        return list(set(final_list))

    @staticmethod
    def set_status(expid: str, noplot: bool, save: bool, final: str, filter_list: str, filter_chunks: str,
                   filter_status: str, filter_section: str, filter_type_chunk: str, filter_type_chunk_split: str,
                   hide: bool, group_by: Optional[str] = None, expand: Optional[list] = None,
                   expand_status: Optional[str] = None, check_wrapper=False, detail=False) -> bool:
        """Set status of jobs.

        :param expid: experiment id
        :param noplot: do not plot
        :param save: save
        :param final: final status
        :param filter_list: list of jobs
        :param filter_chunks: filter chunks
        :param filter_status: filter status
        :param filter_section: filter section
        :param filter_type_chunk: filter type chunk
        :param filter_type_chunk_split: filter chunk split
        :param hide: hide
        :param group_by: group by
        :param expand: Whether to expand during job grouping or not.
        :param expand_status: The status to use when expanding.
        :param check_wrapper: check wrapper
        :param detail: detail
        :return: ``True`` if executed successfully.
        """
        if filter_status:
            filter_status = filter_status.upper()
        # TODO: unify filters. There is already an issue for that
        # TODO: Normalize some filters that are the same ( To avoid changing the usage ... FIX in another PR)
        filter_chunk_section_split = None
        # TODO: TMP fix until the previous TODO is resolved. Select the one that is filled
        for f in [filter_chunks, filter_type_chunk, filter_type_chunk_split]:
            filter_chunk_section_split = f if f else filter_chunk_section_split

        check_ownership(expid, raise_error=True)
        exp_path = os.path.join(BasicConfig.LOCAL_ROOT_DIR, expid)
        tmp_path = os.path.join(exp_path, BasicConfig.LOCAL_TMP_DIR)
        try:
            with Lock(os.path.join(tmp_path, 'autosubmit.lock'), timeout=1):
                Log.info(
                    "Preparing .lock file to avoid multiple instances with same expid.")

                Log.debug(f'Exp ID: {expid}')
                Log.debug(f'Save: {save}')
                Log.debug(f'Final status: {final}')
                Log.debug(f'List of jobs to change: {filter_list}')
                Log.debug(f'Chunks to change: {filter_chunk_section_split}')
                Log.debug(f'Status of jobs to change: {filter_status}')
                Log.debug(f'Sections to change: {filter_section}')

                wrongExpid = 0
                as_conf = AutosubmitConfig(
                    expid, BasicConfig, YAMLParserFactory())
                as_conf.check_conf_files(True)

                # Getting output type from configuration
                output_type = as_conf.get_output_type()
                # Getting db connections
                # To be added in a function that checks which platforms must be connected to
                job_list = Autosubmit.load_job_list(expid, as_conf, monitor=True, new=False)
                submitter = ParamikoSubmitter(as_conf=as_conf)
                hpcarch = as_conf.get_platform()
                for job in job_list.get_job_list():
                    job.platform_name = as_conf.jobs_data.get(job.section, {}).get("PLATFORM", "").upper()
                    if not job.platform_name:
                        job.platform_name = hpcarch
                    # noinspection PyTypeChecker
                    job.platform = submitter.platforms[job.platform_name]
                platforms_to_test = set()
                platforms = submitter.platforms
                for job in job_list.get_job_list():
                    job.submitter = submitter
                    if job.platform_name is None:
                        job.platform_name = hpcarch
                    # noinspection PyTypeChecker
                    job.platform = platforms[job.platform_name]
                    # noinspection PyTypeChecker
                    if job.status in [Status.QUEUING, Status.SUBMITTED, Status.RUNNING]:
                        platforms_to_test.add(platforms[job.platform_name])
                # establish the connection to all platforms
                definitive_platforms = list()
                for platform in platforms_to_test:
                    try:
                        Autosubmit.restore_platforms([platform], as_conf=as_conf)
                        definitive_platforms.append(platform.name)
                    except Exception:
                        pass
                ##### End of the ""function""
                # This will raise an autosubmit critical if any of the filters has issues in the format specified by the user
                Autosubmit._validate_set_status_filters(as_conf, job_list, filter_list, filter_chunk_section_split, filter_status,
                                                        filter_section)
                #### Starts the filtering process ####
                Log.info("Filtering jobs...")
                final_list = []
                final_status = Autosubmit._get_status(final)
                # I have the impression that whoever did this function thought about the possibility of having multiple filters at the same time
                # But, as it was, it is not possible to have multiple filters at the same time due to the way the code is written
                if filter_section:
                    ft = filter_section.split()
                    if str(ft).upper() == 'ANY':
                        for job in job_list.get_job_list():
                            final_list.append(job)
                    else:
                        for section in ft:
                            for job in job_list.get_job_list():
                                if job.section == section:
                                    final_list.append(job)
                # TODO: unify filters in the args. There is already an issue for that
                if filter_chunks or filter_type_chunk or filter_chunk_section_split:
                    start = time.time()
                    # The extend is because the code was thought to have multiple filters at the same time (but it is not possible for some combinations)
                    final_list.extend(Autosubmit.filter_jobs_by_chunks_splits(job_list, filter_chunk_section_split))
                    final_list = list(set(final_list))
                    Log.info(f"Chunk filtering took {time.time() - start:.2f} seconds.")

                if filter_status:
                    status_list = filter_status.split()
                    Log.debug(f"Filtering jobs with status {filter_status}")
                    if str(status_list).upper() == 'ANY':
                        for job in job_list.get_job_list():
                            final_list.append(job)
                    else:
                        for status in status_list:
                            fs = Autosubmit._get_status(status)
                            for job in [j for j in job_list.get_job_list() if j.status == fs]:
                                final_list.append(job)

                if filter_list:
                    jobs = filter_list.split()
                    expidJoblist = defaultdict(int)
                    for x in filter_list.split():
                        expidJoblist[str(x[0:4])] += 1
                    if str(expid) in expidJoblist:
                        wrongExpid = jobs.__len__() - expidJoblist[expid]
                    if wrongExpid > 0:
                        Log.warning(f"There are {wrongExpid} job.name with an invalid Expid")
                    if str(jobs).upper() == 'ANY':
                        for job in job_list.get_job_list():
                            final_list.append(job)
                    else:
                        for job in job_list.get_job_list():
                            if job.name in jobs:
                                final_list.append(job)

                # Time to change status
                final_list = list(set(final_list))
                performed_changes = {}
                Log.info(f"The selected number of jobs to change is: {len(final_list)}")
                for job in final_list:
                    if final_status in [Status.WAITING, Status.PREPARED, Status.DELAYED, Status.READY]:
                        job.fail_count = 0
                    if job.status in [Status.QUEUING, Status.RUNNING,
                                      Status.SUBMITTED] and job.platform.name not in definitive_platforms:
                        Log.printlog(f"JOB: [{job.platform.name}] is ignored as the [{job.name}] platform is currently"
                                     f" offline", 6000)
                        continue
                    if job.status != final_status:
                        # Only real changes
                        performed_changes[job.name] = str(
                            Status.VALUE_TO_KEY[job.status]) + " -> " + str(final)
                        Autosubmit.change_status(
                            final, final_status, job, save)
                # If changes have been performed
                if performed_changes:
                    if detail:
                        current_length = len(job_list.get_job_list())
                        if current_length > 1000:
                            Log.warning(
                                "-d option: Experiment has too many jobs to be printed in the terminal. Maximum job quantity is 1000, your experiment has " + str(
                                    current_length) + " jobs.")
                        else:
                            Log.info(job_list.print_with_status(
                                status_change=performed_changes))
                else:
                    Log.warning("No changes were performed.")
                Log.info(f"Updating JobList for experiment {expid}...")
                job_list.update_list(as_conf, False, True)
                start = time.time()
                if save and wrongExpid == 0:
                    job_list.recover_last_data(final_list)
                    job_list.save()
                    end = time.time()
                    Log.info(f"JobList saved in {end - start:.2f} seconds.")
                    exp_history = ExperimentHistory(expid, jobdata_dir_path=BasicConfig.JOBDATA_DIR,
                                                    historiclog_dir_path=BasicConfig.HISTORICAL_LOG_DIR)
                    exp_history.initialize_database()
                    exp_history.process_status_changes(job_list.get_job_list(),
                                                       chunk_unit=as_conf.get_chunk_size_unit(),
                                                       chunk_size=as_conf.get_chunk_size(),
                                                       current_config=as_conf.get_full_config_as_json())
                    Autosubmit.database_backup(expid)
                else:
                    Log.printlog(
                        "Changes NOT saved to the JobList!!!!:  use -s option to save", 3000)
                # Visualization stuff that should be in a function common to monitor , create, -cw flag, inspect and so on
                if not noplot:
                    from .monitor.monitor import Monitor
                    if as_conf.get_wrapper_type() != 'none' and check_wrapper:
                        packages_persistence = JobPackagePersistence(expid)
                        if BasicConfig.DATABASE_BACKEND == 'sqlite':
                            os.chmod(os.path.join(BasicConfig.LOCAL_ROOT_DIR,
                                                  expid, "pkl", "job_packages_" + expid + ".db"), 0o775)
                        packages_persistence.reset_table(True)
                        job_list_wr = Autosubmit.load_job_list(expid, as_conf, monitor=True, new=False)

                        Autosubmit.generate_scripts_andor_wrappers(as_conf, job_list_wr, job_list_wr.get_job_list(),
                                                                   packages_persistence, True)

                        packages = packages_persistence.load(True)
                    else:
                        packages = JobPackagePersistence(expid).load()
                    groups_dict = dict()
                    if group_by:
                        status = list()
                        if expand_status:
                            for s in expand_status.split():
                                status.append(
                                    Autosubmit._get_status(s.upper()))

                        job_grouping = JobGrouping(group_by, copy.deepcopy(job_list.get_job_list()), job_list,
                                                   expand_list=expand,
                                                   expanded_status=status)
                        groups_dict = job_grouping.group_jobs()
                    Log.info("\nPlotting joblist...")
                    monitor_exp = Monitor()
                    monitor_exp.generate_output(expid,
                                                job_list.get_job_list(),
                                                os.path.join(
                                                    exp_path, "/tmp/LOG_", expid),
                                                output_format=output_type,
                                                packages=packages,
                                                show=not hide,
                                                groups=groups_dict,
                                                job_list_object=job_list)
                return True
        except Exception:
            raise

    @staticmethod
    def _get_status(s):
        """Convert job status from str to Status.

        :param s: status string
        :type s: str
        :return: status instance
        :rtype: Status
        """
        s = s.upper()
        if s == 'READY':
            return Status.READY
        elif s == 'COMPLETED':
            return Status.COMPLETED
        elif s == 'WAITING':
            return Status.WAITING
        elif s == 'HELD':
            return Status.HELD
        elif s == 'SUSPENDED':
            return Status.SUSPENDED
        elif s == 'FAILED':
            return Status.FAILED
        elif s == 'RUNNING':
            return Status.RUNNING
        elif s == 'QUEUING':
            return Status.QUEUING
        elif s == 'UNKNOWN':
            return Status.UNKNOWN

    @staticmethod
    def _get_members(out):
        """Function to get a list of members from json.

        :param out: json member definition
        :type out: str
        :return: list of members
        :rtype: list
        """
        count = 0
        data = []
        # noinspection PyUnusedLocal
        for element in out:
            if count % 2 == 0:
                ms = {'m': out[count],
                      'cs': Autosubmit._get_chunks(out[count + 1])}
                data.append(ms)
                count += 1
            else:
                count += 1

        return data

    @staticmethod
    def _get_chunks(out):
        """Function to get a list of chunks from json.

        :param out: json member definition
        :type out: str
        :return: list of chunks
        :rtype: list
        """
        data = []
        for element in out:
            if element.find("-") != -1:
                numbers = element.split("-")
                for count in range(int(numbers[0]), int(numbers[1]) + 1):
                    data.append(str(count))
            else:
                data.append(element)

        return data

    @staticmethod
    def _get_submitter(as_conf) -> ParamikoSubmitter:
        """Returns the submitter corresponding to the communication defined on autosubmit's config file."""
        return ParamikoSubmitter(as_conf=as_conf)

    @staticmethod
    def _create_json(text):
        """Function to parse rerun specification from json format.

        :param text: text to parse
        :type text: str
        :return: parsed output
        """
        count = 0
        data = []

        # text = "[ 19601101 [ fc0 [1 2 3 4] fc1 [1] ] 16651101 [ fc0 [1-30 31 32] ] ]"

        def parse_date(datestring):
            result = []
            startindex = datestring.find('(')
            endindex = datestring.find(')')
            if startindex > 0 and endindex > 0:
                try:
                    startstring = datestring[:startindex]
                    startrange = datestring[startindex + 1:].split('-')[0]
                    endrange = datestring[startindex:-1].split('-')[1]
                    startday = int(startrange[-2:])
                    endday = int(endrange[-2:])

                    frommonth = int(startrange[:2])
                    tomonth = int(endrange[:2])

                    for i in range(frommonth, tomonth + 1):
                        for j in range(startday, endday + 1):
                            result.append(startstring + f"{i:.2d}" + f"{j:.2d}")
                except Exception as exp:
                    raise AutosubmitCritical(f"Autosubmit couldn't parse your input format. Exception: {exp}")

            else:
                result = [datestring]
            return result

        out = nestedExpr('[', ']').parseString(text).asList()

        # noinspection PyUnusedLocal
        for element in out[0]:
            if count % 2 == 0:
                datelist = parse_date(out[0][count])
                for item in datelist:
                    sd = {'sd': item, 'ms': Autosubmit._get_members(
                        out[0][count + 1])}
                    data.append(sd)
                count += 1
            else:
                count += 1

        sds = {'sds': data}
        result = json.dumps(sds)
        return result

    @staticmethod
    def _get_job_list_persistence(expid, as_conf) -> JobListPersistence:
        """Returns the JobListPersistence corresponding to the storage type defined on autosubmit's config file.

        :return: job_list_persistence
        :rtype: JobListPersistence
        """
        if BasicConfig.DATABASE_BACKEND != 'sqlite':
            return JobListPersistenceDb(expid)

        storage_type = as_conf.get_storage_type()
        if storage_type == 'pkl':
            return JobListPersistencePkl()
        elif storage_type == 'db':
            return JobListPersistenceDb(expid)
        raise AutosubmitCritical('Storage type not known', 7014)

    @staticmethod
    def testcase(description, hpc=None, copy_id=None, minimal_configuration=False, git_repo=None, git_branch=None,
                 git_as_conf=None, use_local_minimal=False):
        """Method to conduct a test for a given experiment. It creates a new experiment for a given experiment with a
        given number of chunks with a random start date and a random member to be run on a random HPC.
        :param description: description of the experiment
        :param hpc: HPC to be used by the test. If None, a random HPC will be chosen
        :param copy_id: copy id to be used by the test. If None, a random copy id will be chosen
        :param minimal_configuration: if True, the experiment will be run with a minimal configuration
        :param git_repo: git repository to be used by the test. If None, a random git repository will be chosen
        :param git_branch: git branch to be used by the test. If None, a random git branch will be chosen
        :param git_as_conf: git autosubmit configuration to be used by the test.
        If None, a random git autosubmit configuration will be chosen.
        :param use_local_minimal: if True, the experiment will be run with a local minimal configuration
        :return: Whether the test case ID was generated successfully or not.
        """
        test_case_id = Autosubmit.expid(description, hpc, copy_id, False, minimal_configuration, git_repo,
                                        git_branch, git_as_conf, use_local_minimal=use_local_minimal, testcase=True)
        return test_case_id.strip() != ''

    # TODO: To be moved to utils
    @staticmethod
    def load_job_list(expid, as_conf, monitor=False, new=True) -> JobList:
        rerun = as_conf.get_rerun()
        job_list = JobList(expid, as_conf, YAMLParserFactory(),
                           Autosubmit._get_job_list_persistence(expid, as_conf))
        run_only_members = as_conf.get_member_list(run_only=True)
        date_list = as_conf.get_date_list()
        date_format = ''
        if as_conf.get_chunk_size_unit() == 'hour':
            date_format = 'H'
        for date in date_list:
            if date.hour > 1:
                date_format = 'H'
            if date.minute > 1:
                date_format = 'M'
        wrapper_jobs = dict()
        for wrapper_section, wrapper_data in as_conf.experiment_data.get("WRAPPERS", {}).items():
            if isinstance(wrapper_data, dict):
                wrapper_jobs[wrapper_section] = wrapper_data.get("JOBS_IN_WRAPPER", [])

        job_list.generate(as_conf, date_list, as_conf.get_member_list(), as_conf.get_num_chunks(),
                          as_conf.get_chunk_ini(),
                          as_conf.experiment_data, date_format, as_conf.get_retrials(),
                          as_conf.get_default_job_type(), wrapper_jobs,
                          new=new, run_only_members=run_only_members, monitor=monitor)

        if str(rerun).lower() == "true":
            rerun_jobs = as_conf.get_rerun_jobs()
            job_list.rerun(rerun_jobs, as_conf, monitor=monitor)
        else:
            job_list.remove_rerun_only_jobs()

        return job_list

    @staticmethod
    def cat_log(exp_or_job_id: str, file: Union[None, str], mode: Union[None, str], inspect: bool = False) -> bool:
        """The cat-log command allows users to view Autosubmit logs using the command-line.

        It is possible to use ``autosubmit cat-log`` for Workflow and for Job logs. It decides
        whether to show Workflow or Job logs based on the ``ID`` given. Shorter ID's, such as
        ``a000` are considered Workflow ID's, so it will display logs for that workflow. For
        longer ID's, such as ``a000_20220401_fc0_1_GSV``, the command will display logs for
        that specific job.

        Users can choose the log file using the ``FILE`` parameter, to display an error or
        output log file, for instance.

        Finally, the ``MODE`` parameter allows users to choose whether to display the complete
        file contents (similar to the ``cat`` command) or to start tailing its output (akin to
        ``tail -f``).

        :param exp_or_job_id: A workflow or job ID.
        :type exp_or_job_id: str
        :param file: the type of the file to be printed (not the file path!).
        :type file: Union[None, str]
        :param mode: the mode to print the file (e.g. cat, tail).
        :type mode: Union[None, str]
        :param inspect: when True it will use job files in tmp/ instead of tmp/LOG_a000/.
        :type inspect: bool
        :return:
        :rtype: bool
        """

        def view_file(log_file: Path, mode: str):
            if mode == 'c':
                cmd = ['cat', str(log_file)]
                subprocess.Popen(
                    cmd,
                    stdin=subprocess.DEVNULL,
                    stdout=None
                )
                return 0
            elif mode == 't':
                cmd = [
                    'tail',
                    '--lines=+1',
                    '--retry',
                    '--follow=name',
                    workflow_log_file
                ]
                proc = subprocess.Popen(cmd, stdin=subprocess.DEVNULL)
                with suppress(KeyboardInterrupt):
                    return proc.wait() == 0

        MODES = {
            'c': 'cat',
            't': 'tail'
        }
        FILES = {
            'o': 'output',
            'j': 'job',
            'e': 'error',
            's': 'status'
        }
        if file is None:
            file = 'o'
        if file not in FILES.keys():
            raise AutosubmitCritical(f'Invalid cat-log file {file}. Expected one of {[f for f in FILES.keys()]}', 7011)
        if mode is None:
            mode = 'c'
        if mode not in MODES.keys():
            raise AutosubmitCritical(f'Invalid cat-log mode {mode}. Expected one of {[m for m in MODES.keys()]}', 7011)

        is_workflow = '_' not in exp_or_job_id

        expid = exp_or_job_id if is_workflow else exp_or_job_id[:4]

        # Workflow folder.
        # e.g. ~/autosubmit/a000
        exp_path = Path(BasicConfig.LOCAL_ROOT_DIR, expid)
        # Directory with workflow temporary/volatile files. Contains the output of commands such as inspect,
        # and also STAT/COMPLETED files for each workflow task.
        # e.g. ~/autosubmit/a000/tmp
        tmp_path = exp_path / BasicConfig.LOCAL_TMP_DIR
        # Directory with logs for Autosubmit executed commands (create, run, etc.) and jobs statuses files.
        # e.g. ~/autosubmit/a000/tmp/ASLOGS
        aslogs_path = tmp_path / BasicConfig.LOCAL_ASLOG_DIR
        # Directory with the logs of the workflow run, for each workflow task. Includes the generated
        # .cmd files, and STAT/COMPLETED files for the run. The files with similar names in the parent
        # directory are generated with inspect, while these are with the run subcommand.
        # e.g. ~/autosubmit/a000/tmp/LOG_a000
        exp_logs_path = tmp_path / f'LOG_{expid}'

        if is_workflow:
            if file not in ['o', 'e', 's']:
                raise AutosubmitCritical(f'Invalid arguments for cat-log: workflow logs only support o(output), '
                                         f'e(error), and s(status). Requested: {mode}', 7011)

            if file in ['e', 'o']:
                search_pattern = '*_run_err.log' if file == 'e' else '*_run.log'
                workflow_log_files = sorted(aslogs_path.glob(search_pattern))
            else:
                search_pattern = f'{expid}_*.txt'
                status_files_path = exp_path / 'status'
                workflow_log_files = sorted(status_files_path.glob(search_pattern))

            if not workflow_log_files:
                Log.info('No logs found.')
                return True

            workflow_log_file = workflow_log_files[-1]
            if not workflow_log_file.is_file():
                raise AutosubmitCritical(f'The workflow log file found is not a file: {workflow_log_file}', 7011)

            return view_file(workflow_log_file, mode) == 0
        else:
            job_logs_path = tmp_path if inspect else exp_logs_path
            if file == 'j':
                workflow_log_file = job_logs_path / f'{exp_or_job_id}.cmd'
            elif file == 's':
                workflow_log_file = job_logs_path / f'{exp_or_job_id}_TOTAL_STATS'
            else:
                search_pattern = f'{exp_or_job_id}.*.{"err" if file == "e" else "out"}'
                workflow_log_files = sorted(job_logs_path.glob(search_pattern))
                if not workflow_log_files:
                    Log.info('No logs found.')
                    return True
                workflow_log_file = workflow_log_files[-1]

            if not workflow_log_file.exists():
                Log.info('No logs found.')
                return True

            if not workflow_log_file.is_file():
                raise AutosubmitCritical(f'The job log file {file} found is not a file: {workflow_log_file}', 7011)

            return view_file(workflow_log_file, mode) == 0

    @staticmethod
    def stop(expids: str, force=False, all_expids=False, force_all=False, cancel=False,
             current_status="", status="FAILED", force_yes=False) -> bool:
        """The stop command allows users to stop the desired experiments.

        :param expids: expids to stop
        :type expids: str
        :param force: force the stop of the experiment
        :type force: bool
        :param all_expids: stop all experiments
        :type all_expids: bool
        :param force_all: force the stop of all experiments
        :type force_all: bool
        :param cancel: cancel the jobs of the experiment
        :type cancel: bool
        :param current_status: what status to change # defaults to all active jobs.
        :type current_status: str
        :param status: status to change the active jobs to
        :type status: str
        :param force_yes: force yes answer to prompts
        :type force_yes: bool
        """
        from autosubmit.helpers.processes import process_id, retrieve_expids
        from autosubmit.job.job_utils import cancel_jobs

        if status not in Status.VALUE_TO_KEY.values():
            raise AutosubmitCritical(f"Invalid status. Expected one of {Status.VALUE_TO_KEY.keys()}", 7011)

        try:
            current_status = current_status.replace(',', ' ').split(' ')
            current_status = [status.upper() for status in filter(lambda x: x, current_status)]
            current_status = [Status.KEY_TO_VALUE[x.strip()] for x in current_status]
        except KeyError:
            raise AutosubmitCritical(f"Invalid status -fs. All values must match one "
                                     f"of {Status.VALUE_TO_KEY.keys()}", 7011)

        if all_expids:
            expid_list: list[str] = retrieve_expids()
        else:
            expid_list = expids.replace(',', ' ').split(' ')
            expid_list = [expid.lower() for expid in filter(lambda x: x, expid_list)]

        truthy_values = ["true", "yes", "y", "1", ""]
        if not force_all:
            expid_list: list[str] = [
                expid
                for expid in expid_list
                if force_yes or input(
                    f"Confirm stopping the experiment: {expid} (y/n)[enter=y]? ").lower() in truthy_values
            ]

        sig_to_process = signal.SIGKILL if force else signal.SIGINT
        killed_expids = []
        for expid in expid_list:
            pid: int = process_id(expid)
            if not pid or pid <= 1:
                Log.info(f"Expid {expid} was not running")
                continue
            try:
                os.kill(pid, sig_to_process)
                killed_expids.append(expid)
            except Exception as e:
                Log.warning(f"An error occurred while stopping the autosubmit process for expid '{expid}': {str(e)}")

        for expid in killed_expids:
            if not force:
                Log.info(f"Checking the status of the expid: {expid}")
                while True:
                    if not process_id(expid):
                        Log.info(f"Expid {expid} is stopped")
                        break
                    Log.info(f"Waiting for the autosubmit run to safety stop: {expid}")
                    sleep(5)
            if cancel:
                job_list, _, _, _, _, _, _, _ = Autosubmit.prepare_run(expid, check_scripts=False)
                cancel_jobs(job_list, active_jobs_filter=current_status, target_status=status)
        return True
