from matrix_client.api import MatrixRequestError
from neb import NebError
from neb.plugins import CommandNotFoundError
from neb.webhook import NebHookServer

import json
import logging as log
import pprint


class Engine(object):
    """Orchestrates plugins and the matrix API/endpoints."""
    PREFIX = "!"

    def __init__(self, matrix_api, config):
        self.plugin_cls = {}
        self.plugins = {}
        self.config = config
        self.matrix = matrix_api
        self.sync_token = None  # set later by initial sync

    def setup(self):
        self.webhook = NebHookServer(8500)
        self.webhook.daemon = True
        self.webhook.start()

        # init the plugins
        for cls_name in self.plugin_cls:
            self.plugins[cls_name] = self.plugin_cls[cls_name](
                self.matrix,
                self.config,
                self.webhook
            )

        sync = self.matrix.sync(timeout_ms=30000, since=self.sync_token)
        self.parse_sync(sync, initial_sync=True)
        log.debug("Notifying plugins of initial sync results")
        for plugin_name in self.plugins:
            plugin = self.plugins[plugin_name]
            plugin.on_sync(sync)

            # see if this plugin needs a webhook
            if plugin.get_webhook_key():
                self.webhook.set_plugin(plugin.get_webhook_key(), plugin)

    def _help(self):
        return (
            "Installed plugins: %s - Type '%shelp <plugin_name>' for more." %
            (self.plugins.keys(), Engine.PREFIX)
        )

    def add_plugin(self, plugin):
        log.debug("add_plugin %s", plugin)
        if not plugin.name:
            raise NebError("No name for plugin %s" % plugin)

        self.plugin_cls[plugin.name] = plugin

    def parse_membership(self, event):
        log.info("Parsing membership: %s", event)
        if (event["state_key"] == self.config.user_id
                and event["content"]["membership"] == "invite"):
            user_id = event["sender"]
            if user_id in self.config.admins:
                self.matrix.join_room(event["room_id"])
            else:
                log.info(
                    "Refusing invite, %s not in admin list. Event: %s",
                    user_id, event
                )

    def parse_msg(self, event):
        body = event["content"]["body"]
        if (event["sender"] == self.config.user_id or
                event["content"]["msgtype"] == "m.notice"):
            return
        if body.startswith(Engine.PREFIX):
            room = event["room_id"]  # room_id added by us
            try:
                segments = body.split()
                cmd = segments[0][1:]
                if self.config.case_insensitive:
                    cmd = cmd.lower()

                if cmd == "help":
                    if len(segments) == 2 and segments[1] in self.plugins:
                        # return help on a plugin
                        self.matrix.send_message(
                            room,
                            self.plugins[segments[1]].__doc__,
                            msgtype="m.notice"
                        )
                    else:
                        # return generic help
                        self.matrix.send_message(room, self._help(), msgtype="m.notice")
                elif cmd in self.plugins:
                    plugin = self.plugins[cmd]
                    responses = None

                    try:
                        responses = plugin.run(
                            event,
                            unicode(" ".join(body.split()[1:]).encode("utf8"))
                        )
                    except CommandNotFoundError as e:
                        self.matrix.send_message(
                            room,
                            str(e),
                            msgtype="m.notice"
                        )
                    except MatrixRequestError as ex:
                        self.matrix.send_message(
                            room,
                            "Problem making request: (%s) %s" % (ex.code, ex.content),
                            msgtype="m.notice"
                        )

                    if responses:
                        log.debug("[Plugin-%s] Response => %s", cmd, responses)
                        if type(responses) == list:
                            for res in responses:
                                if type(res) in [str, unicode]:
                                    self.matrix.send_message(
                                        room,
                                        res,
                                        msgtype="m.notice"
                                    )
                                else:
                                    self.matrix.send_message_event(
                                        room, "m.room.message", res
                                    )
                        elif type(responses) in [str, unicode]:
                            self.matrix.send_message(
                                room,
                                responses,
                                msgtype="m.notice"
                            )
                        else:
                            self.matrix.send_message_event(
                                room, "m.room.message", responses
                            )
            except NebError as ne:
                self.matrix.send_message(room, ne.as_str(), msgtype="m.notice")
            except Exception as e:
                log.exception(e)
                self.matrix.send_message(
                    room,
                    "Fatal error when processing command.",
                    msgtype="m.notice"
                )
        else:
            try:
                for p in self.plugins:
                    self.plugins[p].on_msg(event, body)
            except Exception as e:
                log.exception(e)

    def event_proc(self, event):
        etype = event["type"]
        switch = {
            "m.room.member": self.parse_membership,
            "m.room.message": self.parse_msg
        }
        try:
            switch[etype](event)
        except KeyError:
            try:
                for p in self.plugins:
                    self.plugins[p].on_event(event, etype)
            except Exception as e:
                log.exception(e)
        except Exception as e:
            log.error("Couldn't process event: %s", e)

    def event_loop(self):
        while True:
            j = self.matrix.sync(timeout_ms=30000, since=self.sync_token)
            self.parse_sync(j)

    def parse_sync(self, sync_result, initial_sync=False):
        self.sync_token = sync_result["next_batch"]  # for when we start syncing

        # check invited rooms
        rooms = sync_result["rooms"]["invite"]
        for room_id in rooms:
            events = rooms[room_id]["invite_state"]["events"]
            self.process_events(events, room_id)

        # return early if we're performing an initial sync (ie: don't parse joined rooms, just drop the state)
        if initial_sync:
            return

        # check joined rooms
        rooms = sync_result["rooms"]["join"]
        for room_id in rooms:
            events = rooms[room_id]["timeline"]["events"]
            self.process_events(events, room_id)

    def process_events(self, events, room_id):
        for event in events:
            event["room_id"] = room_id
            self.event_proc(event)


class RoomContextStore(object):
    """Stores state events for rooms."""

    def __init__(self, event_types, content_only=True):
        """Init the store.

        Args:
            event_types(list<str>): The state event types to store.
            content_only(bool): True to only store the content for state events.
        """
        self.state = {}
        self.types = event_types
        self.content_only = content_only

    def get_content(self, room_id, event_type, key=""):
        if self.content_only:
            return self.state[room_id][(event_type, key)]
        else:
            return self.state[room_id][(event_type, key)]["content"]

    def get_room_ids(self):
        return self.state.keys()

    def update(self, event):
        try:
            room_id = event["room_id"]
            etype = event["type"]
            if etype in self.types:
                if room_id not in self.state:
                    self.state[room_id] = {}
                key = (etype, event["state_key"])

                s = event
                if self.content_only:
                    s = event["content"]

                self.state[room_id][key] = s
        except KeyError:
            pass

    def init_from_sync(self, sync):
        for room_id in sync["rooms"]["join"]:
            # see if we know anything about these rooms
            room = sync["rooms"]["join"][room_id]

            self.state[room_id] = {}

            try:
                for state in room["state"]["events"]:
                    if state["type"] in self.types:
                        key = (state["type"], state["state_key"])

                        s = state
                        if self.content_only:
                            s = state["content"]

                        self.state[room_id][key] = s
            except KeyError:
                pass

        log.debug(pprint.pformat(self.state))


class KeyValueStore(object):
    """A persistent JSON store."""

    def __init__(self, config_loc, version="1"):
        self.config = {
            "version": version
        }
        self.config_loc = config_loc
        self._load()

    def _load(self):
        try:
            with open(self.config_loc, 'r') as f:
                self.config = json.loads(f.read())
        except:
            self._save()

    def _save(self):
        with open(self.config_loc, 'w') as f:
            f.write(json.dumps(self.config, indent=4))

    def has(self, key):
        return key in self.config

    def set(self, key, value, save=True):
        self.config[key] = value
        if save:
            self._save()

    def get(self, key):
        return self.config[key]