import hashlib
import re
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any

import dateutil.parser
from pytz import timezone

from .arguments import Arguments
from .safedict import SafeDict
from .settings import Settings
from .logger import Logger, Log


class Pipe:
    """
    Pipe takes care of adding custom data fields and finally
    format data into comment and output file strings
    """

    def __init__(self, format_dictionary: Dict[str, Any]):
        """
        Pipe
        :param format_dictionary: Comment format
        """
        self.format_dictionary: Dict[str, Any] = format_dictionary

        # Combine regular format and action_format if provided.
        self.combined_formats: str = ''
        if 'format' in self.format_dictionary:
            self.combined_formats += self.format_dictionary['format']
        if 'action_format' in self.format_dictionary:
            self.combined_formats += self.format_dictionary['action_format']

    def format(self, data: Dict[str, Any]) -> str:
        """
        Format comment
        :param data: Input data
        :return:
        """
        self.mapper(data)

        return self.reduce(data)

    def comment(self, comment_data: Dict[str, Any]) -> str:
        """
        Format comment data to string
        :param comment_data: Comment data
        :return: Formatted comment line
        """
        return self.format(comment_data)

    def output(self, video_data: Dict[str, Any]) -> str:
        """
        Format output path from data
        :param video_data: Video data
        :return: Output string
        """

        # Video title
        video_data['title'] = Pipe.get_valid_filename(video_data['title'])

        # Output directory and file
        output_string = '/'.join([Pipe.get_valid_filename(s) for s in self.format(video_data).split('/')])

        return '{}/{}'.format(Arguments().output.rstrip('/').rstrip('\\'), output_string)

    @staticmethod
    def get_valid_filename(string: str) -> str:
        """
        Strip invalid filename characters from value.
        :param string: Filename with potentially invalid characters
        :return: Valid filename
        """
        valid_characters: str = r''+Settings().config.get('valid_filename_regex', r'[^-\w.()\[\]{}@%! ]')
        try:
            return re.sub(r'(?u)' + valid_characters, '', string.strip())
        except re.error:
            Logger().log(f'Invalid filename regex, check your settings: {valid_characters}', Log.ERROR)
            exit(1)

    @staticmethod
    def timestamp(date_format: str, date_value: str, timezone_name: Optional[str] = None) -> str:
        """
        Parse timestamp, format it and change timezone if a timezone name is given
        :param date_format: Wanted date format
        :param date_value: Input value to be parsed
        :param timezone_name: Timezone name
        :return: Timestamp in string format
        """
        date: datetime = dateutil.parser.parse(date_value)

        # Convert to another timezone
        if timezone_name is not None:
            date = date.astimezone(timezone(timezone_name))

        return date.strftime(date_format)

    @staticmethod
    def timestamp_relative(seconds: float) -> str:
        # Todo: support formatting
        delta = timedelta(seconds=seconds)
        delta = delta - timedelta(microseconds=delta.microseconds)
        return str(delta)

    def reduce(self, data: dict) -> str:
        """
        Main formatting

        Map data dictionary to format string
        :param data: Input data
        :return: Formatted string
        """

        # If action format is defined and comment is an action
        if 'action_format' in self.format_dictionary and 'is_action' in data and bool(data['is_action']):
            try:
                return str(self.format_dictionary['action_format']).format_map(SafeDict(data))
            except TypeError:
                print('Invalid action format in settings file:', self.format_dictionary['is_action'])
                exit(1)
        else:
            try:
                return str(self.format_dictionary['format']).format_map(SafeDict(data))
            except TypeError:
                print('Invalid format in settings file:', self.format_dictionary['format'])
                exit(1)

    def mapper(self, data: Dict[str, Any]) -> Dict[str, Any]:
        """
        Make custom changes to the input data according to the format dictionary
        :param data: Input data
        :return: Data (input data dict is mutated)
        """
        self._map_timestamps(data)
        self._map_user_colors(data)
        self._map_user_badges(data)

        return data

    def _map_timestamps(self, data: Dict[str, Any]):
        if 'timestamp' in self.format_dictionary and '{timestamp' in self.combined_formats:

            data['timestamp'] = {}

            # Absolute timestamp
            if 'absolute' in self.format_dictionary['timestamp'] and '{timestamp[absolute]}' in self.combined_formats:

                # Millisecond precision - remove $f (milliseconds) from time format
                if 'millisecond_precision' in self.format_dictionary:
                    self.format_dictionary['timestamp']['absolute'] = str(
                        self.format_dictionary['timestamp']['absolute']).replace(
                        '%f', '_MILLISECONDS_')

                # Format timestamp
                data['timestamp']['absolute'] = self.timestamp(self.format_dictionary['timestamp']['absolute'],
                                                               data['created_at'],
                                                               Arguments().timezone)

                # Millisecond precision - add milliseconds to timestamp
                if 'millisecond_precision' in self.format_dictionary:
                    milliseconds: str = self.timestamp('%f', data['created_at'], Arguments().timezone)
                    milliseconds = milliseconds[:self.format_dictionary['millisecond_precision']]
                    data['timestamp']['absolute'] = str(data['timestamp']['absolute']).replace(
                        '_MILLISECONDS_',
                        milliseconds)

            # Relative timestamp
            if '{timestamp[relative]}' in self.combined_formats:
                # Todo: 'relative' in self.format_dictionary['timestamp'] when relative formatting is implemented.
                data['timestamp']['relative'] = self.timestamp_relative(
                    float(data['content_offset_seconds']))

    def _map_user_colors(self, data: Dict[str, Any]):
        if 'message' in data:

            # Set color
            if 'user_color' not in data['message']:
                if 'default_user_color' in self.format_dictionary and self.format_dictionary[
                    'default_user_color'] not in ['random',
                                                  'hash']:
                    data['message']['user_color'] = self.format_dictionary['default_user_color']
                else:
                    # Assign color based on commenter's ID
                    sha256 = hashlib.sha256()
                    sha256.update(str.encode(data['commenter']['_id']))

                    # Truncate hash and mod it by 0xffffff-1 for color hex.
                    color: str = hex(int(sha256.hexdigest()[:32], 16) % int(hex(0xffffff), 16)).lstrip('0x')

                    # Add any missing digits
                    while len(color) < 6:
                        color = color + '0'

                    data['message']['user_color'] = '#{color}'.format(color=color[:6])

            # SSA Color
            if 'message[ssa_user_color]' in self.combined_formats:
                data['message']['ssa_user_color'] = '#{b}{g}{r}'.format(
                    b=data['message']['user_color'][5:7],
                    g=data['message']['user_color'][3:5],
                    r=data['message']['user_color'][1:3])

    def _map_user_badges(self, data: Dict[str, Any]):
        # The Twitch API returns an array of badges, ordered by their importance (descending).
        if '{commenter[badge]}' in self.combined_formats and 'message' in data:

            # Add empty badge if no badge
            if 'user_badges' not in data['message']:
                data['message']['user_badges'] = [{'_id': '', 'version': 1}]

            # Default badges
            if 'badges' not in self.format_dictionary:
                self.format_dictionary['badges'] = {
                    'turbo': '[turbo]',
                    'premium': '[prime]',
                    'bits': '[bits]',
                    'subscriber': '[subscriber]',
                    'moderator': '[moderator]',
                    'global_mod': '[global mod]',
                    'admin': '[admin]',
                    'staff': '[staff]',
                    'broadcaster': '[streamer]',
                }

            # Default badges setting
            if 'multiple_badges' not in self.format_dictionary:
                self.format_dictionary['multiple_badges'] = False

            # Get badge display text
            badges: List[str] = []
            for badge in data['message']['user_badges']:
                badges.append(self.format_dictionary['badges'].get(badge['_id'], ''))

            # Display multiple badges or not
            if self.format_dictionary['multiple_badges']:
                data['commenter']['badge'] = ''.join(badges)
            else:
                data['commenter']['badge'] = ''

                # Find first defined user badge
                for badge in badges:
                    if badge != '':
                        data['commenter']['badge'] = badge
                        break