import couchconnection
import json
import re

(db, conn) = couchconnection.arsnova_connection("/etc/arsnova/arsnova.properties")

migrations_document_id = "arsnova_migrations"
db_url = "/" + db
migrations_url = db_url + "/" + migrations_document_id

def bump(next_version):
    conn.request("GET", migrations_url)
    res = conn.getresponse()
    migration = json.loads(res.read())
    migration["version"] = next_version
    res = conn.json_put(migrations_url, json.dumps(migration))
    return res.read()

def migrate(migration):
    global db_url, migrations_url
    all_docs_url = db_url + "/_all_docs"
    bulk_url = db_url + "/_bulk_docs"
    cleanup_url = db_url + "/_view_cleanup"
    current_version = migration["version"]

    # Changes to 'skill_question' and 'skill_question_answer':
    #   added 'questionVariant' field, defaulting to 'lecture' value
    if current_version == 0:
        def question_migration():
            questions = "{ \"map\": \"function(doc) { if (doc.type == 'skill_question') emit(doc._id, doc); }\" }"
            answers = "{ \"map\": \"function(doc) { if (doc.type == 'skill_question_answer') emit(doc._id, doc); }\" }"

            # We are doing three steps:
            #   1) Load all documents we are going to migrate in bulk
            #   2) Each document that is not migrated yet is changed
            #   3) Update all changed documents in bulk
            #
            # Because the documents could change in the database while
            # we perform any of these steps, we will get an error for
            # those documents. To solve this we repeat all steps until
            # no more errors occur.
            def migrate_with_temp_view(temp_view):
                while True:
                    res = conn.temp_view(db_url, temp_view)
                    doc = json.loads(res.read())
                    ds = []
                    for col in doc["rows"]:
                        val = col["value"]
                        if not val.has_key("questionVariant"):
                            ds.append(val)
                    for d in ds:
                        d["questionVariant"] = "lecture"
                    res = conn.json_post(bulk_url, json.dumps({"docs":ds}))
                    result_docs = json.loads(res.read())
                    errors = []
                    for result in result_docs:
                        if result.has_key("error"):
                            errors.append(result)
                    if not errors:
                        # All documents were migrated.
                        # jump out of loop and exit this function
                        break
            print "Migrating all Question documents..."
            migrate_with_temp_view(questions)
            print "Migrating all Answer documents..."
            migrate_with_temp_view(answers)

        # skill_question
        question_migration()
        # bump database version
        current_version = 1
        print bump(current_version)

    if current_version == 1:
        print "Deleting obsolete food vote design document..."
        if not conn.delete(db_url + "/_design/food_vote"):
            print "Food vote design document not found"
        # bump database version
        current_version = 2
        print bump(current_version)

    if current_version == 2:
      print "Deleting obsolete user ranking, understanding, and admin design documents..."
      if not conn.delete(db_url + "/_design/user_ranking"):
          print "User ranking design document not found"
      if not conn.delete(db_url + "/_design/understanding"):
          print "Understanding design document not found"
      if not conn.delete(db_url + "/_design/admin"):
          print "Admin design document not found"
      # bump database version
      current_version = 3
      print bump(current_version)

    if current_version == 3:
        def add_variant_to_freetext_abstention_answers():
            answers = "{ \"map\": \"function(doc) { if (doc.type == 'skill_question_answer' && typeof doc.questionVariant === 'undefined' && doc.abstention == true) emit(doc._id, doc.questionId); }\" }"

            # get all bug-affected answer documents
            res = conn.temp_view_with_params(db_url, "?include_docs=true", answers)
            doc = json.loads(res.read())
            questions = []
            answers = []
            for col in doc["rows"]:
                questions.append(col["value"])
                answers.append(col["doc"])
            # bulk fetch all (unique) question documents of which we found problematic answers
            res = conn.json_post(all_docs_url + "?include_docs=true", json.dumps({"keys":list(set(questions))}))
            result_docs = json.loads(res.read())
            # we need to find the variant of each question so that we can put it into the answer document
            questions = []
            for result in result_docs["rows"]:
                questions.append(result["doc"])
            for answer in answers:
                for question in questions:
                    if answer["questionId"] == question["_id"]:
                        answer["questionVariant"] = question["questionVariant"]
            # bulk update the answers
            res = conn.json_post(bulk_url, json.dumps({"docs":answers}))
            result_docs = json.loads(res.read())
            print result_docs

        print "Fixing freetext answers (abstentions) with missing question variant (#13313)..."
        add_variant_to_freetext_abstention_answers()
        # bump database version
        current_version = 4;
        print bump(current_version)

    if current_version == 4:
        print "Deleting obsolete learning_progress design documents..."
        if not conn.delete(db_url + "/_design/learning_progress_course_answers"):
            print "course_answers design document not found"
        if not conn.delete(db_url + "/_design/learning_progress_maximum_value"):
            print "maximum_value design document not found"
        if not conn.delete(db_url + "/_design/learning_progress_user_values"):
            print "learning_progress_user_values design document not found"
        # bump database version
        current_version = 5
        print bump(current_version)

    if current_version == 5:
        print "Deleting misspelled 'statistic' design document..."
        if not conn.delete(db_url + "/_design/statistic"):
            print "'statistic' design document not found"
        # bump database version
        current_version = 6
        print bump(current_version)

    if current_version == 6:
        print "Transforming pre-picture-answer freetext questions into text only questions (#15613)..."
        def add_text_answer_to_freetext_questions():
            old_freetext_qs = "{ \"map\": \"function(doc) { if (doc.type == 'skill_question' && doc.questionType == 'freetext' && typeof doc.textAnswerEnabled === 'undefined') emit(doc._id); }\" }"

            # get all bug-affected documents
            res = conn.temp_view_with_params(db_url, "?include_docs=true", old_freetext_qs)
            doc = json.loads(res.read())
            questions = []
            for result in doc["rows"]:
                questions.append(result["doc"])
            # add missing properties
            for question in questions:
                question["imageQuestion"] = False
                question["textAnswerEnabled"] = True
            # bulk update the documents
            res = conn.json_post(bulk_url, json.dumps({"docs":questions}))
            result_docs = json.loads(res.read())
            print result_docs

        add_text_answer_to_freetext_questions()
        # bump database version
        current_version = 7
        print bump(current_version)

    if current_version == 7:
        print "Transforming session documents to new learning progress options format (#15617)..."
        def change_learning_progress_property_on_session():
            sessions = "{ \"map\": \"function(doc) { if (doc.type == 'session' && doc.learningProgressType) emit(doc._id); }\" }"

            res = conn.temp_view_with_params(db_url, "?include_docs=true", sessions)
            doc = json.loads(res.read())
            sessions = []
            for result in doc["rows"]:
                sessions.append(result["doc"])
            # change property 'learningProgressType' to 'learningProgressOptions'
            for session in sessions:
                currentProgressType = session.pop("learningProgressType", "questions")
                progressOptions = { "type": currentProgressType, "questionVariant": "" }
                session["learningProgressOptions"] = progressOptions
            # bulk update sessions
            res = conn.json_post(bulk_url, json.dumps({"docs":sessions}))
            result_docs = json.loads(res.read())
            print result_docs

        change_learning_progress_property_on_session()
        # bump database version
        current_version = 8
        print bump(current_version)

    if current_version == 8:
        print "Migrating DB and LDAP user IDs to lowercase..."
        conn.request("GET", db_url + "/_design/user/_view/all")
        res = conn.getresponse()
        doc = json.loads(res.read())
        affected_users = {}
        unaffected_users = []
        bulk_docs = []

        # Look for user documents where user ID is not in lowercase
        #   1) Delete document if account has not been activated
        #   2) Lock account if a lowercase version already exists
        #   3) Convert user ID to lowercase if only one captitalization exists
        for user_doc in doc["rows"]:
            if user_doc["key"] != user_doc["key"].lower():
                # create a list of user documents since there might be multiple
                # items for different captitalizations
                affected_users.setdefault(user_doc["key"].lower(), []).append(user_doc["value"])
            else:
                unaffected_users.append(user_doc["key"])
        for uid, users in affected_users.iteritems():
            migration_targets = []
            for user in users:
                if "activationKey" in user:
                    print "User %s has not been activated. Deleting document %s..." % (user["username"], user["_id"])
                    conn.delete(db_url + "/" + user["_id"])
                elif uid in unaffected_users:
                    print "Migration target exists. Locking duplicate user %s (document %s)..." % (user["username"], user["_id"])
                    user["locked"] = True
                    bulk_docs.append(user)
                else:
                    migration_targets.append(user)
            if len(migration_targets) > 1:
                print "Cannot migrate some users automatically. Conflicting duplicate users found:"
                for user in migration_targets:
                    print "Locking user %s (document %s)..." % (user["username"], user["_id"])
                    user["locked"] = True
                    bulk_docs.append(user)
            elif migration_targets:
                print "Migrating user %s (document %s)..." % (user["username"], user["_id"])
                user["username"] = uid
                bulk_docs.append(user)

        # Look for data where assigned user's ID is not in lowercase
        #   1) Migrate if user ID was affected by previous migration step
        #   2) Exclude Facebook and Google account IDs
        #   3) Exclude guest account IDs
        #   4) Migrate all remaining IDs (LDAP)
        def reassign_data(type, user_prop):
            print "Reassigning %s data to migrated users..." % type
            migration_view = "{ \"map\": \"function(doc) { function check(doc, type, uid) { return doc.type === type && uid !== uid.toLowerCase() && uid.indexOf('Guest') !== 0; } if (check(doc, '%s', doc.%s)) { emit(doc._id, doc); }}\" }" % (type, user_prop)
            res = conn.temp_view(db_url, migration_view)
            doc = json.loads(res.read())
            print "Documents: %d" % len(doc["rows"])
            for affected_doc in doc["rows"]:
                val = affected_doc["value"]
                print affected_doc["id"], val[user_prop]
                # exclude Facebook and Google accounts from migration (might be
                # redundant)
                if (not re.match("https?:", val[user_prop]) and not "@" in val[user_prop]) or val[user_prop].lower() in affected_users:
                    val[user_prop] = val[user_prop].lower()
                    bulk_docs.append(val)
                else:
                    print "Skipped %s (Facebook/Google account)" % val[user_prop]

        reassign_data("session", "creator")
        reassign_data("interposed_question", "creator")
        reassign_data("skill_question_answer", "user")
        reassign_data("logged_in", "user")
        reassign_data("motdlist", "username")

        # bulk update users and assignments
        res = conn.json_post(bulk_url, json.dumps({"docs": bulk_docs}))
        if res:
            res.read()
            # bump database version
            current_version = 9
            print bump(current_version)

    if current_version == 9:
        # Next migration goes here
        pass

    conn.json_post(cleanup_url)

conn.request("GET", migrations_url)
res = conn.getresponse()
mig = res.read()
if res.status == 404:
    res = conn.json_post(db_url, json.dumps({"_id":migrations_document_id, "version":0}))
    res.read()
    migrate({"version":0})
else:
    migrate(json.loads(mig))