# coding=utf-8

import base64
import html
import tempfile
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Any, Dict, List, IO, TYPE_CHECKING

import pydub
import requests
import telegram.ext

from ehforwarderbot import MsgType
from .locale_mixin import LocaleMixin


# TODO: Isolate voice recognition as middleware.

if TYPE_CHECKING:
    from . import TelegramChannel
    from .bot_manager import TelegramBotManager


class VoiceRecognitionManager(LocaleMixin):
    """
    Methods related to voice recognition function of ETM.
    """

    voice_engines = []

    def __init__(self, channel: 'TelegramChannel'):
        """
        Load voice recognition engine objects with tokens into ``self.voice_engines``.

        Override this method with ``super`` to support other engines.

        Args:
             channel: The channel.
        """
        self.channel: 'TelegramChannel' = channel
        self.bot: 'TelegramBotManager' = self.channel.bot_manager

        self.bot.dispatcher.add_handler(
            telegram.ext.CommandHandler("recog", self.recognize_speech, pass_args=True))

        tokens: Dict[str, Any] = self.channel.config.get("speech_api", dict())
        self.voice_engines = []
        if "bing" in tokens:
            self.voice_engines.append(BingSpeech(self.channel, tokens['bing']))
        if "baidu" in tokens:
            self.voice_engines.append(BaiduSpeech(self.channel, tokens['baidu']))

    def recognize_speech(self, bot, update, args=[]):
        """
        Recognise voice message. Triggered by `/recog`.

        Args:
            bot: Telegram Bot instance
            update: Message update
            args: Arguments from message
        """

        if not getattr(update.message, "reply_to_message", None):
            text = self._("/recog lang_code\n"
                          "Reply to a voice with this command to recognize it.\n"
                          "examples:\n/recog zh\n/recog en-US\n\nSupported languages:\n")
            text += "\n".join("%s: %r" % (i.engine_name, i.lang_list) for i in self.voice_engines)
            return self.bot.reply_error(update, text)
        if not getattr(update.message.reply_to_message, "voice"):
            return self.bot.reply_error(update,
                                        self._("Reply only to a voice with this command "
                                               "to recognize it. (RS02)"))

        if update.message.reply_to_message.voice.duration > 60:
            return self.bot.reply_error(update, self._("Only voice shorter than 60s "
                                                       "is supported. (RS04)"))

        file, _, _ = self.bot.download_file(update.message, update.message.reply_to_message.voice, MsgType.Audio)

        results = OrderedDict()
        for i in self.voice_engines:
            results["%s (%s)" % (i.engine_name, args[0])] = i.recognize(file.name, args[0])

        msg = ""
        for i in results:
            msg += "\n<b>%s</b>:\n" % html.escape(i)
            for j in results[i]:
                msg += "%s\n" % html.escape(j)
        msg = self._("Results:\n{0}").format(msg)
        self.bot.send_message(update.message.reply_to_message.chat.id, msg,
                              reply_to_message_id=update.message.reply_to_message.message_id,
                              parse_mode=telegram.ParseMode.HTML)

        file.close()


class SpeechEngine(ABC):
    """Name of the speech recognition engine"""
    engine_name: str = __name__
    """List of languages codes supported"""
    lang_list: List[str] = []

    @abstractmethod
    def recognize(self, file: IO[bytes], lang: str):
        raise NotImplementedError()


class BaiduSpeech(SpeechEngine, LocaleMixin):
    key_dict = None
    access_token = None
    full_token = None
    engine_name = "Baidu"
    lang_list = ['zh', 'ct', 'en']

    def __init__(self, channel, key_dict):
        self.channel = channel
        self.key_dict = key_dict
        d = {
            "grant_type": "client_credentials",
            "client_id": key_dict['api_key'],
            "client_secret": key_dict['secret_key']
        }
        r = requests.post("https://openapi.baidu.com/oauth/2.0/token", data=d).json()
        self.access_token = r['access_token']
        self.full_token = r

    def recognize(self, file, lang):
        if hasattr(file, 'read'):
            pass
        elif isinstance(file, str):
            file = open(file, 'rb')
        else:
            return [self._("ERROR!"), self._("File must be a path string or a file object in `rb` mode.")]
        if lang.lower() not in self.lang_list:
            return [self._("ERROR!"), self._("Invalid language.")]

        audio = pydub.AudioSegment.from_file(file)
        audio = audio.set_frame_rate(16000)
        d = {
            "format": "pcm",
            "rate": 16000,
            "channel": 1,
            "cuid": "testing_user",
            "token": self.access_token,
            "lan": lang,
            "len": len(audio.raw_data),
            "speech": base64.b64encode(audio.raw_data).decode()
        }
        r = requests.post("http://vop.baidu.com/server_api", json=d)
        rjson = r.json()
        if rjson['err_no'] == 0:
            return rjson['result']
        else:
            return [self._("ERROR!"), rjson['err_msg']]


class BingSpeech(SpeechEngine, LocaleMixin):
    keys = None
    access_token = None
    engine_name = "Bing"
    lang_list = ['ar-EG', 'de-DE', 'en-US', 'es-ES', 'fr-FR',
                 'it-IT', 'ja-JP', 'pt-BR', 'ru-RU', 'zh-CN']

    @staticmethod
    def first(data, key):
        """
        Look for first element in a list that matches a criteria.

        Args:
            data (list): List of elements
            key (function with one argument that returns Boolean value):
                Function to decide if an element matches the criteria.

        Returns:
            The first element found, or ``None``.
        """
        for i in data:
            if key(i):
                return i
        return None

    def __init__(self, channel, keys):
        self.channel = channel
        self.keys = keys

    def recognize(self, path, lang):
        if isinstance(path, str):
            file = open(path, 'rb')
        else:
            return [self._("ERROR!"), self._("File must be a path string.")]
        if lang not in self.lang_list:
            lang = self.first(self.lang_list, lambda a: a.split('-')[0] == lang.split('-')[0])
            if lang not in self.lang_list:
                return [self._("ERROR!"), self._("Invalid language.")]

        with tempfile.NamedTemporaryFile() as f:
            audio = pydub.AudioSegment.from_file(file)
            audio = audio.set_frame_rate(16000)
            audio.export(f.name, format="wav")
            header = {
                "Ocp-Apim-Subscription-Key": self.keys,
                "Content-Type": "audio/wav; samplerate=16000"
            }
            d = {
                "language": lang,
                "format": "detailed",
            }
            f.seek(0)
            r = requests.post("https://speech.platform.bing.com/speech/recognition/conversation/cognitiveservices/v1",
                              params=d, data=f.read(), headers=header)

            try:
                rjson = r.json()
            except ValueError:
                return [self._("ERROR!"), r.text]

            if r.status_code == 200:
                return [i['Display'] for i in rjson['NBest']]
            else:
                return [self._("ERROR!"), r.text]
