"""This module holds the ``clean_media`` command"""

import os

from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.management.color import supports_color
from django.utils import termcolors

from mtp.apps.common.utils import get_file_fields


def get_media_on_disk():
    """Get the path of all media present on disk

    Returns
    -------
    set
        The set of the path of all media on disk, relative to MEDIA_ROOT:

    """

    paths = set()

    for root, __, files in os.walk(settings.MEDIA_ROOT):
        paths.update([
            os.path.relpath(os.path.join(root, filename), settings.MEDIA_ROOT)
            for filename in files
        ])

    return paths


def get_media_on_db_by_model():
    """Get all path of media store in database for each model-field

    Returns
    -------
    dict
        A dict with an entry for each model, then as value a dict with an entry for each
        field, holding a set with all paths (relative to MEDIA_ROOT) for this field.
        Only fields with at least one path are present, and only model with at least one
        field are present too:

        {
            mtp.apps.theapp.TheModel: {
                field1: {'path1', 'path2',..},
                ...
            },
            ...
        }

    """

    data = {}

    for model, fields in get_file_fields().items():

        if len(fields) == 1:

            # Only one field, we can filter in db to get filled paths

            by_field = {
                fields[0].name: model.objects.filter(
                    # when the field is not null
                    **{'%s__isnull' % fields[0].name: False}
                ).exclude(
                    # but also not an empty string
                    **{fields[0].name: ''}
                ).values_list(
                    # get only the paths
                    fields[0].name,
                    flat=True
                )
            }

        else:

            # In case of many field it's easier to get all values at once then filter them

            field_names = [field.name for field in fields]

            # we'll have [(field1A, field2A, ...), (field1B, field2B, ...), ...]
            model_data = model.objects.values_list(*field_names)

            # Use inner zip to get [(field1A, field1B, ...), (field2A, field2B, ...), ...]
            # Use filter to remove None or empty paths
            # Use dict(zip()) to get a dict by field
            by_field = dict(zip(
                field_names,
                [set(filter(None, paths)) for paths in zip(*model_data)]  # pylint: disable=bad-builtin
            ))

        # Remove fields without paths
        by_field = {field_name: paths for field_name, paths in by_field.items() if paths}

        # Save for the model only if we have something
        if by_field:
            data[model] = by_field

    return data


def get_media_on_db():
    """Get all media stored on database

    Returns
    -------
    set
        A set containing all paths in database, relative to MEDIA_ROOT

    """

    all_paths = set()

    for fields in get_media_on_db_by_model().values():
        for paths in fields.values():
            all_paths.update(paths)

    return all_paths


def get_on_disk_not_on_db(exclude_default=False):
    """Get all media present on disk with no matching on database

    Parameters
    ----------
    exclude_default: bool
        When set to ``True``, the returned paths won't include the default media.
        Default to ``False``

    Returns
    -------
    set
        A set containing all paths on disk, relative to MEDIA_ROOT

    """

    on_db = get_media_on_db()
    on_disk = get_media_on_disk()

    result = on_disk.difference(on_db)

    if exclude_default and result:
        result = exclude_default_media(result)

    return result


def exclude_default_media(paths):
    """Exclude from the given path all the default media

    Parameters
    ----------
    paths: iterable
        An iterable of path from which to remove the default media

    Returns
    -------
    set
        A set of all given path without the default media

    """

    return set([path for path in paths if not path.startswith('default/')])


class Command(BaseCommand):
    """Command to clean unused media"""

    help = "Clean unused media."

    def add_arguments(self, parser):
        """Add the --dry-run argument

        For the parameters, see ``BaseCommand.add_arguments``

        """
        parser.add_argument(
            '--dry-run',
            action='store_true',
            dest='dry_run',
            default=False,
            help="Just show what media would have been deleted; don't actually delete them."
        )

    def handle(self, **options):
        """Find the non-used media and delete them if not in dry-run mode

        For the parameters, see ``BaseCommand.handle``

        """

        dry_run = options.get('dry_run', False)

        # ``self.style.SUCCESS`` is only available for django 1.9+
        if options['no_color'] or not supports_color():
            self.style.SUCCESS = lambda text: text
        else:
            self.style.SUCCESS = termcolors.make_style(fg='green', opts=['bold'])

        paths = list(sorted(get_on_disk_not_on_db(exclude_default=True)))
        nb_paths = len(paths)

        self.stdout.write("")  # add a blank line

        if not paths:
            self.stdout.write("No orphan media files detected to delete.")
            return

        self.stdout.write("%s orphan media file%s detected to delete%s:" % (
            nb_paths, 's' if nb_paths > 1 else '', ' (dry-run)' if dry_run else ''
        ))

        self.stdout.write("")  # add a blank line

        errors = 0
        for path in paths:
            self.stdout.write(path, ending="")  # no newline, to add OK/ERROR when done

            if dry_run:
                self.stdout.write("")  # to end previous write without ending
            else:
                full_path = os.path.join(settings.MEDIA_ROOT, path)

                try:
                    os.remove(full_path)
                except Exception as exc:  # pylint: disable=broad-except
                    errors += 1
                    self.stdout.write(self.style.ERROR("  ERROR"))
                    self.stderr.write("-> ERROR while removing %s: %s" % (full_path, exc))
                else:
                    self.stdout.write(self.style.SUCCESS("  OK"))

        self.stdout.write("")  # add a blank line

        if dry_run:
            self.stdout.write(
                "No orphan media files deleted (%s found) because of dry-run mode." % nb_paths
            )

        elif errors:
            success = nb_paths - errors
            self.stdout.write("%s orphan media file%s deleted, and %s couldn't." % (
                success, 's' if success > 1 else '', errors
            ))
        else:
            self.stdout.write("%s orphan media file%s deleted." % (
                nb_paths, 's' if nb_paths > 1 else ''
            ))
