import time
import zlib
import json
import urlparse
import hashlib
import datetime
import traceback

from functools import wraps
from collections import defaultdict

from sqlalchemy import func, or_, and_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload_all, joinedload

from appcomposer import db, rlock, redis_store
from appcomposer.application import app
from appcomposer.languages import obtain_languages, obtain_groups
from appcomposer.translator.suggestions import translate_texts
from appcomposer.models import TranslatedApp, TranslationUrl, TranslationBundle, ActiveTranslationMessage, TranslationMessageHistory, TranslationKeySuggestion, TranslationValueSuggestion, GoLabOAuthUser, TranslationSyncLog, TranslationSubscription, TranslationNotificationRecipient, RepositoryApp

DEBUG = False

LANGUAGES = obtain_languages()
GROUPS = obtain_groups()

def get_golab_default_user():
    default_email = app.config.get('TRANSLATOR_DEFAULT_EMAIL', 'weblab+appcomposer@deusto.es')
    default_user = db.session.query(GoLabOAuthUser).filter_by(email = default_email).first()
    if default_user is None:
        default_user = GoLabOAuthUser(email = default_email, display_name = "AppComposer")
        db.session.add(default_user)
        try:
            db.session.commit()
        except IntegrityError:
            default_user = db.session.query(GoLabOAuthUser).filter_by(email = default_email).first()
            db.session.rollback()
        except:
            db.session.rollback()
            raise
        else:
            db.session.remove()
            default_user = db.session.query(GoLabOAuthUser).filter_by(email = default_email).first()
    return default_user


def calculate_content_hash(app_url):
    """Given an App URL generate the hash of the values of the translations. This way, can quickly know if an app was changed or not in a single query, and not do the whole
    expensive DB processing for those which have not changed."""
    
    translated_app = db.session.query(TranslatedApp).filter_by(url=app_url).first()
    if translated_app is None:
        return

    translation_url = translated_app.translation_url
    if translation_url is None:
        return

    app_translations = []
    # 
    # Important: all these contents must be sorted
    # 
    # [
    #      {
    #           'lang': 'es_ES',
    #           'messages: ' [
    #              {
    #                  'key': 'key1',
    #                  'value': 'message1',
    #              },
    #              {
    #                  'key': 'key2',
    #                  'value': 'message2',
    #              }
    #           ],
    #      }
    # ]

    organized_data = {
        # es_ES: {
        #      key: value,
        # }
    }

    for atm in db.session.query(ActiveTranslationMessage).filter(ActiveTranslationMessage.bundle_id == TranslationBundle.id, TranslationBundle.translation_url == translation_url).options(joinedload('bundle')).all():
        bundle_key = '{}_{}'.format(atm.bundle.language, atm.bundle.target)
        if bundle_key not in organized_data:
            organized_data[bundle_key] = {}
        organized_data[bundle_key][atm.key] = atm.value

    # Sort data
    for bundle_key in sorted(organized_data.keys()):
        bundle_data = {
            'lang': bundle_key,
            'messages': []
        }
        for message_key in sorted(organized_data[bundle_key].keys()):
            bundle_data['messages'].append({
                'key': message_key,
                'value': organized_data[bundle_key][message_key],
            })

        app_translations.append(bundle_data)

    app_translations_str= json.dumps(app_translations)
    return unicode(zlib.crc32(app_translations_str))

def _get_or_create_app(app_url, translation_url, metadata):
    # Create the translation url if not present
    automatic = metadata.get('automatic', True)
    attribs = metadata.get('attribs', '')
    db_translation_url = db.session.query(TranslationUrl).filter_by(url = translation_url).first()
    if not db_translation_url:
        db_translation_url = TranslationUrl(url = translation_url, automatic = automatic, attribs = attribs)
        db.session.add(db_translation_url)
    else:
        if db_translation_url.automatic != automatic:
            db_translation_url.automatic = automatic
        if db_translation_url.attribs != attribs:
            db_translation_url.attribs = attribs

    SUBSCRIPTION_MECHANISM = 'translation-url'
    subscribed_emails = set([ email for email, in db.session.query(TranslationNotificationRecipient.email).filter(TranslationSubscription.translation_url == db_translation_url, TranslationSubscription.mechanism == SUBSCRIPTION_MECHANISM, TranslationSubscription.recipient_id == TranslationNotificationRecipient.id).all() ])

    subscription_requests = set(metadata.get('mails', []))
    
    pending_subscriptions = subscription_requests - subscribed_emails
    subscriptions_to_delete = subscribed_emails - subscription_requests

    if subscriptions_to_delete:
        for db_subscription in db.session.query(TranslationSubscription).filter(TranslationSubscription.mechanism == SUBSCRIPTION_MECHANISM, TranslationSubscription.translation_url == db_translation_url, TranslationSubscription.recipient_id == TranslationNotificationRecipient.id, TranslationNotificationRecipient.email.in_(list(subscriptions_to_delete))).all():
            db.session.delete(db_subscription)

    if pending_subscriptions:
        for pending_subscription in pending_subscriptions:
            recipient = db.session.query(TranslationNotificationRecipient).filter_by(email = pending_subscription).first()
            if not recipient:
                recipient = TranslationNotificationRecipient(pending_subscription)
                db.session.add(recipient)

            db.session.add(TranslationSubscription(translation_url = db_translation_url, recipient = recipient, mechanism = SUBSCRIPTION_MECHANISM))

    # Create the app if not present
    db_app_url = db.session.query(TranslatedApp).filter_by(url = app_url).first()
    if db_app_url:
        if db_app_url.translation_url is None:
            db_app_url.translation_url = db_translation_url
        elif db_app_url.translation_url != db_translation_url:
            # If present with a different translation url, copy the old one if possible
            _deep_copy_translations(db_app_url.translation_url, db_translation_url)
            db_app_url.translation_url = db_translation_url
    else:
        db_app_url = TranslatedApp(url = app_url, translation_url = db_translation_url)
    db.session.add(db_app_url)
    return db_translation_url

def _get_or_create_bundle(app_url, translation_url, metadata, language, target, from_developer):
    db_translation_url = _get_or_create_app(app_url, translation_url, metadata)

    # Create the bundle if not present
    db_translation_bundle = db.session.query(TranslationBundle).filter_by(translation_url = db_translation_url, language = language, target = target).first()
    if not db_translation_bundle:
        db_translation_bundle = TranslationBundle(language, target, db_translation_url, from_developer)
        db.session.add(db_translation_bundle)
    return db_translation_bundle

def get_bundles_by_key_namespaces(pairs):
    """ given a list of pairs (key, namespace), return the list of bundles which contain translations like those """
    keys = [ pair['key'] for pair in pairs ]
    namespaces = [ pair['namespace'] for pair in pairs if pair['namespace'] ]

    pairs_found = {}

    if keys and namespaces:
        for key, namespace, bundle_id in db.session.query(ActiveTranslationMessage.key, ActiveTranslationMessage.namespace, ActiveTranslationMessage.bundle_id).filter(ActiveTranslationMessage.key.in_(keys), ActiveTranslationMessage.namespace.in_(namespaces), ActiveTranslationMessage.taken_from_default == False).all():
            if (key, namespace) not in pairs_found:
                pairs_found[key, namespace] = set()
            pairs_found[key, namespace].add(bundle_id)

    bundle_ids = set()

    for pair in pairs:
        key = pair['key']
        namespace = pair['namespace']
        new_bundle_ids = pairs_found.get((key, namespace))
        if new_bundle_ids:
            bundle_ids.update(new_bundle_ids)

    bundles = []
    existing_bundles = []
    if bundle_ids:
        for lang, target in db.session.query(TranslationBundle.language, TranslationBundle.target).filter(TranslationBundle.id.in_(bundle_ids)).all():
            key = "%s@%s" % (target, lang)
            if key not in existing_bundles:
                existing_bundles.append(key)
                bundles.append({
                    'language' : lang,
                    'target' : target,
                })
    return bundles

def locking(func):
    """Lock certain complex write operations"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        lock_key = 'locks:{}'.format(func.__name__)
        db.session.remove()

        counter = 10
        import sys, time

        while True: 
            # Maximum: lock for 15 seconds
            lock_obj = rlock.lock(lock_key, 15 * 1000)
            if lock_obj and lock_obj.resource:
                # print '[{}] {} {}'.format(time.asctime(), "Entering", lock_key, lock_obj)
                sys.stdout.flush()
                try:
                    return func(*args, **kwargs)
                finally:
                    # print '[{}] {} {}'.format(time.asctime(), "Exiting", lock_key, lock_obj)
                    sys.stdout.flush()
                    try:
                        rlock.unlock(lock_obj)
                    except:
                        raise Exception("Unable to lock")
            counter = counter - 1
            if counter < 0:
                raise Exception("Unable to lock")

    return wrapper

@locking
def add_full_translation_to_app(user_email, app_url, translation_url, app_metadata, language, target, translated_messages, original_messages, from_developer):
    user = db.session.query(GoLabOAuthUser).filter_by(email=user_email).first()
    db_translation_bundle = _get_or_create_bundle(app_url, translation_url, app_metadata, language, target, from_developer)
    # 
    # <NO SHIELD NEW BEHAVIOR>
    # 
    #     We have recently removed the shields that protected messages to be overriden by users' messages.
    #     In the past, when a user attempted to update a message which was provided by the developer, we 
    #     automatically discarded it. Now we enable the user to delete it. Furthermore, if the developer
    #     changes a piece of text, and developers update it in their servers with a different message, we
    #     now give a higher priority to that message rather to that from the developer.
    # 
    if from_developer:
        # If it comes from the developer, now the expected thing is to check if it is different to the current message and, if it is different, check if it 
        # was different to the last message coming from the developer in the history with developer = True. If it is different (i.e., there has been really a chanage)
        # then proceed with the change. Otherwise, discard that message.
        # 
        # In other words, we have to do the translated_messages.pop() thing with those messages where there is a history and developer = True with the last message being equal
        if translated_messages is not None:
            translated_messages = translated_messages.copy()
            active_msgs = db.session.query(ActiveTranslationMessage).filter_by(bundle = db_translation_bundle).all()
            active_msgs_by_key = {  
                # key: value
            }
            for active_msg in active_msgs:
                active_msgs_by_key[active_msg.key] = active_msg.value

            # select atm.`key`, atm.value from TranslationMessageHistory atm 
            #         inner join (select max(datetime) as max_date, `key` from TranslationMessageHistory where from_developer = true and bundle_id = 953 group by bundle_id, `key`) atm2 
            #         on atm.datetime = atm2.max_date and atm.`key` = atm2.`key` where from_developer = true and bundle_id = 953;
            # 
            tmh_subquery = db.session.query(
                                    func.max(TranslationMessageHistory.datetime).label('tmh_date'), 
                                    TranslationMessageHistory.key.label('tmh_key')
                                ).filter_by(
                                    from_developer=True, 
                                    bundle=db_translation_bundle
                                ).group_by(
                                    TranslationMessageHistory.bundle_id, TranslationMessageHistory.key
                                ).subquery()

            latest_message_history = db.session.query(
                                TranslationMessageHistory.key, 
                                TranslationMessageHistory.value
                            ).join(
                                tmh_subquery, 
                                and_(
                                    tmh_subquery.c.tmh_date == TranslationMessageHistory.datetime, 
                                    tmh_subquery.c.tmh_key == TranslationMessageHistory.key
                                )
                            ).filter(
                                TranslationMessageHistory.from_developer == True, 
                                TranslationMessageHistory.bundle == db_translation_bundle
                            )

            historic_msgs_by_key = dict(latest_message_history.all())
                 # key: latest value from developer
            # }

            for key, value in historic_msgs_by_key.iteritems():
                # If the message is the same as it was in the latest message stored from developer,
                # and it comes from developer, do not take it into account (since it could be overriding
                # the user's message)
                if key in translated_messages and value == translated_messages[key] and translated_messages[key] != active_msgs_by_key.get(key):
                    translated_messages.pop(key, None)

    #
    # </NO SHIELD NEW BEHAVIOR>
    #
    if from_developer and not db_translation_bundle.from_developer:
        # If this is an existing translation and it comes from a developer, establish that it is from developer
        db_translation_bundle.from_developer = from_developer

    #
    # # CODE COMMENTED as part of the no shield removal:
    # if not from_developer and db_translation_bundle.from_developer:
    #     # If this is an existing translation from a developer and it comes from a user (and not a developer)
    #     # then it should not be accepted.
    #     if translated_messages is not None:
    #         translated_messages = translated_messages.copy()
    #         for msg in db_translation_bundle.active_messages:
    #             if msg.from_developer:
    #                 translated_messages.pop(msg.key, None)
    #         # Continue with the remaining translated_messages

    if translated_messages is not None and len(translated_messages) == 0:
        translated_messages = None

    existing_namespaces = set()
    existing_namespace_keys = set()
    existing_active_translations_with_namespace_with_default_value = []

    # First, update translations

    for existing_active_translation in db.session.query(ActiveTranslationMessage).filter_by(bundle = db_translation_bundle).all():
        key = existing_active_translation.key

        position = original_messages.get(key, {}).get('position')
        if position is not None and existing_active_translation.position != position:
            existing_active_translation.position = position

        category = original_messages.get(key, {}).get('category')
        if existing_active_translation.category != category:
            existing_active_translation.category = category

        namespace = original_messages.get(key, {}).get('namespace')
        if existing_active_translation.namespace != namespace:
            existing_active_translation.namespace = namespace

        tool_id = original_messages.get(key, {}).get('tool_id')
        if existing_active_translation.tool_id != tool_id:
            existing_active_translation.tool_id = tool_id

        fmt = original_messages.get(key, {}).get('format')
        if existing_active_translation.fmt != fmt:
            existing_active_translation.fmt = fmt

        same_tool = original_messages.get(key, {}).get('same_tool')
        if existing_active_translation.same_tool != same_tool:
            existing_active_translation.same_tool = same_tool

        if namespace is not None and existing_active_translation.taken_from_default:
            existing_namespaces.add(namespace)
            existing_namespace_keys.add(key)
            existing_active_translations_with_namespace_with_default_value.append(existing_active_translation)

    # Then, check namespaces

    if existing_namespaces:
        # 
        # If there are namespaces in the current bundle with words taken from default, maybe those words are already translated somewhere else.
        # So I take the existing translations for that (namespace, key, bundle), and if they exist, I use them and delete the current message
        # 
        existing_namespace_translations = {}
        _user_ids = set()

        if existing_namespace_keys:
            for key, namespace, value, current_from_developer, existing_user_id in db.session.query(
                    ActiveTranslationMessage.key,
                    ActiveTranslationMessage.namespace,
                    ActiveTranslationMessage.value,
                    ActiveTranslationMessage.from_developer,
                    TranslationMessageHistory.user_id
                ).filter(
                    ActiveTranslationMessage.history_id == TranslationMessageHistory.id,
                    ActiveTranslationMessage.key.in_(list(existing_namespace_keys)),
                    ActiveTranslationMessage.namespace.in_(list(existing_namespaces)),
                    ActiveTranslationMessage.bundle_id == TranslationBundle.id,
                    TranslationBundle.language == db_translation_bundle.language,
                    TranslationBundle.target == db_translation_bundle.target,
                    ActiveTranslationMessage.bundle_id != db_translation_bundle.id,
                    ActiveTranslationMessage.taken_from_default == False,
                    # And make sure it's in the repository (ignore others)
                    TranslationBundle.translation_url_id == TranslatedApp.translation_url_id,
                    RepositoryApp.url == TranslatedApp.url
                ).all():

                existing_namespace_translations[key, namespace] = (value, current_from_developer, existing_user_id)
                _user_ids.add(existing_user_id)

        existing_users = {}
        if _user_ids:
            for user in db.session.query(GoLabOAuthUser).filter(GoLabOAuthUser.id.in_(list(_user_ids))).all():
                existing_users[user.id] = user

        for wrong_message in existing_active_translations_with_namespace_with_default_value:
            now = datetime.datetime.utcnow()
            pack = existing_namespace_translations.get((wrong_message.key, wrong_message.namespace))
            if pack:
                value, current_from_developer, existing_user_id = pack
                existing_user = existing_users[existing_user_id]
                key = wrong_message.key
                wrong_history = wrong_message.history
                wrong_history_parent_id = wrong_history.id
                wrong_message_position = wrong_message.position
                wrong_message_category = wrong_message.category
                wrong_message_tool_id = wrong_message.tool_id
                wrong_message_same_tool = wrong_message.same_tool
                wrong_message_fmt = wrong_message.fmt

                # 1st) Delete the current translation message
                db.session.delete(wrong_message)

                # 2nd) Create a new historic translation message
                new_db_history = TranslationMessageHistory(db_translation_bundle, key, value, existing_user, now, wrong_history_parent_id, 
                                    taken_from_default = False, same_tool = wrong_message_same_tool, tool_id = wrong_message_tool_id, fmt = wrong_message_fmt, 
                                    position = wrong_message_position, category = wrong_message_category, from_developer = current_from_developer, namespace = wrong_message.namespace)
                db.session.add(new_db_history)

                # 3rd) Create a new active translation message
                db.session.query(ActiveTranslationMessage).filter_by(bundle=db_translation_bundle, key=key).delete()
                new_db_active_translation_message = ActiveTranslationMessage(db_translation_bundle, key, value, new_db_history, now, False, wrong_message_position, wrong_message_category, current_from_developer, namespace, wrong_message_tool_id, wrong_message_same_tool, wrong_message_fmt)
                db.session.add(new_db_active_translation_message)

    if translated_messages is not None:
        # Delete active translations that are going to be replaced
        # Store which were the parents of those translations and
        # what existing translations don't need to be replaced
        unchanged = []
        parent_translation_ids = {}

        for existing_active_translation in db.session.query(ActiveTranslationMessage).filter_by(bundle = db_translation_bundle).all():
            key = existing_active_translation.key
            if key in translated_messages:
                if (translated_messages[key] and existing_active_translation.value != translated_messages[key]) or (not from_developer and existing_active_translation.taken_from_default):
                    parent_translation_ids[key] = existing_active_translation.history.id
                    db.session.delete(existing_active_translation)
                else:
                    unchanged.append(key)

        # For each translation message
        now = datetime.datetime.utcnow()
        for key, value in translated_messages.iteritems():
            if value is None:
                value = ""

            if key not in unchanged and key in original_messages:
                position = original_messages[key]['position']
                category = original_messages[key]['category']
                namespace = original_messages[key]['namespace']
                tool_id = original_messages[key]['tool_id']
                same_tool = original_messages[key]['same_tool']
                fmt = original_messages[key]['format']

                same_text = original_messages.get(key, {}).get('text', object()) == value

                if value.strip() == "":
                    taken_from_default = False # Force that it's empty, it's taken from default. After all, empty messages will not appear in the UI
                elif from_developer and same_text:
                    taken_from_default = True
                else:
                    taken_from_default = False

                # Create a new history message
                parent_translation_id = parent_translation_ids.get(key, None)
                db_history = TranslationMessageHistory(db_translation_bundle, key, value, user, now, parent_translation_id, taken_from_default = taken_from_default,
                                        same_tool = same_tool, tool_id = tool_id, fmt = fmt, position = position, category = category, from_developer = from_developer, namespace = namespace)
                db.session.add(db_history)

                # Establish that thew new active message points to this history message

                # We are going to add a new message: delete the one existing if any
                db.session.query(ActiveTranslationMessage).filter_by(bundle=db_translation_bundle, key=key).delete()
                db_active_translation_message = ActiveTranslationMessage(db_translation_bundle, key, value, db_history, now, taken_from_default, position, category, from_developer, namespace, tool_id, same_tool, fmt)
                db.session.add(db_active_translation_message)

                if same_text:
                    # If the message in the original language is the same as in the target language or the value is empty and it shouldn't, then
                    # it can be two things: 
                    # 
                    #   1) that it has been filled with the original language. In this case it should not be later displayed as a suggestion
                    #   2) that the message is the same in the original language and in the target language
                    # 
                    # Given that the original language will be a suggestion anyway, it's better to avoid storing this message as suggestion
                    continue


                if namespace:
                    # 
                    # If namespace, maybe this key is present in other translations. Therefore, I search for other translations
                    # out there in other bundles but with same language and target and the same namespace, where they are not from developer
                    # and I copy my translation to them.
                    # 
                    for wrong_message in db.session.query(ActiveTranslationMessage).filter(
                                                        ActiveTranslationMessage.key == key, 
                                                        ActiveTranslationMessage.namespace == namespace, 
                                                        ActiveTranslationMessage.value != value, 
                                                        ActiveTranslationMessage.bundle_id == TranslationBundle.id, 
                                                        TranslationBundle.language == db_translation_bundle.language, 
                                                        TranslationBundle.target == db_translation_bundle.target, 
                                                        TranslationBundle.id != db_translation_bundle.id,
                                                        # And make sure it's in the repository (ignore others)
                                                        TranslationBundle.translation_url_id == TranslatedApp.translation_url_id,
                                                        RepositoryApp.url == TranslatedApp.url
                                            ).options(joinedload_all('bundle')).all():
                        # wrong_message is a message for same language, target, key and namespace with a different value.
                        # We must update it with the current credentials
                        wrong_history = wrong_message.history
                        wrong_history_parent_id = wrong_history.id
                        wrong_message_position = wrong_message.position
                        wrong_message_category = wrong_message.category
                        wrong_message_bundle = wrong_message.bundle
                        wrong_message_tool_id = wrong_message.tool_id
                        wrong_message_same_tool = wrong_message.same_tool
                        wrong_message_fmt = wrong_message.fmt

                        # 1st) Delete the current translation message
                        db.session.delete(wrong_message)