"""
LGPO - Registry.pol
===================

.. versionadded:: 3006.0

A module for working with registry based policies in Windows Local Group Policy
(LGPO). This module contains functions for working with the ``Registry.pol``
file. The ``Registry.pol`` file is the source of truth for registry settings
and LGPO.

Group Policy is refreshed every 90 seconds by default. During that refresh the
contents of the ``Registry.pol`` file are applied to the Registry. If the
setting is changed outside of Group Policy to something other than what is
contained in the ``Registry.pol`` file, it will be changed back during the next
refresh.

In the Group Policy Editor (``gpedit.msc``) these policies can be set to three
states:

- Not Configured
- Enabled
- Disabled

A policy that is "Not Configured" does not have an entry in the ``Registry.pol``
file. A Group Policy refresh will not make any changes to key/value pairs in the
registry that are not specified in the ``Registry.pol`` file.

An "Enabled" policy will have an entry in the ``Registry.pol`` files that
contains its key path, value name, value type, value size, and value data. When
Group Policy is refreshed, existing values will be overwritten with those
contained in the ``Registry.pol`` file.

A "Disabled" policy will have an entry in the ``Registry.pol`` file with the key
path and the value name, but the value name will be prepended with ``**del.``.
When Group Policy is refreshed the key/value will be deleted from the registry.
If the key contains no values, it will also be deleted.

Working with LGPO Reg
---------------------

The easiest way to figure out the values needed for this module is to set the
policy using the Group Policy Editor (``gpedit.msc``) and then run the
``lgpo_reg.read_reg_pol`` function. This function will display a dictionary of
all registry-based policies in the ``Registry.pol`` file. From its return you
can get the ``key``, ``v_name``, ``v_type``, and ``v_data`` required to "enable"
that policy. Use those values to set/disable/delete policies using this module.
The same values can also be used to create states for setting these policies.

.. note::
    Not all policies in the Group Policy Editor (``gpedit.msc``) that write to
    the registry make that change in the ``Registry.pol`` file. Those policies
    could still be enforced via the ``Registry.pol`` file... theoretically. But
    you will have to find the values needed to set them with this module using a
    different method.
"""

import logging

import salt.utils.platform
import salt.utils.win_lgpo_reg
import salt.utils.win_reg
import salt.utils.winapi
from salt.exceptions import SaltInvocationError

log = logging.getLogger(__name__)
__virtualname__ = "lgpo_reg"

LOCAL_POLICY_GPO_ID = "{00000000-0000-0000-0000-000000000000}"

_RSOP_VALUE_TYPES = {
    0: "REG_NONE",
    1: "REG_SZ",
    2: "REG_EXPAND_SZ",
    3: "REG_BINARY",
    4: "REG_DWORD",
    5: "REG_DWORD_BIG_ENDIAN",
    6: "REG_LINK",
    7: "REG_MULTI_SZ",
    11: "REG_QWORD",
}


def __virtual__():
    """
    Only works on Windows
    """
    if not salt.utils.platform.is_windows():
        return False, "LGPO_REG Module: Only available on Windows"

    return __virtualname__


def _find_value(pol_data, key, v_name):
    """
    Helper function to find the value in the registry.pol file. Sometimes the
    value is prepended by an action that needs to happen with that value, such
    as `**del.` to delete it from the registry.

    Returns:
        tuple: A tuple containing: found_key, found_name
    """
    found_key = ""
    found_name = ""
    for p_key in pol_data:
        if key.lower() == p_key.lower():
            found_key = p_key
            for p_name in pol_data[p_key]:
                if p_name.lower().startswith("**del."):
                    if v_name.lower() == p_name.lower().split(".", 1)[1]:
                        found_name = p_name
                else:
                    if v_name.lower() == p_name.lower():
                        found_name = p_name
    return found_key, found_name


def read_reg_pol(policy_class="Machine"):
    r"""
    Read the contents of the Registry.pol file. Display the contents as a
    human-readable dictionary.

    Args:

        policy_class (:obj:`str`, optional):
            The registry class to retrieve. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy class

    Returns:
        dict: A dictionary representing the contents of the Registry.pol file

    CLI Example:

    .. code-block:: bash

        # Read the machine Registry.pol
        salt '*' lgpo_reg.read_reg_pol
    """
    # Verify policy_class
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
    else:
        raise SaltInvocationError("An invalid policy class was specified")
    pol_info = salt.utils.win_lgpo_reg.CLASS_INFO[policy_class]
    pol_file_data = salt.utils.win_lgpo_reg.read_reg_pol_file(
        reg_pol_path=pol_info["policy_path"]
    )

    return salt.utils.win_lgpo_reg.reg_pol_to_dict(pol_file_data)


def write_reg_pol(data, policy_class="Machine"):
    r"""
    Write data to the Registry.pol file. The data is a dictionary that is then
    converted to the appropriate bytes format expected by Registry.pol

    Args:
        data (dict): A dictionary containing Registry.pol data

        policy_class (:obj:`str`, optional):
            The registry class to write to. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy class
        CommandExecutionError: On failure

    Returns:
        bool: True if successful

    CLI Example:

    .. code-block:: bash

        # Write to Machine Registry.pol
        salt '*' lgpo_reg.write_reg_pol "{'SOFTWARE\MyKey': {'MyValue': 'data': 1, 'type': 'REG_DWORD'}}"
    """
    # Maybe have this data passed instead of opening it here
    # Verify policy_class
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
    else:
        raise SaltInvocationError("An invalid policy class was specified")

    policy_class_info = salt.utils.win_lgpo_reg.CLASS_INFO[policy_class]

    policy_file_data = salt.utils.win_lgpo_reg.dict_to_reg_pol(data)

    return salt.utils.win_lgpo_reg.write_reg_pol_data(
        policy_file_data,
        policy_class_info["policy_path"],
        policy_class_info["gpt_extension_location"],
        policy_class_info["gpt_extension_guid"],
    )


def get_value(key, v_name, policy_class="Machine"):
    r"""
    Get the value of a single value pair as set in the ``Registry.pol``
    file.

    Args:
        key (str): The registry key where the value name resides

        v_name (str): The value name to retrieve

        policy_class (:obj:`str`, optional):
            The registry class to read from. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy class

    Returns:
        dict: A dictionary containing the value data and the value type found

    CLI Example:

    .. code-block:: bash

        # Get a value
        salt '*' lgpo_reg.get_value "SOFTWARE\MyKey" "MyValue"
    """
    # Verify input
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
    else:
        raise SaltInvocationError("An invalid policy class was specified")
    pol_data = read_reg_pol(policy_class=policy_class)

    found_key, found_name = _find_value(pol_data, key, v_name)

    if found_key:
        if found_name:
            if "**del." in found_name:
                pol_data[found_key][found_name]["data"] = found_name
            return pol_data[found_key][found_name]

    return {}


def get_key(key, policy_class="Machine"):
    r"""
    Get all the values set in a key in the ``Registry.pol`` file.

    Args:
        key (str): The registry key where the values reside

        policy_class (:obj:`str`, optional):
            The registry class to read from. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy class

    Returns:
        dict: A dictionary containing the value data and the value type

    CLI Example:

    .. code-block:: bash

        # Get all values from a key
        salt '*' lgpo_reg.get_key "SOFTWARE\MyKey"
    """
    # Verify input
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
    else:
        raise SaltInvocationError("An invalid policy class was specified")
    pol_data = read_reg_pol(policy_class=policy_class)
    found_key = ""
    for p_key in pol_data:
        if key.lower() == p_key.lower():
            found_key = p_key

    if found_key:
        return pol_data[found_key]

    return {}


def get_rsop_value(key, v_name):
    r"""
    Query the Resultant Set of Policy (RSoP) for a specific Machine registry
    key/value. Returns information about the winning Group Policy Object (GPO)
    for that value, including whether it is managed by a Domain GPO.

    .. note::
        Only Machine (computer) policy is supported. User policy RSoP requires
        per-user SID scoping and a different WMI namespace, which is not
        practical when Salt runs as SYSTEM.

    Args:
        key (str): The registry key path

        v_name (str): The registry value name

    Returns:
        dict: A dictionary containing the RSoP information, or ``{}`` if the
        value is not found in RSoP, if the machine is not domain-joined, or if
        WMI is unavailable. Keys when a result is found:

        - key (str): The registry key path
        - name (str): The registry value name
        - data: The value data
        - type (str): The registry value type (e.g. ``REG_DWORD``)
        - gpo_id (str): The GUID of the winning GPO
        - gpo_name (str): The display name of the winning GPO
        - precedence (int): The policy precedence (1 = winning)
        - domain_managed (bool): ``True`` if managed by a Domain GPO

    CLI Example:

    .. code-block:: bash

        salt '*' lgpo_reg.get_rsop_value "SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" VulnerableChannelAllowList
    """

    try:
        import wmi
    except ImportError:
        log.debug("LGPO_REG Mod: wmi module not available, cannot query RSoP")
        return {}

    try:
        with salt.utils.winapi.Com():
            conn = wmi.WMI(namespace="root\\rsop\\computer")
            wmi_key = key.replace("\\", "\\\\")
            results = conn.query(
                f"SELECT * FROM RSOP_RegistryPolicySetting "
                f"WHERE RegistryKey = '{wmi_key}' "
                f"AND ValueName = '{v_name}' "
                f"AND Precedence = 1"
            )
            if not results:
                return {}

            setting = results[0]
            gpo_id = setting.GPOID
            gpo_name = gpo_id

            try:
                gpos = conn.query(f"SELECT * FROM RSOP_GPO WHERE ID = '{gpo_id}'")
                if gpos:
                    gpo_name = gpos[0].Name
            except Exception:  # pylint: disable=broad-exception-caught
                pass

            value_type_int = getattr(setting, "ValueType", None)
            value_type_str = _RSOP_VALUE_TYPES.get(
                value_type_int, f"REG_TYPE_{value_type_int}"
            )

            return {
                "key": key,
                "name": v_name,
                "data": setting.Value,
                "type": value_type_str,
                "gpo_id": gpo_id,
                "gpo_name": gpo_name,
                "precedence": setting.Precedence,
                "domain_managed": gpo_id != LOCAL_POLICY_GPO_ID,
            }

    except Exception as exc:  # pylint: disable=broad-exception-caught
        log.debug("LGPO_REG Mod: Failed to query RSoP: %s", exc)
        return {}


def set_value(
    key,
    v_name,
    v_data,
    v_type="REG_DWORD",
    policy_class="Machine",
):
    r"""
    Add a key/value pair to the registry.pol file. This bypasses the admx/adml
    style policies. This is the equivalent of setting a policy to ``Enabled``

    Args:
        key (str): The registry key path

        v_name (str): The registry value name within the key

        v_data(str): The registry value

        v_type (:obj:`str`, optional):
            The registry value type. Must be one of the following:

            - REG_BINARY
            - REG_DWORD
            - REG_EXPAND_SZ
            - REG_MULTI_SZ
            - REG_QWORD
            - REG_SZ

            Default is REG_DWORD.

        policy_class (:obj:`str`, optional):
            The registry class to write to. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy_class
        SaltInvocationError: Invalid v_type
        SaltInvocationError: v_data doesn't match v_type

    Returns:
        bool: ``True`` if successful, otherwise ``False``.

    CLI Example:

    .. code-block:: bash

        # Set REG_DWORD value (default)
        salt '*' lgpo_reg.set_value "SOFTWARE\MyKey" "MyValue" 1

        # Set REG_SZ value
        salt '*' lgpo_reg.set_value "SOFTWARE\MyKey" "MyValue" "string value" "REG_SZ"
    """
    # Verify input
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
        hive = "HKLM"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
        hive = "HKCU"
    else:
        raise SaltInvocationError("An invalid policy class was specified")

    v_type = v_type.upper()
    valid_types = [
        "REG_BINARY",
        "REG_DWORD",
        "REG_EXPAND_SZ",
        "REG_MULTI_SZ",
        "REG_QWORD",
        "REG_SZ",
    ]
    if v_type not in valid_types:
        msg = f"Invalid type: {v_type}"
        raise SaltInvocationError(msg)

    if v_type in ["REG_SZ", "REG_EXPAND_SZ"]:
        if isinstance(v_data, int):
            v_data = str(v_data)
        if not isinstance(v_data, str):
            msg = f"{v_type} data must be a string"
            raise SaltInvocationError(msg)
    elif v_type == "REG_MULTI_SZ":
        if not isinstance(v_data, list):
            msg = f"{v_type} data must be a list"
            raise SaltInvocationError(msg)
    elif v_type in ["REG_DWORD", "REG_QWORD"]:
        try:
            int(v_data)
        except (TypeError, ValueError):
            msg = f"{v_type} data must be an integer"
            raise SaltInvocationError(msg)

    pol_data = read_reg_pol(policy_class=policy_class)

    found_key, found_name = _find_value(pol_data, key, v_name)

    if found_key:
        if found_name:
            if "**del." in found_name:
                log.debug("LGPO_REG Mod: Found disabled name: %s", found_name)
                pol_data[found_key][v_name] = pol_data[found_key].pop(found_name)
                found_name = v_name
            log.debug("LGPO_REG Mod: Updating value: %s", found_name)
            pol_data[found_key][found_name] = {"data": v_data, "type": v_type}
        else:
            log.debug("LGPO_REG Mod: Setting new value: %s", found_name)
            pol_data[found_key][v_name] = {"data": v_data, "type": v_type}
    else:
        log.debug("LGPO_REG Mod: Adding new key and value: %s", found_name)
        pol_data[key] = {v_name: {"data": v_data, "type": v_type}}

    success = True
    if not write_reg_pol(pol_data, policy_class=policy_class):
        log.error("LGPO_REG Mod: Failed to write registry.pol file")
        success = False

    # We only want to modify the actual registry value if this is machine policy
    # The user policy will be applied by the user registry.pol when the user
    # logs in. Setting it here only sets it on the user running the salt minion,
    # most likely SYSTEM, which doesn't make sense here
    if policy_class == "Machine":
        if not salt.utils.win_reg.set_value(
            hive=hive,
            key=key,
            vname=v_name,
            vdata=v_data,
            vtype=v_type,
        ):
            log.error("LGPO_REG Mod: Failed to set registry entry")
            success = False

    if success and policy_class == "Machine":
        rsop = get_rsop_value(key=key, v_name=v_name)
        if rsop.get("domain_managed"):
            log.warning(
                "LGPO_REG Mod: '%s\\%s' is managed by Domain GPO '%s'. "
                "Changes may be overridden on the next Group Policy refresh.",
                key,
                v_name,
                rsop.get("gpo_name", rsop.get("gpo_id")),
            )

    return success


def disable_value(key, v_name, policy_class="machine"):
    r"""
    Mark a registry value for deletion in the registry.pol file. This bypasses
    the admx/adml style policies. This is the equivalent of setting the policy
    to ``Disabled`` in the Group Policy editor (``gpedit.msc``)

    Args:
        key (str): The registry key path

        v_name (str): The registry value name within the key

        policy_class (:obj:`str`, optional):
            The registry class to write to. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy_class
        CommandExecutionError: On failure

    Returns:
        bool: ``True`` if successful, otherwise ``False``
        None: If already disabled

    CLI Example:

    .. code-block:: bash

        # Delete a value
        salt '*' lgpo_reg.delete_value "SOFTWARE\MyKey" "MyValue"
    """
    # Verify input
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
        hive = "HKLM"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
        hive = "HKCU"
    else:
        raise SaltInvocationError("An invalid policy class was specified")

    pol_data = read_reg_pol(policy_class=policy_class)

    found_key, found_name = _find_value(pol_data, key, v_name)

    pol_modified = False
    if found_key:
        if found_name:
            if "**del." in found_name:
                log.debug("LGPO_REG Mod: Already disabled: %s", v_name)
            else:
                log.debug("LGPO_REG Mod: Disabling value name: %s", v_name)
                pol_data[found_key].pop(found_name)
                found_name = f"**del.{found_name}"
                pol_data[found_key][found_name] = {"data": " ", "type": "REG_SZ"}
                pol_modified = True
        else:
            log.debug("LGPO_REG Mod: Setting new disabled value name: %s", v_name)
            pol_data[found_key][f"**del.{v_name}"] = {
                "data": " ",
                "type": "REG_SZ",
            }
            pol_modified = True
    else:
        log.debug(
            "LGPO_REG Mod: Adding new key and disabled value name: %s", found_name
        )
        pol_data[key] = {f"**del.{v_name}": {"data": " ", "type": "REG_SZ"}}
        pol_modified = True

    success = True
    if pol_modified:
        if not write_reg_pol(pol_data, policy_class=policy_class):
            log.error("LGPO_REG Mod: Failed to write registry.pol file")
            success = False

    # We only want to modify the actual registry value if this is machine policy
    # The user policy will be applied by the user registry.pol when the user
    # logs in. Setting it here only sets it on the user running the salt minion,
    # most likely SYSTEM, which doesn't make sense here
    reg_ret = None
    if policy_class == "Machine":
        reg_ret = salt.utils.win_reg.delete_value(hive=hive, key=key, vname=v_name)
        if not reg_ret:
            if reg_ret is None:
                log.debug("LGPO_REG Mod: Registry key/value already missing")
            else:
                log.error("LGPO_REG Mod: Failed to remove registry entry")
                success = False

    # Return None only when pol was already disabled and registry was already absent
    if not pol_modified and reg_ret is None:
        return None

    if success and policy_class == "Machine":
        rsop = get_rsop_value(key=key, v_name=v_name)
        if rsop.get("domain_managed"):
            log.warning(
                "LGPO_REG Mod: '%s\\%s' is managed by Domain GPO '%s'. "
                "Changes may be overridden on the next Group Policy refresh.",
                key,
                v_name,
                rsop.get("gpo_name", rsop.get("gpo_id")),
            )

    return success


def delete_value(key, v_name, policy_class="Machine"):
    r"""
    Delete a key/value pair from the Registry.pol file. This bypasses the
    admx/adml style policies. This is the equivalent of setting the policy to
    ``Not Configured``.

    Args:
        key (str): The registry key path

        v_name (str): The registry value name within the key

        policy_class (:obj:`str`, optional):
            The registry class to write to. Can be one of the following:

            - Computer
            - Machine
            - User

            Default is ``Machine``.

    Raises:
        SaltInvocationError: Invalid policy_class
        CommandExecutionError: On failure

    Returns:
        bool: ``True`` if successful, otherwise ``False``
        None: Key/value not present

    CLI Example:

    .. code-block:: bash

        # Delete all values under a key
        salt '*' lgpo_reg.delete_value "SOFTWARE\MyKey" "MyValue"
    """

    # Verify input
    if policy_class.lower() in ["computer", "machine"]:
        policy_class = "Machine"
        hive = "HKLM"
    elif policy_class.lower() in ["user"]:
        policy_class = "User"
        hive = "HKCU"
    else:
        raise SaltInvocationError("An invalid policy class was specified")

    pol_data = read_reg_pol(policy_class=policy_class)

    found_key, found_name = _find_value(pol_data, key, v_name)

    pol_modified = False
    if found_key:
        if found_name:
            log.debug("LGPO_REG Mod: Removing value name: %s", found_name)
            pol_data[found_key].pop(found_name)
            pol_modified = True
            if len(pol_data[found_key]) == 0:
                log.debug("LGPO_REG Mod: Removing empty key: %s", found_key)
                pol_data.pop(found_key)
        else:
            log.debug("LGPO_REG Mod: Value name not found: %s", v_name)
    else:
        log.debug("LGPO_REG Mod: Key not found: %s", key)

    success = True
    if pol_modified:
        if not write_reg_pol(pol_data, policy_class=policy_class):
            log.error("LGPO_REG Mod: Failed to write registry.pol file")
            success = False

    # We only want to modify the actual registry value if this is machine policy
    # The user policy will be applied by the user registry.pol when the user
    # logs in. Setting it here only sets it on the user running the salt minion,
    # most likely SYSTEM, which doesn't make sense here
    reg_ret = None
    if policy_class == "Machine":
        reg_ret = salt.utils.win_reg.delete_value(hive=hive, key=key, vname=v_name)
        if not reg_ret:
            if reg_ret is None:
                log.debug("LGPO_REG Mod: Registry key/value already missing")
            else:
                log.error("LGPO_REG Mod: Failed to remove registry entry")
                success = False

    # Return None only when there was nothing to do in either pol or registry
    if not pol_modified and reg_ret is None:
        return None

    if success and policy_class == "Machine":
        rsop = get_rsop_value(key=key, v_name=v_name)
        if rsop.get("domain_managed"):
            log.warning(
                "LGPO_REG Mod: '%s\\%s' is managed by Domain GPO '%s'. "
                "Changes may be overridden on the next Group Policy refresh.",
                key,
                v_name,
                rsop.get("gpo_name", rsop.get("gpo_id")),
            )

    return success


# This is for testing different settings and verifying that we are writing the
# values correctly
# def test():
#     pol_info = salt.utils.win_lgpo_reg.CLASS_INFO["Machine"]
#     reg_data = salt.utils.win_lgpo_reg.read_reg_pol_file(reg_pol_path=pol_info["policy_path"])
#     print(reg_data)
#     dict_data = salt.utils.win_lgpo_reg.reg_pol_to_dict(reg_data)
#     print(salt.utils.win_lgpo_reg.dict_to_reg_pol(dict_data))
