#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# ==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE
# ------------------------------------------------------------------------------

"""
NAME:
    swarm_rename_users.py

DESCRIPTION:
    This script renames Swarm users. Expects parameter to be a file containing:
        old_username new_username   (separated by whitespace)

    You can run like this:

        python3 swarm_rename_users.py -i users.list

    Swarm key values updated:

    p4 keys -e swarm-user-*
    p4 keys -e swarm-workflow-*

    Users/src/Model/Config.php:    const KEY_PREFIX = 'swarm-user-';

        swarm-user-jim = {"delayedComments":[],"follows":null,"user_notification_settings":null,"user_settings":null}
        swarm-user-john = {"delayedComments":[],"follows":null,"user_notification_settings":null,"user_settings":null}


"""

# Python 2.7/3.3 compatibility.
from __future__ import print_function

import sys
import os
import textwrap
import argparse
import logging
import P4
import json
from collections import defaultdict
from six.moves import range
import six

script_name = os.path.basename(os.path.splitext(__file__)[0])

# Avoid utf encoding issues when writing to stdout
sys.stdout = os.fdopen(sys.stdout.buffer.fileno(), 'w', encoding='utf8')

LOGDIR = os.getenv('LOGS', '/p4/1/logs')

DEFAULT_LOG_FILE = "log-%s.log" % script_name
if os.path.exists(LOGDIR):
    DEFAULT_LOG_FILE = os.path.join(LOGDIR, "%s.log" % script_name)
DEFAULT_VERBOSITY = 'DEBUG'
LOGGER_NAME = 'P4Triggers'

class SwarmObject(object):
    "Wrapper around Swarm JSON"

    updated = False
    json = None

    def __init__(self, jsonStr, keyword=None):
        if keyword and keyword in jsonStr:
            self.json = jsonStr[keyword]
        else:
            self.json = jsonStr

    def getVal(self, k):
        if k in self.json:
            return self.json[k]
        return None

    def getKeys(self, k):
        d = self.getVal(k)
        if not d:
            return None
        results = []
        for u, _ in six.iteritems(d):
            results.append(u)
        return results

    def setVal(self, k, v):
        self.json[k] = v
        self.updated = True

    def updateVal(self, k, usersToRename):
        u = self.getVal(k)
        if u and u in usersToRename:
            self.setVal(k, usersToRename[u])

    def updateDictKeyVal(self, k, usersToRename):
        "Update 'key': {'u1': []}"
        if not self.json or not k in self.json or not self.json[k]:
            return
        users = []
        for u, _ in six.iteritems(self.json[k]):
            users.append(u)
        for u in users:
            if u in usersToRename:
                # Copy old entry to new name and delete old
                self.json[k][usersToRename[u]] = self.json[k][u]
                del self.json[k][u]
                self.updated = True

    def updateArrayVal(self, k, usersToRename):
        "Update 'key': [user array]"
        if not k in self.json or not self.json[k]:
            return
        for i, u in enumerate(self.json[k]):
            if u in usersToRename:
                self.json[k][i] = usersToRename[u]
                self.updated = True

    def updateArrayDictVal(self, k, k2, usersToRename):
        """ Entry is array of dicts with key
        project = {"k": [{"k2": "u1"}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                u = self.json[k][i][k2]
                if u in usersToRename:
                    self.json[k][i][k2] = usersToRename[u]
                    self.updated = True

    def updateDictArrayVal(self, k, k2, usersToRename):
        """ Entry is a dict with value as array
        project = {"k": {"k2": ["u1", "u2"]}}
        """
        if not k in self.json or not self.json[k] or not k2 in self.json[k]:
            return
        for i, u in enumerate(self.json[k][k2]):
            if u in usersToRename:
                self.json[k][k2][i] = usersToRename[u]
                self.updated = True

    def updateDictArrayDictVal(self, k, k2, usersToRename):
        """ Entry is an array of dicts with value as array
        project = {"k": [{"j": ["u1", "u2"]}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                for j, u in enumerate(self.json[k][i][k2]):
                    if u in usersToRename:
                        self.json[k][i][k2][j] = usersToRename[u]
                        self.updated = True

    def updateDictArrayDictDictVal(self, k, k2, k3, usersToRename):
        """ Entry is an array of dicts with value as a dict containing an array
        project = {"k": [{"k2": {"k3": ["u1", "u2"]}}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                if k3 in self.json[k][i][k2]:
                    for j, u in enumerate(self.json[k][i][k2][k3]):
                        if u in usersToRename:
                            self.json[k][i][k2][k3][j] = usersToRename[u]
                            self.updated = True

class Activity(SwarmObject):
    "Swarm Activity"

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'activity')

    def user(self):
        return self.getVal('user')

    def behalfOf(self):
        return self.getVal('behalfOf')

    def Update(self, usersToRename):
        self.updateVal("user", usersToRename)
        self.updateVal("behalfOf", usersToRename)

class Comment(SwarmObject):
    "Swarm Comment"

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'comment')

    def user(self):
        return self.getVal('user')

    def readBy(self):
        return self.getVal('readBy')

    def Update(self, usersToRename):
        self.updateVal("user", usersToRename)
        self.updateArrayVal("readBy", usersToRename)

class Project(SwarmObject):
    "Swarm Project"

    # 'members' - this is via group swarm-<project> and the Users: field

    # swarm-project-test = {
    # "name": "Test Swarm Project",
    # "defaults": {
    #     "reviewers": ["jim"]
    # },
    # "description": "Test for demo purposes",
    # "owners": [
    #     "fred",
    #     "bill"
    # ],
    # "moderators": [
    #     "fred",
    #     "bill"
    # ],

    # "branches": [
    #     {
    #         "id": "main",
    #         "name": "Main",
    #         "workflow": null,
    #         "paths": [
    #             "\/\/depot\/p4-test\/main\/..."
    #         ],
    #         "defaults": {
    #             "reviewers": []
    #         },
    #         "minimumUpVotes": null,
    #         "retainDefaultReviewers": false,
    #         "moderators": [],
    #         "moderators-groups": []
    #     },

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'project')

    def owners(self):
        return self.getVal('owners')

    def moderators(self):
        return self.getVal('moderators')

    def branches(self):
        return self.getVal('branches')

    def defaultReviewers(self):
        defaults = self.getVal('defaults')
        k = 'reviewers'
        if defaults and k in defaults:
            return defaults[k]
        return None

    def branchDefaultReviewers(self):
        branches = self.getVal('branches')
        if not branches:
            return None
        results = []
        for b in branches:
            k = 'defaults'
            j = 'reviewers'
            if b and k in b and j in b[k]:
                results.append({b['id']: b[k][j]})
        return results

    def branchModerators(self):
        branches = self.getVal('branches')
        if not branches:
            return None
        results = []
        for b in branches:
            k = 'moderators'
            if b and k in b:
                results.append({b['id']: b[k]})
        return results

    def Update(self, usersToRename):
        self.updateArrayVal("owners", usersToRename)
        self.updateArrayVal("moderators", usersToRename)
        self.updateDictArrayVal("defaults", "reviewers", usersToRename)
        self.updateDictArrayDictVal("branches", "moderators", usersToRename)
        self.updateDictArrayDictDictVal("branches", "defaults", "reviewers", usersToRename)

class Review(SwarmObject):
    "Swarm Review"

    # "author":"martin",
    # "approvals":{"jane":[1]},
    # "participants":{"martin":[],"jane":{"vote":{"value":1,"version":1}}
    # "versions": [{"user": "fred", "difference": 1}]

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'review')

    def author(self):
        return self.getVal('author')

    def approvals(self):
        return self.getKeys('approvals')

    def participants(self):
        return self.getKeys('participants')

    def versionUsers(self):
        d = self.getVal('versions')
        if not d:
            return None
        results = []
        for v in d:
            if 'user' in v:
                results.append(v['user'])
        return results

    def Update(self, usersToRename):
        self.updateVal("author", usersToRename)
        self.updateDictKeyVal("approvals", usersToRename)
        self.updateDictKeyVal("participants", usersToRename)
        self.updateArrayDictVal("versions", "user", usersToRename)

class FileInfo(SwarmObject):

    # swarm-fileInfo-1018362-468732cb9f10670c2599d6844a82cd36 = {"readBy":{
    #     "jim":{"version":28,"digest":"EEFF792157ADBB2311938D7358F0588B"},
    #     "mike":{"version":30,"digest":"1E43325205374FBBE0E72DAB5930F8DB"}

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'fileInfo')

    def readBy(self):
        return self.getKeys('readBy')

    def Update(self, usersToRename):
        try:
            self.updateDictKeyVal("readBy", usersToRename)
        except:
            print("Error process FileInfo: %s" % self.json)

class Workflow(SwarmObject):

    # swarm-workflow-fffffff5 = {"on_submit":{"with_review":{"rule":"approved","mode":"inherit"},"without_review":{"rule":"reject","mode":"inherit"}},
    # "owners":["swarm-group-dev","dave", "scott"],
    # "end_rules":{"update":{"rule":"no_checking","mode":"inherit"}},
    # "auto_approve":{"rule":"never","mode":"inherit"},
    # etc...

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'workflow')

    def owners(self):
        return self.getVal('owners')

    def Update(self, usersToRename):
        self.updateArrayVal("owners", usersToRename)

class SwarmRenameUsers(object):
    """See module doc string for details"""

    def __init__(self, *args, **kwargs):
        self.parse_args(__doc__, args)

    def parse_args(self, doc, args):
        """Common parsing and setting up of args"""
        desc = textwrap.dedent(doc)
        parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=desc,
            epilog="Copyright (c) 2021 Perforce Software, Inc."
        )
        self.add_parse_args(parser)     # Should be implemented by subclass
        self.options = parser.parse_args(args=args)
        self.init_logger()
        self.logger.debug("Command Line Options: %s\n" % self.options)

    def add_parse_args(self, parser, default_log_file=None, default_verbosity=None):
        """Default trigger arguments - common to all triggers
        :param default_verbosity:
        :param default_log_file:
        :param parser:
        """
        if not default_log_file:
            default_log_file = DEFAULT_LOG_FILE
        if not default_verbosity:
            default_verbosity = DEFAULT_VERBOSITY
        parser.add_argument('-p', '--port', default=None,
                            help="Perforce server port - set using %%serverport%%. Default: $P4PORT")
        parser.add_argument('-u', '--user', default=None, help="Perforce user. Default: $P4USER")
        parser.add_argument('-L', '--log', default=default_log_file, help="Default: " + default_log_file)
        parser.add_argument('-i', '--input', help="Name of a file containing a list of users: oldname newname")
        parser.add_argument('-v', '--verbosity',
                            nargs='?',
                            const="INFO",
                            default=default_verbosity,
                            choices=('DEBUG', 'WARNING', 'INFO', 'ERROR', 'FATAL'),
                            help="Output verbosity level. Default is: " + default_verbosity)

    def init_logger(self, logger_name=None):
        if not logger_name:
            logger_name = LOGGER_NAME
        self.logger = logging.getLogger(logger_name)
        self.logger.setLevel(self.options.verbosity)
        logformat = '%(levelname)s %(asctime)s %(filename)s %(lineno)d: %(message)s'
        logging.basicConfig(format=logformat, filename=self.options.log, level=self.options.verbosity)
        formatter = logging.Formatter('%(message)s')
        ch = logging.StreamHandler(sys.stderr)
        ch.setLevel(logging.INFO)
        ch.setFormatter(formatter)
        self.logger.addHandler(ch)

    def renameObject(self, p4, usersToRename, p4key, klassName, klass):
        updateCount = 0
        count = 0
        values = p4.run_keys("-e", p4key)
        self.logger.info("%s: %d" % (klassName, len(values)))
        for v in values:
            count += 1
            if count % 1000 == 0:
                self.logger.info("Progress %s: %d updated, %d so far" % (klassName, updateCount, count))
            obj = klass(json.loads(v['value']))
            obj.Update(usersToRename)
            if obj.updated:
                updateCount += 1
                p4.run_key(v['key'], json.dumps(obj.json, separators=(',', ':')))
        self.logger.info("%s Updated: %d" % (klassName, updateCount))

    def renameUserRecords(self, p4, usersToRename, p4key, klassName):
        updateCount = 0
        count = 0
        values = p4.run_keys("-e", p4key)
        users = {}
        for v in values:
            users[v['key']] = v['value']
        self.logger.info("%s: %d" % (klassName, len(values)))
        for v in values:
            count += 1
            if count % 100 == 0:
                self.logger.info("Progress %s: %d updated, %d so far" % (klassName, updateCount, count))
            u = v['key'].replace("swarm-user-", "")
            if u in usersToRename:
                updateCount += 1
                userID = "swarm-user-%s" % usersToRename[u]
                p4.run_key(userID, v['value'])
                p4.run_key('-d', v['key'])
        self.logger.info("%s Updated: %d" % (klassName, updateCount))

    def renameUsers(self, p4, usersToRename):
        self.renameObject(p4, usersToRename, "swarm-activity-*", "Activity", Activity)
        self.renameObject(p4, usersToRename, "swarm-comment-*", "Comment", Comment)
        self.renameObject(p4, usersToRename, "swarm-project-*", "Project", Project)
        self.renameObject(p4, usersToRename, "swarm-review-*", "Review", Review)
        self.renameObject(p4, usersToRename, "swarm-fileInfo-*", "FileInfo", FileInfo)
        self.renameObject(p4, usersToRename, "swarm-workflow-*", "Workflow", Workflow)
        self.renameUserRecords(p4, usersToRename, "swarm-user-*", "User")

    def run(self):
        """Runs script"""
        try:
            self.logger.debug("%s: starting" % script_name)

            p4 = P4.P4()
            if self.options.port:
                p4.port = self.options.port
            if self.options.user:
                p4.user = self.options.user
            p4.connect()

            usersToRename = {}
            if self.options.input:
                with open(self.options.input, "r") as f:
                    for line in f.readlines():
                        oldUser, newUser = line.split()
                        usersToRename[oldUser] = newUser

            self.renameUsers(p4, usersToRename)

        except Exception as e:
            self.logger.exception(e)
            print(str(e))

if __name__ == '__main__':
    """ Main Program"""
    obj = SwarmRenameUsers(*sys.argv[1:])
    sys.exit(obj.run())
