# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
"""Define all legal items and values for cylc workflow definition files."""

import contextlib
import re
from textwrap import dedent
from typing import (
    Any,
    Dict,
    Literal,
    Optional,
    Set,
)

from metomi.isodatetime.data import Calendar

from cylc.flow import LOG
from cylc.flow.cfgspec.globalcfg import (
    DIRECTIVES_DESCR,
    DIRECTIVES_ITEM_DESCR,
    EVENTS_DESCR,
    EVENTS_SETTINGS,
    EXECUTION_POLL_DESCR,
    LOG_RETR_SETTINGS,
    MAIL_DESCR,
    MAIL_FOOTER_DESCR,
    MAIL_FROM_DESCR,
    MAIL_INTERVAL_DESCR,
    MAIL_TO_DESCR,
    MAIN_LOOP_DESCR,
    MAIN_LOOP_PLUGIN_DESCR,
    MAIN_LOOP_PLUGIN_INTERVAL_DESCR,
    REPLACES,
    SCHEDULER_DESCR,
    SUBMISSION_POLL_DESCR,
    SUBMISSION_RETY_DESCR,
    TASK_EVENTS_DESCR,
    TASK_EVENTS_SETTINGS,
    UTC_MODE_DESCR,
)
import cylc.flow.flags
from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults
from cylc.flow.parsec.config import (
    ConfigNode as Conf,
    ParsecConfig,
)
from cylc.flow.parsec.exceptions import UpgradeError
from cylc.flow.parsec.upgrade import (
    converter,
    upgrader,
)
from cylc.flow.parsec.validate import (
    CylcConfigValidator as VDR,
    DurationFloat,
    cylc_config_validate,
)
from cylc.flow.platforms import (
    fail_if_platform_and_host_conflict,
    get_platform_deprecated_settings,
    is_platform_definition_subshell,
)
from cylc.flow.run_modes import (
    TASK_CONFIG_RUN_MODES,
    RunMode,
)
from cylc.flow.task_events_mgr import EventData


# Regex to check whether a string is a command
REC_COMMAND = re.compile(r'(`|\$\()\s*(.*)\s*([`)])$')

REPLACED_BY_PLATFORMS = '''
.. deprecated:: 8.0.0

   This is used to select a matching platform.
   It will be removed in a future version of Cylc 8.
   Please set a suitable platform in
   :cylc:conf:`flow.cylc[runtime][<namespace>]platform` instead.
   :ref:`See the migration guide <MajorChangesPlatforms>`.
'''

CYCLE_POINT_CONSTRAINTS = '''
Rules to allow only certain {0} datetime cycle points.

.. admonition:: Use Case

   Writing a workflow where users may change the {0}
   cycle point, but where only some {0} cycle points are
   reasonable.

Set by defining a list of truncated time points, which
the {0} cycle point must match.

Examples:

- ``T00, T06, T12, T18`` - only at 6 hourly intervals.
- ``T-30`` - only at half-past an hour.
- ``01T00`` - only at midnight on the first day of a month.

.. seealso::

   :ref:`Recurrence tutorial <tutorial-inferred-recurrence>`.
'''


def global_default(text: str, config_path: str) -> str:
    """Insert a link to this config item's global counterpart after the first
    paragraph of the description text. Also dedents.
    """
    first, sep, rest = dedent(text).partition('\n\n')
    return (
        f"{first}\n\n"
        "The default value is set in the global config: "
        f":cylc:conf:`global.cylc{config_path}`."
        f"{sep}{rest}"
    )


def get_script_common_text(this: str, example: Optional[str] = None):
    text = dedent('''

    See also :ref:`JobScripts`.

    Other user-defined script items:

    ''')
    for item in (
        'init-script', 'env-script', 'pre-script', 'script', 'post-script',
        'err-script', 'exit-script'
    ):
        if item != this:
            text += f"* :cylc:conf:`[..]{item}`\n"
    text += dedent(f'''

    Example::

       {example if example else 'echo "Hello World"'}
    ''')
    return text


with Conf(
    'flow.cylc',
    desc='''
        Defines a Cylc workflow configuration.

        After processing any embedded templating code
        (see :ref:`Jinja`) the resultant raw flow.cylc file
        must be valid. See also :ref:`FlowConfigFile` for a descriptive
        overview of flow.cylc files, including syntax (:ref:`Syntax`).

        .. versionchanged:: 8.0.0

           The configuration file was previously named ``suite.rc``, but that
           name is now deprecated.
           The ``suite.rc`` file name now activates :ref:`cylc_7_compat_mode`.
           Rename to ``flow.cylc`` to turn off compatibility mode.
    '''
) as SPEC:

    with Conf('meta', desc='''
        Metadata for this workflow.

        Cylc defines and uses
        the terms "title", "description" and "URL".
        Users can define more terms, and use these in event handlers.

        Example::

           A user could define "workflow-priority". An event handler
           would then respond to failure events in a way set by
           "workflow-priority".
    '''):
        Conf('description', VDR.V_STRING, '', desc='''
            A multi-line description of the workflow.

            It can be retrieved at run time with the ``cylc show`` command.
        ''')
        Conf('title', VDR.V_STRING, '', desc='''
            A single line description of the workflow.

            It can be retrieved at run time with the ``cylc show`` command.
        ''')
        Conf('URL', VDR.V_STRING, '', desc='''
            A web URL to workflow documentation.

            The URL can be retrieved at run time with the ``cylc show``
            command.

            The template variable ``%(workflow)s`` will be replaced with the
            actual workflow ID.

            .. deprecated:: 8.0.0

               The ``%(suite_name)s`` template variable is deprecated, please
               use ``%(workflow)s``.

            .. seealso::

               :cylc:conf:`flow.cylc[runtime][<namespace>][meta]URL`.

            Example:

            ``http://my-site.com/workflows/%(workflow)s/index.html``

        ''')
        Conf('<custom metadata>', VDR.V_STRING, '', desc='''
            Any user-defined metadata item.

            Like title, description and URL these can be
            passed to workflow event handlers to be interpreted according to
            your needs.

            For example, a user could define an item called
            "workflow-priority". An event handler could then respond to
            failure events in a way set by "workflow-priority".
        ''')
    with Conf('scheduler', desc=(
        global_default(SCHEDULER_DESCR, "[scheduler]")
    )):
        Conf('UTC mode', VDR.V_BOOLEAN, desc=(
            global_default(UTC_MODE_DESCR, "[scheduler]UTC mode")
        ))

        Conf('allow implicit tasks', VDR.V_BOOLEAN, default=False, desc='''
            Allow tasks in the graph that are not defined in
            :cylc:conf:`flow.cylc[runtime]`.

            :term:`Implicit tasks <implicit task>` are tasks without explicit
            definitions in :cylc:conf:`flow.cylc[runtime]`. By default,
            these are not allowed, as they are often typos. However,
            this setting can be set to ``True`` to allow implicit tasks.
            It is recommended to only set this to ``True`` if required during
            development/prototyping of a workflow graph, but set it to
            ``False`` after finishing the :cylc:conf:`flow.cylc[runtime]`
            section.

            .. admonition:: Cylc 7 compatibility mode

               In :ref:`Cylc_7_compat_mode`, implicit tasks are still
               allowed unless you explicitly set this to ``False``, or
               unless a ``rose-suite.conf`` file is present (to maintain
               backward compatibility with Rose 2019).

            .. versionadded:: 8.0.0
        ''')

        Conf('install', VDR.V_STRING_LIST, desc='''
            Configure custom directories and files to be installed on remote
            hosts.

            .. note::

               The following directories already get installed by default:

               ``ana/``
                  Rose ana analysis modules
               ``app/``
                  Rose applications
               ``bin/``
                  Cylc bin directory (added to ``PATH``)
               ``etc/``
                  Miscellaneous resources
               ``lib/``
                  Cylc lib directory (``lib/python`` added to ``PYTHONPATH``
                  for workflow config)

               These should be located in the top level of your Cylc workflow,
               i.e. the directory that contains your ``flow.cylc`` file.

            Directories must have a trailing slash.
            For example, to add the following items to your file installation:

            .. code-block:: none

                ~/cylc-run/workflow_x
                |-- dir1/
                |-- dir2/
                |-- file1
                `-- file2

            .. code-block:: cylc

                [scheduler]
                    install = dir/, dir2/, file1, file2

            .. versionadded:: 8.0.0
        ''')

        Conf('cycle point format', VDR.V_CYCLE_POINT_FORMAT, desc='''
            Set the datetime format and precision that Cylc uses for
            :term:`cycle points<cycle point>` in :term:`datetime cycling`
            workflows.

            .. seealso::

               * To alter the time zone used in the datetime cycle point
                 format, see
                 :cylc:conf:`flow.cylc[scheduler]cycle point time zone`.
               * To alter the number of expanded year digits (for years
                 below 0 or above 9999), see
                 :cylc:conf:`flow.cylc
                 [scheduler]cycle point num expanded year digits`.

            By default, Cylc uses a ``CCYYMMDDThhmmZ`` (``Z`` in the special
            case of UTC) or ``CCYYMMDDThhmm±hhmm`` format for writing
            datetime cycle points, following the :term:`ISO 8601` standard.

            You may use the `isodatetime library's syntax
            <https://github.com/metomi/isodatetime#dates-and-times>`_ to set
            the cycle point format.

            You can also use a subset of the strptime/strftime POSIX
            standard - supported tokens are ``%F``, ``%H``, ``%M``, ``%S``,
            ``%Y``, ``%d``, ``%j``, ``%m``, ``%s``, ``%z``.

            If specifying a format here, we recommend including a time zone -
            this will be used for displaying cycle points only. To avoid
            confusion, we recommend using the same time zone as
            :cylc:conf:`flow.cylc[scheduler]cycle point time zone`.

            The ISO 8601 *extended* datetime format (``CCYY-MM-DDThh:mm``)
            cannot be used, as cycle points are used in job-log and work
            directory paths where the ":" character is invalid.

            .. warning::

               The smallest unit included in the format sets the precision
               of cycle points in the workflow.
               If the precision is lower than the smallest unit
               in a graph recurrence, the workflow will fail.
               For example, if you set a format of ``CCYY``, and have a
               recurrence ``R/2000/P8M``, then both the first and second
               cycle points will be ``2000``, which is invalid.
        ''')
        Conf('cycle point num expanded year digits', VDR.V_INTEGER, 0, desc='''
            Enable negative years or years more than four digits long.

            For years below 0 or above 9999, the ISO 8601 standard specifies
            that an extra number of year digits and a sign should be used.
            This extra number needs to be written down somewhere (here).

            For example, if this extra number is set to 2, 00Z on the 1st of
            January in the year 10040 will be represented as
            ``+0100400101T0000Z`` (2 extra year digits used). With this number
            set to 3, 06Z on the 4th of May 1985 would be written as
            ``+00019850504T0600Z``.

            This number defaults to 0 (no sign or extra digits used).
        ''')
        Conf('cycle point time zone', VDR.V_CYCLE_POINT_TIME_ZONE, desc='''
            Time zone to be used for datetime cycle points if not otherwise
            specified.

            This time zone will be used for
            datetime cycle point dumping and inferring the time zone of cycle
            points that are input without time zones.

            Time zones should be expressed as :term:`ISO8601` time zone offsets
            from UTC, such as ``+13``, ``+1300``, ``-0500`` or ``+0645``,
            with ``Z`` representing the special case of ``+0000`` (UTC).
            Cycle points will be converted to the time zone you give and will
            be represented with this string at the end.

            If not set, it will default to UTC (``Z``).

            .. admonition:: Cylc 7 compatibility mode

               In :ref:`Cylc_7_compat_mode`, it will default to the
               local/system time zone, rather than UTC.

            The time zone will persist over reloads/restarts following any
            local time zone changes (e.g. if the
            workflow is run during winter time, then stopped, then restarted
            after summer time has begun, the cycle points will remain
            in winter time). Changing this setting after the workflow has
            first started will have no effect.

            If you use a custom
            :cylc:conf:`flow.cylc[scheduler]cycle point format`, it is a good
            idea to set the same time zone here. If you specify a different
            one here, it will only be used for inferring timezone-less cycle
            points; cycle points will be displayed in the time zone from the
            cycle point format.

            .. caution::

               It is not recommended to write the time zone with a ":"
               (e.g. ``+05:30``), given that the time zone is used as part of
               task output filenames.

            .. seealso::

               :cylc:conf:`flow.cylc[scheduler]UTC mode`

            .. versionchanged:: 7.8.9/7.9.4

               The value set here now persists over reloads/restarts after a
               system time zone change.

            .. versionchanged:: 8.0.0

               The default time zone is now ``Z`` instead of the local time of
               the first workflow start.
        ''')
        with Conf('experimental', desc='''
            Activate experimental features.

            These are preview features which will become the default in future
            releases.

            .. versionadded:: 8.6.0
        '''):
            Conf('all', VDR.V_BOOLEAN, False, desc='''
                Activate all experimental features.

                Encouraged for canary testing.

                .. versionadded:: 8.6.0
            ''')
            Conf('expire triggers', VDR.V_BOOLEAN, False, desc='''
                This reimplements "suicide triggers" as "expire triggers".

                * When the condition is met, the task will generate the
                  ``expired`` output rather than just being removed.
                * The ``expired`` output will be marked as
                  :term:`optional <optional output>` for the triggered task,
                  but a custom
                  `flow.cylc[runtime][<namespace>]completion` condition
                  will need to be modified accordingly.
                * This should be functionally equivalent to "suicide triggers"
                  in that the triggered task will not run.
                * However, the triggered task will now be left in the
                  ``expired`` state making it clearer in the GUI/logs that
                  the task has been triggered in this way.
                * It is possible to trigger other tasks off of this ``expired``
                  output for more advanced failure recovery.

                .. versionadded:: 8.6.0
            ''')

        with Conf(   # noqa: SIM117 (keep same format)
            'main loop',
            desc=global_default(MAIN_LOOP_DESCR, "[scheduler][main loop]")
        ):
            with Conf('<plugin name>', desc=(
                global_default(
                    MAIN_LOOP_PLUGIN_DESCR,
                    "[scheduler][main loop][<plugin name>]"
                )
            )):
                Conf('interval', VDR.V_INTERVAL, desc=(
                    global_default(
                        MAIN_LOOP_PLUGIN_INTERVAL_DESCR,
                        "[scheduler][main loop][<plugin name>]interval"
                    )
                ))

        with Conf(
            'events', desc=global_default(EVENTS_DESCR, '[scheduler][events]')
        ):
            for item, val in EVENTS_SETTINGS.items():
                if isinstance(val, dict):
                    val = val.copy()
                    desc: str = val.pop('desc')
                else:
                    desc = val
                    val = {}
                desc = global_default(desc, f"[scheduler][events]{item}")
                vdr_type = VDR.V_STRING_LIST
                default: Any = Conf.UNSET
                if item in {'handlers', 'handler events', 'mail events'}:
                    # Note: default of None for V_STRING_LIST is used to
                    # differentiate between not set vs set to empty
                    default = None
                elif item.endswith("handlers"):
                    desc = desc + '\n\n' + dedent(f'''
                        .. rubric:: Examples:

                        .. code-block:: cylc

                           # configure a single event handler
                           {item} = echo foo

                           # provide context to the handler
                           {item} = echo %(workflow)s

                           # configure multiple event handlers
                           # (which will run in parallel)
                           {item} = \\
                               'echo %(workflow)s %(event)s', \\
                               'my_exe %(event)s %(message)s', \\
                               'curl -X PUT -d event=%(event)s host:port'
                    ''')
                elif item.startswith("abort on"):
                    vdr_type = VDR.V_BOOLEAN
                elif item.endswith("timeout"):
                    vdr_type = VDR.V_INTERVAL
                Conf(item, vdr_type, default, desc=desc, **val)

            Conf('expected task failures', VDR.V_STRING_LIST, desc='''
                (For Cylc developers writing a functional tests only)
                List of tasks that are expected to fail in the test.
            ''')

        with Conf('mail', desc=(
            global_default(MAIL_DESCR, "[scheduler][mail]")
        )):
            Conf('footer', VDR.V_STRING, desc=(
                global_default(MAIL_FOOTER_DESCR, "[scheduler][mail]footer")
            ))
            Conf('to', VDR.V_STRING, desc=(
                global_default(MAIL_TO_DESCR, "[scheduler][mail]to")
            ))
            Conf('from', VDR.V_STRING, desc=(
                global_default(MAIL_FROM_DESCR, "[scheduler][mail]from")
            ))
            Conf('task event batch interval', VDR.V_INTERVAL, desc=(
                global_default(
                    MAIL_INTERVAL_DESCR,
                    "[scheduler][mail]task event batch interval"
                )
            ))

    with Conf('task parameters', desc=f'''
        Set task parameters and parameter templates.

        Define parameter values here for use in expanding
        :ref:`parameterized tasks <User Guide Param>`.

        .. versionchanged:: 8.0.0

           {REPLACES}``[cylc][parameters]``.
    '''):
        Conf('<parameter>', VDR.V_PARAMETER_LIST, desc='''
            A custom parameter to use in a workflow.

            .. rubric:: Examples:

            - ``run = control, test1, test2``
            - ``mem = 1..5``  (equivalent to ``1, 2, 3, 4, 5``).
            - ``mem = -11..-7..2``  (equivalent to ``-11, -9, -7``).
        ''')
        with Conf('templates', desc=f'''
            Cylc will expand each parameterized task name using a string
            template.

            You can set templates for any parameter name here to override the
            default template.

            .. versionchanged:: 8.0.0

               {REPLACES}``[cylc][parameter templates]``.
        '''):
            Conf('<parameter>', VDR.V_STRING, desc='''
                A template for a parameter.

                For example:

                If you set:

                .. code-block:: cylc

                   [task parameters]
                       myparameter = 1..3
                       [[templates]]
                           myparameter = _run_%(myparameter)s

                task name ``foo<myparameter>`` becomes ``foo_run_3`` for
                ``run == 3``.

                .. note::

                   The default parameter templates are:

                   For integer parameters:
                      ``_p%(p)0Nd``
                      where ``N`` is the number of digits of the maximum
                      integer value, i.e. If the largest parameter value is
                      3142 then N = 4.

                   Default for non-integer parameters:
                      ``_%(p)s`` e.g. ``foo<run>`` becomes ``foo_top`` for
                      ``run`` value ``top``.
            ''')

    with Conf('scheduling', desc='''
        This section allows Cylc to determine when tasks are ready to run.

        Any cycle points defined here without a time zone will use the
        time zone from
        :cylc:conf:`flow.cylc[scheduler]cycle point time zone`.
    '''):
        Conf('initial cycle point', VDR.V_CYCLE_POINT, desc='''
            The earliest cycle point at which any task can run.

            In a cold start each cycling task (unless specifically excluded
            under :cylc:conf:`[..][special tasks]`) will be loaded into the
            workflow with this cycle point, or with the closest subsequent
            valid cycle point for the task.

            In integer cycling, the default is ``1``.

            The string ``now`` converts to the current datetime on the workflow
            host when first starting the workflow (with precision determined
            by :cylc:conf:`flow.cylc[scheduler]cycle point format`).

            For more information on setting the initial cycle point relative
            to the current time see :ref:`setting-the-icp-relative-to-now`.

            This item can be overridden on the command line using
            ``cylc play --initial-cycle-point`` or ``--icp``.

            .. rubric:: Examples:

            ``2000-01-01T00:00Z``
               The 1st of January at midnight.
            ``2000``
               Shorthand for ``2000-01-01T00:00``.
            ``now``
               The exact time when the workflow is started. By default this is
               to the minute, e.g: ``2001-02-03T12:34``.
            ``previous(T-00)``
               The hour when the workflow was started. I.e, The time when the
               workflow is started with the minute cut off.
            ``previous(T00)``
               The day when the workflow was started. I.e, the time when the
               workflow was started with the hour and minute cut off.
            ``next(T00)``
               The day after the workflow was started (at midnight).
        ''')
        # NOTE: final cycle point is not a V_CYCLE_POINT to allow expressions
        # such as '+P1Y' (relative to initial cycle point)
        Conf('final cycle point', VDR.V_CYCLE_POINT_WITH_OFFSETS, desc='''
            The (optional) last cycle point at which tasks are run.

            Once all tasks have reached this cycle point, the
            workflow will shut down.

            This item can be overridden on the command line using
            ``cylc play --final-cycle-point`` or ``--fcp``.

            .. rubric:: Examples:

            ``2000-01-01T00:00Z``
               The 1st of January at midnight.
            ``2000``
               Shorthand for ``2000-01-01T00:00``.
            ``+P1D``
               The initial cycle point plus one day.
            ``2000 +P1D +P1Y``
               The year ``2000`` plus one day and one year.
        ''')
        Conf('initial cycle point constraints', VDR.V_STRING_LIST,
             desc=CYCLE_POINT_CONSTRAINTS.format('initial') + dedent('''

            .. note::

               This setting does not coerce :cylc:conf:`[..]
               initial cycle point = now`.
        '''))
        Conf('final cycle point constraints', VDR.V_STRING_LIST,
             desc=CYCLE_POINT_CONSTRAINTS.format('final'))
        Conf('hold after cycle point', VDR.V_CYCLE_POINT, desc=f'''
            Hold all tasks that pass this cycle point.

            Unlike the final
            cycle point, the workflow does not shut down once all tasks have
            passed this point. If this item is set you can override it on the
            command line using ``--hold-after``.

            .. versionchanged:: 8.0.0

               {REPLACES}``[scheduling]hold after point``.
        ''')
        Conf('stop after cycle point', VDR.V_CYCLE_POINT_WITH_OFFSETS, desc='''
            Shut down the workflow after all tasks pass this cycle point.

            The stop cycle point can be overridden on the command line using
            ``cylc play --stop-cycle-point=POINT``

            .. note::

               Not to be confused with :cylc:conf:`[..]final cycle point`:
               There can be more graph beyond this point, but you are
               choosing not to run that part of the graph. You can play
               the workflow and continue.

            .. rubric:: Examples:

            - ``2000`` - Shorthand for ``2000-01-01T00:00``.
            - ``+P1D`` - The initial cycle point plus one day.
            - ``2000 +P1D +P1Y`` - The year ``2000`` plus one day and one year.

            .. versionadded:: 8.0.0

            .. versionchanged:: 8.3.0

               This now supports offsets (e.g. ``+P1D``) in the same way the
               :cylc:conf:`[..]final cycle point` does.
        ''')
        Conf('cycling mode', VDR.V_STRING, Calendar.MODE_GREGORIAN,
             options=list(Calendar.MODES) + ['integer'], desc='''
            Choice of :term:`integer cycling` or one of several
            :term:`datetime cycling` calendars.

            Cylc runs workflows using the proleptic Gregorian calendar
            by default. This setting allows you to instead choose
            integer cycling, or one of the other supported non-Gregorian
            datetime calendars: 360 day (12 months of 30 days in a year),
            365 day (never a leap year) and 366 day (always a leap year).
        ''')
        Conf('runahead limit', VDR.V_STRING, 'P4', desc='''
            The runahead limit prevents a workflow from getting too far ahead
            of the oldest :term:`active cycle`.

            A cycle is considered to be active if it contains any
            :term:`active tasks <active task>`.

            An integer interval value of ``Pn`` allows up to ``n+1`` cycles
            to be active at once.

            The default runahead limit is ``P4``, which means there may be up
            to 5 active cycles.

            Datetime cycling workflows can optionally use a datetime interval
            value instead, in which case the number of cycles
            within the interval depends on the cycling intervals present.

            .. rubric:: Examples:

            ``P0``
                Only one cycle can be active at a time.
            ``P2``
                Allow up to two cycles ahead of the oldest active cycle.
            ``P3D``
                Allow cycles up to three days ahead of the oldest active cycle.

            .. seealso::

               :ref:`RunaheadLimit`

            .. versionchanged:: 8.0.0

               The integer format ``Pn`` was introduced to replace the
               deprecated ``[scheduling]max active cycle points = m``
               (with ``n = m-1``) and unify it with the existing datetime
               interval ``runahead limit`` setting.
        ''')

        with Conf('queues', desc='''
            Internal task queues to limit job activity.

            Queues delay job submission in member tasks, until the active
            job count (for members) drops below the queue limit.

            There is a ``default`` queue with a configurable limit of 100, for
            all tasks that are not assigned to other queues.

            You can define custom queues to limit job activity across other
            groups of tasks.

            .. code-block:: cylc

               [[queues]]
                   [[[my_queue]]]
                       limit = 2
                       members = task1, task2, FAMILY1

            .. seealso::

               :ref:`InternalQueues`.
        '''):
            with Conf('<queue name>', desc='''
                Section for configuring a single queue.
            ''') as Queue:
                Conf('limit', VDR.V_INTEGER, 0, desc='''
                    The active job limit for this queue. 0 means no limit.
                ''')
                Conf('members', VDR.V_SPACELESS_STRING_LIST, desc='''
                    A list of task or family names to assign to this queue.
                ''')
            with Conf('default', meta=Queue, desc='''
                The default queue, for all tasks not assigned to other queues.
            '''):
                Conf('limit', VDR.V_INTEGER, 100, desc='''
                    The job limit for the ``default`` queue. 0 means no limit.
                ''')

        with Conf('special tasks', desc='''
            This section is used to identify tasks with special behaviour.

            Family names can be used in special task lists as shorthand for
            listing all member tasks.
        '''):
            Conf('clock-trigger', VDR.V_STRING_LIST, desc='''
            Legacy clock trigger definitions.

            .. deprecated:: 8.0.0

               These are now auto-upgraded to the newer wall_clock xtriggers
               (see :ref:`Section External Triggers`). The old way defining
               clock-triggers will be removed in an upcoming Cylc version.

            Clock-triggered tasks (see :ref:`ClockTriggerTasks`) wait on a wall
            clock time specified as an offset from their own cycle point.

            Example:

            ``foo(PT1H30M), bar(PT1.5H), baz``
            ''')
            Conf('external-trigger', VDR.V_STRING_LIST, desc='''
                Legacy external trigger definition section.

                .. deprecated:: 8.0.0

                   Please read :ref:`Section External Triggers` before
                   using the older mechanism described in this section.

                Externally triggered tasks (see :ref:`Old-Style External
                Triggers`) wait on external events reported via the
                ``cylc ext-trigger`` command. To constrain triggers to a
                specific cycle point, include ``$CYLC_TASK_CYCLE_POINT``
                in the trigger message string and pass the cycle point to the
                ``cylc ext-trigger`` command.
            ''')
            Conf('clock-expire', VDR.V_STRING_LIST, desc='''
                Don't submit jobs if they are too late in wall clock time.

                Clock-expire tasks enter the ``expired`` state and skip job
                submission if too far behind the wall clock when they become
                ready to run.

                The expiry time is specified as an offset from the task's
                cycle point. The offset:

                   * May be positive or negative
                   * May be omitted if it is zero

                .. seealso::

                   :ref:`ClockExpireTasks`.

                .. rubric:: Examples:

                ``foo(PT1H)`` - expire task ``foo`` if the current wall clock
                time has reached 1 hour after the task's cycle point.

                ``bar(-PT5M)`` - expire task ``bar`` if the current wall clock
                time has reached 5 minutes *before* the task's cycle point.
            ''')
            Conf('sequential', VDR.V_STRING_LIST, desc='''
                A list of tasks which automatically depend on their own
                previous-cycle instance.

                .. tip::

                   Recommend best practice is now to use explicit inter-cycle
                   triggers rather than sequential tasks.

                .. seealso::

                    :ref:`SequentialTasks`.
            ''')

        Conf('sequential xtriggers', VDR.V_BOOLEAN, False,
             desc='''
            If ``True``, tasks that only depend on xtriggers will not spawn
            until the xtrigger of previous (cycle point) instance is satisfied.
            Otherwise, they will all spawn at once out to the runahead limit.

            This setting can be overridden by the reserved keyword argument
            ``sequential`` in individual xtrigger declarations.

            One sequential xtrigger on a parentless task with multiple
            xtriggers will cause sequential spawning.

            .. versionadded:: 8.3.0
        ''')
        with Conf('xtriggers', desc='''
            This section is for *External Trigger* function declarations -
            see :ref:`Section External Triggers`.
        '''):
            Conf('<xtrigger name>', VDR.V_XTRIGGER, desc='''
                Any user-defined event trigger function declarations and
                corresponding labels for use in the graph.

                See :ref:`Section External Triggers` for details.

                Example::

                ``my_trigger(arg1, arg2, kwarg1, kwarg2):PT10S``
            ''')

        with Conf('graph', desc=f'''
            The workflow graph is defined under this section.

            You can plot the dependency graph as you work on it, with
            ``cylc graph``.

            .. seealso::

               :ref:`User Guide Scheduling`.

            .. versionchanged:: 8.0.0

               {REPLACES}``[runtime][dependencies][graph]``.
        '''):
            Conf('<recurrence>', VDR.V_STRING, desc=f'''
                The recurrence defines the sequence of cycle points
                for which the dependency graph is valid.

                .. seealso::

                   :ref:`User Guide Scheduling`

                Cycle points should be specified in our ISO 8601 derived
                sequence syntax, or as integers, in integer cycling mode:

                Example Recurrences:

                datetime cycling:
                   * ``R1`` - once at the initial cycle point
                   * ``T00,T06,T12,T18`` - daily at 00:00, 06:00, 12:00
                     & 18:00
                   * ``PT6H`` - every six hours starting at the initial
                     cycle point
                integer cycling:
                   * ``R1`` - once at the initial cycle point
                   * ``P2`` - every other cycle
                   * ``P3,P5`` - every third or fifth cycle

                .. note::

                   Unlike other Cylc configurations duplicate recurrences
                   are additive and do not override.

                   For example this:

                   .. code-block:: cylc

                      [scheduling]
                          [[graph]]
                              R1 = a => b
                              R1 = c => d

                   Is equivalent to this:

                   .. code-block:: cylc

                      [scheduling]
                          [[graph]]
                              R1 = """
                                  a => b
                                  c => d
                              """

                   See :ref:`GraphTypes` for more on recurrence expressions,
                   and how multiple graphs combine.

                The value should be a dependency graph the given recurrence.
                Syntax examples follow; see also :ref:`User Guide Scheduling`
                and :ref:`TriggerTypes`.

                Example Graph Strings:

                  .. code-block:: cylc-graph

                     # baz and waz both trigger off bar
                     foo => bar => baz & waz

                     # bar triggers off foo[-P1D-PT6H]
                     foo[-P1D-PT6H] => bar

                     # faz triggers off a message output of baz
                     baz:out1 => faz

                     # Y triggers if X starts executing
                     X:start => Y

                     # Y triggers if X fails
                     X:fail => Y

                     # bar triggers if foo[-PT6H] fails
                     foo[-PT6H]:fail => bar

                     # Y suicides if X succeeds
                     X => !Y

                     # Z triggers if X succeeds or fails
                     X | X:fail => Z

                     # Z triggers if X succeeds or fails
                     X:finish => Z

                     # general conditional triggers
                     (A | B & C ) | D => foo

                     # bar triggers if foo is successfully submitted
                     foo:submit => bar

                     # bar triggers if submission of foo fails
                     foo:submit-fail => bar

                .. versionchanged:: 8.0.0

                   {REPLACES}
                   ``[runtime][dependencies][graph][<recurrence>]graph``.
            ''')

    with Conf('runtime',  # noqa: SIM117 (keep same format)
              desc='''
        This section is used to specify settings for tasks to be run.

        You can specify:

        - What scripts or commands you want to execute.
        - Which compute resource (platform) you wish to use.
        - How to run your task.

        If multiple tasks need the same settings, they can share settings by
        inheriting them from one or more other tasks.

        Precedence is determined by the same C3
        linearization algorithm used to find the *method resolution order*
        in Python language class hierarchies.

        .. seealso::

           For details and examples see :ref:`User Guide Runtime`.
    '''):
        with Conf('<namespace>', desc='''
            The name(s) of one or more tasks or task families.

            A namespace (i.e. task or family name) or a comma-separated list
            of namespace names, and repeat as needed to define all tasks in
            the workflow. Names may contain letters, digits, underscores, and
            hyphens.  A namespace represents a group or family of tasks if
            other namespaces inherit from it, or a task if no others inherit
            from it.

            .. important::

               Names may not contain ``:`` or ``.``.

               See :ref:`task namespace rules. <namespace-names>`

            .. rubric:: Examples:

            - ``[foo]``
            - ``[foo, bar, baz]``

            If multiple names are listed the subsequent settings apply to
            all.

            All tasks or task families inherit initially from ``root``, which
            can be explicitly configured to provide or override default
            settings for all tasks in the workflow.
        '''):
            Conf('completion', VDR.V_STRING, desc='''
                Define the condition for task output completion.

                The completion condition is evaluated when a task reaches
                a final state - i.e. once it finished executing (``succeeded``
                or ``failed``) or it ``submit-failed``, or ``expired``.
                It is a validation check which confirms that the
                task has generated the outputs it was expected to.

                If the task fails this check its outputs are considered
                :term:`incomplete <output completion>` and a warning will be
                raised alerting you that something has gone wrong which
                requires investigation.

                .. note::

                   An event hook for this warning will follow in a future
                   release of Cylc.

                By default, the completion condition ensures that all required
                outputs, i.e. outputs which appear in the graph but are not
                marked as optional with the ``?`` character, are completed.

                E.g., in this example, the task ``foo`` must generate the
                required outputs ``succeeded`` and ``x`` and it may or may not
                generate the optional output ``y``:

                .. code-block:: cylc-graph

                   foo => bar
                   foo:x => x
                   foo:y? => y

                The default completion condition would be this:

                .. code-block:: python

                   # the task must succeed and generate the custom output "x"
                   succeeded and x

                You can override this default to suit your needs. E.g., in this
                example, the task ``foo`` has three optional outputs, ``x``,
                ``y`` and ``z``:

                .. code-block:: cylc-graph

                   foo:x? => x
                   foo:y? => y
                   foo:z? => z
                   x | y | z => bar

                Because all three of these outputs are optional, if none of
                them are generated, the task's outputs will still be
                considered complete.

                If you wanted to require that at least one of these outputs is
                generated you can configure the completion condition like so:

                .. code-block:: python

                   # the task must succeed and generate at least one of the
                   # outputs "x" or "y" or "z":
                   succeeded and (x or y or z)

                .. note::

                   For the completion expression, hyphens in task outputs
                   must be replaced with underscores to allow evaluation by
                   Python, e.g.:

                   .. code-block:: cylc

                      [runtime]
                          [[foo]]
                              completion = succeeded and my_output # underscore
                              [[[outputs]]]
                                  my-output = 'my custom task output' # hyphen

                .. note::

                   In some cases the ``succeeded`` output might not explicitly
                   appear in the graph, e.g:

                   .. code-block:: cylc-graph

                      foo:x? => x

                   In these cases success is presumed to be required unless
                   explicitly stated otherwise, either in the graph e.g:

                   .. code-block:: cylc-graph

                      foo?
                      foo:x? => x

                   Or in the completion expression e.g:

                   .. code-block:: cylc

                      completion = x  # no reference to succeeded
                      # or
                      completion = succeeded or failed  # success is optional


                .. hint::

                   If task outputs are optional in the graph they must also
                   be optional in the completion condition and vice versa.

                   For example this graph conflicts with the completion
                   statement:

                   .. code-block:: cylc-graph

                      # "a" must succeed
                      a => b

                   .. code-block:: cylc

                      # "a" may either succeed or fail
                      completion = succeeded or failed

                   Which could be fixed by amending the graph like so:

                   .. code-block:: cylc-graph

                      # "a" may either succeed or fail
                      a? => b

                .. rubric:: Examples

                ``succeeded``
                   The task must succeed.
                ``succeeded or (failed and my_error)``
                   The task can fail, but only if it also yields the custom
                   output ``my_error``.
                ``succeeded and (x or y or z)``
                   The task must succeed and yield at least one of the
                   custom outputs, x, y or z.
                ``(a and b) or (c and d)``
                   One pair of these outputs must be yielded for the task
                   to be complete.

                .. versionadded:: 8.3.0
            ''')
            Conf('platform', VDR.V_STRING, desc='''
                The platform to submit jobs to.

                The
                :cylc:conf:`platform <global.cylc[platforms][<platform name>]>`
                specifies the host(s) that the tasks' jobs
                will run on and where (if necessary) files need to be
                installed, and what job runner will be used.

                This can be:

                * A :cylc:conf:`platform <global.cylc[platforms]>`,
                * a :cylc:conf:`platform group <global.cylc[platform groups]>`,
                * or a command which returns a platform or platform group.

                To see what platforms have been configured at your site, run
                ``cylc config --platform-names``.

                .. rubric:: Commands:

                The ``platform`` can be set to a command which returns the name
                of the platform to submit jobs to using the ``$()`` syntax,
                i.e:

                .. code-block:: cylc

                   platform = $(command)

                The configured command will be evaluated for each job
                submission (i.e, different submissions of the same task may
                submit on different platforms).

                Cylc batches job submissions, so when multiple jobs are
                submitted at the same time, using a platform defined by the
                same command, the command will be run once, and all jobs in the
                batch will submit to the same platform.

                Note: do not use a command to configure a list of login nodes.
                Instead, define a platform and configure the login nodes it
                can use; see
                :ref:`config.platforms.cluster_with_multiple_login_nodes`.

                .. rubric:: Examples:

                .. code-block:: cylc

                   # run the job on the same host the Cylc scheduler runs on
                   platform = localhost

                   # run the job on a platform (or platform group) called hpc
                   platform = hpc

                   # run a command to select the platform (or platform group):
                   platform = $(select-platform)
                   platform = prefix-$(select-platform)-suffix

                .. versionadded:: 8.0.0
            ''')
            Conf('inherit', VDR.V_STRING_LIST, desc='''
                A list of the immediate parent(s) of this task or task family.

                If no parents are listed default is ``root``.
            ''')
            Conf('script', VDR.V_STRING, desc=dedent('''
                The main custom script run from the job script.

                It can be an external command or script, or inlined scripting.

                See :ref:`Task Job Script Variables` for the list of variables
                available in the task execution environment.
            ''') + get_script_common_text(
                this='script', example='my_script.sh'
            ))
            Conf('init-script', VDR.V_STRING, desc=dedent('''
                Custom script run by the job script before the task
                execution environment is configured.

                By running before the task execution environment is configured,
                this script does not have
                access to any workflow or task environment variables. It can be
                an external command or script, or inlined scripting. The
                original intention for this item was to allow remote tasks to
                source login scripts to configure their access to cylc, but
                this should no longer be necessary.
            ''') + get_script_common_text(this='init-script'))
            Conf('env-script', VDR.V_STRING, desc=dedent('''
                Custom script run by the job script between the
                cylc-defined environment (workflow and task identity, etc.) and
                the user-defined task runtime environment.

                The env-script has access to the Cylc environment (and the task
                environment has access to variables defined by this scripting).
                It can be an external command or script, or inlined scripting.
            ''') + get_script_common_text(this='env-script'))
            Conf('err-script', VDR.V_STRING, desc=('''
                Script run when a job error is detected.

                Custom script to be run at the end of the error trap,
                which is triggered due to failure of a command in the job
                script or trappable job kill.

                The output of this script will always
                be sent to STDERR and ``$1`` is set to the name of the signal
                caught by the error trap. The script should be fast and use
                very little system resource to ensure that the error trap can
                return quickly.  Companion of :cylc:conf:`[..]exit-script`,
                which is executed on job success.  It can be an external
                command or script, or inlined scripting.
            ''') + get_script_common_text(
                this='err-script', example='echo "Uh oh, received ${1}"'
            ))
            Conf('exit-script', VDR.V_STRING, desc=dedent('''
                Custom script run at the very end of *successful* job
                execution, just before the job script exits.

                The exit-script should execute very quickly.
                Companion of :cylc:conf:`[..]err-script`,
                which is executed on job failure. It can be an external
                command or script, or inlined scripting.
            ''') + get_script_common_text(
                this='exit-script', example='rm -f "$TMP_FILES"'
            ))
            Conf('pre-script', VDR.V_STRING, desc=dedent('''
                Custom script run by the job script immediately
                before :cylc:conf:`[..]script`.

                The pre-script can be an external command or script, or
                inlined scripting.
            ''') + get_script_common_text(
                this='pre-script',
                example='echo "Hello from workflow ${CYLC_WORKFLOW_ID}!"'
            ))
            Conf('post-script', VDR.V_STRING, desc=dedent('''
                Custom script run by the job script immediately
                after :cylc:conf:`[..]script`.

                The post-script can be an external
                command or script, or inlined scripting.
            ''') + get_script_common_text(this='post-script'))

            Conf('work sub-directory', VDR.V_STRING, desc='''
                The directory from which tasks are executed.

                Job scripts are executed from within *work directories*
                created automatically under the workflow run directory. A task
                can get its own work directory from ``$CYLC_TASK_WORK_DIR``
                (or ``$PWD`` if it does not ``cd`` elsewhere at
                runtime). The default directory path contains task name and
                cycle point, to provide a unique workspace for every instance
                of every task. If several tasks need to exchange files and
                simply read and write from their from current working
                directory, setting ``work sub-directory`` can be used to
                override the default to make them all use the same workspace.

                The top level share and work directory location can be changed
                (e.g. to a large data area) by a global config setting (see
                :cylc:conf:`global.cylc[install][symlink dirs]`).

                .. caution::

                   If you omit cycle point from the work sub-directory path
                   successive instances of the task will share the same
                   workspace. Consider the effect on cycle point offset
                   housekeeping of work directories before doing this.

                Example:

                   ``$CYLC_TASK_CYCLE_POINT/shared/``
            ''')
            Conf(
                'execution polling intervals', VDR.V_INTERVAL_LIST, None,
                desc=global_default(
                    EXECUTION_POLL_DESCR,
                    "[platforms][<platform name>]execution polling intervals"
                )
            )
            Conf('execution retry delays', VDR.V_INTERVAL_LIST, None, desc=f'''
                Cylc can automate resubmission of failed jobs.

                Execution retry delays is a list of ISO 8601 durations which
                tell Cylc how long to wait before the next try.

                The job environment variable ``$CYLC_TASK_TRY_NUMBER``
                increments with each automatic retry, allowing you to vary task
                behaviour between retries.

                Tasks only go to the ``failed`` state if job execution fails
                with no retries left.

                .. versionchanged:: 8.0.0

                   {REPLACES}``[runtime][<namespace>][job]execution
                   retry delays``.
            ''')
            Conf('execution time limit', VDR.V_INTERVAL, desc=f'''
                Set the execution (:term:`wallclock <wallclock time>`) time
                limit of a job.

                For the ``background`` and ``at`` job runners, Cylc runs the
                job's script using the timeout command. For other job runners,
                Cylc will convert execution time limit to a :term:`directive`.

                If a job exceeds its execution time limit, Cylc will
                poll the job multiple times at the
                intervals configured in :cylc:conf:`global.cylc[platforms]
                [<platform name>]execution time limit polling intervals`

                .. versionchanged:: 8.0.0

                   {REPLACES}``[runtime][<namespace>][job]execution
                   time limit``.
            ''')
            Conf(
                'submission polling intervals', VDR.V_INTERVAL_LIST, None,
                desc=global_default(
                    SUBMISSION_POLL_DESCR,
                    "[platforms][<platform name>]submission polling intervals"
                )
            )
            Conf(
                'submission retry delays', VDR.V_INTERVAL_LIST, None,
                desc=global_default(
                    SUBMISSION_RETY_DESCR,
                    "[platforms][<platform name>]submission retry delays"
                )
            )
            Conf(
                'run mode', VDR.V_STRING,
                options=list(TASK_CONFIG_RUN_MODES),
                default=RunMode.LIVE.value,
                desc=f'''
                    When the workflow is running in live mode, run this *task*
                    in one of the following modes:

                    ``{RunMode.LIVE.value}`` (default):
                        {RunMode.LIVE.describe()}
                    ``{RunMode.SKIP.value}``:
                        {RunMode.SKIP.describe()}

                        .. note::

                           This is primarily intended to be set at runtime via
                           a broadcast; Cylc will warn you about any tasks
                           set to run in skip mode in the workflow
                           configuration at validation time.
                           If you are using skip mode to create a dummy task,
                           you can ignore this warning.

                    .. seealso::

                       - :ref:`task-run-modes.skip`
                       - :cylc:conf:`flow.cylc[runtime][<namespace>][skip]`

                    .. versionadded:: 8.4.0

            ''')
            with Conf('meta', desc=r'''
                Metadata for the task or task family.

                The ``meta`` section contains metadata items for this task or
                family namespace. The items ``title``, ``description`` and
                ``URL`` are pre-defined and are used by Cylc. Others can be
                user-defined and passed to task event handlers to be
                interpreted according to your needs. For example, the value of
                an "importance" item could determine how an event handler
                responds to task failure events.

                Any workflow meta item can now be passed to task event handlers
                by prefixing the string template item name with ``workflow_``,
                for example:

                .. code-block:: cylc

                   [runtime]
                       [[root]]
                           [[[events]]]
                               failed handlers = """
                                   send-help.sh \
                                       %(workflow_title)s \
                                       %(workflow_importance)s \
                                       %(title)s
                                """
            '''):
                Conf('title', VDR.V_STRING, '', desc='''
                    A single line description of this task or task family.

                    It is displayed by the ``cylc list`` command and can be
                    retrieved from running tasks with the ``cylc show``
                    command.
                ''')
                Conf('description', VDR.V_STRING, '', desc='''
                    A multi-line description of this task or task family.

                    It is retrievable from running tasks with the
                    ``cylc show`` command.
                ''')
                Conf(
                    'URL', VDR.V_STRING, '', desc='''
                        A URL link to task documentation for this task or task
                        family.

                        The templates ``%(workflow)s`` and
                        ``%(task)s`` will be replaced with the actual
                        workflow ID and task name.

                        .. deprecated:: 8.0.0

                           The ``%(suite_name)s`` template variable is
                           deprecated, please use ``%(workflow)s``.

                           The ``%(task_name)s`` template variable is
                           deprecated, please use ``%(task)s``.

                        See also :cylc:conf:`[meta]URL <flow.cylc[meta]URL>`.

                        Example:

                        ``http://my-site.com/workflows/%(workflow)s/'''
                    '%(task)s.html``')
                Conf('<custom metadata>', VDR.V_STRING, '', desc='''
                    Any user-defined metadata item.

                    These, like title, description and URL. can be passed to
                    task event handlers to be interpreted according to your
                    needs. For example, the value of an "importance" item could
                    determine how an event handler responds to task failure
                    events.
                ''')
            with Conf('skip', desc='''
                Task configuration for :ref:`task-run-modes.skip`.

                .. seealso::

                   - :ref:`task-run-modes.skip`
                   - :cylc:conf:`flow.cylc[runtime][<namespace>]run mode`

                .. versionadded:: 8.4.0
            '''):
                Conf(
                    'outputs',
                    VDR.V_STRING_LIST,
                    desc='''
                        Outputs to be emitted by a task in skip mode.

                        * By default, all required outputs will be generated
                          plus succeeded if success is optional.
                        * If skip-mode outputs is specified and does not
                          include either succeeded or failed then succeeded
                          will be produced.
                        * The outputs submitted and started are always
                          produced and do not need to be defined in here.

                        .. versionadded:: 8.4.0
                    '''
                )
                Conf(
                    'disable task event handlers',
                    VDR.V_BOOLEAN,
                    default=True,
                    desc='''
                        Task event handlers are turned off by default for
                        skip mode tasks. Changing this setting to ``False``
                        will re-enable task event handlers.

                        .. versionadded:: 8.4.0
                    '''
                )
            with Conf('simulation', desc='''
                Task configuration for workflow *simulation* and *dummy* run
                modes.

                For a full description of simulation and dummy run modes see
                :ref:`workflow-run-modes.simulation`.
            '''):
                Conf('default run length', VDR.V_INTERVAL, DurationFloat(10),
                     desc='''
                    The default simulated job run length.

                    Used if :cylc:conf:`flow.cylc[runtime][<namespace>]
                    execution time limit` **and**
                    :cylc:conf:`flow.cylc[runtime][<namespace>][simulation]
                    speedup factor` are not set.
                ''')
                Conf('speedup factor', VDR.V_FLOAT, desc='''
                    Simulated run length = speedup factor * execution time
                    limit.

                    If :cylc:conf:`flow.cylc[runtime][<namespace>]
                    execution time limit` is set, the task
                    simulated run length is computed by dividing it by this
                    factor.
                ''')
                Conf('time limit buffer', VDR.V_INTERVAL, DurationFloat(30),
                     desc='''
                    For dummy jobs :cylc:conf:`flow.cylc[runtime][<namespace>]
                    execution time limit` is extended
                    by ``time limit buffer``.

                    The time limit buffer is added to prevent dummy jobs
                    being killed after exceeding the ``execution time limit``.
                ''')
                Conf('fail cycle points', VDR.V_STRING_LIST, desc='''
                    Configure simulated or dummy jobs to fail at certain cycle
                    points.

                    Example:

                    - ``all`` - all instance of the task will fail
                    - ``2017-08-12T06, 2017-08-12T18`` - these instances of
                      the task will fail

                    If you set :cylc:conf:`[..][..]execution retry delays`
                    the second attempt will succeed unless you set
                    :cylc:conf:`[..]fail try 1 only = False`.
                ''')
                Conf('fail try 1 only', VDR.V_BOOLEAN, True, desc='''
                    If ``True`` only the first run of the task
                    instance will fail, otherwise retries will fail too.

                    Task instances must be set to fail by
                    :cylc:conf:`[..]fail cycle points`.

                    .. note::

                       This setting is designed for use with automatic
                       retries. Subsequent manual submissions will not
                       change the outcome of the task.
                ''')
                Conf('disable task event handlers', VDR.V_BOOLEAN, True,
                     desc='''
                    If ``True`` configured task event handlers
                    will not be called in simulation or dummy modes.
                ''')

            with Conf('environment filter', desc='''
                This section contains environment variable inclusion and
                exclusion lists that can be used to filter the inherited
                environment.

                *This is not intended as an alternative to a
                well-designed inheritance hierarchy that provides each task
                with just the variables it needs.*

                Filters can improve workflows with tasks which inherit a lot
                of environment variables: Filters can make it clear which
                variables each task uses.

                You can use filters as explicit "task environment interfaces".
                They make sure that variables filtered out of the inherited
                environment are not used. However, using filters in this way
                will make your workflow definition longer.

                .. note::
                   Environment filtering is done after inheritance is
                   completely worked out, not at each level on the way, so
                   filter lists in higher-level namespaces only have an effect
                   if they are not overridden by descendants.
            '''):
                Conf('include', VDR.V_STRING_LIST, desc='''
                    If given, **only** variables named in this list will be
                    included from the inherited environment.

                    Other variables will be filtered out. Variables may also
                    be explicitly excluded by an ``exclude`` list.
                ''')
                Conf('exclude', VDR.V_STRING_LIST, desc='''
                    Variables named in this list will be filtered out of the
                    inherited environment.

                    Variables may also be implicitly
                    excluded by omission from an ``include`` list.
                ''')

            with Conf('job', desc=REPLACED_BY_PLATFORMS + dedent('''
                This section configures the means by which cylc submits task
                job scripts to run.
            ''')):
                Conf('batch system', VDR.V_STRING, desc='''
                    .. deprecated:: 8.0.0

                       Kept for back compatibility but replaced by
                       :cylc:conf:`global.cylc[platforms][<platform name>]
                       job runner`.

                    Batch/queuing system (aka job runner) to submit task
                    jobs to.
                ''')
                Conf('batch submit command template', VDR.V_STRING, desc='''
                    .. deprecated:: 8.0.0

                       Kept for back compatibility but replaced by
                       :cylc:conf:`global.cylc[platforms][<platform name>]
                       job runner command template`.

                    Override the default job submission command for the chosen
                    batch system.
                ''')

            with Conf('remote', desc=f'''
                      Job host settings.
                      {REPLACED_BY_PLATFORMS}
             '''):
                Conf('host', VDR.V_STRING, desc=f'''
                     Hostname or IP address of the job host.
                     {REPLACED_BY_PLATFORMS}
                ''')
                Conf('owner', VDR.V_STRING, desc="""
                    Your username on the job host, if different from that on
                    the scheduler host.

                    .. warning::

                       This setting is obsolete at Cylc 8.

                       See :ref:`documentation on changes to remote owner
                       <728.remote_owner>`
                """)
                Conf('retrieve job logs', VDR.V_BOOLEAN,
                     desc=f'''
                     {LOG_RETR_SETTINGS['retrieve job logs']}
                     {REPLACED_BY_PLATFORMS}
                ''')
                Conf('retrieve job logs max size', VDR.V_STRING,
                     desc=f'''
                     {LOG_RETR_SETTINGS['retrieve job logs max size']}
                     {REPLACED_BY_PLATFORMS}
                ''')
                Conf('retrieve job logs retry delays',
                     VDR.V_INTERVAL_LIST, None,
                     desc=f'''
                     {LOG_RETR_SETTINGS['retrieve job logs retry delays']}
                     {REPLACED_BY_PLATFORMS}
                ''')
            with Conf('events', desc=(
                global_default(TASK_EVENTS_DESCR, "[task events]")
            )):
                Conf('handlers', VDR.V_STRING_LIST, None, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['handlers'],
                        "[task events]handlers"
                    )
                ))
                Conf('handler events', VDR.V_STRING_LIST, None, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['handler events'],
                        "[task events]handler events"
                    )
                ))
                Conf('handler retry delays', VDR.V_INTERVAL_LIST, None, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['handler retry delays'],
                        "[task events]handler retry delays"
                    )
                ))
                Conf('mail events', VDR.V_STRING_LIST, None, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['mail events'],
                        "[task events]mail events"
                    )
                ))
                Conf('execution timeout', VDR.V_INTERVAL, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['execution timeout'],
                        "[task events]execution timeout"
                    )
                ))
                Conf('submission timeout', VDR.V_INTERVAL, desc=(
                    global_default(
                        TASK_EVENTS_SETTINGS['submission timeout'],
                        "[task events]submission timeout"
                    )
                ))
                Conf('expired handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task has expired.

                    .. seealso::

                       :ref:`task-job-states`

                    .. caution::

                       Changes to the scheduling algorithm in Cylc 8 mean
                       this event will not be triggered until the expired task
                       is ready to run.  Earlier expired-task detection will be
                       implemented in a future Cylc release.
                ''')
                Conf('late offset', VDR.V_INTERVAL, None, desc='''
                    Offset from cycle point, in real time, at which this task
                    is considered to be "running late" (i.e. the time by which
                    it would normally have started running).

                    .. caution::

                       Changes to the scheduling algorithm in Cylc 8 mean
                       this event will not be triggered until the late task
                       is ready to run.  Earlier late-task detection will be
                       implemented in a future Cylc release.

                    .. seealso::

                       :cylc:conf:`flow.cylc[runtime][<namespace>][events]
                       late handlers`.
                ''')
                Conf('late handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task is late.

                    .. caution::

                       Due to changes to the Cylc 8 scheduling algorithm
                       this event is unlikely to occur until the task is about
                       to submit anyway.
                ''')
                Conf('submitted handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run when this task is submitted.
                ''')
                Conf('started handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run when this task starts executing.
                ''')
                Conf('succeeded handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task succeeds.
                ''')
                Conf('failed handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task fails.
                ''')
                Conf('submission failed handlers', VDR.V_STRING_LIST, None,
                     desc='''
                        Handlers to run if submission of this task fails.
                ''')
                Conf('warning handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task runs ``cylc message``
                    with severity level "WARNING".
                ''')
                Conf('critical handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task runs ``cylc message``
                    with severity level "CRITICAL".
                ''')
                Conf('retry handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task failed but is retrying.
                ''')
                Conf('submission retry handlers', VDR.V_STRING_LIST, None,
                     desc='''
                        Handlers to run if a job failed to submit but is
                        retrying.

                        .. seealso::

                           :ref:`task-job-states`

                ''')
                Conf('execution timeout handlers', VDR.V_STRING_LIST, None,
                     desc='''
                        Handlers to run if this task execution exceeds
                        :cylc:conf:`flow.cylc[runtime][<namespace>]
                        execution time limit`.
                ''')
                Conf('submission timeout handlers', VDR.V_STRING_LIST, None,
                     desc='''
                        Handlers to run if this task exceeds
                        :cylc:conf:`flow.cylc[runtime][<namespace>][events]
                        submission timeout` in the submitted state.
                ''')
                Conf('custom handlers', VDR.V_STRING_LIST, None, desc='''
                    Handlers to run if this task runs ``cylc message``
                    with severity level "CUSTOM".
                ''')

            with Conf('mail', desc='''
                Email notification settings for task events.

                .. versionadded:: 8.0.0
            '''):
                Conf('from', VDR.V_STRING, desc=f'''
                    Specify an alternate ``from:`` email address for event
                    notifications.

                    .. versionchanged:: 8.0.0

                       {REPLACES}``[runtime][task][events]mail from``
                ''')
                Conf('to', VDR.V_STRING, desc=f'''
                    A list of email addresses to send task event
                    notifications.

                    The list can be any address accepted by the
                    ``mail`` command.

                    .. versionchanged:: 8.0.0

                       {REPLACES}``[runtime][task][events]mail to``
                ''')

            with Conf('workflow state polling', desc=f'''
                Deprecated support for automatic workflow state polling tasks
                as described in :ref:`WorkflowStatePolling`. Note the Cylc 7
                "user" and "host" config items are not supported.

                .. versionchanged:: 8.0.0

                   {REPLACES}``[runtime][<namespace>]suite state polling``.

                .. deprecated:: 8.3.0

                Please use the :ref:`workflow_state xtrigger
                <Built-in Workflow State Triggers>` instead.
            '''):
                Conf('interval', VDR.V_INTERVAL, desc='''
                    Polling interval.
                ''')
                Conf('max-polls', VDR.V_INTEGER, desc='''
                    Maximum number of polls to attempt before the task fails.
                ''')
                Conf('message', VDR.V_STRING, desc='''
                    Target task output (task message, not trigger name).
                ''')
                Conf('alt-cylc-run-dir', VDR.V_STRING, desc='''
                    The cylc-run directory location of the target workflow.
                    Use to poll workflows owned by other users.
                ''')
                Conf('verbose mode', VDR.V_BOOLEAN, desc='''
                    Run the ``cylc workflow-state`` command in verbose mode.
                ''')

            with Conf('environment', desc='''
                The user defined task execution environment.

                Variables defined here can refer to cylc workflow and task
                identity variables, which are exported earlier in the job
                script. Variable assignment expressions can use cylc
                utility commands because access to cylc is also configured
                earlier in the script.

                .. seealso::

                   :ref:`TaskExecutionEnvironment`.

                You can also specify job environment templates here for
                :ref:`parameterized tasks <User Guide Param>`.
            '''):
                Conf('<variable>', VDR.V_STRING, desc=r'''
                    A custom user defined variable for a task execution
                    environment.

                    The order of definition is preserved that each variable can
                    refer to previously defined
                    variables. Values are passed through to the job
                    script without evaluation or manipulation by Cylc
                    (with the exception of valid Python string templates
                    that match parameterized task names - see below), so any
                    variable assignment expression that is legal in the job
                    submission shell can be used.  White space around the
                    ``=`` is allowed (as far as cylc's flow.cylc parser is
                    concerned these are just normal configuration items).

                    Examples::

                       FOO = $HOME/bar/baz
                       BAR = ${FOO}$GLOBALVAR
                       BAZ = $( echo "hello world" )
                       WAZ = ${FOO%.jpg}.png
                       NEXT_CYCLE = $( cylc cycle-point --offset=PT6H )
                       ZAZ = "${FOO#bar}"
                       # ^ quoted to escape the flow.cylc comment character
                       DICE = [$((($RANDOM % 6) + 1)) $((($RANDOM % 6) + 1))]

                    For parameter environment templates, use Python string
                    templates for parameter substitution. This is only
                    relevant for
                    :ref:`parameterized tasks <User Guide Param>`.
                    The job script will export the named variables specified
                    here (in addition to the standard ``CYLC_TASK_PARAM_<key>``
                    variables), with the template strings substituted with
                    the parameter values.

                    Examples::

                       MYNUM = %(i)d
                       MYITEM = %(item)s
                       MYFILE = /path/to/%(i)03d/%(item)s

                    .. note::

                       As with other Cylc configurations, leading or trailing
                       whitespace will be stripped, so the following two
                       examples are equivalent:

                       .. list-table::
                          :class: grid-table

                          * - .. code-block:: cylc

                                 [environment]
                                     FOO = " a "
                                     BAR = """
                                       $(foo bar baz)
                                   """
                            - .. code-block:: cylc

                                 [environment]
                                     FOO = "a"
                                     BAR = "$(foo bar baz)"

                       If leading or trailing whitespace is required, consider
                       using the ``\0`` escape character, or set the variable
                       in :cylc:conf:`[..][..]env-script`.

                    .. versionchanged:: 7.8.7/7.9.2

                       Parameter environment templates (previously in
                       ``[runtime][X][parameter environment templates]``) have
                       moved here.
                ''')

            with Conf('directives', desc=(
                global_default(
                    DIRECTIVES_DESCR,
                    "[platforms][<platform name>][directives]"
                )
            )):
                Conf('<directive>', VDR.V_STRING, desc=DIRECTIVES_ITEM_DESCR)

            with Conf('outputs', desc='''
                Register custom task outputs for use in message triggering in
                this section (:ref:`MessageTriggers`)
            '''):
                Conf('<output>', VDR.V_STRING, desc='''
                    Define custom task outputs (aka :ref:`MessageTriggers`).

                    :term:`Custom outputs <custom output>` allow you to extend
                    the built-in task outputs e.g. ``succeeded`` and ``failed``
                    in order to provide more detailed information about task
                    state. Custom outputs can be used to express dependencies
                    in the graph as with built-in outputs.

                    Custom outputs are defined in the form:

                    .. code-block:: cylc

                       output = message

                    Where ``output`` is the name of the output as it is used in
                    the graph, and ``message`` is the task message sent by
                    the ``cylc message`` command which tells Cylc that this
                    output has been completed. See :ref:`MessageTriggers` for
                    more details.

                    Examples:

                    .. code-block:: cylc

                       out1 = "sea state products ready"
                       out2 = "NWP restart files completed"

                    Custom outputs must satisfy these rules:

                    .. autoclass:: cylc.flow.unicode_rules.TaskOutputValidator
                       :noindex:

                    Task messages must satisfy these rules:

                    .. autoclass:: cylc.flow.unicode_rules.TaskMessageValidator
                       :noindex:
                ''')

            with Conf('parameter environment templates', desc='''
                .. deprecated:: 7.8.7/7.9.2

                   Parameter environment templates have moved to
                   :cylc:conf:`flow.cylc[runtime][<namespace>][environment]`.

                This was done to allow users to control the order of
                definition of the variables. This section will be removed
                in a future version of Cylc 8.

                For the time being, the contents of this section will be
                prepended to the ``[environment]`` section when running
                a workflow.
            '''):
                Conf('<parameter>', VDR.V_STRING, desc='''
                    .. deprecated:: 7.8.7/7.9.2

                       Parameter environment templates have moved to
                       :cylc:conf:`flow.cylc[runtime]
                       [<namespace>][environment]`.
                ''')


def upg(
    cfg: dict, descr: str, broadcast: bool | Literal["cancel"] = False
) -> upgrader:
    """Upgrade old workflow configuration.

    NOTE: We are silencing deprecation (and only deprecation) warnings
    when in Cylc 7 compat mode to help support Cylc 7/8 compatible workflows
    (which would loose Cylc 7 compatibility if users were to follow the
    warnings and upgrade the syntax).

    Args:
        broadcast:
            If truthy, tailors warning messages for the context of broadcasts.
            If "cancel", extra validation steps which inspect configuration
            values will be skipped. This is used for
            "cylc broadcast --cancel" where the values are not known.
            See https://github.com/cylc/cylc-flow/issues/6950.

    """
    u = upgrader(cfg, descr, broadcast=bool(broadcast))

    u.obsolete(
        '7.8.0', ['runtime', '__MANY__', 'suite state polling', 'template']
    )
    u.obsolete('7.8.1', ['cylc', 'events', 'reset timer'])
    u.obsolete('7.8.1', ['cylc', 'events', 'reset inactivity timer'])
    u.obsolete('8.0.0', ['cylc', 'force run mode'])
    u.obsolete('7.8.1', ['runtime', '__MANY__', 'events', 'reset timer'])
    u.obsolete('8.0.0', ['cylc', 'authentication'], is_section=True)
    u.obsolete('8.0.0', ['cylc', 'include at start-up'])
    u.obsolete('8.0.0', ['cylc', 'exclude at start-up'])
    u.obsolete('8.0.0', ['cylc', 'log resolved dependencies'])
    u.obsolete('8.0.0', ['cylc', 'required run mode'])
    u.obsolete(
        '8.0.0',
        ['cylc', 'health check interval'])
    u.obsolete('8.0.0', ['runtime', '__MANY__', 'events', 'mail retry delays'])
    u.obsolete('8.0.0', ['runtime', '__MANY__', 'extra log files'])
    u.obsolete('8.0.0', ['runtime', '__MANY__', 'job', 'shell'])
    u.obsolete(
        '8.0.0',
        ['runtime', '__MANY__', 'remote', 'suite definition directory']
    )
    u.obsolete('8.0.0', ['cylc', 'abort if any task fails'])
    u.obsolete('8.0.0', ['cylc', 'disable automatic shutdown'])
    u.obsolete('8.0.0', ['cylc', 'environment'], is_section=True)
    u.obsolete('8.0.0', ['cylc', 'reference test'], is_section=True)
    u.obsolete(
        '8.0.0',
        ['cylc', 'simulation', 'disable suite event handlers'])
    u.obsolete('8.0.0', ['cylc', 'simulation'], is_section=True)
    u.obsolete('8.0.0', ['visualization'], is_section=True)
    u.obsolete('8.0.0', ['scheduling', 'spawn to max active cycle points'])
    u.deprecate(
        '8.0.0',
        ['cylc', 'task event mail interval'],
        ['cylc', 'mail', 'task event batch interval'],
        silent=cylc.flow.flags.cylc7_back_compat,
    )
    u.deprecate(
        '8.0.0',
        ['runtime', '__MANY__', 'suite state polling'],
        ['runtime', '__MANY__', 'workflow state polling'],
        silent=cylc.flow.flags.cylc7_back_compat,
        is_section=True,
    )
    u.obsolete(
        '8.0.0', ['runtime', '__MANY__', 'workflow state polling', 'host'])
    u.obsolete(
        '8.0.0', ['runtime', '__MANY__', 'workflow state polling', 'user'])

    u.deprecate(
        '8.3.0',
        ['runtime', '__MANY__', 'workflow state polling', 'run-dir'],
        ['runtime', '__MANY__', 'workflow state polling', 'alt-cylc-run-dir'],
        silent=cylc.flow.flags.cylc7_back_compat,
    )

    u.deprecate(
        '8.0.0',
        ['cylc', 'parameters'],
        ['task parameters'],
        silent=cylc.flow.flags.cylc7_back_compat,
        is_section=True,
    )
    u.deprecate(
        '8.0.0',
        ['cylc', 'parameter templates'],
        ['task parameters', 'templates'],
        silent=cylc.flow.flags.cylc7_back_compat,
        is_section=True,
    )
    # Whole workflow task mail settings
    for mail_setting in ['to', 'from', 'footer']:
        u.deprecate(
            '8.0.0',
            ['cylc', 'events', f'mail {mail_setting}'],
            ['cylc', 'mail', mail_setting],
            silent=cylc.flow.flags.cylc7_back_compat,
        )
    # Task mail settings in [runtime][TASK]
    for mail_setting in ['to', 'from']:
        u.deprecate(
            '8.0.0',
            ['runtime', '__MANY__', 'events', f'mail {mail_setting}'],
            ['runtime', '__MANY__', 'mail', mail_setting],
            silent=cylc.flow.flags.cylc7_back_compat,
        )
    u.deprecate(
        '8.0.0',
        ['cylc', 'events', 'mail smtp'],
        None,  # This is really a .obsolete(), just with a custom message
        cvtr=converter(lambda x: x, (
            'DELETED (OBSOLETE) - use "global.cylc[scheduler][mail]smtp" '
            'instead')
        ),
        silent=cylc.flow.flags.cylc7_back_compat,
    )
    u.deprecate(
        '8.0.0',
        ['runtime', '__MANY__', 'events', 'mail smtp'],
        None,
        cvtr=converter(lambda x: x, (
            'DELETED (OBSOLETE) - use "global.cylc[scheduler][mail]smtp" '
            'instead')
        ),
        silent=cylc.flow.flags.cylc7_back_compat,
    )
    u.deprecate(
        '8.0.0',
        ['scheduling', 'max active cycle points'],
        ['scheduling', 'runahead limit'],
        cvtr=converter(
            lambda x: f'P{int(x) - 1}' if x != '' else '',
            '"{old}" -> "{new}"'
        ),
        silent=cylc.flow.flags.cylc7_back_compat,
    )
    u.deprecate(
        '8.0.0',
        ['scheduling', 'hold after point'],
        ['scheduling', 'hold after cycle point'],
        silent=cylc.flow.flags.cylc7_back_compat,
    )

    for job_setting in [
        'execution polling intervals',
        'execution retry delays',
        'execution time limit',
        'submission polling intervals',
        'submission retry delays'
    ]:
        u.deprecate(
            '8.0.0',
            ['runtime', '__MANY__', 'job', job_setting],
            ['runtime', '__MANY__', job_setting],
            silent=cylc.flow.flags.cylc7_back_compat,
        )

    # Workflow timeout is now measured from start of run.
    # The old timeout was measured from start of stall.
    for old, new in [
        ('timeout', 'stall timeout'),
        ('abort on timeout', 'abort on stall timeout'),
        ('inactivity', 'inactivity timeout'),
        ('abort on inactivity', 'abort on inactivity timeout'),
        ('startup handler', 'startup handlers'),
        ('shutdown handler', 'shutdown handlers'),
        ('timeout handler', 'stall timeout handlers'),
        ('stalled handler', 'stall handlers'),
        ('aborted handler', 'abort handlers'),
        ('inactivity handler', 'inactivity timeout handlers'),
    ]:
        u.deprecate(
            '8.0.0',
            ['cylc', 'events', old],
            ['cylc', 'events', new],
            silent=cylc.flow.flags.cylc7_back_compat,
        )

    for old in [
        "expired handler",
        "late handler",
        "submitted handler",
        "started handler",
        "succeeded handler",
        "failed handler",
        "submission failed handler",
        "warning handler",
        "critical handler",
        "retry handler",
        "submission retry handler",
        "execution timeout handler",
        "submission timeout handler",
        "custom handler"
    ]:
        u.deprecate(
            '8.0.0',
            ['runtime', '__MANY__', 'events', old],
            ['runtime', '__MANY__', 'events', f"{old}s"],
            silent=cylc.flow.flags.cylc7_back_compat,
        )

    for old in [
        'abort on stalled',
        'abort if startup handler fails',
        'abort if shutdown handler fails',
        'abort if timeout handler fails',
        'abort if inactivity handler fails',
        'abort if stalled handler fails',
    ]:
        u.obsolete('8.0.0', ['cylc', 'events', old])

    u.deprecate(
        '8.0.0',
        ['cylc'],
        ['scheduler'],
        silent=cylc.flow.flags.cylc7_back_compat,
        is_section=True,
    )
    u.upgrade()

    if broadcast != "cancel":
        upgrade_graph_section(cfg, descr)
        upgrade_param_env_templates(cfg, descr)
        warn_about_depr_platform(cfg)
        warn_about_depr_event_handler_tmpl(cfg)

    return u


def upgrade_graph_section(cfg: Dict[str, Any], descr: str) -> None:
    """Upgrade Cylc 7 `[scheduling][dependencies][X]graph` format to
    `[scheduling][graph]X`."""
    # Parsec upgrader cannot do this type of move
    with contextlib.suppress(KeyError):
        note = ''
        if 'dependencies' in cfg['scheduling']:
            if ['graph'] == cfg['scheduling']['dependencies'].keys():
                msg_old = '[scheduling][dependencies]graph'
                msg_new = '[scheduling][graph]R1'
                list_cp = False
            else:
                note = (
                    '\n   ([scheduling][dependencies]graph moves to'
                    ' [scheduling][graph]R1)'
                    if 'graph' in cfg['scheduling']['dependencies'] else '')
                msg_old = '[scheduling][dependencies][X]graph'
                msg_new = '[scheduling][graph]X - for X in:'
                list_cp = True

            if 'graph' in cfg['scheduling']:
                raise UpgradeError(
                    f'Cannot upgrade deprecated item "{msg_old} -> {msg_new}" '
                    f'because {msg_new[:-1]} already exists.'
                )
            else:
                keys: Set[str] = set()
                cfg['scheduling'].setdefault('graph', {})
                cfg['scheduling']['graph'].update(
                    cfg['scheduling'].pop('dependencies')
                )
                graphdict: Dict[str, Any] = cfg['scheduling']['graph']
                for key, value in graphdict.copy().items():
                    if isinstance(value, dict) and 'graph' in value:
                        graphdict[key] = value['graph']
                        keys.add(key)
                    elif key == 'graph' and isinstance(value, str):
                        graphdict[key] = value
                        keys.add(key)
                if keys and not cylc.flow.flags.cylc7_back_compat:
                    msg = (
                        'graph items were automatically upgraded '
                        f'in "{descr}":\n'
                        f' * (8.0.0) {msg_old} -> {msg_new}'
                    )
                    if list_cp:
                        msg += f"\n       {', '.join(sorted(keys))}"
                    LOG.warning(msg + note)


def upgrade_param_env_templates(cfg, descr):
    """Prepend contents of `[runtime][X][parameter environment templates]` to
    `[runtime][X][environment]`."""

    if 'runtime' in cfg:
        dep = '[runtime][%s][parameter environment templates]'
        new = '[runtime][%s][environment]'
        first_warn = True
        for task_name, task_items in cfg['runtime'].items():
            if 'parameter environment templates' not in task_items:
                continue
            if not cylc.flow.flags.cylc7_back_compat:
                if first_warn:
                    LOG.warning(upgrader.depr_msg)
                    first_warn = False
                LOG.warning(
                    f' * (8.0.0) {dep % task_name} contents prepended to '
                    f'{new % task_name}'
                )
            for key, val in reversed(
                    task_items['parameter environment templates'].items()):
                if 'environment' in task_items:
                    if key in task_items['environment']:
                        LOG.warning(
                            f' *** {dep % task_name} {key} ignored as {key} '
                            f'already exists in {new % task_name}'
                        )
                        continue
                else:
                    task_items['environment'] = OrderedDictWithDefaults()
                task_items['environment'].prepend(key, val)
            task_items.pop('parameter environment templates')


def warn_about_depr_platform(cfg):
    """Validate platforms config.

    - Warn if deprecated host or batch system appear in config.
    - Raise if platforms section is also present.
    - Or raise if using invalid subshell syntax for platform def.
    """
    if 'runtime' not in cfg:
        return
    for task_name, task_cfg in cfg['runtime'].items():
        if 'platform' in task_cfg and task_cfg['platform']:
            fail_if_platform_and_host_conflict(task_cfg, task_name)
            # Fail if backticks subshell e.g. platform = `foo`:
            is_platform_definition_subshell(task_cfg['platform'])
        elif not cylc.flow.flags.cylc7_back_compat:
            depr = get_platform_deprecated_settings(task_cfg, task_name)
            if depr:
                msg = "\n".join(depr)
                LOG.warning(
                    "deprecated settings found "
                    f"(please replace with [runtime][{task_name}]platform):"
                    f"\n{msg}"
                )


def warn_about_depr_event_handler_tmpl(cfg):
    """Warn if deprecated template strings appear in event handlers."""
    if 'runtime' not in cfg or cylc.flow.flags.cylc7_back_compat:
        return
    deprecation_msg = (
        'The event handler template variable "%({0})s" is deprecated - '
        'use "%({1})s" instead.')
    for task in cfg['runtime']:
        if 'events' not in cfg['runtime'][task]:
            continue
        for handler in cfg['runtime'][task]['events'].values():
            if f'%({EventData.JobID_old.value})' in handler:
                LOG.warning(
                    deprecation_msg.format(EventData.JobID_old.value,
                                           EventData.JobID.value)
                )
            if f'%({EventData.JobRunnerName_old.value})' in handler:
                LOG.warning(
                    deprecation_msg.format(EventData.JobRunnerName_old.value,
                                           EventData.JobRunnerName.value)
                )
            if f'%({EventData.Suite.value})' in handler:
                LOG.warning(
                    deprecation_msg.format(EventData.Suite.value,
                                           EventData.Workflow.value)
                )
            if f'%({EventData.SuiteUUID.value})' in handler:
                LOG.warning(
                    deprecation_msg.format(EventData.SuiteUUID.value,
                                           EventData.UUID.value)
                )


class RawWorkflowConfig(ParsecConfig):
    """Raw workflow configuration."""

    def __init__(self, fpath, output_fname, tvars, options):
        """Return the default instance."""
        ParsecConfig.__init__(
            self, SPEC, upg, output_fname, tvars, cylc_config_validate,
            options
        )
        self.loadcfg(fpath, "workflow definition")
