import re
from typing import List, Tuple, Type, Set, Dict
from mautrix.types import (UserID, RoomID, EventType, TextMessageEventContent, MessageType, Format)
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent
from maubot.handlers import command


class Config(BaseProxyConfig):
    def do_update(self, helper: ConfigUpdateHelper) -> None:
        helper.copy('prefix')
        helper.copy('url')
        helper.copy('user')
        helper.copy('pass')
        helper.copy('whitelist')
        helper.copy('filter_properties')
        helper.copy('filter_entry')


class RT(Plugin):
    prefix: str
    whitelist: Set[UserID]
    api: str
    login: dict
    headers = {'User-agent': 'maubot-rt'}
    regex_id = re.compile(r'[0-9]+')
    regex_properties = re.compile(r'([a-zA-z]+): (.+)')
    regex_history = re.compile(r'([0-9]+): (.+)')
    regex_entry = re.compile(r'([a-zA-z]+): (.+(?:\n {8}.*)*)', re.MULTILINE)
    interesting = ['Ticket created', 'Correspondence added', 'Comments added']

    async def start(self) -> None:
        self.on_external_config_update()

    def on_external_config_update(self) -> None:
        self.config.load_and_update()
        self.prefix = self.config['prefix']
        self.whitelist = set(self.config['whitelist'])
        self.url = self.config['url']
        self.rest = f'{self.url}/REST/1.0/'
        self.display = f'{self.url}/Ticket/Display.html'
        self.login = {'user': self.config['user'], 'pass': self.config['pass']}
        self.filter_properties = set(self.config['filter_properties'])
        self.filter_entry = set(self.config['filter_entry'])

    @classmethod
    def get_config_class(cls) -> Type[BaseProxyConfig]:
        return Config

    def valid_id(self, id: str) -> bool:
        return True if self.regex_id.match(id) else False

    def filter_dict(self, raw: dict, keys: Set) -> dict:
        return {k: v for k, v in raw.items() if k in keys}

    def markdown_link(self, id: str) -> str:
        return f'[rt#{id}]({self.display}?id={id})'

    def html_link(self, id: str) -> str:
        return f'<a href="{self.display}?id={id}">rt#{id}</a>'

    def can_manage(self, evt: MessageEvent) -> bool:
        if evt.sender in self.whitelist:
            return True
        return False

    async def _member_mxids(self, room_id: RoomID) -> Dict[UserID, str]:
        room_members = await self.client.get_joined_members(room_id)
        member_mxids = {}
        for mxid in room_members.keys():
            displayname = await self._displayname(room_id, mxid)
            member_mxids[displayname] = mxid
        return member_mxids

    async def _displayname(self, room_id: RoomID, user_id: UserID) -> str:
        event = await self.client.get_state_event(room_id, EventType.ROOM_MEMBER, user_id)
        return event.displayname

    async def _properties(self, id: str) -> dict:
        await self.http.post(self.rest, data=self.login, headers=self.headers)
        rest = f'{self.rest}ticket/{id}/show'
        async with self.http.get(rest, headers=self.headers) as response:
            content = await response.text()
        raw = dict(self.regex_properties.findall(content))
        return self.filter_dict(raw, self.filter_properties)

    async def _edit(self, id: str, properties: dict) -> None:
        rest = f'{self.rest}ticket/{id}/edit'
        content = {'content': '\n'.join([f'{k}: {v}' for k, v in properties.items()])}
        data = {**self.login, **content}
        await self.http.post(rest, data=data, headers=self.headers)

    async def _comment(self, id: str, comment: str) -> None:
        rest = f'{self.rest}ticket/{id}/comment'
        content = {'content': f'id: {id}\nAction: comment\nText: {comment}'}
        data = {**self.login, **content}
        await self.http.post(rest, data=data, headers=self.headers)

    async def _history(self, id: str) -> dict:
        await self.http.post(self.rest, data=self.login, headers=self.headers)
        rest = f'{self.rest}ticket/{id}/history'
        async with self.http.get(rest, headers=self.headers) as response:
            content = await response.text()
        return dict(self.regex_history.findall(content))

    async def _entry(self, id: str, entry: str) -> dict:
        await self.http.post(self.rest, data=self.login, headers=self.headers)
        rest = f'{self.rest}ticket/{id}/history/id/{entry}'
        async with self.http.get(rest, headers=self.headers) as response:
            content = await response.text()
        raw = dict(self.regex_entry.findall(content))
        entry = self.filter_dict(raw, self.filter_entry)
        if 'Content' in entry and '\n' in entry['Content']:
            block = '  \n```\n' + entry['Content'].replace('\n' + ' ' * 9, '\n').rstrip() + '\n```'
            entry['Content'] = block
        return entry

    @command.passive('((^| )([rR][tT]#?))([0-9]+)', multiple=True)
    async def handler(self, evt: MessageEvent, subs: List[Tuple[str, str]]) -> None:
        await evt.mark_read()
        msg_lines = []
        await self.http.post(self.rest, data=self.login, headers=self.headers)
        for sub in subs:
            id = sub[4]
            rest = f'{self.rest}ticket/{id}/show'
            async with self.http.get(rest, headers=self.headers) as response:
                content = await response.text()
            ticket = dict(self.regex_properties.findall(content))
            markdown = '{} is **{}** in **{}** from {}  \n{}'.format(
                self.markdown_link(id),
                ticket['Status'],
                ticket['Queue'],
                ticket['Creator'],
                ticket['Subject']
            )
            msg_lines.append(markdown)

        if msg_lines:
            await evt.respond('  \n'.join(msg_lines))

    @command.new(name=lambda self: self.prefix,
                 help='Manage RT tickets', require_subcommand=True)
    async def rt(self) -> None:
        pass

    @rt.subcommand('properties', aliases=('p', 'prop'), help='Show all ticket properties.')
    @command.argument('id', 'ticket id', parser=str)
    async def properties(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        properties_dict = await self._properties(id)
        properties = '  \n'.join([f'{k}: {v}' for k, v in properties_dict.items()])
        await evt.respond(f'{self.markdown_link(id)} properties:  \n{properties}')

    @rt.subcommand('resolve', aliases=('r', 'res'), help='Mark the ticket as resolved.')
    @command.argument('id', 'ticket id', parser=str)
    async def resolve(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        await self._edit(id, {'Status': 'resolved'})
        await evt.respond(f'{self.markdown_link(id)} resolved 😃')

    @rt.subcommand('open', aliases=('o', 'op'), help='Mark the ticket as open.')
    @command.argument('id', 'ticket id', parser=str)
    async def open(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        await self._edit(id, {'Status': 'open'})
        await evt.respond(f'{self.markdown_link(id)} opened 😐️')

    @rt.subcommand('stall', aliases=('st', 'sta'), help='Mark the ticket as stalled.')
    @command.argument('id', 'ticket id', parser=str)
    async def stall(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        await self._edit(id, {'Status': 'stalled'})
        await evt.respond(f'{self.markdown_link(id)} stalled 😴')

    @rt.subcommand('delete', aliases=('d', 'del'), help='Mark the ticket as deleted.')
    @command.argument('id', 'ticket id', parser=str)
    async def delete(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        await self._edit(id, {'Status': 'deleted'})
        await evt.respond(f'{self.markdown_link(id)} deleted 🤬')

    @rt.subcommand('autoresolve', help='Ask the bot to automatically answer and resolve tickets.')
    async def autoresolve(self, evt: MessageEvent) -> None:
        if not self.can_manage(evt):
            return
        await evt.mark_read()
        await evt.react('😂🤣🦄🌈')

    @rt.subcommand('comment', aliases=('c', 'com'), help='Add a comment.')
    @command.argument('id', 'ticket id', parser=str)
    @command.argument('comment', 'comment text', pass_raw=True)
    async def comment(self, evt: MessageEvent, id: str, comment: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        await self._comment(id, comment)
        await evt.respond(f'{self.markdown_link(id)} comment added 🤓')

    @rt.subcommand('history', aliases=('h', 'hist'), help='Get a list of all history entries.')
    @command.argument('id', 'ticket id', parser=str)
    async def history(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        history_dict = await self._history(id)
        history = '  \n'.join([f'{k}: {v}' for k, v in history_dict.items()])
        await evt.respond(f'{self.markdown_link(id)} history entries:  \n{history}')

    @rt.subcommand('entry', aliases=('e', 'ent'), help='Gets a single history entry.')
    @command.argument('id', 'ticket id', parser=str)
    @command.argument('entryid', 'history entry id', parser=str)
    async def entry(self, evt: MessageEvent, id: str, entryid: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        entry_dict = await self._entry(id, entryid)
        entry = '  \n'.join([f'{k}: {v}' for k, v in entry_dict.items()])
        await evt.respond(f'{self.markdown_link(id)} history entry {entryid}:  \n{entry}')

    @rt.subcommand('last', aliases=('l', 'la'), help='Gets the last entry.')
    @command.argument('id', 'ticket id', parser=str)
    async def last(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        history = await self._history(id)
        mails = {k: v for k, v in history.items() if any(i in v for i in self.interesting)}
        entryid = max(mails, key=int)
        entry_dict = await self._entry(id, entryid)
        entry = '  \n'.join([f'{k}: {v}' for k, v in entry_dict.items()])
        await evt.respond(f'{self.markdown_link(id)} history entry {entryid}:  \n{entry}')

    @rt.subcommand('show', aliases=('s', 'sh'), help='Show all information about the ticket.')
    @command.argument('id', 'ticket id', parser=str)
    async def show(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        prop_dict = await self._properties(id)
        props = '  \n'.join([f'{k}: {v}' for k, v in prop_dict.items()])
        await evt.respond(f'{self.markdown_link(id)} properties:  \n{props}')
        history = await self._history(id)
        for entryid, entry_text in history.items():
            if any(i in entry_text for i in self.interesting + ['Requestor']):
                if 'Requestor' in entry_text:
                    await evt.respond(f'history entry {entryid}: {entry_text}')
                    continue
                entry_dict = await self._entry(id, entryid)
                entry = '  \n'.join([f'{k}: {v}' for k, v in entry_dict.items()])
                await evt.respond(f'history entry {entryid}:  \n{entry}')

    @rt.subcommand('take', aliases=('t', 'ta', 'steal'), help='Take or steal the ticket.')
    @command.argument('id', 'ticket id', parser=str)
    async def take(self, evt: MessageEvent, id: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        displayname = await self._displayname(evt.room_id, evt.sender)
        await self._edit(id, {'Owner': evt.sender[1:].split(':')[0]})
        content = TextMessageEventContent(
            msgtype=MessageType.NOTICE, format=Format.HTML,
            body=f'{displayname} took rt#{id} 👍️',
            formatted_body=f'<a href="https://matrix.to/#/{evt.sender}">{evt.sender}</a> '
            f'took {self.html_link(id)} 👍️')
        await evt.respond(content)

    @rt.subcommand('give', aliases=('g', 'gi', 'assign'), help='Give the ticket to somebody.')
    @command.argument('id', 'ticket id', parser=str)
    @command.argument('user', 'matrix user', parser=str)
    async def give(self, evt: MessageEvent, id: str, user: str) -> None:
        if not self.can_manage(evt) or not self.valid_id(id):
            return
        await evt.mark_read()
        member_mxids = await self._member_mxids(evt.room_id)
        if user[0] == '@':
            if ':' in user:
                user = {v: k for k, v in member_mxids.items()}[user]
            else:
                user = user[1:]
        if user not in member_mxids.keys() and user not in member_mxids.values():
            await evt.respond(f'hmm... **{user}** is not the in room 🤔')
            return
        displayname = await self._displayname(evt.room_id, evt.sender)
        target_mxid = member_mxids[user]
        target_username = target_mxid[1:].split(':')[0]
        await self._edit(id, {'Owner': target_username})
        content = TextMessageEventContent(
            msgtype=MessageType.NOTICE, format=Format.HTML,
            body=f'{displayname} assigned rt#{id} to {user} 😜',
            formatted_body=f'<a href="https://matrix.to/#/{evt.sender}">{evt.sender}</a> '
            f'assigned {self.html_link(id)} to '
            f'<a href="https://matrix.to/#/{target_mxid}">{target_mxid}</a> 😜')
        await evt.respond(content)