"""Render the applications and users SPOTs once and park them as play-scoped carrier facts.

Ansible forks a fresh worker per task, so the in-process render caches of
``lookup('applications')`` and ``lookup('users')`` are cold in every task
until something renders in a place all later workers inherit. This action
renders in the worker of one constructor task and returns both payloads as
host facts; every later worker inherits them at fork time and
``get_merged_applications`` / ``get_merged_users`` serve them instead of
rendering. The users carrier is keyed like the applications carrier plus
the token store digest and ``inventory_hostname``::

    - name: 🧊 Render the applications SPOT once and carry it into every task's worker
      applications_carrier: {}

``tasks/stages/01_constructor.yml`` parks three of them: a block-scoped one
for the merge block, which it drops again at the block's end, one after the
``add_host`` tasks for the ``update`` role, the pre-flight checks, the package
batch and the bootstrap roles, and the stage-wide one after the token store. ``merged_applications_cache_key`` reads ``group_names``, so a
carrier whose groups no longer match is rejected and the reader renders
afresh. ``merged_users_cache_key`` adds a digest of the tokens the users are
hydrated from (the tokens file, or the ``sys_token_store_users`` fact once the
token store loaded; both key alike for the same tokens), so changed tokens
reject the users carrier only::

    - name: 🧊 Drop the merge-block carrier so later stages re-render
      applications_carrier:
        clear: true

``tasks/utils/setup/role.yml`` parks one after every role,
``tasks/utils/load/app.yml`` one after every nested application load and
``roles/sys-token-store/tasks/write_many.yml`` one after every store write;
where the key still matches they serve the carried payload without rendering,
and a users carrier that still matches is left in place instead of being
returned again. A store write therefore re-renders the users alone.

The result is masked by the action itself: the facts are the whole rendered
trees, about 0.4 MB with every application's and user's credentials, and
printing them at ``-v`` is neither credential debugging nor readable.

Args:
    clear: drop every carrier fact instead of rendering. Defaults to false.
    provider_view: on a host that does not run the onion-serving mail
        provider, also park the applications payload of the provider host's
        view in ``_INFINITO_PROVIDER_VIEW_RENDERED`` (see
        ``plugins.lookup.email.provider_view_carrier``), unless the parked one
        or the provider host's own carrier still matches. Defaults to false.
    Any other argument is an error.
"""

from __future__ import annotations

from typing import Any

from ansible.errors import AnsibleActionFail
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.plugins.loader import lookup_loader

from plugins.lookup.email import provider_view_carrier
from utils.cache.carrier import (
    APPLICATIONS_RENDERED_FACT,
    PROVIDER_VIEW_RENDERED_FACT,
    USERS_RENDERED_FACT,
    _carried_users,
    revalidate_tokens_file,
)


class ActionModule(ActionBase):
    TRANSFERS_FILES = False

    def run(
        self, tmp: Any = None, task_vars: dict[str, Any] | None = None
    ) -> dict[str, Any]:
        result = super().run(tmp, task_vars)
        task_vars = task_vars or {}
        args = self._task.args or {}
        unexpected = sorted(set(args) - {"clear", "provider_view"})
        if unexpected:
            raise AnsibleActionFail(
                f"applications_carrier: unknown argument(s) {', '.join(unexpected)}; "
                "the only arguments are 'clear' and 'provider_view'"
            )

        if boolean(args.get("clear", False)):
            result.update(
                changed=False,
                ansible_facts={
                    APPLICATIONS_RENDERED_FACT: None,
                    USERS_RENDERED_FACT: None,
                    PROVIDER_VIEW_RENDERED_FACT: None,
                },
            )
            return result

        revalidate_tokens_file(task_vars)
        render_vars = dict(task_vars)
        render_vars.pop("application_id", None)
        previous = self._templar.available_variables
        self._templar.available_variables = render_vars
        try:
            carriers = {
                fact: lookup_loader.get(
                    name, loader=self._loader, templar=self._templar
                ).run([], variables=render_vars, carrier=True)[0]
                for fact, name in (
                    (APPLICATIONS_RENDERED_FACT, "applications"),
                    (USERS_RENDERED_FACT, "users"),
                )
            }
        finally:
            self._templar.available_variables = previous

        users_key = carriers[USERS_RENDERED_FACT]["key"]
        if _carried_users(task_vars, (users_key[0], tuple(users_key[1]))) is not None:
            del carriers[USERS_RENDERED_FACT]

        if boolean(args.get("provider_view", False)):
            view_carrier = provider_view_carrier(task_vars, templar=self._templar)
            if view_carrier is not None:
                carriers[PROVIDER_VIEW_RENDERED_FACT] = view_carrier

        result.update(
            changed=False,
            ansible_facts=carriers,
            _ansible_no_log=True,
        )
        return result
