import asyncio
import json
import logging

from defence360agent.contracts.config import HookEvents
from defence360agent.contracts.hooks import HooksConfig
from defence360agent.contracts.license import LicenseCLN
from defence360agent.hooks.execute import _validate_hook_path
from defence360agent.model.event_hook import EventHook
from defence360agent.rpc_tools import ValidationError
from defence360agent.rpc_tools.delegated_admin import current_delegated_admin
from defence360agent.rpc_tools.lookup import RootEndpoints, bind
from defence360agent.subsys import notifier
from defence360agent.utils import dict_deep_update
from defence360agent.utils.trusted_path import verify_root_owned_path

logger = logging.getLogger(__name__)


def _script_lists(data):
    rules = data.get("rules") if isinstance(data, dict) else None
    if not isinstance(rules, dict):
        return
    for rule in rules.values():
        script = rule.get("SCRIPT") if isinstance(rule, dict) else None
        if isinstance(script, dict) and isinstance(
            script.get("scripts"), list
        ):
            yield script["scripts"]


async def _reject_untrusted_scripts(data):
    """DEF-56083: a delegated DirectAdmin admin may only point
    SCRIPT.scripts (run as root by imunify-notifier) at files root put
    there; a root caller keeps the previous, unrestricted behaviour.

    The patch is merged into the stored config, so the whole merged
    result is checked -- a script the caller merely leaves out of the
    form still counts -- and the entries the caller sent are replaced by
    their resolved paths, so what the notifier re-reads later is exactly
    what was verified."""
    admin = current_delegated_admin()
    if admin is None:
        return
    loop = asyncio.get_event_loop()

    async def verify(path):
        try:
            return await loop.run_in_executor(
                None, verify_root_owned_path, str(path)
            )
        except ValueError as e:
            logger.warning(
                "notifications-config patch from delegated DirectAdmin admin"
                " (pid=%s, SUDO_USER=%s) rejected: %s",
                admin.pid,
                admin.sudo_user,
                e,
            )
            raise ValidationError(
                "Notification script {} was rejected: {}. Scripts must be"
                " root-owned files in root-owned directories.".format(path, e)
            )

    effective = HooksConfig().get()
    try:
        dict_deep_update(effective, json.loads(json.dumps(data)))
    except (AttributeError, TypeError):
        # a body the notifications schema is about to reject anyway;
        # HooksConfig.update raises the proper validation error for it
        effective = data
    for scripts in _script_lists(effective):
        for path in scripts:
            await verify(path)
    for scripts in _script_lists(data):
        scripts[:] = [await verify(path) for path in scripts]


class HooksEndpoints(RootEndpoints):
    def _check_event(self, event, extra=None):
        if event not in HookEvents.EVENTS and event != extra:
            raise ValidationError(
                '"{}" is not valid event for hook'.format(event)
            )

    async def _check_path(self, path, native):
        # The same check the runner applies at event time: a hook that
        # would be refused later must not be registered at all.
        loop = asyncio.get_event_loop()
        try:
            await loop.run_in_executor(None, _validate_hook_path, path, native)
        except ValueError as e:
            raise ValidationError(str(e))

    @bind("hook", "add")
    async def hook_add(self, event, path):
        self._check_event(event)
        await self._check_path(path, native=False)
        result = EventHook.add_hook(event=event, path=path)
        if not result:
            raise ValidationError(
                'Unable to add hook "{} {}"'.format(event, path)
            )
        result["status"] = "registered"
        return {"items": result}

    @bind("hook", "delete")
    async def hook_delete(self, event, path):
        self._check_event(event)
        result = EventHook.delete_hook(event=event, path=path)
        if not result:
            raise ValidationError(
                'Unable to delete hook "{} {}"'.format(event, path)
            )
        result["status"] = "unregistered"
        return {"items": result}

    @bind("hook", "list")
    async def hook_list(self, event):
        self._check_event(event, "all")
        result = EventHook.list_events(event)
        return {"items": result}

    @bind("hook", "add-native")
    async def hook_add_native(self, event, path):
        self._check_event(event)
        await self._check_path(path, native=True)
        result = EventHook.add_hook(event=event, path=path, native=True)
        if not result:
            raise ValidationError(
                'Unable to add native hook "{} {}"'.format(event, path)
            )
        result["status"] = "registered"
        return {"items": result}

    @bind("notifications-config", "show")
    async def show(self):
        return {"items": HooksConfig().get()}

    @bind("notifications-config", "update")
    async def update(self, items=None, data=None):
        if LicenseCLN.is_demo():
            raise ValidationError("This action is not allowed in demo version")
        if items:
            data = items[0]
        new_data = json.loads(data)
        HooksConfig().update(new_data)
        await notifier.config_updated()
        return await self.show()

    @bind("notifications-config", "patch")
    async def update_ui(self, data=None):
        if LicenseCLN.is_demo():
            raise ValidationError("This action is not allowed in demo version")
        await _reject_untrusted_scripts(data)
        HooksConfig().update(data)
        await notifier.config_updated()
        return await self.show()
