from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk.errors import SlackApiError

import logging
import configuration

from . import message_helpers


config = configuration.parsed["SLACK"]
models = configuration.models

class BotApp(App):
    logger = logging.getLogger(__name__)

    def __init__(self, callback, *args, **kwargs):

        super().__init__(*args, **kwargs)
        self.callback = callback

    def pre_start(self):
        message_helpers.init(self.client)
        missed_messages, missed_reactions = message_helpers.get_unhandled_messages()

        [self.handle_incoming_message(m) for m in missed_messages]
        [self.handle_incoming_reaction(r) for r in missed_reactions]

        # self.react_missed_reactions(missed_reactions)
        # self.react_missed_messages(missed_messages)
        self.startup_status()



    def handle_incoming_reaction(self, reaction):
        if isinstance(reaction, dict): #else: the reaction is already being passed as a model
            # CAUTION: filter for 'changed reactions' those are nasty (usually when adding an url)
            reaction = message_helpers.reaction_dict_to_model(reaction)

        thread = reaction.message.thread
        article_object = thread.article
        if not article_object is None:
            reaction = reaction.type
            status = 1 if reaction == "white_check_mark" else -1

            # self.logger.info(f"Applying reaction {reaction} to its root message.")
            article_object.verified = status
            article_object.save()


    def handle_incoming_message(self, message):
        """Reacts to all messages inside channel archiving. Must then
        distinguish between threaded replies and new requests
        and react accordingly"""
        if isinstance(message, dict): #else: the message is already being passed as a model
            # CAUTION: filter for 'changed messages' those are nasty (usually when adding an url)
            if message.get("subtype", "not bad") == "message_changed":
                return False
            message = message_helpers.message_dict_to_model(message)

        # First check: belongs to thread?
        is_threaded = message.thread.message_count > 1 and message != message.thread.initiator_message
        if is_threaded:
            self.incoming_thread_message(message)
        else:
            self.incoming_channel_message(message)
            

    def incoming_thread_message(self, message):
        if message.user.user_id == config["bot_id"]:
            return True # ignore the files uploaded by the bot. We handled them already!
        
        thread = message.thread
        if thread.is_fully_processed:
            return True

        self.logger.info("Receiving thread-message")
        self.respond_thread_message(message)


    def incoming_channel_message(self, message):
        self.logger.info(f"Handling message {message} ({len(message.urls)} urls)")

        if not message.urls: # no urls in a root-message => IGNORE
            message.is_processed_override = True
            message.save()
            return

        # ensure thread is still empty, this is a scenario encountered only in testing, but let's just filter it
        if message.thread.message_count > 1:
            self.logger.info("Discarded message because it is actually processed.")
            return
        
        if len(message.urls) > 1:
            message_helpers.say_substitute("Only the first url is being handled. Please send any subsequent url as a separate message", thread_ts=message.thread.slack_ts)
        
        self.callback(message)
        # for url in message.urls:
            # self.callback(url, message)
            # stop here!



    def respond_thread_message(self, message, say=message_helpers.say_substitute):
        thread = message.thread
        article = thread.article
        if message.perma_link: # file upload means new data    
            fname = message_helpers.save_as_related_file(message.perma_link, article)
            say("File was saved as 'related file' under `{}`.".format(fname),
                thread_ts=thread.slack_ts
            )
        else: # either a pointer to a new file (too large to upload), or trash
            success = message_helpers.react_file_path_message(message.text, article)
            if success:
                say("File was saved as 'related file'", thread_ts=thread.slack_ts)
            else:
                self.logger.error("User replied to thread {} but the response did not contain a file/path".format(thread))
                say("Cannot process response without associated file.",
                    thread_ts=thread.slack_ts
                )


    def respond_channel_message(self, thread, say=message_helpers.say_substitute):
        article = thread.article
        answers = article.slack_info
        for a in answers:
            if a["file_path"]:
                try: # upload resulted in an error
                    self.client.files_upload(
                        channels = config["archive_id"],
                        initial_comment = f"<@{config['responsible_id']}> \n {a['reply_text']}",
                        file = a["file_path"],
                        thread_ts = thread.slack_ts
                    )
                    status = True
                except SlackApiError as e:
                    say(
                        "File {} could not be uploaded.".format(a),
                        thread_ts=thread.slack_ts
                    )
                    status = False
                    self.logger.error(f"File upload failed: {e}")
            else: # anticipated that there is no file!
                say(
                    f"<@{config['responsible_id']}> \n {a['reply_text']}",
                    thread_ts=thread.slack_ts
                )
                status = True
        

    def startup_status(self):
        threads = [t for t in models.Thread.select()]
        all_threads = len(threads)
        fully_processed = len([t for t in threads if t.is_fully_processed])
        fully_unprocessed = len([t for t in threads if t.message_count == 1])
        articles_unprocessed = len(models.ArticleDownload.select().where(models.ArticleDownload.verified < 1))
        self.logger.info(f"[bold]STATUS[/bold]: Fully processed {fully_processed}/{all_threads} threads. {fully_unprocessed} threads have 0 replies. Article-objects to verify: {articles_unprocessed}", extra={"markup": True})


    


class BotRunner():
    """Stupid encapsulation so that we can apply the slack decorators to the BotApp"""
    def __init__(self, callback, *args, **kwargs) -> None:
        self.bot_worker = BotApp(callback, token=config["auth_token"])

        @self.bot_worker.event(event="message", matchers=[message_helpers.is_message_in_archiving])
        def handle_incoming_message(message, say):
            return self.bot_worker.handle_incoming_message(message)

        @self.bot_worker.event(event="reaction_added", matchers=[message_helpers.is_reaction_in_archiving])
        def handle_incoming_reaction(event, say):
            return self.bot_worker.handle_incoming_reaction(event)

        self.handler = SocketModeHandler(self.bot_worker, config["app_token"])


    def start(self):
        self.bot_worker.pre_start()
        self.handler.start()


    def stop(self):
        self.handler.close()
        print("Bye handler!")

    # def respond_to_message(self, message):
    #     self.bot_worker.handle_incoming_message(message)