# -*- coding: utf-8 -*-
"""
The Django Admin Generator is a project which can automatically generate
(scaffold) a Django Admin for you. By doing this it will introspect your
models and automatically generate an Admin with properties like:

 - `list_display` for all local fields
 - `list_filter` for foreign keys with few items
 - `raw_id_fields` for foreign keys with a lot of items
 - `search_fields` for name and `slug` fields
 - `prepopulated_fields` for `slug` fields
 - `date_hierarchy` for `created_at`, `updated_at` or `joined_at` fields

The original source and latest version can be found here:
https://github.com/WoLpH/django-admin-generator/
"""

import re

from django.apps import apps
from django.conf import settings
from django.core.management.base import LabelCommand, CommandError
from django.db import models

from django_extensions.management.utils import signalcommand

# Configurable constants
MAX_LINE_WIDTH = getattr(settings, "MAX_LINE_WIDTH", 78)
INDENT_WIDTH = getattr(settings, "INDENT_WIDTH", 4)
LIST_FILTER_THRESHOLD = getattr(settings, "LIST_FILTER_THRESHOLD", 25)
RAW_ID_THRESHOLD = getattr(settings, "RAW_ID_THRESHOLD", 100)

LIST_FILTER = getattr(
    settings,
    "LIST_FILTER",
    (
        models.DateField,
        models.DateTimeField,
        models.ForeignKey,
        models.BooleanField,
    ),
)

SEARCH_FIELD_NAMES = getattr(
    settings,
    "SEARCH_FIELD_NAMES",
    (
        "name",
        "slug",
    ),
)

DATE_HIERARCHY_NAMES = getattr(
    settings,
    "DATE_HIERARCHY_NAMES",
    (
        "joined_at",
        "updated_at",
        "created_at",
    ),
)

PREPOPULATED_FIELD_NAMES = getattr(settings, "PREPOPULATED_FIELD_NAMES", ("slug=name",))

PRINT_IMPORTS = getattr(
    settings,
    "PRINT_IMPORTS",
    """# -*- coding: utf-8 -*-
from django.contrib import admin

from .models import %(models)s
""",
)

PRINT_ADMIN_CLASS = getattr(
    settings,
    "PRINT_ADMIN_CLASS",
    """

@admin.register(%(name)s)
class %(name)sAdmin(admin.ModelAdmin):%(class_)s
""",
)

PRINT_ADMIN_PROPERTY = getattr(
    settings,
    "PRINT_ADMIN_PROPERTY",
    """
    %(key)s = %(value)s""",
)


class UnicodeMixin:
    """
    Mixin class to handle defining the proper __str__/__unicode__
    methods in Python 2 or 3.
    """

    def __str__(self):
        return self.__unicode__()


class AdminApp(UnicodeMixin):
    def __init__(self, app_config, model_res, **options):
        self.app_config = app_config
        self.model_res = model_res
        self.options = options

    def __iter__(self):
        for model in self.app_config.get_models():
            admin_model = AdminModel(model, **self.options)

            for model_re in self.model_res:
                if model_re.search(admin_model.name):
                    break
            else:
                if self.model_res:
                    continue

            yield admin_model

    def __unicode__(self):
        return "".join(self._unicode_generator())

    def _unicode_generator(self):
        models_list = [admin_model.name for admin_model in self]
        yield PRINT_IMPORTS % dict(models=", ".join(models_list))

        admin_model_names = []
        for admin_model in self:
            yield PRINT_ADMIN_CLASS % dict(
                name=admin_model.name,
                class_=admin_model,
            )
            admin_model_names.append(admin_model.name)

    def __repr__(self):
        return "<%s[%s]>" % (
            self.__class__.__name__,
            self.app.name,
        )


class AdminModel(UnicodeMixin):
    PRINTABLE_PROPERTIES = (
        "list_display",
        "list_filter",
        "raw_id_fields",
        "search_fields",
        "prepopulated_fields",
        "date_hierarchy",
    )

    def __init__(
        self,
        model,
        raw_id_threshold=RAW_ID_THRESHOLD,
        list_filter_threshold=LIST_FILTER_THRESHOLD,
        search_field_names=SEARCH_FIELD_NAMES,
        date_hierarchy_names=DATE_HIERARCHY_NAMES,
        prepopulated_field_names=PREPOPULATED_FIELD_NAMES,
        **options,
    ):
        self.model = model
        self.list_display = []
        self.list_filter = []
        self.raw_id_fields = []
        self.search_fields = []
        self.prepopulated_fields = {}
        self.date_hierarchy = None
        self.search_field_names = search_field_names
        self.raw_id_threshold = raw_id_threshold
        self.list_filter_threshold = list_filter_threshold
        self.date_hierarchy_names = date_hierarchy_names
        self.prepopulated_field_names = prepopulated_field_names

    def __repr__(self):
        return "<%s[%s]>" % (
            self.__class__.__name__,
            self.name,
        )

    @property
    def name(self):
        return self.model.__name__

    def _process_many_to_many(self, meta):
        raw_id_threshold = self.raw_id_threshold
        for field in meta.local_many_to_many:
            if hasattr(field, "remote_field"):
                related_model = getattr(
                    field.remote_field, "related_model", field.remote_field.model
                )
            else:
                raise CommandError("Unable to process ManyToMany relation")
            related_objects = related_model.objects.all()
            if related_objects[:raw_id_threshold].count() < raw_id_threshold:
                yield field.name

    def _process_fields(self, meta):
        parent_fields = meta.parents.values()
        for field in meta.fields:
            name = self._process_field(field, parent_fields)
            if name:
                yield name

    def _process_foreign_key(self, field):
        raw_id_threshold = self.raw_id_threshold
        list_filter_threshold = self.list_filter_threshold
        max_count = max(list_filter_threshold, raw_id_threshold)
        if hasattr(field, "remote_field"):
            related_model = getattr(
                field.remote_field, "related_model", field.remote_field.model
            )
        else:
            raise CommandError("Unable to process ForeignKey relation")
        related_count = related_model.objects.all()
        related_count = related_count[:max_count].count()

        if related_count >= raw_id_threshold:
            self.raw_id_fields.append(field.name)

        elif related_count < list_filter_threshold:
            self.list_filter.append(field.name)

        else:  # pragma: no cover
            pass  # Do nothing :)

    def _process_field(self, field, parent_fields):
        if field in parent_fields:
            return

        field_name = str(field.name)
        self.list_display.append(field_name)
        if isinstance(field, LIST_FILTER):
            if isinstance(field, models.ForeignKey):
                self._process_foreign_key(field)
            else:
                self.list_filter.append(field_name)

        if field.name in self.search_field_names:
            self.search_fields.append(field_name)

        return field_name

    def __unicode__(self):
        return "".join(self._unicode_generator())

    def _yield_value(self, key, value):
        if isinstance(value, (list, set, tuple)):
            return self._yield_tuple(key, tuple(value))
        elif isinstance(value, dict):
            return self._yield_dict(key, value)
        elif isinstance(value, str):
            return self._yield_string(key, value)
        else:  # pragma: no cover
            raise TypeError("%s is not supported in %r" % (type(value), value))

    def _yield_string(self, key, value, converter=repr):
        return PRINT_ADMIN_PROPERTY % dict(
            key=key,
            value=converter(value),
        )

    def _yield_dict(self, key, value):
        row_parts = []
        row = self._yield_string(key, value)
        if len(row) > MAX_LINE_WIDTH:
            row_parts.append(self._yield_string(key, "{", str))
            for k, v in value.items():
                row_parts.append("%s%r: %r" % (2 * INDENT_WIDTH * " ", k, v))

            row_parts.append(INDENT_WIDTH * " " + "}")
            row = "\n".join(row_parts)

        return row

    def _yield_tuple(self, key, value):
        row_parts = []
        row = self._yield_string(key, value)
        if len(row) > MAX_LINE_WIDTH:
            row_parts.append(self._yield_string(key, "(", str))
            for v in value:
                row_parts.append(2 * INDENT_WIDTH * " " + repr(v) + ",")

            row_parts.append(INDENT_WIDTH * " " + ")")
            row = "\n".join(row_parts)

        return row

    def _unicode_generator(self):
        self._process()
        for key in self.PRINTABLE_PROPERTIES:
            value = getattr(self, key)
            if value:
                yield self._yield_value(key, value)

    def _process(self):
        meta = self.model._meta

        self.raw_id_fields += list(self._process_many_to_many(meta))
        field_names = list(self._process_fields(meta))

        for field_name in self.date_hierarchy_names[::-1]:
            if field_name in field_names and not self.date_hierarchy:
                self.date_hierarchy = field_name
                break

        for k in sorted(self.prepopulated_field_names):
            k, vs = k.split("=", 1)
            vs = vs.split(",")
            if k in field_names:
                incomplete = False
                for v in vs:
                    if v not in field_names:
                        incomplete = True
                        break

                if not incomplete:
                    self.prepopulated_fields[k] = vs

        self.processed = True


class Command(LabelCommand):
    help = """Generate a `admin.py` file for the given app (models)"""
    # args = "[app_name]"
    can_import_settings = True

    def add_arguments(self, parser):
        parser.add_argument("app_name")
        parser.add_argument("model_name", nargs="*")
        parser.add_argument(
            "-s",
            "--search-field",
            action="append",
            default=SEARCH_FIELD_NAMES,
            help="Fields named like this will be added to `search_fields`"
            " [default: %(default)s]",
        )
        parser.add_argument(
            "-d",
            "--date-hierarchy",
            action="append",
            default=DATE_HIERARCHY_NAMES,
            help="A field named like this will be set as `date_hierarchy`"
            " [default: %(default)s]",
        )
        parser.add_argument(
            "-p",
            "--prepopulated-fields",
            action="append",
            default=PREPOPULATED_FIELD_NAMES,
            help="These fields will be prepopulated by the other field."
            "The field names can be specified like `spam=eggA,eggB,eggC`"
            " [default: %(default)s]",
        )
        parser.add_argument(
            "-l",
            "--list-filter-threshold",
            type=int,
            default=LIST_FILTER_THRESHOLD,
            metavar="LIST_FILTER_THRESHOLD",
            help="If a foreign key has less than LIST_FILTER_THRESHOLD items "
            "it will be added to `list_filter` [default: %(default)s]",
        )
        parser.add_argument(
            "-r",
            "--raw-id-threshold",
            type=int,
            default=RAW_ID_THRESHOLD,
            metavar="RAW_ID_THRESHOLD",
            help="If a foreign key has more than RAW_ID_THRESHOLD items "
            "it will be added to `list_filter` [default: %(default)s]",
        )

    @signalcommand
    def handle(self, *args, **options):
        app_name = options["app_name"]

        try:
            app = apps.get_app_config(app_name)
        except LookupError:
            self.stderr.write("This command requires an existing app name as argument")
            self.stderr.write("Available apps:")
            app_labels = [app.label for app in apps.get_app_configs()]
            for label in sorted(app_labels):
                self.stderr.write("    %s" % label)
            return

        model_res = []
        for arg in options["model_name"]:
            model_res.append(re.compile(arg, re.IGNORECASE))

        self.stdout.write(AdminApp(app, model_res, **options).__str__())
