# -*- coding: utf-8; -*-
#
# This file is part of Superdesk.
#
# Copyright 2013, 2014 Sourcefabric z.u. and contributors.
#
# For the full copyright and license information, please see the
# AUTHORS and LICENSE files distributed with this source code, or
# at https://www.sourcefabric.org/superdesk/license

import io
import re
import logging
import collections

from flask import json, current_app as app, request
from simplejson.errors import JSONDecodeError
from eve.utils import config

from superdesk.errors import SuperdeskApiError
from superdesk.services import BaseService
from superdesk.notification import push_notification
from apps.dictionaries.resource import DICTIONARY_FILE, DictionaryType
from flask_babel import _


FILE_ID = '_file_id'


logger = logging.getLogger(__name__)


def encode_dict(words_dict):
    return json.dumps(words_dict)


def decode_dict(words_list):
    if isinstance(words_list, dict):
        return words_list
    return json.loads(words_list)


def train(features):
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model


def words(text):
    return [w.strip() for w in re.findall('[^\n]+', text) if not w.isdigit()]


def add_word(words, word, count):
    """Add word with count to words model.

    :param words: words model
    :param word
    :param count
    """
    words.setdefault(word, 0)
    try:
        words[word] += count
    except TypeError as e:
        logger.error(word)
        logger.error(count)
        raise e


def add_words(nwords, text, val=1):
    for word in words(text):
        add_word(nwords, word, val)


def read(stream):
    return stream.read().decode('utf-8').replace('\ufeff', '')


def merge(doc, words):
    doc.setdefault('content', {})
    for word, count in words.items():
        add_word(doc['content'], word, count)


def fetch_dict(doc):
    """Fetch dictionary content after storing it, from gridfs or json.

    :param doc
    """
    if doc and doc.get(FILE_ID):
        content_file = app.storage.get(doc[FILE_ID])
        content = json.loads(content_file.read())
        return content

    if doc and doc.get('content'):
        return decode_dict(doc['content'])

    return {}


def store_dict(updates, original):
    """Store dictionary content.

    In case it's too big - use gridfs, otherwise just json encode it.

    :param updates
    :param original
    """
    content = updates.pop('content', {})
    if content:
        content_json = json.dumps(content)
        if is_big(content_json):
            content_binary = io.BytesIO(content_json.encode('utf-8'))
            updates[FILE_ID] = app.storage.put(content_binary)
            updates['content'] = None
        else:
            updates['content'] = content_json
            updates[FILE_ID] = None

    if original.get(FILE_ID):
        app.storage.delete(original[FILE_ID])


def is_big(json_data):
    """Test if given json data is too big for mongo document.

    Using 1MB as limit atm.

    :param json_data
    """
    return len(json_data) > 1000000


def read_from_file(doc):
    """Read from plain text file

    One word per line
    UTF-8 encoding
    """
    content = doc.pop(DICTIONARY_FILE)
    if 'text/' not in content.mimetype:
        raise SuperdeskApiError.badRequestError(_('A text dictionary file is required'))
    return train(words(read(content)))


class DictionaryService(BaseService):
    def on_create(self, docs):
        if len(docs) == 1 and 'content' in docs[0]:
            # works around Eve behaviour which creates sub-dict on each "." it finds in keys
            # cf. SDESK-3083
            # We also test request.data because it is not set in behave tests, and they would fail without it
            try:
                docs[0]['content'] = json.loads(request.data.decode('utf-8'))['content']
            except (KeyError, JSONDecodeError, RuntimeError):
                # request.data is not set during tests, so we ignore those errors
                pass

        for doc in docs:
            if self.is_duplicate_dictionary(doc):
                raise SuperdeskApiError.badRequestError(message=_('The dictionary already exists'),
                                                        payload={'name': 'duplicate'})

            self.__set_default(doc)
            self._validate_dictionary(doc)

            if doc.get(DICTIONARY_FILE):
                words = read_from_file(doc)
                merge(doc, words)

            store_dict(doc, {})

    def on_created(self, docs):
        for doc in docs:
            push_notification('dictionary:created', language=doc.get('language_id'))

    def find_one(self, req, **lookup):
        doc = super().find_one(req, **lookup)
        if doc:
            doc['content'] = fetch_dict(doc)
        return doc

    def _validate_dictionary(self, updates, original=None):
        if original is None:
            original = {}
        dict_type = updates.get('type', original.get('type', DictionaryType.DICTIONARY.value))
        if dict_type == DictionaryType.ABBREVIATIONS.value and not updates.get('user', original.get('user')):
            raise SuperdeskApiError.badRequestError(message=_('User is required for the abbreviations dictionary.'),
                                                    payload={'user': 'missing'})

        if original and dict_type != original.get('type', DictionaryType.DICTIONARY.value):
            raise SuperdeskApiError.badRequestError(message=_('The dictionary type cannot be changed.'))

    def get_base_language(self, lang):
        if lang and lang.find('-') > 0:
            return lang.split('-')[0]

    def is_duplicate_dictionary(self, doc):
        return self.find_one(req=None, name=doc['name'],
                             language_id=doc['language_id'],
                             type=doc.get('type', DictionaryType.DICTIONARY.value))

    def get_dictionaries(self, lang):
        """Returns all the active dictionaries.

        If both the language (en-AU)
        and the base language (en) available, it will return the dict with language

        :param lang:
        :return:
        """
        languages = [{'language_id': lang}]
        base_language = self.get_base_language(lang)
        if base_language:
            languages.append({'language_id': base_language})

        lookup = {'$and': [{'$or': languages},
                           {'$or': [{'is_active': {'$exists': 0}}, {'is_active': 'true'}]},
                           {'$or': [{'type': {'$exists': 0}}, {'type': DictionaryType.DICTIONARY.value}]}]}
        dicts = list(self.get(req=None, lookup=lookup))
        langs = [d['language_id'] for d in dicts]

        if base_language and base_language in langs and lang in langs:
            dicts = [d for d in dicts if d['language_id'] != base_language]

        return dicts

    def get_model_for_lang(self, lang):
        """Get model for given language.

        It will use all active dictionaries for given language combined.

        :param lang: language code
        """
        model = {}
        dicts = self.get_dictionaries(lang)

        for _dict in dicts:
            content = fetch_dict(_dict)
            for word, count in content.items():
                add_word(model, word, count)
        return model

    def on_update(self, updates, original):
        if 'content' in updates:
            # works around Eve behaviour which creates sub-dict on each "." it finds in keys
            # cf. SDESK-3083
            try:
                updates['content'] = json.loads(request.data.decode('utf-8'))['content']
            except (KeyError, JSONDecodeError, RuntimeError):
                # request.data is not set during tests, so we ignore those errors
                pass

        user = original.get('user')
        name = updates.get('name')
        language_id = updates.get('language_id')

        # if user is present it means dictonary is personal/abbrevations.
        if user and language_id:
            personal_dictionary = {
                'name': str(user) + ':' + language_id,
                'language_id': language_id,
                'type': original.get('type'),
            }

        if (user and language_id and language_id != original.get('language_id')
                and self.is_duplicate_dictionary(personal_dictionary)):
            raise SuperdeskApiError.badRequestError(message=_('The dictionary already exists'),
                                                    payload={'name': 'duplicate'})
        elif (name and name != original.get('name') and self.find_one(req=None, name=name)):
            raise SuperdeskApiError.badRequestError(message=_('The dictionary already exists'),
                                                    payload={'name': 'duplicate'})

        # parse json list
        if updates.get('content_list'):
            updates['content'] = json.loads(updates.pop('content_list'))

        if 'type' not in original:
            self.__set_default(updates)

        self._validate_dictionary(updates, original)

        # handle manual changes
        if original.get('type', DictionaryType.DICTIONARY.value) == DictionaryType.DICTIONARY.value:
            nwords = fetch_dict(original).copy()
            for word, val in updates.get('content', {}).items():
                if val:
                    add_words(nwords, word, val)
                else:
                    nwords.pop(word, None)

            updates['content'] = nwords

        # handle uploaded file
        if updates.get(DICTIONARY_FILE):
            file_words = read_from_file(updates)
            merge(updates, file_words)

        store_dict(updates, original)

    def on_updated(self, updates, original):
        push_notification('dictionary:updated', language=updates.get('language_id', original.get('language_id')))

    def __set_default(self, doc):
        if 'type' not in doc:
            doc['type'] = DictionaryType.DICTIONARY.value

    def on_fetched_item(self, doc):
        self.__enhance_items([doc])

    def on_fetched(self, docs):
        self.__enhance_items(docs[config.ITEMS])

    def __enhance_items(self, docs):
        for doc in docs:
            self.__set_default(doc)
