
# cache, marketplace, plugin, task, creds manager aka main manager to handle these threads.

import threading
from multiprocessing import Process, Value
import time
import os
import sys
import json
import hashlib
import socket
from uuid import uuid4
from hawkSoar.manager.common_db_binding import ManagerDBInterface, ManagerListener, ManagerListenerAndResponder
from hawkSoar.common.logger import get_logger
from hawkSoar.common.workload import WorkloadStatistic
from hawkSoar.security import HybridTaskExecutor, HybridToolCatalog

from hawkSoar.lib.soar import soarAPI

from hawkSoar.schedulers.ingest import IngestScheduler
from hawkSoar.schedulers.contain import ContainScheduler
from hawkSoar.schedulers.decision import DecisionScheduler
from hawkSoar.schedulers.match import MatchScheduler
from hawkSoar.schedulers.investigate import InvestigateScheduler
from hawkSoar.schedulers.notify import NotifyScheduler
from hawkSoar.schedulers.record import RecordScheduler
from hawkSoar.schedulers.recovery import RecoveryScheduler
from hawkSoar.schedulers.utility import UtilityScheduler


class NullScheduler(object):
    def __init__(self, name="null"):
        self.name = name

    def set_playbooks(self, playbooks):
        return None

    def get_plugins(self):
        return {}

    def get_response(self):
        return None

    def get_scheduled_workload(self):
        return 0

    def check_exceptions(self):
        return False

    def shutdown(self):
        return None

    def set_task(self, id, msg):
        return None


class TaskBroker(threading.Thread):

    apiObj = None

    def __init__(self, apiObj, playbooks, ingest, contain, match, decision, investigate, notify, record, recover, utility):
        threading.Thread.__init__(self)
        self.logger = get_logger()
        self.apiObj = apiObj
        self.isRunning = True

        self.ingest_service = ingest
        self.contain_service = contain
        self.decision_service = decision
        self.match_service = match
        self.investigate_service = investigate
        self.notify_service = notify
        self.record_service = record
        self.recover_service = recover
        self.utility_service = utility
        self.playbooks = playbooks
        self.hybrid_executor = HybridTaskExecutor(
            config=getattr(apiObj, "config", {}),
            expected_group_id_getter=self._get_expected_group_id,
        )
        self.hybrid_catalog = HybridToolCatalog(config=getattr(apiObj, "config", {}))

    def set_playbooks(self, playbooks):
        self.playbooks = playbooks

    def shutdown(self):
        self.logger.debug("Shutdown received, task broker is now ending...")
        self.isRunning = False
        self.apiObj.shutdown()

    def get_step_by_id(self, playbook_id, step_id):
        for i in self.playbooks:
            if i['id'] == playbook_id:
                for w in i['workspace']:
                    if w['id'] == step_id:
                        return w

    def run(self):

        # reads incoming messages and adds to proper queue
        while self.isRunning:
            msg = self.apiObj.get_task()

            # this is an update/response from a call we have already made
            if msg:
                if msg.get('cmd') == 'hybrid_task':
                    self.handle_hybrid_task(msg)
                elif msg.get('cmd') == 'hybrid_catalog':
                    self.handle_hybrid_catalog(msg)
                elif msg.get('cmd') in ('query', 'user_query', 'octorepl_query'):
                    self.handle_query(msg)
                elif msg.get('cmd') == 'task':
                    if msg.get('route') == 'get':
                        # add to queue
                        # lookup step and determine what type it is.
                        if 'data' in msg:
                            step = self.get_step_by_id(
                                msg['data']['playbookId'], msg['data']['step'])
                            self.assign_message(step, msg)

                    elif msg.get('route') == 'publish':
                        # response that our publish succeeded
                        pass
                    else:
                        self.logger.error('Unknown message')
                        self.logger.error(msg)

            self.handle_responses()

            if not msg:
                time.sleep(0.01)

    def _get_expected_group_id(self):
        try:
            return self.apiObj.authObj.getParentGroup()
        except Exception:
            return None

    def handle_hybrid_task(self, msg):
        route = (msg.get("route") or "execute").lower()
        if route not in ("execute", "run"):
            # Ignore non-execution envelopes (for example async ack/result notifications).
            return

        task = msg.get("data", {})
        if not isinstance(task, dict):
            task = {}
        signature = msg.get("signature")
        if not signature and isinstance(task, dict):
            signature = task.get("signature")

        expected_group = self._get_expected_group_id()
        envelope_group = msg.get("group_id")
        payload_group = task.get("group_id") if isinstance(task, dict) else None

        if not expected_group:
            response_data = self._blocked_hybrid_response(
                task=task,
                error="Local node group is unavailable; refusing hybrid task.",
            )
        elif envelope_group and envelope_group != expected_group:
            response_data = self._blocked_hybrid_response(
                task=task,
                error="Envelope group_id does not match local node group.",
            )
        elif payload_group and payload_group != expected_group:
            response_data = self._blocked_hybrid_response(
                task=task,
                error="Payload group_id does not match local node group.",
            )
        elif envelope_group and payload_group and envelope_group != payload_group:
            response_data = self._blocked_hybrid_response(
                task=task,
                error="Envelope and payload group_id values do not match.",
            )
        else:
            response_data = self.hybrid_executor.process(task, signature=signature)

        # Use envelope group_id if present, otherwise fall back to local node group.
        # The backend requires group_id at top level for routing the result back.
        response_group_id = envelope_group or expected_group
        original_id = msg.get("id")
        response = {
            "id": original_id or str(uuid4()),
            "cmd": "hybrid_task",
            "route": "result",
            "group_id": response_group_id,
            "data": response_data,
        }
        self.logger.info(
            "[hybrid-task-result] Sending result id=%s action=%s status=%s group=%s original_id_present=%s",
            response["id"][:8] if response.get("id") else "none",
            task.get("action", "unknown") if task else "unknown",
            response_data.get("status", "unknown") if response_data else "unknown",
            response_group_id[:8] if response_group_id else "none",
            original_id is not None,
        )
        self.apiObj.send_response(response)
        self._submit_hybrid_artifact_if_requested(task=task, response_data=response_data)
        if (
            isinstance(task, dict)
            and task.get("action") == "register_dynamic_tool"
            and isinstance(response_data, dict)
            and response_data.get("status") == "SUCCESS"
        ):
            self._refresh_hybrid_advertisement()

    def handle_hybrid_catalog(self, msg):
        expected_group = self._get_expected_group_id()
        route = msg.get("route", "get")
        request_data = msg.get("data") if isinstance(msg.get("data"), dict) else {}
        if route == "trigger":
            trigger = request_data.get("trigger")
            case_id = request_data.get("case_id")
            include_trigger_only = bool(request_data.get("include_trigger_only", False))
            if not trigger:
                data = {
                    "catalog_version": self.hybrid_catalog.VERSION,
                    "group_id": expected_group,
                    "trigger": None,
                    "tasks": [],
                    "error": "Missing required trigger.",
                }
            else:
                data = self.hybrid_catalog.build_trigger_plan(
                    trigger=trigger,
                    group_id=expected_group,
                    case_id=case_id,
                    include_trigger_only=include_trigger_only,
                )
        else:
            data = self.hybrid_catalog.export(group_id=expected_group)

        response = {
            "id": msg.get("id", str(uuid4())),
            "cmd": "hybrid_catalog",
            "route": "result",
            "data": data,
        }
        self.apiObj.send_response(response)

    def handle_query(self, msg):
        route = (msg.get("route") or "get").lower()
        data = msg.get("data") if isinstance(msg.get("data"), dict) else {}

        if route in ("catalog", "get", "list"):
            expected_group = self._get_expected_group_id()
            payload = self.hybrid_catalog.export(group_id=expected_group)
            self.apiObj.send_response({
                "id": msg.get("id", str(uuid4())),
                "cmd": msg.get("cmd", "query"),
                "route": "result",
                "data": payload,
            })
            return

        if route in ("execute", "run"):
            task = data.get("task") if isinstance(data.get("task"), dict) else dict(data)
            expected_group = self._get_expected_group_id()
            task.setdefault("task_id", str(uuid4()))
            task.setdefault("group_id", expected_group or "")
            task.setdefault("case_id", data.get("case_id", "interactive"))
            task.setdefault("category", data.get("category", "NETWORK"))
            task.setdefault("action", data.get("action", "dns_lookup"))
            task.setdefault("parameters", data.get("parameters", {}))
            task.setdefault("requires_approval", bool(data.get("requires_approval", False)))
            task.setdefault("timeout", int(data.get("timeout", 60)))
            task.setdefault("priority", data.get("priority", "LOW"))
            signature = msg.get("signature") or task.get("signature")
            result = self.hybrid_executor.process(task, signature=signature)
            self.apiObj.send_response({
                "id": msg.get("id", str(uuid4())),
                "cmd": msg.get("cmd", "query"),
                "route": "result",
                "data": result,
            })
            return

        self.apiObj.send_response({
            "id": msg.get("id", str(uuid4())),
            "cmd": msg.get("cmd", "query"),
            "route": "result",
            "data": {
                "status": "FAILED",
                "error": "Unsupported query route. Use one of: catalog|get|list|execute|run",
            },
        })

    def _blocked_hybrid_response(self, task, error):
        group_id = ""
        task_id = ""
        if isinstance(task, dict):
            group_id = task.get("group_id", "")
            task_id = task.get("task_id", "")
        return {
            "task_id": task_id,
            "group_id": group_id,
            "status": "BLOCKED",
            "host": socket.gethostname(),
            "execution_time_ms": 0,
            "output": {},
            "raw_output_hash": hashlib.sha256(str(error).encode("utf-8")).hexdigest(),
            "error": error,
        }

    def _submit_hybrid_artifact_if_requested(self, task, response_data):
        if not isinstance(task, dict) or not isinstance(response_data, dict):
            return
        if response_data.get("status") != "SUCCESS":
            return
        params = task.get("parameters")
        if not isinstance(params, dict):
            return
        if not bool(params.get("submit_to_octorepl", False)):
            return

        output = response_data.get("output")
        if not isinstance(output, dict):
            return
        artifact = output.get("artifact")
        if not isinstance(artifact, dict):
            return

        submission = {
            "id": str(uuid4()),
            "cmd": "hybrid_artifact",
            "route": "submit",
            "group_id": response_data.get("group_id"),
            "data": {
                "task_id": response_data.get("task_id"),
                "group_id": response_data.get("group_id"),
                "case_id": task.get("case_id"),
                "artifact": artifact,
                "submission": {
                    "provider": params.get("submission_provider", "virustotal"),
                    "labels": params.get("submission_labels", []),
                },
            },
        }
        self.apiObj.send_response(submission)

    def _refresh_hybrid_advertisement(self):
        expected_group = self._get_expected_group_id()
        if not expected_group:
            return
        payload = self.hybrid_catalog.export(group_id=expected_group)
        payload["node_id"] = self.apiObj.authObj.getUUID()
        self._log_hybrid_catalog_payload(payload, context="runtime_refresh")
        self.apiObj.register_hybrid_tools(payload)
        available_tasks = []
        for tool in payload.get("tools", []):
            if tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                available_tasks.append(tool["action"])
        self.apiObj.register_profile(
            self.apiObj.build_node_profile(available_tasks=sorted(set(available_tasks)))
        )

    def _log_hybrid_catalog_payload(self, payload, context="startup"):
        try:
            tools = payload.get("tools", []) if isinstance(payload, dict) else []
            actions = []
            for tool in tools:
                if isinstance(tool, dict) and tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                    actions.append(tool.get("action"))
            actions = sorted(set(actions))
            sample = actions[:12]
            tail = actions[-12:] if len(actions) > 12 else []
            self.logger.info(
                "Hybrid catalog registration [%s]: tools=%d executable_actions=%d sample=%s tail=%s" % (
                    context,
                    len(tools),
                    len(actions),
                    sample,
                    tail,
                )
            )
        except Exception as e:
            self.logger.warning("Unable to log hybrid catalog payload (%s): %s" % (context, str(e)))

    # for step_id in response['data']['steps']:
    def assign_message(self, step, msg):
        if not step:
            self.logger.warning("Task message ignored: no matching playbook step found.")
            return

        if step['type'] == 'action':
            if step['module'].startswith('playbook.ingest'):
                # XXX: should never be called
                for id in step['config']:
                    self.ingest_service.set_task(id, msg)
            elif step['module'].startswith('playbook.contain'):
                for id in step['config']:
                    self.contain_service.set_task(id, msg)
            elif step['module'].startswith('playbook.investigate'):
                for id in step['config']:
                    self.investigate_service.set_task(
                        id, msg)
            elif step['module'].startswith('playbook.notify'):
                for id in step['config']:
                    self.notify_service.set_task(id, msg)
            elif step['module'].startswith('playbook.record'):
                for id in step['config']:
                    self.record_service.set_task(id, msg)
            elif step['module'].startswith('playbook.recover'):
                for id in step['config']:
                    self.recover_service.set_task(id, msg)
            elif msg['data']['task_type'] == 'utility':
                for id in step['config']:
                    self.utility_service.set_task(id, msg)
        elif step['type'] == 'match':
            self.match_service.set_task('match', msg)
        elif step['type'] == 'decision':
            self.decision_service.set_task('decision', msg)

    def shortcut_response(self, response):

        #print("OUTGOING ENTRY: " + json.dumps(response))
        if response and response['route'] == 'publish':
            #print("Short time pass response rewrite")
            # print(json.dumps(response))
            if isinstance(response['data']['step'], list):
                for step_id in response['data']['step']:
                    # print("Getting step for local forward, pid: %s  step: %s" %
                    #      (response['data']['playbookId'], step_id))
                    step = self.get_step_by_id(
                        response['data']['playbookId'], step_id)

                    if step['type'] == 'match' or step['type'] == 'decision':
                        response['route'] = 'get'
                        response['data']['step'] = step_id
                        self.assign_message(step, response)
                    else:
                        response['route'] = 'publish'
                        #print("PUBLISHING RESPONSE TO API 1")
                        self.apiObj.send_response(response)
            else:
                step_id = response['data']['step']
                # print("Getting step for local forward, pid: %s  step: %s" %
                #    response['data']['playbookId'], step_id))
                step = self.get_step_by_id(
                    response['data']['playbookId'], step_id)
                if step['type'] == 'match' or step['type'] == 'decision':
                    response['route'] = 'get'
                    response['data']['step'] = step_id
                    self.assign_message(step, response)
                else:
                    response['route'] = 'publish'
                    #print("PUBLISHING RESPONSE TO API 2")
                    self.apiObj.send_response(response)
        elif response:
            #print("PUBLISHING RESPONSE TO API")
            self.apiObj.send_response(response)

    def handle_responses(self):

        response = self.match_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.decision_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.ingest_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.contain_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.investigate_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.notify_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.record_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.recover_service.get_response()
        if response:
            self.shortcut_response(response)

        response = self.utility_service.get_response()
        if response:
            self.shortcut_response(response)


class Manager(object):

    apiObj = None

    def __init__(self,
                 config=None
                 ):
        self.logger = get_logger()
        self.config = config

        if self.config['manager_poll_delay']:
            self.poll_delay = self.config['manager_poll_delay']
        else:
            self.poll_delay = 1

        self.stop_condition = Value('i', 0)
        self.process_list = []
        try:
            self.hybrid_advertise_interval = int(self.config.get("hybrid_advertise_interval", 60))
        except Exception:
            self.hybrid_advertise_interval = 60
        if self.hybrid_advertise_interval < 0:
            self.hybrid_advertise_interval = 0

        self.apiObj = soarAPI(self.config)

    def shutdown(self):
        if self.isRunning:
            self.isRunning = False
            self.stop_condition.value = 1

        try:
            if self.apiObj:
                self.apiObj.shutdown()
                if self.apiObj.is_alive() and threading.current_thread() is not self.apiObj:
                    self.apiObj.join(timeout=5)
        except Exception:
            pass

        self.isRunning = False

    def _log_hybrid_catalog_payload(self, payload, context="startup"):
        try:
            tools = payload.get("tools", []) if isinstance(payload, dict) else []
            actions = []
            for tool in tools:
                if isinstance(tool, dict) and tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                    actions.append(tool.get("action"))
            actions = sorted(set(actions))
            sample = actions[:12]
            tail = actions[-12:] if len(actions) > 12 else []
            self.logger.info(
                "Hybrid catalog registration [%s]: tools=%d executable_actions=%d sample=%s tail=%s" % (
                    context,
                    len(tools),
                    len(actions),
                    sample,
                    tail,
                )
            )
        except Exception as e:
            self.logger.warning("Unable to log hybrid catalog payload (%s): %s" % (context, str(e)))

    def _advertise_hybrid_runtime(self, context="periodic"):
        try:
            hybrid_catalog = HybridToolCatalog(self.config)
            hybrid_payload = hybrid_catalog.export(group_id=self.apiObj.authObj.getParentGroup())
            # Include explicit node_id so the backend can identify the correct Nodes
            # record even when authenticated via API token (where session.userProfile.id
            # is the *user* id, not the node id).
            hybrid_payload["node_id"] = self.apiObj.authObj.getUUID()
            self._log_hybrid_catalog_payload(hybrid_payload, context=context)
            self.apiObj.register_hybrid_tools(hybrid_payload)
            available_tasks = []
            for tool in hybrid_payload.get("tools", []):
                if tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                    available_tasks.append(tool["action"])
            self.apiObj.register_profile(
                self.apiObj.build_node_profile(available_tasks=sorted(set(available_tasks)))
            )
            return True
        except Exception as e:
            self.logger.warning("Hybrid runtime advertise failed (%s): %s" % (context, str(e)))
            return False

    def startup(self):

        self.logger.info('SOAR Manager Starting...')
        self.isRunning = True
        self.logger.debug(" In manager startup(); Parent PID: %s, My Pid: %s" % (
            os.getppid(), os.getpid()))

        while self.isRunning:
            try:
                if not self.apiObj.login():
                    raise Exception("Unable to login and register with host.")
                else:
                    break

            except Exception as e:
                if str(e) == 'node pending approval.':
                    self.logger.info("Manager: pending approval, sleeping...")
                    time.sleep(10)
                else:
                    self.logger.error(
                        "Manager: unable to login, please check your configuration and try again")
                    raise e

        if not self.isRunning:
            if not self.config['debug']:
                # will trigger shutdown above
                _pid = os.getpid()
                _pgid = os.getpgid(_pid)
                os.killpg(_pgid, 9)
                return

        if bool(self.config.get("listener_only", False)):
            self.logger.info("Manager: listener_only mode enabled; skipping playbook pipeline.")
            self.playbooks = []
            self.ingest_service = NullScheduler("ingest")
            self.contain_service = NullScheduler("contain")
            self.match_service = NullScheduler("match")
            self.decision_service = NullScheduler("decision")
            self.investigate_service = NullScheduler("investigate")
            self.notify_service = NullScheduler("notify")
            self.record_service = NullScheduler("record")
            self.recover_service = NullScheduler("recover")
            self.utility_service = NullScheduler("utility")

            self.start_listener()

            try:
                hybrid_catalog = HybridToolCatalog(self.config)
                appList = [{
                    "id": "hybrid-task-api",
                    "name": "Hybrid Task API",
                    "type": "utility",
                    "version": hybrid_catalog.VERSION,
                    "module": "playbook.hybrid.task",
                    "capabilities": {
                        "catalog_cmd": "hybrid_catalog",
                        "task_cmd": "hybrid_task",
                        "query_cmds": ["query", "user_query", "octorepl_query"],
                        "executable_tools": hybrid_catalog.get_executable_count()
                    }
                }]
                self.apiObj.register_apps(appList)
                hybrid_payload = hybrid_catalog.export(group_id=self.apiObj.authObj.getParentGroup())
                hybrid_payload["node_id"] = self.apiObj.authObj.getUUID()
                self._log_hybrid_catalog_payload(hybrid_payload, context="listener_only_startup")
                self.apiObj.register_hybrid_tools(hybrid_payload)
                available_tasks = []
                for tool in hybrid_payload.get("tools", []):
                    if tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                        available_tasks.append(tool["action"])
                self.apiObj.register_profile(
                    self.apiObj.build_node_profile(available_tasks=sorted(set(available_tasks)))
                )
            except Exception as e:
                self.logger.exception(e)
                raise e

            self.taskBroker = TaskBroker(self.apiObj, self.playbooks, self.ingest_service, self.contain_service, self.match_service, self.decision_service, self.investigate_service,
                                         self.notify_service, self.record_service, self.recover_service, self.utility_service)
            self.taskBroker.start()
            next_hybrid_advertise = time.time() + self.hybrid_advertise_interval if self.hybrid_advertise_interval > 0 else None

            try:
                while self.isRunning:
                    if next_hybrid_advertise and time.time() >= next_hybrid_advertise:
                        self._advertise_hybrid_runtime(context="listener_only_periodic")
                        next_hybrid_advertise = time.time() + self.hybrid_advertise_interval
                    time.sleep(self.poll_delay)
            except KeyboardInterrupt:
                self.logger.info("Manager: keyboard interrupt received; stopping listener-only mode.")
                self.isRunning = False

            self.logger.info('Shutting down listener-only mode...')
            self.taskBroker.shutdown()
            self.taskBroker.join()
            try:
                if self.apiObj and self.apiObj.is_alive():
                    self.apiObj.join(timeout=5)
            except Exception:
                pass
            self.logger.info('SOAR Manager Stopped')
            return

        try:
            self.playbooks = self.apiObj.get_playbooks()
        except Exception as e:
            self.logger.error(
                "Manager: failed to fetch playbooks, unable to continue...")
            raise e

        # Assign playbooks to schedulers

        self.ingest_service = IngestScheduler(self.config)
        self.ingest_service.set_playbooks(self.playbooks)

        self.contain_service = ContainScheduler(self.config)
        self.contain_service.set_playbooks(self.playbooks)

        self.match_service = MatchScheduler(self.config)
        self.match_service.set_playbooks(self.playbooks)

        self.decision_service = DecisionScheduler(self.config)
        self.decision_service.set_playbooks(self.playbooks)

        self.investigate_service = InvestigateScheduler(self.config)
        self.investigate_service.set_playbooks(self.playbooks)

        self.notify_service = NotifyScheduler(self.config)
        self.notify_service.set_playbooks(self.playbooks)

        self.record_service = RecordScheduler(self.config)
        self.record_service.set_playbooks(self.playbooks)

        self.recover_service = RecoveryScheduler(self.config)
        self.recover_service.set_playbooks(self.playbooks)

        self.utility_service = UtilityScheduler(self.config)
        self.utility_service.set_playbooks(self.playbooks)

        try:
            self.ingest_publisher = PluginPublisher(
                self.config, self.ingest_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load ingestion plugins: %s" % e)

        try:
            self.match_publisher = PluginPublisher(
                self.config, self.match_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load match plugins: %s" % e)

        try:
            self.decision_publisher = PluginPublisher(
                self.config, self.decision_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load decision plugins: %s" % e)

        try:
            self.investigate_publisher = PluginPublisher(
                config=self.config, service=self.investigate_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load investigation plugins: %s" % e)

        try:
            self.notify_publisher = PluginPublisher(
                config=self.config, service=self.notify_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load notification plugins: %s" % e)

        try:
            self.record_publisher = PluginPublisher(
                config=self.config, service=self.record_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load recording plugins: %s" % e)

        try:
            self.recover_publisher = PluginPublisher(
                config=self.config, service=self.recover_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load recovery plugins: %s" % e)

        try:
            self.utility_publisher = PluginPublisher(
                config=self.config, service=self.utility_service)
        except Exception as e:
            self.logger.error(
                "Manager: unable to load utility plugins: %s" % e)

        # all plugins registered and listening for work.
        # connect to websocket

        # starts websocket listener
        self.start_listener()

        # provide a list of apps
        try:
            appList = []
            # build app list manually
            objs = {
                'ingest': self.ingest_service,
                'contain': self.contain_service,
                'decision': self.decision_service,
                'match': self.match_service,
                'investigate': self.investigate_service,
                'notify': self.notify_service,
                'record': self.record_service,
                'recover': self.recover_service,
                'utility': self.utility_service
            }
            for key in objs:
                obj = objs[key]
                for (key2, plugin) in obj.get_plugins().items():
                    if not plugin.ENABLED:
                        continue
                    app = {
                        "id": plugin.ID,
                        "name": plugin.NAME,
                        "type": plugin.TYPE,
                        "version": plugin.VERSION,
                        "module": plugin.MODULE
                    }
                    appList.append(app)
            hybrid_catalog = HybridToolCatalog(self.config)
            appList.append({
                "id": "hybrid-task-api",
                "name": "Hybrid Task API",
                "type": "utility",
                "version": hybrid_catalog.VERSION,
                "module": "playbook.hybrid.task",
                "capabilities": {
                    "catalog_cmd": "hybrid_catalog",
                    "task_cmd": "hybrid_task",
                    "executable_tools": hybrid_catalog.get_executable_count()
                }
            })
            self.apiObj.register_apps(appList)
            hybrid_payload = hybrid_catalog.export(group_id=self.apiObj.authObj.getParentGroup())
            hybrid_payload["node_id"] = self.apiObj.authObj.getUUID()
            self._log_hybrid_catalog_payload(hybrid_payload, context="full_pipeline_startup")
            self.apiObj.register_hybrid_tools(hybrid_payload)
            available_tasks = []
            for tool in hybrid_payload.get("tools", []):
                if tool.get("execution_mode") == "executable" and isinstance(tool.get("action"), str):
                    available_tasks.append(tool["action"])
            self.apiObj.register_profile(
                self.apiObj.build_node_profile(available_tasks=sorted(set(available_tasks)))
            )
        except Exception as e:
            self.logger.exception(e)
            raise e

        # for each plugin that's unique and present, attempt to call its start function <-- not needed, since __init__ is called

        # for each playbook plugin step we own, register with api to listen for events
        # how do we know if we own it?
        #   - is step a local or cloud step
        #   - are we a cloud or local node?

        # for each event matching the queue we are listening to, add that event to set_tasset_task of the scheduler based on task type.
        self.taskBroker = TaskBroker(self.apiObj, self.playbooks, self.ingest_service, self.contain_service, self.match_service, self.decision_service, self.investigate_service,
                                     self.notify_service, self.record_service, self.recover_service, self.utility_service)

        self.taskBroker.start()
        next_hybrid_advertise = time.time() + self.hybrid_advertise_interval if self.hybrid_advertise_interval > 0 else None

        #   - inside here, the scheduler will determine which plugin or set of plugins will get the task
        # a communication class needs to run independently and handle the back & forth

        self.work_load_stat = WorkloadStatistic(
            config=self.config, api=self.apiObj)

        try:
            while self.isRunning:
                if next_hybrid_advertise and time.time() >= next_hybrid_advertise:
                    self._advertise_hybrid_runtime(context="full_pipeline_periodic")
                    next_hybrid_advertise = time.time() + self.hybrid_advertise_interval
                self.work_load_stat.update(ingest_workload=self.ingest_service.get_scheduled_workload(),
                                           contain_workload=self.contain_service.get_scheduled_workload(), match_workload=self.match_service.get_scheduled_workload(), decision_workload=self.decision_service.get_scheduled_workload(),
                                           investigate_workload=self.investigate_service.get_scheduled_workload(), notify_workload=self.notify_service.get_scheduled_workload(),
                                           record_workload=self.record_service.get_scheduled_workload(), recover_workload=self.recover_service.get_scheduled_workload(),
                                           utility_workload=self.utility_service.get_scheduled_workload()
                                           )
                if any((self.ingest_service.check_exceptions(), self.contain_service.check_exceptions(),
                        self.match_service.check_exceptions(), self.decision_service.check_exceptions(),
                        self.investigate_service.check_exceptions(), self.notify_service.check_exceptions(),
                        self.record_service.check_exceptions(),
                        self.recover_service.check_exceptions(), self.utility_service.check_exceptions()
                        )):
                    self.logger.error("Exceptions found, stopping")
                    break

                time.sleep(self.poll_delay)
        except KeyboardInterrupt:
            self.logger.info("Manager: keyboard interrupt received; stopping pipeline.")
            self.isRunning = False

            """
            # this is for testing startup and shutdown behavior, to ensure its healthy
            if self.config['debug']:
                self.logger.info('Halting, debug mode selected')
                break
            """

        self.logger.info('Shutting down pipeline...')
        self.logger.debug('Shutting down task broker...')
        self.taskBroker.shutdown()
        self.taskBroker.join()
        self.logger.debug('Shutting down workload...')
        self.work_load_stat.shutdown()
        self.logger.debug('Shutting down ingest...')
        self.ingest_service.shutdown()
        self.logger.debug('Shutting down contain...')
        self.contain_service.shutdown()
        self.logger.debug('Shutting down match...')
        self.match_service.shutdown()
        self.logger.debug('Shutting down decision...')
        self.decision_service.shutdown()
        self.logger.debug('Shutting down investigation...')
        self.investigate_service.shutdown()
        self.logger.debug('Shutting down notify...')
        self.notify_service.shutdown()
        self.logger.debug('Shutting down record...')
        self.record_service.shutdown()
        self.logger.debug('Shutting down recovery...')
        self.recover_service.shutdown()
        self.logger.debug('Shutting down utility...')
        self.utility_service.shutdown()

        self.logger.info('Waiting for processes to complete...')
        for item in self.process_list:
            item.shutdown()
            item.join()
        try:
            if self.apiObj and self.apiObj.is_alive():
                self.apiObj.join(timeout=5)
        except Exception:
            pass
        self.logger.info('SOAR Manager Stopped')

        if not self.config['debug']:
            # will trigger shutdown above
            _pid = os.getpid()
            _pgid = os.getpgid(_pid)
            os.killpg(_pgid, 9)

        sys.exit(0)

    def start_listener(self):

        # start our websocket listener
        self.apiObj.start()  # run_loop, self.apiObj.set_task)
        """
        self._start_listener(self.apiObj.run_loop, self.apiObj.set_task)
        """

    def _start_listener(self, worker, do_after_function):
        process = Process(target=worker, args=(
            do_after_function,))
        process.start()
        self.process_list.append(process)

    def set_response(self, response):
        self.apiObj.set_response(response)


class PluginPublisher(ManagerDBInterface):

    def __init__(self, config=None, service=None):
        super(ManagerDBInterface, self).__init__(config)
        self.publish_available_plugins(service)

        # XXX: not sure why this is here
        # self.client.close()

    def publish_available_plugins(self, service):
        """ Overwrite the data in DB with plugin info. """
        available_plugin_dictionary = service.get_plugin_dict()


class ManagerBackendIngestTask(ManagerListener):

    CONNECTION_TYPE = 'ingest_task'


if __name__ == '__main__':
    pass
