import datetime
from django.conf import settings
from django import forms 
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from ideatree.utils import myutils
from django.utils.safestring import mark_safe
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.db.models import Q, UniqueConstraint
from django.http import HttpResponse 

from allauth.account.signals import user_signed_up #(request, user)
from allauth.account.signals import email_confirmed #(request, email_address)
from allauth.account.signals import email_changed #(request, user, from_email_address, to_email_address)

#import pdb


# need to create a row whenever a new User is created?  See:
# http://stackoverflow.com/questions/35030556/django-user-profile-in-1-9/35031078

class Colors(models.Model):
  colorname = models.CharField(max_length=20) 
  hexvalue = models.CharField(max_length=8) 
  class Meta:
    constraints = [ UniqueConstraint(fields=['colorname'], name='unique_colorname')]


class UserProfile(models.Model):
  ACCOUNT_TYPE_CHOICES = (
    (settings.FREE_ACCT, 'free'),
    (settings.REGULAR_ACCT, 'regular'),
    (settings.PREMIUM_ACCT, 'premium'),
  )
  user = models.OneToOneField(User, null=False, on_delete=models.CASCADE)
  accounttype = models.CharField(max_length=1, default=settings.FREE_ACCT, choices=ACCOUNT_TYPE_CHOICES) 
  registerdate = models.DateTimeField(auto_now_add=True, null=False)
  trialperioddays = models.PositiveSmallIntegerField(default=settings.FREE_ACCT_TRIALPERIOD_DAYS)
  mapsettings = models.CharField(max_length=255, null=True, default=settings.USER_DEFAULT_MAP_SETTINGS) 
  nummapsallowed = models.PositiveSmallIntegerField(default=settings.FREE_USER_NUM_MAPS_ALLOWED)
  invitationpending = models.BooleanField(null=False, default=False)
  stripecustomerid = models.CharField(max_length=120, default='')
  deleteglobally = models.BooleanField(null=False, default=False)  # when a cancelled user is deleted from the db, whether to also delete their comments and nodes in non-owned graphs.


# Signal to automatically create a user profile whenever a user is created.	
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
  try:
    if created: # the user is a new one
      import stripe
      # Create a Stripe user for synchronizing with Stripe when the user later upgrades to a paying account or deletes the subscription.
      stripe.api_key = settings.STRIPE_API_KEY
      stripe_customer_object = stripe.Customer.create(
        description="IdeaTree paid account customer",
        email=instance.email, 
        metadata={ 'user_id': instance.pk },
      )
      userProfile = UserProfile.objects.create(user=instance, stripecustomerid=stripe_customer_object.id)
      userProfile.save()
  except stripe.error.InvalidRequestError as err:
      print(str(err))
  except Exception as err:
      print(str(err))

# FIXME
"""
# Signal to automatically update a user profile whenever a user is saved.	
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
  # print("save_user_profile RECEIVED")
  try:
    instance.userprofile.save()
  except Exception as err:
    return HttpResponse(str(err), status=406)
"""


# FIXME
# Signal to create an Stripe Customer object for (optional) use in synchronizing with Stripe if this User makes an upgrade payment.
"""
@receiver(user_signed_up, sender=User,  dispatch_uid="Stripe Customer object creation.")
def create_stripe_customer_object(sender, request, user, **kwargs):
  try:
    # NOTE assumes the user profile has already been created at this point.
    import stripe
    stripe.api_key = settings.STRIPE_SECRET_KEY
    stripe_customer_object = stripe.Customer.create(
      email = user.email,
      description="IdeaTree Premium account customer",
    )
    newProfile = UserProfile.objects.get(user=user)
    setattr(newProfile,'stripecustomerid',stripe_customer_object.id)
    newProfile.save()
  except stripe.error.InvalidRequestError as err:
    return HttpResponse(str(err), status=406)  # FIXME URGENT not the right thing to do, here or elsewhere. Return something the customer can see, and send email to admin. 
  except Exception as err:
    return HttpResponse(str(err), status=406)
"""


# NOTE: The signal name is a misnomer.  It's actually received when an email is changed to primary status.
# Update the Stripe Customer object.
@receiver(email_changed, sender=User,  dispatch_uid="Stripe Customer object update.")
def update_stripe_customer_object(sender, request, user, from_email_address, to_email_address, **kwargs):
  # FIXME: Stripe emails can be up to 512 characters.  Make sure IdeaTree emails are similarly limited.
  try:
    import stripe
    profile = UserProfile.objects.get(user=user)
    stripe.api_key = settings.STRIPE_SECRET_KEY
    stripe.Customer.modify(
      profile.stripecustomerid,
      email=to_email_address.email,
    )
  # FIXME: if it fails, should roll back the change to User, or at least warn the user that Stripe wouldn't accept the email change.
  except stripe.error.InvalidRequestError as err:
    return HttpResponse(str(err), status=406)
  except Exception as err:
    return HttpResponse(str(err), status=406)



# Signal to automatically remove a Customer object from Stripe when a user is deleted.	
# NOTE: when an IdeaTree user selects 'Cancel Account', the Stripe *subscription* is merely cancelled and the Ideatree User set to 'inactive'.
# It is intended that the following happen at some later time, by cron job cleanup of IdeaTree User records.
@receiver(pre_delete, sender=User)
def delete_stripe_customer(sender, instance, using, dispatch_uid="Remove Customer object from Stripe.", **kwargs):
  try:
    # FIXME URGENT:  test this.
    import stripe
    stripe.api_key = settings.STRIPE_SECRET_KEY
    profile = UserProfile.objects.get(user=user)
    result = stripe.Customer.delete(profile.stripecustomerid) 
  except stripe.error.InvalidRequestError as err:
    return HttpResponse(str(err), status=406)
  except Exception as err:
    return HttpResponse(str(err), status=406)






class ContactUs(models.Model):
	date = models.DateTimeField(auto_now_add=True, null=False)
	user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
	email = models.EmailField(blank=False, null=False)
	message = models.CharField(max_length=550, blank=False, null=False)


class Map_desc(models.Model):
	MAP_STATUS_CHOICES =  (
			(settings.MAP_ACTIVE, "Active"),
			(settings.MAP_DELETED, "Deleted"),
	)
	owner = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
	mapname = models.CharField(max_length=55, blank=True, null=False)
	description = models.CharField(max_length=125, blank=True)
	status = models.CharField(max_length=1, choices=MAP_STATUS_CHOICES, default=settings.MAP_ACTIVE) 
	bgcolor = models.CharField(max_length=8, blank=True, null=False, default="FeFeFaFF")
	nodesleft = models.PositiveSmallIntegerField(null=True)
	orientation = models.CharField(max_length=2, default=settings.DEFAULT_MAP_LAYOUT_ORIENTATION, choices=settings.MAP_LAYOUT_ORIENTATIONS,blank=True)
	layoutengine = models.CharField(max_length=5, blank=True, null=True, default="dot")
	defaultNodeStyle = models.CharField(max_length=25, null=False, default='filled,rounded',blank=True)
	defaultNodeShape = models.PositiveSmallIntegerField(default=0,blank=True)
	defaultNodeFillcolor = models.CharField(max_length=8, default='F0E68C',blank=True)
	defaultNodeHeight = models.FloatField(default=2.5, blank=True)
	defaultNodeWidth = models.FloatField(default=2.5, blank=True)
	defaultNodeType = models.CharField(max_length=1, default=settings.ORIGINAL_NODE, blank=True) 

	def __str__(self):
		return self.mapname 


class Friend(models.Model):
	# NOTE: since there's no date field, deleted Friends are actually dropped from the db every time the cron job runs, not kept around for some grace period.
	FRIEND_STATUS_CHOICES = (
    (settings.FRIEND_ACCEPTED, 'accepted'),
    (settings.FRIEND_INVITED, 'invited'),
    (settings.FRIEND_DECLINED, 'declined'),
    (settings.FRIEND_DELETED, 'deleted'),
	)
	initiator = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
	friend = models.ForeignKey(User, null=True, related_name='friendTablefriendField', on_delete=models.CASCADE)  # can be null because initiator may send them an invite email and it takes a while for them to respond and sign up
	email = models.EmailField(blank=False, null=True) # can be null because only used when inviting by email, not when adding a friend within 
	status = models.CharField(max_length=1, choices=FRIEND_STATUS_CHOICES, default=settings.FRIEND_INVITED) 

	


class Mapmember(models.Model):
	MAPMEMBERSHIP_STATUS_CHOICES =  (
			(settings.MAPMEMBERSHIP_ACTIVE, "Active"),
			(settings.MAPMEMBERSHIP_DELETED, "Deleted"),
	)
	member = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
	ofmap = models.ForeignKey(Map_desc, null=False, on_delete=models.CASCADE)
	status = models.CharField(max_length=1, choices=MAPMEMBERSHIP_STATUS_CHOICES, default=settings.MAPMEMBERSHIP_ACTIVE) 

	# See http://stackoverflow.com/questions/2281179/adding-extra-constraints-into-fields-in-django
	def save(self, *args, **kwargs):
		mapOwner = self.ofmap.owner
		if self.member == mapOwner:
			super(Mapmember, self).save(*args, **kwargs)
		else:
			isFriend = Friend.objects.filter( Q(friend = self.member, initiator = mapOwner) | Q(friend = mapOwner, initiator = self.member)).count()
			if isFriend:
				super(Mapmember, self).save(*args, **kwargs)
			else:
				raise Exception("Users sharing a map must exist as a friend.") 

	def __str__(self):
		return(str(self.member.username))


class Map_access_changed(models.Model):  # FIXME: remove.  error-prone if not updated correctly.
	user = models.OneToOneField(User, null=False, on_delete=models.CASCADE)
	changed = models.BooleanField(null=False)


class WhosViewingMap(models.Model):
	user = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
	mapViewed = models.ForeignKey(Map_desc, null=False, on_delete=models.CASCADE)


class Node(models.Model):
	id = models.BigAutoField(primary_key=True)
	# null=True because provisional nodes don't have origin, target, or label
	#FIXME: figure out how to make_maxlength depend on nodetype, and set differently for clusters to cluster_label_length
	label = models.CharField(max_length=settings.NODE_LABEL_LENGTH, blank=True, null=True)
	# FIXME: take out underscore
	created_date = models.DateTimeField('date created',auto_now=True)
	owner = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
	ofmap = models.ForeignKey(Map_desc, null=False, on_delete=models.CASCADE)

	# FIXME: replace with real-time queries.  But client reads this, so has to be put in change message.
	targetmapname = models.CharField(max_length=55, blank=True, null=True, default=None)
	tunnelfarendmap = models.ForeignKey(Map_desc, blank=True, null=True, related_name='+', on_delete=models.CASCADE)
	tunnelfarendnode = models.ForeignKey('self', related_name='+', blank=True, null=True, default=None, on_delete=models.CASCADE) 
	clusterid = models.PositiveIntegerField(default=0)
	status = models.CharField(max_length=1, choices=settings.NODE_STATUS_CHOICES, default=settings.NODE_ACTIVE) 
	nodetype = models.CharField(max_length=1, choices=settings.NODE_TYPE_CHOICES, default=None) 
	xpos = models.FloatField(default=0, blank=True)
	ypos = models.FloatField(default=0, blank=True)
	size = models.CharField(max_length=17, default='2.5,2.5', blank=True) # FIXME: can the client limit this to fewer decimal points to save storage?
	lwidth = models.FloatField(default=None, null=True)
	shape = models.SmallIntegerField(null=True, default=0)
	style = models.CharField(max_length=17, null=False, default='filled,rounded')
	# FIXME: does it need a default if Map_desc has a default?
	fillcolor = models.CharField(max_length=8, default='F0E68C')
	pencolor = models.CharField(max_length=8, default='000000')
	fontcolor = models.CharField(max_length=8, default='000000')
	description = models.CharField(max_length=settings.NODE_DESCRIPTION_LENGTH, blank=True, null=True, default=None)
	numcomments = models.PositiveSmallIntegerField(default=0)
	# FIXME: Video nodes don't store a whole url, only a src parameter in the url, so use a different filter.
	url = models.URLField(blank=True, null=True, default=None, max_length=settings.NODE_URLFIELD_LENGTH)
  # FIXME: 64 bits seems a lot.  Can client limit itself to 10 digits, so PositiveIntegerField can be used?  See Translate.js.
	hiddenbranch = models.BigIntegerField(null=True, default=None)
	isbranchhead= models.BooleanField(default=False)  # FIXME not found in client js

	def __str__(self):
		# FIXME: can we get around the ORM vs. native integer ID thing by making this return the ID?
		if self.label:
			return self.label
		else:
			return str(self.id) 

	def __url__(self):
		return myutils.tostr(self.url)

	def __description__(self):
		return myutils.tostr(self.description)

	def was_published_recently(self):
		now = timezone.now()
		return now - datetime.timedelta(days=1) <= self.pub_date <= now;




class NodeComment(models.Model):
	NODE_COMMENT_CHOICES =  (
			(settings.NODE_COMMENT_ACTIVE, "Active"),
			(settings.NODE_COMMENT_DELETED, "Deleted"),
	)
	id = models.BigAutoField(primary_key=True, blank=False)
	node = models.ForeignKey(Node, null=False, on_delete=models.CASCADE)
	comment = models.CharField(max_length=1024, blank=True, null=True, default=None)
  # FIXME does 'user' mean 'owner'?
	user = models.ForeignKey(User, null=False, on_delete=models.CASCADE) # NOTE: alternative: set to anonymous user.  See also Vote.
	date = models.DateTimeField('date',auto_now=True)
	status = models.CharField(max_length=1, choices=NODE_COMMENT_CHOICES, default=settings.NODE_COMMENT_ACTIVE) 

	def was_published_recently(self):
		now = timezone.now()
		return now - datetime.timedelta(days=1) <= self.date <= now;

	#def clean_fields(self, exclude=None):
		#super(NodeComment, self).clean_fields(exclude=exclude)
		#print("EXCLUDED FROM NodeComment CLEAN:" + str(exclude))



class Vote(models.Model):
	node = models.ForeignKey(Node, null=False, on_delete=models.CASCADE)
	vote = models.PositiveSmallIntegerField(default=5)
	user = models.ForeignKey(User, null=False, on_delete=models.CASCADE) # NOTE: alternative: set to anonymous user.  See also NodeComment.
	date = models.DateTimeField('date',auto_now=True)
	status = models.CharField(max_length=1, choices=settings.VOTABLE_STATUS_CHOICES, default=settings.VOTABLE_ACTIVE) 


"""
If you delete Node, by default Edge will be deleted as well. Deleting Edge will not delete Node, because Node does not have
a ForeignKey relationship to edge. Edge should only exist if Node exists, but Node can exist without Edge.
"""
# FIXME: Edges use status to mean provisional/active/deleted.  Nodes use 'nodetype' to mean provisional or not.
# Edge way is more efficient, doesn't need exclude() on the query.
class Edge(models.Model):
        EDGE_STATUS_CHOICES = ( (settings.EDGE_PROVISIONAL, 'provisional'), (settings.EDGE_ACTIVE, 'active'), (settings.EDGE_DELETED, 'deleted'),)

        id = models.BigAutoField(primary_key=True)
        creation_date = models.DateTimeField('creation date',auto_now=True)
        # blank=True on label because setNodeAttribute sets other fields, one at a time, separately, and can't be stopped because this one is blank,
        # and also because provisional edges don't have origin, target, or label.
        label = models.CharField(max_length=255, default='', blank=True, null=True)
        status = models.CharField(max_length=1, choices=EDGE_STATUS_CHOICES, default=settings.EDGE_ACTIVE) 
        ofmap = models.ForeignKey(Map_desc, blank=False, on_delete=models.CASCADE)
        origin = models.ForeignKey(Node, null=True, default=None, related_name='connectedEdgeOrigin', on_delete=models.CASCADE)
        owner = models.ForeignKey(User, blank=False, on_delete=models.CASCADE)
        # FIXME: can null be False?  That would enforce the dependency on having a node to connect to. 
        target = models.ForeignKey(Node, null=True, default=None, related_name='connectedEdgeTarget', on_delete=models.CASCADE)
        pheremone = models.BooleanField(default=False)
        color = models.CharField(max_length=8, blank=True, default="000000")
        penwidth = models.FloatField(default=1.0)
        arrowhead = models.PositiveSmallIntegerField(default=0)
        draw = models.CharField(max_length=65535, default='', blank=True) # Django won't let field names end with an underscore, but the Graphviz attribute is _draw_.
        # FIXME: 64 bits seems a lot.  Can client limit itself to 10 digits, so PositiveIntegerField can be used?  See Translate.js.
        hiddenbranch = models.BigIntegerField(null=True, default=None)
        cost = models.SmallIntegerField(default=settings.DEFAULTEDGE_COST, blank=True)
        class Meta:
            managed = True 
            db_table = 'ideatree_edge'

        def __str__(self):
            return self.label




class WhosLoggedIn(models.Model):
	currentmap = models.ForeignKey(Map_desc, on_delete=models.CASCADE)
	user = models.ForeignKey(User, on_delete=models.CASCADE)



class ClientPermission(models.Model):
	action = models.CharField(max_length=20, blank=False, null=False)
	iown = models.BooleanField(default=False)
	permitted = models.BooleanField(default=False)



class MapPathEndpoints(models.Model):
	ofmap = models.OneToOneField(Map_desc, null=False, on_delete=models.CASCADE)
	pathBeginNode = models.OneToOneField(Node, null=True, on_delete=models.SET_NULL)
	pathEndNode = models.OneToOneField(Node, null=True, related_name='+', on_delete=models.SET_NULL)


