# Mattermost Bot.
#  Copyright (c) 2016-2020 by Someone <someone@somenet.org> (aka. Jan Vales <jan@jvales.net>)
#  published under MIT-License


import asyncio
import atexit
import json
import logging
import os
import pprint
import signal
import sys
import threading
import traceback
import urllib

from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn

import mattermost
import mattermost.ws


class MMBot():
    def __init__(self, local_websrv_hostname="localhost", local_websrv_port=18065, api_user=None, api_user_pw=None, mm_api_url="http://localhost:8065/api", mm_ws_url="ws://localhost:8065/api/v4/websocket", debug_chan_id=None):
        self.local_websrv_hostname = local_websrv_hostname
        self.local_websrv_port = local_websrv_port
        self.api_user = api_user
        self.mm_api_url = mm_api_url
        self.mm_ws_url = mm_ws_url

        self.local_websrv_url = ("http://"+self.local_websrv_hostname+":"+str(self.local_websrv_port)+"/").strip("/")
        self.modules = dict()
        self.wsmodules = dict()
        self.api = None
        self.debug_chan_id = None
        self.loop = asyncio.new_event_loop()
        self.command_stats = dict()
        self.admin_ids = []

        self.mmws = None

        atexit.register(self.shutdown)
        signal.signal(signal.SIGUSR1, self.command_stats_dump)
        signal.signal(signal.SIGTERM, self.sig_shutdown)
        signal.signal(signal.SIGINT, self.sig_shutdown)


        # Core-Command: /use-data
        self.USETOPICS = {"bot":("##### Here I am, brain the size of a planet, and they ask me to fill in some missing MM-features and be fun ... It gives me a headache.\n"
                                 "Written by ``@someone`` in python. Big thanks to contributors: ``@ju``, ``@gittenburg``\n"
                                 "Inspired by: ``@bearza``, ``@frunobulax``\n"
                                 "\n"
                                 "Feel like contributing too? Talk to ``@someone``. :)\n"
                                 "The repository is here: https://git.somenet.org/pub/jan/mattermost.git")}

        if api_user is not None and api_user_pw is not None:
            logging.info("User credentials given. Trying to login.")
            self.api = mattermost.MMApi(self.mm_api_url)
            self.api.login(api_user, api_user_pw)
            self.debug_chan_id = debug_chan_id


    # Register a module with the bot.
    def register(self, module):
        if module.TEAM_ID not in self.modules:
            self.modules[module.TEAM_ID] = dict()

        if module.TRIGGER not in self.modules[module.TEAM_ID]:
            self.modules[module.TEAM_ID][module.TRIGGER] = module
            module._on_register(self)

        else:
            raise Exception("Multiple registration attempts for module: "+module.TRIGGER+" and team: "+module.TEAM_ID)


    # Register a websocket handling module with the bot.
    # There is no useful way to discriminate WS-events by originating teams atm. :(
    def register_ws(self, module, eventlist):
        for evtype in eventlist:
            if evtype not in self.wsmodules:
                self.wsmodules[evtype] = dict()

            if module.NAME not in self.wsmodules[evtype]:
                self.wsmodules[evtype][module.NAME] = module
                module._on_register_ws_evtype(self, evtype)

            else:
                raise Exception("Multiple registration attempts for module: "+module.NAME+" and evtype: "+evtype)


    def start(self):
        logging.info("Starting: Almost there.")
        logging.info(pprint.pformat(self.modules))
        logging.info(pprint.pformat(self.wsmodules))

        if self.mm_ws_url is not None:
            self.mmws = mattermost.ws.MMws(self.websocket_handler, self.api, self.mm_ws_url)

        if self.local_websrv_hostname is not None and self.local_websrv_port is not None:
            self.start_webserver()


    def shutdown(self):
        logging.info("Shutting down ...")

        self.mmws.close_websocket()
        # todo: stop webserver + WS?

        for team_modules in self.modules:
            for module in self.modules[team_modules]:
                self.modules[team_modules][module]._on_shutdown()

        for evtype in self.wsmodules:
            for module in self.wsmodules[evtype]:
                self.wsmodules[evtype][module]._on_shutdown()

        self.api.logout()
        self.command_stats_dump()
        logging.info("BYE.")



    ########
    # misc #
    ########
    def debug_chan(self, message):
        if self.debug_chan_id is None:
            logging.info("debug_chan() called, but debug_chan_id is unspecified.")
            return

        self.api.create_post(self.debug_chan_id, "``BOT-AUTODELETE-FAST``\n"+message)


    def command_stats_inc(self, command, amount=1):
        if command in self.command_stats:
            self.command_stats[command] += amount
        else:
            self.command_stats[command] = amount


    def sig_shutdown(self, unk1=None, unk2=None):
        sys.exit(0)


    def command_stats_dump(self, unk1=None, unk2=None):
        self.command_stats_inc("internal::command_stats_dump")
        stats = self.command_stats.copy()
        self.command_stats = dict()

        print("updating command stats: /tmp/somebot_command_stats.json", file=sys.stderr)
        try:
            with open("/tmp/somebot_command_stats.json", "r") as file:
                old_stats = json.load(file)
            for item, cnt in old_stats.items():
                if item in stats:
                    stats[item] += cnt
                else:
                    stats[item] = cnt
        except:
            pass

        with open("/tmp/somebot_command_stats.json", "w", encoding="utf-8") as file:
            json.dump(stats, file, ensure_ascii=False, indent=2)


    ##########################
    # Bot's websocket client #
    ##########################
    def websocket_handler(self, mmws, event_data):
        for evtype in self.wsmodules:
            if evtype == event_data["event"]:
                for module_name in self.wsmodules[evtype]:
                    try:
                        print(module_name)
                        if self.wsmodules[evtype][module_name].on_WS_EVENT(event_data):
                            self.command_stats_inc("ws::"+evtype+"::"+module_name)
                    except:
                        exctxt = "".join(traceback.format_exc())
                        logging.error(exctxt)


    ###################
    # Bot's webserver #
    ###################
    def start_webserver(self):
        logging.info("Starting webserver.")

        class RequestHandler(BaseHTTPRequestHandler):
            bot = None

            def do_POST(self):
                self.handled = False

                if self.headers["Content-Type"] == "application/x-www-form-urlencoded":
                    data = urllib.parse.parse_qs(self.rfile.read(int(self.headers["Content-Length"])).decode("utf-8"), keep_blank_values=True)
                    # only accept first occurence
                    data = dict([(k, v[0]) for k, v in data.items()])
                elif self.headers["Content-Type"] == "application/json":
                    data = json.loads(self.rfile.read(int(self.headers["Content-Length"])).decode("utf-8"))
                else:
                    self.respond(415)
                    return

                splitpath = self.path.strip("/").split("/")
                if self.bot.modules[splitpath[0]] and self.bot.modules[splitpath[0]][splitpath[1]]:
                    module = self.bot.modules[splitpath[0]][splitpath[1]]

                    # /command
                    if len(splitpath) > 2 and splitpath[2] == "command":
                        if "token" in data and module.mm_secret_token == data["token"]:
                            self.bot.command_stats_inc("/"+splitpath[1]+"/"+splitpath[2])
                            module._on_POST(self, data)
                        else:
                            logging.warning("mm_secret_token mismatch expected/got -"+module.mm_secret_token+"-"+data["token"]+"-")
                            self.respond(403)

                    # interactive button-handler. TODO auth!
                    elif len(splitpath) > 2 and splitpath[2] == "interactive":
                        self.bot.command_stats_inc("/"+splitpath[1]+"/"+splitpath[2])
                        module._on_POST_interactive(self, data)

                    # dialog-handler: TODO: auth!
                    elif len(splitpath) > 2 and splitpath[2] == "dialog":
                        self.bot.command_stats_inc("/"+splitpath[1]+"/"+splitpath[2])
                        module._on_POST_dialog(self, data)

                    else: #something weird.
                        self.bot.command_stats_inc("/"+splitpath[1]+' --- SOMETHINGS WEIRD!!!')

                # always try to fail. If previously responded to, nothing will happen.
                self.respond(400)


            def cmd_respond_text_temp(self, message, props=None):
                if props is None:
                    self.respond(200, {"skip_slack_parsing":True, "response_type":"ephemeral", "text":message})
                else:
                    self.respond(200, {"skip_slack_parsing":True, "response_type":"ephemeral", "text":message, "props":props})


            def cmd_respond_text_chan(self, message, props=None):
                if props is None:
                    self.respond(200, {"skip_slack_parsing":True, "response_type":"in_channel", "text":message})
                else:
                    self.respond(200, {"skip_slack_parsing":True, "response_type":"in_channel", "text":message, "props":props})


            def respond(self, err, message=None):
                if self.handled:
                    return

                self.handled = True
                self.send_response(err)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                if message is not None:
                    self.wfile.write(bytes(json.dumps(message), "utf8"))


        class MyHTTPServer(ThreadingMixIn, HTTPServer):
            def serve_forever(self, bot):
                self.RequestHandlerClass.bot = bot
                HTTPServer.serve_forever(self)

        self.httpd = MyHTTPServer((self.local_websrv_hostname, self.local_websrv_port), RequestHandler)
        self.httpd.serve_forever(self)
