import os, sys
from oslo_config import cfg
from swift.common.ring import Ring as swiftRing
from random import randint
import json

# Initialize Arguments
CLI_OPTS = [           
    cfg.IntOpt('nb_tests', short='n', default=500, help='Number of partition to test'),
    cfg.BoolOpt('json', short='j', help='Show full differences in json formatted string'),
    cfg.StrOpt('Ring1', positional=True, required=True, help='First Ring File to compare'),
    cfg.StrOpt('Ring2', positional=True, required=True, help='Second Ring File to compare'),
]

def _prepare_config():
    """
    Prepare the oslo_config of scripts by analyse arguments
    return: the oslo_config object
    """
    CONF = cfg.ConfigOpts()
    CONF.register_cli_opts(CLI_OPTS)
    return CONF

class Ring():
    """Class representing an Ring"""

    def __init__(self,file):
        """
        Instance method to initialize a Ring
        param file: file for the ring (with ring.gz extension)
        """
        self.ring_name = os.path.basename(file)[:-len('.ring.gz')]
        self.ring = swiftRing(file)
        if '-' in self.ring_name and self.ring_name.startswith('object'):
            self.loc = 'objects-' + ring_name.split('-', 1)[1]
        else:
            self.loc = self.ring_name + 's'

    def get_nodes(self,part):
        """
        Instance method that return list of node for a partition
        param part: partition to test
        return: list of node and device that have the partition
        """
        part = int(part)
        primary_nodes = self.ring.get_part_nodes(part)
        return primary_nodes

    def get_md5(self):
        """
        Instance method that return de md5 for the ring
        return: md5
        """
        return self.ring.md5

    def partitions_count(self):
        """
        Instance method that return number of partition for the ring
        return: number of partitions
        """
        power = 24
        while not self.get_nodes(2**power):
            power -= 1
        return 2 ** (power + 1)

    def compare_with(self,other_ring,part):
        """
        Instance method that compare this ring with another
        If more than 1 difference, display an Error Message and exit
        param other_ring: the other ring to compare
        param part: the partition to test
        return: the count of difference and nodes/device affected
        """
        my_nodes = self.get_nodes(part)
        other_nodes = other_ring.get_nodes(part)
        diff_count = 0
        diff = {}
        for i in range(3): # 3 because replica 3 in both rings
            diff_items = {k: my_nodes[i][k] for k in my_nodes[i] if k in other_nodes[i] and my_nodes[i][k] != other_nodes[i][k]}
            if diff_items and not (len(diff_items) == 1 and "weight" in diff_items):
                diff[self.ring_name] = {'ip': my_nodes[i]['ip'],
                                        'port': my_nodes[i]['port'],
                                        'device': my_nodes[i]['device']}
                diff[other_ring.ring_name] = {'ip': other_nodes[i]['ip'],
                                        'port': other_nodes[i]['port'],
                                        'device': other_nodes[i]['device']}
                diff_count += 1
                if diff_count > 1:
                    print("[ERROR] more than 1 differences for one partition, new ring is not valid.", file=sys.stderr)
                    sys.exit(1)
        return diff_count,diff

def main():
    """
    Entry point for the script
    """
    args = _prepare_config()
    try:
        args(sys.argv[1:])
    except cfg.RequiredOptError as E:
        print(E)
    # Get informations for 2 rings
    ring1 = Ring(args.Ring1)
    ring2 = Ring(args.Ring2)
    md5_1 = ring1.get_md5()
    md5_2 = ring2.get_md5()
    partitions_count = ring1.partitions_count()
 
    diffs = {}
    nb_diffs = 0
    # Compare the rings for args.nb_test random partitions
    for i in range(args.nb_tests):
        part = randint(1,partitions_count)
        nb_diff, diff = ring1.compare_with(ring2,part)
        nb_diffs += nb_diff
        if diff:
            diffs[f"Partition {part}"] = diff

    # Show result
    print()
    if args.json:
        diffs[f"Ring {ring1.ring_name} md5"] = md5_1
        diffs[f"Ring {ring2.ring_name} md5"] = md5_2
        print(json.dumps(diffs))
    else:
        if md5_1 == md5_2:
            print("md5 for two are identical !")
            sys.exit(0)
        else:
            print(f"Ring {ring1.ring_name} md5 : {md5_1}")
            print(f"Ring {ring2.ring_name} md5 : {md5_2}")
        if nb_diffs:
            print(f"Number of differences between {ring1.ring_name} and {ring2.ring_name}: {nb_diffs}/{args.nb_tests} partitions tested")
        else:
            print(f"No differences find between rings {ring1.ring_name} and {ring2.ring_name} , you can re-run the script for confirmation")
    sys.exit(nb_diffs)

if __name__ == "__main__":
    sys.exit(main())
