from __future__ import unicode_literals

import datetime, operator
from optparse import make_option

from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
from django.db.models import Q, Count
from django.contrib.contenttypes.models import ContentType
from django.utils.six.moves import input

from reversion.models import Revision, Version
from django.db.utils import DatabaseError


class Command(BaseCommand):
    option_list = BaseCommand.option_list + (
        make_option("--date", "-t",
            dest="date",
            help="Delete only revisions older than the specify date. The date should be a valid date given in the ISO format (YYYY-MM-DD)"),
        make_option("--days", "-d",
            dest="days",
            default=0,
            type="int",
            help="Delete only revisions older than the specify number of days."),
        make_option("--keep-revision", "-k",
            dest="keep",
            default=0,
            type="int",
            help="Keep the specified number of revisions (most recent) for each object."),
        make_option("--force", "-f",
            action="store_true",
            dest="force",
            default=False,
            help="Force the deletion of revisions even if an other app/model is involved"),
        make_option("--no-confirmation", "-c",
            action="store_false",
            dest="confirmation",
            default=True,
            help="Disable the confirmation before deleting revisions"),
        make_option('--manager', '-m', dest='manager',
            help='Delete revisions only from specified manager. Defaults from all managers.'),
        make_option('--database', action='store', dest='database',
            help='Nominates a database to delete revisions from.'),
        )
    args = "[appname, appname.ModelName, ...] [--date=YYYY-MM-DD | days=0] [--keep=0] [--force] [--no-confirmation]"
    help = """Deletes revisions for a given app [and model] and/or before a specified delay or date.
    
If an app or a model is specified, revisions that have an other app/model involved will not be deleted. Use --force to avoid that.

You can specify only apps/models or only delay or date or only a nuber of revision to keep or use all possible combinations of these options.

Examples:

        deleterevisions myapp
    
    That will delete every revisions of myapp (except if there's an other app involved in the revision)
    
        deleterevisions --date=2010-11-01
    
    That will delete every revision created before November 1, 2010 for all apps.
    
        deleterevisions myapp.mymodel --days=365 --force
        
    That will delete every revision of myapp.model that are older than 365 days, even if there's revisions involving other apps and/or models.
    
        deleterevisions myapp.mymodel --keep=10
        
    That will delete only revisions of myapp.model if there's more than 10 revisions for an object, keeping the 10 most recent revisons.
"""

    def handle(self, *app_labels, **options):
        days = options["days"]
        keep = options["keep"]
        force = options["force"]
        confirmation = options["confirmation"]
        manager = options.get('manager')
        database = options.get('database')
        # I don't know why verbosity is not already an int in Django?
        try:
            verbosity = int(options["verbosity"])
        except ValueError:
            raise CommandError("option -v: invalid choice: '%s' (choose from '0', '1', '2')" % options["verbosity"])

        date = None

        # Validating arguments
        if options["date"]:
            if days:
                raise CommandError("You cannot use --date and --days at the same time. They are exclusive.")

            try:
                date = datetime.datetime.strptime(options["date"], "%Y-%m-%d").date()
            except ValueError:
                raise CommandError("The date you give (%s) is not a valid date. The date should be in the ISO format (YYYY-MM-DD)." % options["date"])

        # Find the date from the days arguments.        
        elif days:
            date = datetime.datetime.now() - datetime.timedelta(days)

        # Build the queries
        revision_query = Revision.objects.using(database).all()
        
        if manager:
            revision_query = revision_query.filter(manager_slug=manager)

        if date:
            revision_query = revision_query.filter(date_created__lt=date)

        if app_labels:
            app_list = set()
            mod_list = set()
            for label in app_labels:
                try:
                    app_label, model_label = label.split(".")
                    mod_list.add((app_label, model_label))
                except ValueError:
                    # This is just an app, no model qualifier.
                    app_list.add(label)

            # Remove models that their app is already in app_list            
            for app, model in mod_list.copy():
                if app in app_list:
                    mod_list.remove((app, model))

            # Build apps/models subqueries
            subqueries = []
            if app_list:
                subqueries.append(Q(app_label__in=app_list))
            if mod_list:
                subqueries.extend([Q(app_label=app, model=model) for app, model in mod_list])
            subqueries = reduce(operator.or_, subqueries)

            if force:
                models = ContentType.objects.using(database).filter(subqueries)
                revision_query = revision_query.filter(version__content_type__in=models)
            else:
                models = ContentType.objects.using(database).exclude(subqueries)
                revision_query = revision_query.exclude(version__content_type__in=models)

        if keep:
            objs = Version.objects.using(database).all()

            # If app is specified, to speed up the loop on theses objects,
            # get only the specified subset.
            if app_labels:
                if force:
                    objs = objs.filter(content_type__in=models)
                else:
                    objs = objs.exclude(content_type__in=models)

            # Get all the objects that have more than the maximum revisions
            objs = objs.values("object_id", "content_type_id").annotate(total_ver=Count("object_id")).filter(total_ver__gt=keep)

            revisions_not_keeped = set()

            # Get all ids of the oldest revisions minus the max allowed
            # revisions for all objects.
            # Was not able to avoid this loop...
            for obj in objs:
                revisions_not_keeped.update(list(Version.objects.using(database).filter(content_type__id=obj["content_type_id"], object_id=obj["object_id"]).order_by("-revision__date_created").values_list("revision_id", flat=True)[keep:]))

            revision_query = revision_query.filter(pk__in=revisions_not_keeped)


        # Prepare message if verbose
        if verbosity > 0:
            if not date and not app_labels and not keep:
                print("All revisions will be deleted for all models.")
            else:
                date_msg = ""
                if date:
                    date_msg = " older than %s" % date.isoformat()
                models_msg = " "
                if app_labels:
                    force_msg = ""
                    if not force:
                        force_msg = " only"
                    models_msg = " having%s theses apps and models:\n- %s\n" % (force_msg, "\n- ".join(sorted(app_list.union(["%s.%s" % (app, model) for app, model in mod_list])),))
                    if date:
                        models_msg = " and" + models_msg
                keep_msg = ""
                if keep:
                    keep_msg = " keeping the %s most recent revisions of each object" % keep

                revision_count = revision_query.count()
                if revision_count:
                    version_query = Version.objects.using(database).all()
                    if date or app_labels or keep:
                        version_query = version_query.filter(revision__in=revision_query)
                    print("%s revision(s)%s%swill be deleted%s.\n%s model version(s) will be deleted." % (revision_count, date_msg, models_msg, keep_msg, version_query.count()))
                else:
                    print("No revision%s%sto delete%s.\nDone" % (date_msg, models_msg, keep_msg))
                    return


        # Ask confirmation
        if confirmation:
            choice = input("Are you sure you want to delete theses revisions? [y|N] ")
            if choice.lower() != "y":
                print("Aborting revision deletion.")
                return


        # Delete versions and revisions
        print("Deleting revisions...")
        
        try:
            revision_query.delete()
        except DatabaseError:
            # may fail on sqlite if the query is too long
            print("Delete failed. Trying again with slower method.")
            for item in revision_query:
                item.delete()
                
        print("Done")
