import json
from datetime import datetime
from itertools import groupby

import markdown2
from django.urls import reverse
from django.db import models, transaction
from django.utils import timezone
from django.utils.six.moves.urllib.parse import quote
from django.utils.six.moves.urllib.request import urlopen
from pynaco.naco import normalizeSimplified

from .validators import validate_merged_with


class Identifier_Type(models.Model):
    """Custom Identifier Type.

    Used in conjunction with the Identifier model.
    """
    label = models.CharField(
        max_length=255,
        help_text='What kind of data is this? Personal website? Twitter?')

    icon_path = models.CharField(
        max_length=255,
        blank=True,
        help_text='Path to icon image?')

    homepage = models.URLField(
        blank=True,
        help_text='Homepage of label. Twitter.com, Facebook.com, etc')

    class Meta:
        ordering = ['label']
        verbose_name = 'Identifier Type'

    def __str__(self):
        return self.label


class Identifier(models.Model):
    """An Identifier for a Name models instance. Most commonly
    represented as a permalink.

    This is used in conjunction with the Identifier Type model to
    specify the type of Identifier the instance represents. An example
    instance would have an Identifier Type of `Twitter` and the value
    field would have the permalink to the Name's Twitter profile page.
    """
    type = models.ForeignKey(
        'Identifier_Type',
        help_text="Catagorize this record's identifiers here",
        on_delete=models.CASCADE)

    belong_to_name = models.ForeignKey('Name', on_delete=models.CASCADE)
    value = models.CharField(max_length=500)
    visible = models.BooleanField(default=True)
    order = models.IntegerField(default=0)

    class Meta:
        ordering = ['order', 'type']

    def __str__(self):
        return self.value


class NoteManager(models.Manager):
    """Custom Model Manager for the Note model."""
    def public_notes(self):
        return self.get_queryset().exclude(note_type=self.model.NONPUBLIC)


class Note(models.Model):
    """A note regarding the related Name model instance."""
    BIOGRAPHICAL_HISTORICAL = 0
    DELETION_INFORMATION = 1
    NONPUBLIC = 2
    SOURCE = 3
    OTHER = 4

    NOTE_TYPE_CHOICES = (
        (BIOGRAPHICAL_HISTORICAL, 'Biographical/Historical'),
        (DELETION_INFORMATION, 'Deletion Information'),
        (NONPUBLIC, 'Nonpublic'),
        (SOURCE, 'Source'),
        (OTHER, 'Other')
    )

    note = models.TextField(help_text='Enter notes about this record here')
    note_type = models.IntegerField(choices=NOTE_TYPE_CHOICES)
    belong_to_name = models.ForeignKey('Name', on_delete=models.CASCADE)

    objects = NoteManager()

    class Meta:
        base_manager_name = 'objects'

    def get_note_type_label(self):
        """Returns the label associated with an instance's
        note_type.
        """
        id, note_type = self.NOTE_TYPE_CHOICES[self.note_type]
        return note_type

    def __str__(self):
        return self.note


class Variant(models.Model):
    """Defines an alternative form that a Name may be displayed."""
    ACRONYM = 0
    ABBREVIATION = 1
    TRANSLATION = 2
    EXPANSION = 3
    OTHER = 4

    VARIANT_TYPE_CHOICES = (
        (ACRONYM, 'Acronym'),
        (ABBREVIATION, 'Abbreviation'),
        (TRANSLATION, 'Translation'),
        (EXPANSION, 'Expansion'),
        (OTHER, 'Other')
    )

    belong_to_name = models.ForeignKey('Name', on_delete=models.CASCADE)
    variant_type = models.IntegerField(
        choices=VARIANT_TYPE_CHOICES,
        help_text='Choose variant type.')

    variant = models.CharField(
        max_length=255,
        help_text='Fill in the other name variants, if any.')

    normalized_variant = models.CharField(
        max_length=255,
        editable=False,
        help_text='NACO normalized variant text')

    def get_variant_type_label(self):
        """Returns the label associated with an instance's
        variant_type.
        """
        id, variant_type = self.VARIANT_TYPE_CHOICES[self.variant_type]
        return variant_type

    def save(self):
        self.normalized_variant = normalizeSimplified(self.variant)
        super(Variant, self).save()

    def __str__(self):
        return self.variant


class TicketingManager(models.Manager):
    """Custom manager for the BaseTicketing model."""

    def create(self, *args, **kwargs):
        """Override the create method.

        Create or get the single object. If one already exists,
        delete it and create a new one so we auto-increment the id.
        """
        obj, created = self.get_or_create(stub=self.model.STUB_DEFAULT)
        if not created:
            with transaction.atomic():
                obj.delete()
                obj = self.create(stub=self.model.STUB_DEFAULT)
        return obj


class BaseTicketing(models.Model):
    """Creates a custom app-level identifier.

    This leverages the autoincrement primary key field to
    create custom unique identifier. An example identifier
    would be `nm0000001`.
    """
    # Explicitly set the id of the model, even though it is the same
    # as the one Django gives it.
    id = models.AutoField(null=False, primary_key=True)

    # This is just the smallest placeholder we can create that we can
    # replace into to generate a new id.
    STUB_DEFAULT = True
    stub = models.BooleanField(null=False, default=STUB_DEFAULT, unique=True)

    # Override the default manager
    objects = TicketingManager()

    @property
    def ticket(self):
        """Alias for id"""
        return self.id

    def __str__(self):
        return 'nm{ticket:07d}'.format(ticket=self.ticket)


class NameManager(models.Manager):
    """Custom Manager for the Name model.

    Provides additional methods that are useful in calculating
    statistics on Name model instances.
    """

    def visible(self):
        """Retrieves all Name objects that have an Active record status
        and are not merged with any other Name objects.
        """
        return self.get_queryset().filter(
            record_status=self.model.ACTIVE, merged_with=None)

    def active_type_counts(self):
        """Calculates counts of Name objects by Name Type.

        Statistics are based off of the queryset returned by visible.
        The total number is calculated using the count method. All
        additional figures are calculated using Python to reduce the number
        of queries.
        """
        names = self.visible()
        return {
            'total': names.count(),
            'personal': len([n for n in names if n.is_personal()]),
            'organization': len([n for n in names if n.is_organization()]),
            'event': len([n for n in names if n.is_event()]),
            'software': len([n for n in names if n.is_software()]),
            'building': len([n for n in names if n.is_building()])
        }

    def _counts_per_month(self, date_column):
        """Calculates the number of Names by month according to the
        date_column passed in.

        This will return a ValueQuerySet where each element is in the form
        of
            {
               count: <Number of Names for the month>,
               month: <Datetime object for first day of the given month>
            }
        """
        def grouper(name):
            return (getattr(name, date_column).year,
                    getattr(name, date_column).month)

        def convert_key(year, month):
            datetime_obj = datetime(year=year, month=month, day=1)
            tzinfo = timezone.get_current_timezone()
            return timezone.make_aware(datetime_obj, tzinfo)

        results = self.all().order_by(date_column)

        return [
            dict(month=convert_key(*key), count=len(list(value)))
            for key, value in groupby(results, grouper)
        ]

    def created_stats(self):
        """Returns a list of the number of Names created per
        month.
        """
        return self._counts_per_month('date_created')

    def modified_stats(self):
        """Returns a list of the number of Names modified per
        month.
        """
        return self._counts_per_month('last_modified')


class Name(models.Model):
    """The authorized version of a name that is used to unambiguously
    refer to a person, organization, event, building or piece of
    software.
    """
    ACTIVE = 0
    DELETED = 1
    SUPPRESSED = 2

    RECORD_STATUS_CHOICES = (
        (ACTIVE, 'Active'),
        (DELETED, 'Deleted'),
        (SUPPRESSED, 'Suppressed')
    )

    PERSONAL = 0
    ORGANIZATION = 1
    EVENT = 2
    SOFTWARE = 3
    BUILDING = 4

    NAME_TYPE_CHOICES = (
        (PERSONAL, 'Personal'),
        (ORGANIZATION, 'Organization'),
        (EVENT, 'Event'),
        (SOFTWARE, 'Software'),
        (BUILDING, 'Building')
    )

    DATE_DISPLAY_LABELS = {
        PERSONAL: {
            'type': 'Personal',
            'begin': 'Date of Birth',
            'end': 'Date of Death'
        },
        ORGANIZATION: {
            'type': 'Organization',
            'begin': 'Founded Date',
            'end': 'Defunct'
        },
        EVENT: {
            'type': 'Event',
            'begin': 'Begin Date',
            'end': 'End Date'
        },
        SOFTWARE: {
            'type': 'Software',
            'begin': 'Begin Date',
            'end': 'End Date'
        },
        BUILDING: {
            'type': 'Building',
            'begin': 'Erected Date',
            'end': 'Demolished Date',
        },
        None: {
            'type': None,
            'begin': 'Born/Founded Date',
            'end': 'Died/Defunct Date'
        }
    }

    NAME_TYPE_SCHEMAS = {
        PERSONAL: 'http://schema.org/Person',
        ORGANIZATION: 'http://schema.org/Organization',
        BUILDING: 'http://schema.org/Place'
    }

    name = models.CharField(
        max_length=255,
        help_text='Please use the general reverse order: LAST, FIRST')

    normalized_name = models.CharField(
        max_length=255,
        editable=False,
        help_text='NACO normalized form of the name')

    name_type = models.IntegerField(choices=NAME_TYPE_CHOICES)

    # Date, month or year of birth or incorporation of the name
    begin = models.CharField(
        max_length=25,
        blank=True,
        help_text='Conforms to EDTF format YYYY-MM-DD')

    # Date, month of year of death or un-incorporation of the name
    end = models.CharField(
        max_length=25,
        blank=True,
        help_text='Conforms to EDTF format YYYY-MM-DD')