# -*- coding: utf-8 -*-

import gsxws
import os.path

from django.urls import reverse
from django.template.defaultfilters import slugify
from pytz import common_timezones, country_timezones

from django.db import models
from django.conf import settings

from mptt.managers import TreeManager

from mptt.models import MPTTModel, TreeForeignKey
from django.utils.translation import ugettext_lazy as _

from django.dispatch import receiver
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db.models.signals import post_save

from django.core.cache import cache

from servo import defaults
from servo.lib.utils import empty
from servo.exceptions import ConfigurationError
from servo.validators import file_upload_validator


# Dict for mapping timezones to countries
TIMEZONE_COUNTRY = {}

for cc in country_timezones:
    timezones = country_timezones[cc]
    for timezone in timezones:
        TIMEZONE_COUNTRY[timezone] = cc


class CsvTable(object):
    def __init__(self, colwidth=20):
        self.rowcount = 0
        self.colwidth = colwidth
        self.body = u''
        self.table = u''
        self.header = u''

    def padrow(self, row):
        """
        Pad row to self.colwdith
        """
        r = []
        for c in row:
            r.append(str(c).ljust(self.colwidth))

        return r

    def addheader(self, new_header):
        self.rowcount = self.rowcount + 1
        header = self.padrow(new_header)
        self.header = ''.join(header)

    def addrow(self, new_row):
        row = self.padrow(new_row)
        self.body += ''.join(row) + "\n"

    def has_body(self):
        return self.body != ''

    def __str__(self):
        self.table = self.header + "\n" + self.body
        return self.table

    def __str__(self):
        return str(self).encode('utf-8')



class TaggedItem(models.Model):
    """
    A generic tagged item
    """
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    tag = models.CharField(max_length=128)
    slug = models.SlugField()
    color = models.CharField(max_length=8, default="")

    def save(self, *args, **kwargs):
        self.slug = slugify(self.tag)
        super(TaggedItem, self).save(*args, **kwargs)

    def __str__(self):
        return self.tag

    class Meta:
        app_label = "servo"


class FlaggedItem(models.Model):
    """
    A flagged item. This could also be a tag, but how?
    """
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    flagged_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        on_delete=models.SET_NULL
    )


class Event(models.Model):
    """
    Something that happens
    """
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    description = models.CharField(max_length=255)
    triggered_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        on_delete=models.SET_NULL)
    triggered_at = models.DateTimeField(auto_now_add=True)
    handled_at = models.DateTimeField(null=True)

    action = models.CharField(max_length=32)
    priority = models.SmallIntegerField(default=1)

    notify_users = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name="notifications" # request.user.notifications
    )

    def save(self, *args, **kwargs):
        saved = super(Event, self).save(*args, **kwargs)

        if settings.ENABLE_RULES is True:
            from servo.tasks import apply_rules
            apply_rules.delay(self)

    def get_status(self):
        from servo.models import Status
        return Status.objects.get(title=self.description)

    def get_icon(self):
        return "events/%s-%s" % (self.content_type.name, self.action)

    def get_link(self):
        return self.content_object.get_absolute_url()

    def get_class(self):
        return "disabled" if self.handled_at else ""

    def __str__(self):
        return self.description

    class Meta:
        app_label = "servo"
        ordering = ('priority', '-id',)


class GsxAccount(models.Model):

    title = models.CharField(max_length=128, default=_("New GSX Account"))
    sold_to = models.CharField(max_length=10, verbose_name=_("Sold-To"))
    ship_to = models.CharField(max_length=10, verbose_name=_("Ship-To"))

    user_id = models.CharField(
        blank=True,
        default='',
        max_length=128,
        verbose_name=_("User ID")
    )

    region = models.CharField(
        max_length=3,
        choices=gsxws.GSX_REGIONS,
        verbose_name=_("Region")
    )

    timezone = models.CharField(
        max_length=8,
        default='CEST',
        verbose_name=_('Timezone'),
        choices=gsxws.GSX_TIMEZONES
    )

    environment = models.CharField(
        max_length=2,
        verbose_name=_("Environment"),
        choices=gsxws.ENVIRONMENTS,
        default=gsxws.ENVIRONMENTS[0][0]
    )

    @classmethod
    def get_soldto_choices(cls):
        choices = []
        for i in cls.objects.all():
            choice = (i.sold_to, '%s (%s)' % (i.sold_to, i.title))
            choices.append(choice)

        choices = [('', '------------------'),] + choices
        return choices

    @classmethod
    def get_shipto_choices(cls):
        return cls.objects.values_list('ship_to', 'ship_to')

    @classmethod
    def get_default_account(cls):
        """
        Returns the default GSX account without connecting to it
        """
        from servo.lib.utils import empty
        act_pk = Configuration.conf('gsx_account')

        if empty(act_pk):
            raise ValueError(_('Default GSX account not configured'))

        return GsxAccount.objects.get(pk=act_pk)

    @classmethod
    def get_account(cls, location, queue=None):
        """
        Returns the correct GSX account for the specified user/queue
        """
        try:
            act = location.gsx_accounts.get(sold_to=queue.gsx_soldto)
        except Exception as e:
            act = GsxAccount.get_default_account()

        return act

    @classmethod
    def fallback(cls):
        """
        Connect to fallback GSX account
        """
        act = cls.get_default_account()
        gsxws.connect(user_id=act.user_id,
                      sold_to=act.sold_to,
                      timezone=act.timezone,
                      environment=act.environment,)
        return act

    @classmethod
    def default(cls, user, queue=None):
        """
        Returns the correct GSX account for
        the specified user/queue and connects to it
        """
        try:
            act = GsxAccount.get_account(user.location, queue)
        except ValueError:
            raise gsxws.GsxError(_('Configuration error'))

        return act.connect(user)

    def connect(self, user, location=None):
        """
        Connects to this GSX Account
        """
        if user.gsx_userid:
            self.user_id = user.gsx_userid

        if location is None:
            timezone = user.location.gsx_tz
        else:
            timezone = location.gsx_tz

        gsxws.connect(user_id=self.user_id,
                      sold_to=self.sold_to,
                      environment=self.environment,
                      timezone=timezone)
        return self

    def test(self):
        """
        Tests that the account details are correct
        """
        gsxws.connect(sold_to=self.sold_to,
                      user_id=self.user_id,
                      environment=self.environment)

    def get_admin_url(self):
        return reverse('admin-edit_gsx_account', args=[self.pk])

    def __str__(self):
        return u"%s (%s)" % (self.title, self.get_environment_display())

    class Meta:
        app_label = 'servo'
        get_latest_by = 'id'
        ordering = ['title']
        verbose_name = _("GSX Account")
        verbose_name_plural = _("GSX Accounts")
        unique_together = ('sold_to', 'ship_to', 'environment',)


class Tag(MPTTModel):
    """
    A tag is a simple one-word descriptor for something.
    The type attribute is used to group tags to make them easier
    to associate with different elements
    """
    title = models.CharField(
        unique=True,
        max_length=255,
        default=_('New Tag'),
        verbose_name=_('name')
    )

    TYPES = (
        ('device',  _('Device')),
        ('order',   _('Order')),
        ('note',    _('Note')),
        ('other',   _('Other')),
    )

    type = models.CharField(
        max_length=32,
        choices=TYPES,
        verbose_name=_(u'type')
    )

    parent = TreeForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='children',
        on_delete=models.SET_NULL,
    )

    times_used = models.IntegerField(default=0, editable=False)

    COLORS = (
        ('default',     _('Default')),
        ('success',     _('Green')),
        ('warning',     _('Orange')),
        ('important',   _('Red')),
        ('info',        _('Blue')),
    )

    color = models.CharField(
        max_length=16,
        blank=True,
        null=True,
        choices=COLORS,
        default='default'
    )

    def count_open_orders(self):
        count = self.order_set.filter(state__lt=2).count()
        return count if count > 0 else ''

    def get_admin_url(self):
        return reverse('admin-edit_tag', args=[self.type, self.pk])

    def __str__(self):
        return self.title

    objects = TreeManager()

    class Meta:
        app_label = 'servo'
        verbose_name = _('Tag')
        verbose_name_plural = _('Tags')

    class MPTTMeta:
        order_insertion_by = ['title']


class LocationManager(models.Manager):
    """Custom model manager for enabled locations."""

    def enabled(self, **kwargs):
        """Only locations that are marked as enabled."""
        return self.filter(enabled=True, **kwargs)


class Location(models.Model):
    """
    A Service Location within a company.
    """

    title = models.CharField(
        max_length=255,
        unique=True,
        verbose_name=_(u'Name'),
        default=_('New Location'),
    )
    phone = models.CharField(
        blank=True,
        default='',
        max_length=32,
        verbose_name=_('Phone')
    )
    email = models.EmailField(
        blank=True,
        default='',
        verbose_name=_('Email')
    )
    address = models.CharField(
        blank=True,
        default='',
        max_length=32,
        verbose_name=_('Address')
    )
    zip_code = models.CharField(
        blank=True,
        default='',
        max_length=8,
        verbose_name=_('ZIP Code')
    )
    city = models.CharField(
        blank=True,
        default='',
        max_length=16,
        verbose_name=_('City')
    )

    TIMEZONES = tuple((t, t) for t in common_timezones)

    timezone = models.CharField(
        default='UTC',
        max_length=128,
        choices=TIMEZONES,
        verbose_name=_('Time zone')
    )

    # It would make more sense to just store the Ship-To
    # per-location, but some location can have multiple Ship-Tos :-/
    gsx_accounts = models.ManyToManyField(
        GsxAccount,
        blank=True,
        verbose_name=_('Accounts')
    )

    gsx_shipto = models.CharField(
        max_length=10,
        default='',
        blank=True,
        verbose_name=_('Ship-To')
    )

    gsx_tz = models.CharField(
        max_length=8,
        default='CEST',
        verbose_name=_('Timezone'),
        choices=gsxws.GSX_TIMEZONES
    )

    notes = models.TextField(
        blank=True,
        verbose_name=_('Notes'),
        default=_('Business hours between 9:00 - 18:00'),
        help_text=_('Will be shown on print templates')
    )

    manager = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="managed_locations",
        limit_choices_to={'is_visible': True}
    )

    enabled = models.BooleanField(
        default=True,
        verbose_name=_('Enabled')
    )

    checkin = models.BooleanField(
        default=True,
        verbose_name=_('Use for check-in')
    )

    objects = LocationManager()

    @classmethod
    def get_checkin_list(cls):
        """List service locations that are marked for check-in."""
        results = []
        for l in cls.objects.filter(checkin=True):
            results.append({'pk': l.pk, 'name': l.title})

        return results

    def get_shipto_choices(self):
        return self.gsx_accounts.values_list('ship_to', 'ship_to')

    def get_country(self):
        try:
            return TIMEZONE_COUNTRY[self.timezone]
        except KeyError:
            return 'FI'

    def ship_to_choices(self):
        choices = []
        for i in self.gsx_accounts.all():
            choices.append((i.ship_to, i.ship_to))
        return choices

    def get_admin_url(self):
        return reverse('admin-edit_location', args=[self.pk])

    def gsx_address(self):
        return {
            'city'          : self.city,
            'zipCode'       : self.zip_code,
            'country'       : self.get_country(),
            'primaryPhone'  : self.phone,
            'emailAddress'  : self.email,
        }

    def __str__(self):
        return self.title

    class Meta:
        ordering = ('title',)
        app_label = 'servo'
        get_latest_by = 'id'
        verbose_name = _('Location')
        verbose_name_plural = _('Locations')


class Configuration(models.Model):

    key = models.CharField(max_length=255)
    value = models.TextField(default='', blank=True)

    @classmethod
    def true(cls, key):
        return cls.conf(key) == 'True'

    @classmethod
    def false(cls, key):
        return not cls.true(key)

    @classmethod
    def get_company_logo(cls):
        return cls.conf('company_logo')

    @classmethod
    def default_subject(cls):
        return cls.conf('default_subject')

    @classmethod
    def get_default_sender(cls, user):
        """
        Returns the sender address to use for notifications
        """
        conf = cls.conf()
        sender = conf.get('default_sender')

        if sender == 'user':
            return user.email
        if sender == 'location':
            return user.get_location().email

        return conf.get('default_sender_custom')

    @classmethod
    def track_inventory(cls):
        return cls.true('track_inventory')

    @classmethod
    def notify_location(cls):
        return cls.true('notify_location')

    @classmethod
    def notify_email_address(cls):
        """
        Returns the email address to send reports to
        or None if it's invalid
        """
        from django.core.validators import validate_email
        try:
            validate_email(conf['notify_address'])
            return conf['notify_address']
        except Exception:
            pass

    @classmethod
    def autocomplete_repairs(cls):
        return cls.true('autocomplete_repairs')

    @classmethod
    def smtp_ssl(cls):
        return cls.true('smtp_ssl')

    @classmethod
    def get_smtp_server(cls):
        host, port = cls.conf('smtp_host'), 25

        if empty(host):
            raise ConfigurationError('SMTP server not configured')

        if len(host.split(':')) == 2:
            return host.split(':')

        return host, port

    @classmethod
    def get_imap_server(cls):
        import imaplib
        conf = cls.conf()

        if not conf.get('imap_host'):
            raise ValueError("No IMAP server defined - check your configuration")

        if conf.get('imap_ssl'):
            server = imaplib.IMAP4_SSL(conf['imap_host'])
        else:
            server = imaplib.IMAP4(conf['imap_host'])

        server.login(conf['imap_user'], conf['imap_password'])
        server.select()
        return server

    @classmethod
    def conf(cls, key=None):
        """
        Returns the admin-configurable config of the site
        """
        config = cache.get('config')
        if config is None:
            config = dict()
            for r in Configuration.objects.all():
                config[r.key] = r.value

            cache.set('config', config)

        return config.get(key) if key else config

    @classmethod
    def checkin_enabled(cls):
        return cls.conf('checkin_enable')

    def save(self, *args, **kwargs):
        config = super(Configuration, self).save(*args, **kwargs)
        # Using cache instead of session since it's shared among
        # all the users of the instance
        cache.set('config', config, 60 * 60 * 24 * 1)

    class Meta:
        app_label = 'servo'


class Property(models.Model):
    TYPES = (
        ('customer',    _('Customer')),
        ('order',       _('Order')),
        ('product',     _('Product'))
    )

    title = models.CharField(
        max_length=255,
        default=_('New Field'),
        verbose_name=_('title')
    )

    type = models.CharField(
        max_length=32,
        choices=TYPES,
        default=TYPES[0],
        verbose_name=_('type')
    )
    format = models.CharField(
        blank=True,
        default='',
        max_length=32,
        verbose_name=_('format')
    )
    value = models.TextField(blank=True, default='', verbose_name=_('value'))

    def __str__(self):
        return self.title

    def get_admin_url(self):
        return reverse('admin-edit_field', args=[self.type, self.pk])

    def values(self):
        if self.value is None:
            return []
        else:
            return self.value.split(', ')

    class Meta:
        app_label = 'servo'
        ordering = ['title']
        verbose_name = _('Field')
        verbose_name_plural = _('Fields')


class Search(models.Model):
    query = models.TextField()
    model = models.CharField(max_length=32)
    title = models.CharField(max_length=128)
    shared = models.BooleanField(default=True)

    class Meta:
        app_label = 'servo'


class Notification(models.Model):
    """
    A notification is a user-configurable response to an event
    """
    KINDS = (('ORDER', _('Order')), ('NOTE', _('Note')),)
    ACTIONS = (('created', u'Luotu'), ('edited', u'Muokattu'))

    kind = models.CharField(max_length=16)
    action = models.CharField(max_length=16)
    message = models.TextField()

    class Meta:
        app_label = 'servo'


class Template(models.Model):
    title = models.CharField(
        blank=False,
        unique=True,
        max_length=128,
        verbose_name=_('Title'),
        default=_('New Template')
    )

    content = models.TextField(blank=False, verbose_name=_('Content'))

    @classmethod
    def templates(self):
        choices = Template.objects.all().values_list('title', flat=True)
        return list(choices)

    def render(self, context):
        from django import template
        tpl = template.Template(self.content)
        return tpl.render(template.Context({'order': context}))

    def get_absolute_url(self):
        return reverse('notes-template', args=[self.pk])

    def get_admin_url(self):
        return reverse('admin-edit_template', args=[self.pk])

    def get_delete_url(self):
        return reverse('admin-delete_template', args=[self.pk])

    class Meta:
        ordering = ['title']
        app_label = "servo"
        verbose_name = _('Template')
        verbose_name_plural = _('Templates')


class Attachment(models.Model):
    """
    A file attached to something.
    """
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    mime_type = models.CharField(max_length=64, editable=False)
    content = models.FileField(
        upload_to='attachments',
        verbose_name=_('file'),
        validators=[file_upload_validator]
    )

    @classmethod
    def get_content_type(cls, model):
        return ContentType.objects.get(app_label='servo', model=model)

    @classmethod
    def from_file(cls, file):
        """
        Returns an attachment object from the file data
        """
        attachment = cls(content=file)
        attachment.save()

    def save(self, *args, **kwargs):
        DENIED_EXTENSIONS = ('.htm', '.html', '.py', '.js', '.exe', '.sh',)
        filename = self.content.name.lower()
        ext = os.path.splitext(filename)[1]

        if ext in DENIED_EXTENSIONS:
            raise ValueError(_(u'%s is not of an allowed file type') % filename)

        return super(Attachment, self).save(*args, **kwargs)

    def __str__(self):
        return os.path.basename(self.content.name)

    def from_url(self, url):
        """
        Downloads a file and creates an attachment
        """
        pass

    def get_absolute_url(self):
        return reverse("files-view_file", args=[self.pk])

    class Meta:
        app_label = "servo"
        get_latest_by = "id"


@receiver(post_save, sender=Attachment)
def set_mimetype(sender, instance, created, **kwargs):
    if created:
        import subprocess
        path = instance.content.path
        mimetype = subprocess.check_output(['file', '-b', '--mime-type', path]).strip()
        instance.mime_type = mimetype
        instance.save()
