import asyncio
import json
import os
import tempfile

from defence360agent.contracts.config import Core
from defence360agent.hooks import native as native_hooks
from defence360agent.internals.logger import EventHookLogger
from defence360agent.model.event_hook import EventHook
from defence360agent.model.instance import db
from defence360agent.utils import run, snake_case
from defence360agent.utils.trusted_path import verify_root_owned_path

event_hook_logger = EventHookLogger()


def get_hooks(event):
    # if database is not available (i.e. direct RPC call), do not try to
    # load hooks
    if db.deferred:
        return []
    hooks = EventHook.select().where(EventHook.event == event)
    return list(hooks)


def _validate_hook_path(path, native=False):
    """Return the resolved path of a hook that is safe to run as root.

    Raise ValueError otherwise. Beyond existing and being readable
    (native hooks are loaded via importlib's open()+exec_module, which
    needs R_OK only; a standard 0o644 Python file must keep working, see
    DEF-41583) or executable (subprocess hooks), the file and every
    directory above it must be root-owned and writable by root only --
    a sticky directory such as /tmp may be world-writable, other users
    cannot replace a root-owned entry there. Since DEF-56083: anyone who
    can write the file or rename a component of its path would run code
    as root. The same check runs when the hook is registered.

    Callers must execute the returned real path, not the registered
    spelling, so a symlink component cannot be repointed between this
    check and the exec.
    """
    if not os.path.isfile(path):
        raise ValueError(
            "Hook path does not exist or is not a file: {}".format(path)
        )
    if native:
        if not os.access(path, os.R_OK):
            raise ValueError("Hook path is not readable: {}".format(path))
    else:
        if not os.access(path, os.X_OK):
            raise ValueError("Hook path is not executable: {}".format(path))
    return verify_root_owned_path(path)


async def execute_hook(path, data, native=False):
    try:
        # Path validation runs filesystem syscalls (isfile/access/realpath)
        # which can block the event loop on slow/NFS storage; defer to a
        # threadpool executor. The same checks apply to native hooks
        # because native_hooks.execute_hook imports the file via importlib
        # straight in the agent's root process — a DB-sourced /tmp path
        # there is at least as dangerous as a subprocess fork.
        loop = asyncio.get_event_loop()
        real = (
            await loop.run_in_executor(None, _validate_hook_path, path, native)
            or path
        )
        if native:
            native_hooks.execute_hook(real, data)
            return 0, None
        data = json.dumps(data).encode()
        # exec the verified target, but keep the registered directory as
        # cwd: hooks that load relative files rely on it, and the
        # as-spelled chain check above proved it root-controlled
        cwd = os.path.dirname(path)
        try:
            exit_code, _, err = await run(
                [real], shell=False, input=data, cwd=cwd
            )
        except FileNotFoundError as exc:
            # 127 = shell convention for "command not found".
            return 127, repr(exc)
        except PermissionError as exc:
            # 126 = shell convention for "found but not executable".
            return 126, repr(exc)
        return exit_code, err
    except Exception as e:
        return None, repr(e)


async def execute_hooks(event, tempdir=Core.TMPDIR):
    dump = event.get("DUMP")
    params = dict(event)
    hooks = get_hooks(event.event)

    if not hooks:
        return

    with event_hook_logger(event.event, event.subtype) as event_logger:
        if dump:
            prefix = snake_case(event.__class__.__name__) + "_"
            tmp = tempfile.NamedTemporaryFile(
                mode="w+", prefix=prefix, suffix=".json", dir=tempdir
            )
            json.dump(dump, tmp)
            tmp.flush()
            os.fsync(tmp.fileno())
            params["tmp_filename"] = tmp.name

        data = {
            "event": event.event,
            "subtype": event.subtype,
            "params": params,
        }

        for hook in hooks:
            with event_logger(hook.path, native=hook.native) as hook_logger:
                hook_logger.begin()
                exit_code, err = await execute_hook(
                    hook.path, data, native=hook.native
                )
                hook_logger.finish(exit_code, err)
