
import time
import random
import websocket
import ssl
import json
import threading
from multiprocessing import Queue
from uuid import uuid4
import socket
import psutil

from .auth import Authentication
from hawkSoar.common.logger import get_logger

# for connecting to soar API


class soarAPI(threading.Thread):
    """Communicating with the SOAR API. """
    config = {}
    authObj = None
    uuid = None
    isRunning = False
    ws = None
    incoming_queue = None
    outgoing_queue = None

    def __init__(self, config):
        threading.Thread.__init__(self)
        self.config = config
        self.logging = get_logger()
        self.incoming_queue = Queue()
        self.outgoing_queue = Queue()
        self._sender_thread = None

        try:
            self.authObj = Authentication(random.choice(
                self.config['server']), self.config['clientId'], self.config['clientSecret'])
            if not self.config['insecure']:
                self.authObj.setVerify(True)
            else:
                self.authObj.setVerify(False)

        except Exception as e:
            self.logging.error(str(e))
            raise e

        try:
            if config['client_id_file']:
                self.authObj.setUUIDFile(config['client_id_file'])
        except Exception as e:
            self.logging.error(str(e))
            raise e

        try:
            if config['private_key']:
                self.authObj.setPrivateKey(config['private_key'])
        except Exception as e:
            self.logging.error(str(e))
            raise e

        try:
            if config['public_key']:
                self.authObj.setPublicKey(config['public_key'])
        except Exception as e:
            self.logging.error(str(e))
            raise e

    def get_task(self):
        try:
            return self.incoming_queue.get(False, 1)
        except:
            return None

    def set_task(self, task):
        return self.incoming_queue.put(task)

    def register_apps(self, appList):
        response = {
            "id": str(uuid4()),
            "cmd": "nodes",
            "route": "registerApps",
            "data": appList
        }
        self.send_response(json.dumps(response))

    def register_hybrid_tools(self, tool_catalog):
        response = {
            "id": str(uuid4()),
            "cmd": "nodes",
            "route": "registerHybridTools",
            "data": tool_catalog,
        }
        self.send_response(json.dumps(response))

    def register_profile(self, profile):
        response = {
            "id": str(uuid4()),
            "cmd": "nodes",
            "route": "registerProfile",
            "data": profile,
        }
        self.send_response(json.dumps(response))

    def subscribe_services(self, group_id=None):
        """Subscribe to service-only channel for hybrid_catalog messages.

        This must be called after WebSocket connection to receive hybrid_catalog
        result/submit messages on the {group}-services Redis channel, which is
        only subscribed to by service sessions (not human SOC users).

        The server will subscribe this session to all groups in the authenticated
        session; group_id parameter is optional for logging purposes.
        """
        if not group_id:
            group_id = self.authObj.getParentGroup() if self.authObj else None
        response = {
            "id": str(uuid4()),
            "cmd": "hybrid_catalog",
            "route": "subscribeServices",
            "group_id": group_id,
            "data": {},
        }
        self.logging.info("[hybrid] Subscribing to service channels for group: %s", group_id)
        self.send_response(json.dumps(response))

    def build_node_profile(self, available_tasks=None):
        available_tasks = available_tasks or []
        hostname = socket.gethostname()
        interfaces = []
        primary_ip = None
        primary_netmask = None

        try:
            for if_name, addrs in psutil.net_if_addrs().items():
                for addr in addrs:
                    if getattr(addr, "family", None) != socket.AF_INET:
                        continue
                    if addr.address.startswith("127."):
                        continue
                    entry = {
                        "name": if_name,
                        "ip": addr.address,
                        "netmask": addr.netmask,
                    }
                    interfaces.append(entry)
                    if primary_ip is None:
                        primary_ip = addr.address
                        primary_netmask = addr.netmask
        except Exception:
            pass

        return {
            "profile_id": self.uuid or str(uuid4()),
            "hostname": hostname,
            "ip": primary_ip,
            "netmask": primary_netmask,
            "interfaces": interfaces,
            "available_tasks": available_tasks,
        }

    def get_playbooks(self):

        url = random.choice(
            self.config['server']) + "/api/playbook/" + self.authObj.getParentGroup()
        try:
            response = self.authObj.Session.get(
                url, verify=self.authObj.verify)
        except Exception as e:
            self.logging.error("Failed fetching playbooks at url: %s" % url)
            self.logging.exception(e)
            raise e

        if response.status_code != 200:
            raise Exception("Status response not valid (%d): %s" %
                            (response.status_code, response.content))

        return response.json()

    def login(self):

        try:
            access_token = self.config.get('access_token') or self.config.get('access_key')
            secret_key = self.config.get('secret_key') or self.config.get('secret')

            if not access_token or not secret_key:
                raise Exception(
                    "Service-account authentication requires access_token and secret_key in config.")

            self.authObj.loginApiUser(
                access_token,
                secret_key,
            )
            self.logging.info(
                "Authenticated using service-account token pair; websocket session established.")

            self.uuid = self.authObj.getUUID()

            return True

        except Exception as e:
            self.logging.error(str(e))
            raise e

    def run(self, **args):
        self.run_loop(self.set_task)

    def run_loop(self, callback):
        # websocket
        if not self.authObj.Session:
            raise Exception("No valid authenticated session found, unable to continue.")

        self.isRunning = True

        def on_message(ws, message):
            try:
                data = json.loads(message)
            except Exception as e:
                print("Failed to parse json: %s" % message)
                return

            callback(data)

        def on_error(ws, error):
            self.logging.error("Websocket error detected: %s" % str(error))

        def on_close(ws, close_status_code=None, close_msg=None):
            self.logging.error("Websocket has closed. status=%s msg=%s" %
                               (close_status_code, close_msg))

        def on_open(ws):
            self.logging.info("WebSocket connected")
            # Subscribe to service-only channel for hybrid_catalog messages
            # This must be called after WebSocket connection to receive hybrid results
            try:
                self.subscribe_services()
            except Exception as e:
                self.logging.warning("[hybrid] Service channel subscription on connect failed: %s" % e)

            def run(*args):
                while self.isRunning:
                    # send any messages in our queue
                    try:
                        msg = self.outgoing_queue.get(False, 1)
                    except:
                        continue

                    if not msg:
                        continue

                    if type(msg) != str:
                        self.send_response(json.dumps(msg))
                    else:
                        self.send_response(msg)

                if ws:
                    try:
                        ws.close()
                    except Exception:
                        pass
                self.logging.info(
                    "Websocket communication thread terminating.")
            if self._sender_thread and self._sender_thread.is_alive():
                return
            self._sender_thread = threading.Thread(
                target=run,
                name="soarapi-ws-sender",
                daemon=True,
            )
            self._sender_thread.start()

        if self.config['debug']:
            websocket.enableTrace(True)
        ws_targets = self.config.get('websocket_server') or self.config['server']
        ws_target = random.choice(ws_targets).strip()
        if ws_target.endswith("/"):
            ws_target = ws_target[:-1]

        if ws_target.startswith("ws://") or ws_target.startswith("wss://"):
            connStr = ws_target if ws_target.endswith("/websocket") else ("%s/websocket" % ws_target)
        else:
            connStr = ws_target if ws_target.endswith("/websocket") else ("%s/websocket" % ws_target)
            if connStr.startswith("https://"):
                connStr = "wss://" + connStr[len("https://"):]
            elif connStr.startswith("http://"):
                connStr = "ws://" + connStr[len("http://"):]
        cookies = self.authObj.Session.cookies.get_dict()
        cookies_str = ""
        for k in cookies:
            cookies_str += "%s=%s; " % (k, cookies[k])
        self.logging.debug("Websocket connection string: %s (%s)" %
                           (connStr, cookies_str))

        websocket.enableTrace(False)
        self.ws = websocket.WebSocketApp(connStr,
                                         on_message=on_message,
                                         on_error=on_error,
                                         on_close=on_close,
                                         cookie=cookies_str)
        self.ws.on_open = on_open

        while self.isRunning:  # enables reconnects
            try:
                if self.config['insecure']:
                    self.ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
                else:
                    self.ws.run_forever()
            except Exception as e:
                self.logging.error(
                    'soarAPI: websocket error occurred, reconnecting: %s' % e)
                time.sleep(1)

    def send_response(self, response):
        if self.ws:
            while self.isRunning:
                try:
                    if type(response) == str:
                        self.ws.send(response)
                    else:
                        if not 'id' in response:
                            response['id'] = str(uuid4())
                        self.ws.send(json.dumps(response))
                    # Log successful send for hybrid_task results (diagnostic)
                    if isinstance(response, dict) and response.get('cmd') == 'hybrid_task' and response.get('route') == 'result':
                        self.logging.info(
                            "[hybrid-result-sent] id=%s action=%s status=%s",
                            response.get('id'),
                            (response.get('data') or {}).get('action', 'unknown'),
                            (response.get('data') or {}).get('status', 'unknown')
                        )
                    break
                except Exception as e:
                    self.logging.warning("[hybrid-result-send-error] Exception during send: %s", e)
                    time.sleep(1)
        else:
            self.outgoing_queue.put(response)
        return True

    def set_pause(self):
        """Tells SOAR API that we are at capacity"""

        # not implement nice to have
        pass

    def set_resume(self):
        """Tells SOAR API that we can accept work again"""

        # not implement nice to have
        pass

    def close(self):
        self.authObj.finish()

    def shutdown(self):
        print("soar API shutdown...")
        self.isRunning = False
        if self.ws:
            self.ws.close()
        if self._sender_thread and self._sender_thread.is_alive():
            self._sender_thread.join(timeout=5)
