import datetime
import pytz

from django.contrib.auth.models import User
from django.db import models
import requests


def get_date(raw: str, tzinfo=None):
    if raw is None:
        return None
    datetime_formats = ['%Y', '%Y-%m-%d', '%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S', "%Y-%m-%dT%H:%M:%S.%f+00:00", "%Y-%m-%dT%H:%M:%S+00:00", '%Y-%m-%d %H:%M:%S.%fZ', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%SZ', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', "%Y-%m-%d %H:%M:%S.%f+00:00", "%Y-%m-%d %H:%M:%S+00:00"]
    for dt_format in datetime_formats:
        try:
            d = datetime.datetime.strptime(raw, dt_format)
        except ValueError:
            d = None
        if d is not None:
            if tzinfo is not None:
                return d.replace(tzinfo=tzinfo)
            else:
                return d
    return d


def get_utc_time(raw):
    # raw can be int, float, str convertable to float, or null
    # will try to divide by 1000 if it doesn't work out of the box
    if raw is not None:
        try:
            d = datetime.datetime.utcfromtimestamp(float(raw)).replace(tzinfo=pytz.UTC)
        except ValueError:
            d = datetime.datetime.utcfromtimestamp(float(raw)/1000).replace(tzinfo=pytz.UTC)
        return d
    else:
        return None


class WhoopUser(models.Model):
    """Adds a token to a django User."""

    user = models.OneToOneField(User, on_delete=models.CASCADE)
    access_token = models.CharField(max_length=1024)
    access_token_updated = models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0).replace(tzinfo=pytz.UTC))
    access_token_expires_in = models.IntegerField(default=0)
    refresh_token = models.CharField(max_length=1024)
    whoop_user_id = models.IntegerField(default=-1)
    whoop_createdAt = models.DateTimeField(default=datetime.datetime(1, 1, 1, 0, 0).replace(tzinfo=pytz.UTC))

    def __str__(self):
        return self.user.username

    @classmethod
    def createWithPassword(cls, loggedInUser, username, password):
        headers = {
            "username": username,
            "password": password,
            "grant_type": "password",
            "issueRefresh": True
        }
        auth = requests.post("https://api-7.whoop.com/oauth/token", json=headers)
        print(auth)
        print(auth.text)
        print(auth.status_code)
        # should check we got a 200...
        resp = auth.json()
        whoopUser = cls(
            user=loggedInUser,
            access_token=resp['access_token'],
            access_token_updated=datetime.datetime.now().replace(tzinfo=pytz.UTC),
            access_token_expires_in=resp['expires_in'],
            refresh_token=resp['refresh_token'],
            whoop_user_id=resp['user']['id'],
            # there are two dates to choose from, the profile is 3 days later
            # in my case (ex: '2020-10-18T00:57:18.609Z')
            # createdAt=resp['user']['profile']['createdAt']
            whoop_createdAt=get_date(resp['user']['createdAt'], tzinfo=pytz.UTC)
        )
        return whoopUser


    def refreshWithPassword(self, username, password):
        headers = {
            "username": username,
            "password": password,
            "grant_type": "password",
            "issueRefresh": True
        }
        auth = requests.post("https://api-7.whoop.com/oauth/token", json=headers)
        print(auth)
        print(auth.text)
        print(auth.status_code)
        # should check we got a 200...
        resp = auth.json()
        self.access_token = resp['access_token']
        self.access_token_updated = datetime.datetime.now().replace(tzinfo=pytz.UTC)
        self.access_token_expires_in = resp['expires_in']
        self.refresh_token=resp['refresh_token']
        self.save()

    def refreshIfNeeded(self, check_api=False):
        """
        Refresh the access token if it has expired.

        Args:
            check_api: If True, make an API call to check token validity.
                      If False, check based on timestamp.
        """
        if check_api:
            # Check token validity by making a test API call
            headers = {'authorization': f'bearer {self.access_token}'}
            test_url = f'https://api-7.whoop.com/users/{self.whoop_user_id}/profile'
            try:
                response = requests.get(test_url, headers=headers)
                if response.status_code == 401:
                    # Token is invalid, refresh it
                    self.refreshToken()
            except requests.RequestException:
                # If API call fails, try refreshing anyway
                self.refreshToken()
        else:
            # Check if token has expired based on timestamp
            # Token expires_in is in seconds
            token_age = (datetime.datetime.now(pytz.UTC) - self.access_token_updated).total_seconds()
            # Refresh if token is older than expires_in (with 5 minute buffer)
            if token_age > (self.access_token_expires_in - 300):
                self.refreshToken()

    def refreshToken(self):
        headers = {
            "refresh_token": self.refresh_token,
            "grant_type": "refresh_token"
        }
        auth = requests.post("https://api-7.whoop.com/oauth/token", json=headers)
        print(auth.status_code)
        resp = auth.json()
        print(resp)
        self.access_token = resp['access_token']
        self.access_token_updated = datetime.datetime.now().replace(tzinfo=pytz.UTC)
        self.access_token_expires_in = resp['expires_in']
        self.save()


class Daily(models.Model):
    id = models.IntegerField(primary_key=True)
    user = models.ForeignKey(WhoopUser, on_delete=models.CASCADE)
    day = models.DateField()

    during_bounds = models.CharField(max_length=10, null=True)
    during_lower = models.DateTimeField(null=True)
    during_upper = models.DateTimeField(null=True)

    lastUpdatedAt = models.DateTimeField(null=True)
    predictedEnd = models.DateTimeField(null=True)

    def __str__(self):
        return self.day.strftime('%Y-%m-%d')

    @classmethod
    def fill_from_cycle_response(cls, user, d):
        assert 'id' in d
        assert 'days' in d and len(d['days']) == 1
        during = d.get('during') or dict()
        daily, created = cls.objects.update_or_create(
            user=user.whoopuser,
            id=d['id'],
            defaults={
                'day': get_date(d['days'][0]).date(),
                'during_bounds': during.get('bounds'),
                'during_lower': get_date(during.get('lower'), tzinfo=pytz.UTC),
                'during_upper': get_date(during.get('upper'), tzinfo=pytz.UTC),
                'lastUpdatedAt': get_date(d.get('lastUpdatedAt'), tzinfo=pytz.UTC),
                'predictedEnd': get_date(d.get('predictedEnd'), tzinfo=pytz.UTC)
            }
        )
        daily.save()
        print(f"{('found', 'created')[created]} the day {daily=}")
        if 'recovery' in d and d['recovery'] is not None:
            recovery, created = Recovery.objects.update_or_create(
                day=daily,
                id=d['recovery'].get('id'),
                defaults={
                    'heartRateVariabilityRmssd': d['recovery'].get('heartRateVariabilityRmssd'),
                    'restingHeartRate': d['recovery'].get('restingHeartRate'),
                    'score': d['recovery'].get('score'),
                    'timestamp': get_date(d['recovery'].get('timestamp'), tzinfo=pytz.UTC)
                }
            )
            recovery.save()
            print(f"{('found', 'created')[created]} {recovery=}")
        if 'sleep' in d and d['sleep'] is not None and d['sleep'].get('id') is not None:
            needBreakdown = d['sleep'].get('needBreakdown') or dict()
            sleep, created = Sleep.objects.update_or_create(
                day=daily,
                id=d['sleep']['id'],
                defaults={
                    'needBreakdown_baseline': needBreakdown.get('baseline'),
                    'needBreakdown_debt': needBreakdown.get('debt'),
                    'needBreakdown_naps': needBreakdown.get('naps'),
                    'needBreakdown_strain': needBreakdown.get('strain'),
                    'needBreakdown_total': needBreakdown.get('total'),
                    'qualityDuration': d['sleep'].get('qualityDuration'),
                    'score': d['sleep'].get('score')
                }
            )
            print(f"{('found', 'created')[created]} {sleep=}")
            sleep.save()
            if d['sleep'].get('naps') is not None:
                for sleepdetail in d['sleep']['naps']:
                    # could do the below with isNap=True
                    pass
            if d['sleep'].get('sleeps') is not None:
                for sleepdetail in d['sleep']['sleeps']:
                    during = sleepdetail.get('during') or dict()
                    if sleepdetail.get('id') is not None:
                        sleepdetail_obj, created = SleepDetail.objects.update_or_create(
                            sleep=sleep,
                            id=sleepdetail['id'], # = models.IntegerField() # 131838178,
                            defaults={
                                'cyclesCount': sleepdetail.get('cyclesCount'), # = models.IntegerField(null=True) # 5,
                                'disturbanceCount': sleepdetail.get('disturbanceCount'), # = models.IntegerField(null=True) # 12,
                                'during_bounds': during.get('bounds'), # = models.CharField(null=True) # "[)",
                                'during_lower': get_date(during.get('lower'), tzinfo=pytz.UTC), # = models.DateTimeField(null=True) # "2020-10-25T03:01:00+00:00",
                                'during_upper': get_date(during.get('upper'), tzinfo=pytz.UTC), # = models.DateTimeField(null=True) # "2020-10-25T11:33:00+00:00",
                                'inBedDuration': sleepdetail.get('inBedDuration'), # = models.IntegerField(null=True) # 30718493,
                                'isNap': False, # = models.BooleanField(default=False) # false,
                                'latencyDuration': sleepdetail.get('latencyDuration'), # = models.IntegerField(null=True) # 0,
                                'lightSleepDuration': sleepdetail.get('lightSleepDuration'), # = models.IntegerField(null=True) # 12011596,
                                'noDataDuration': sleepdetail.get('noDataDuration'), # = models.IntegerField(null=True) # 0,
                                'qualityDuration': sleepdetail.get('qualityDuration'), # = models.IntegerField(null=True) # 25139744,
                                'remSleepDuration': sleepdetail.get('remSleepDuration'), # = models.IntegerField(null=True) # 5948524,
                                'respiratoryRate': sleepdetail.get('respiratoryRate'), # = models.FloatField(null=True) # 14.3262,
                                'score': sleepdetail.get('score'), # = models.IntegerField(null=True) # 73,
                                'sleepConsistency': sleepdetail.get('sleepConsistency'), # = models.IntegerField(null=True) # 89,
                                'sleepEfficiency': sleepdetail.get('sleepEfficiency'), # = models.FloatField(null=True) # 0.818391,
                                'slowWaveSleepDuration': sleepdetail.get('slowWaveSleepDuration'), # = models.IntegerField(null=True) # 7179624,
                                'timezoneOffset': sleepdetail.get('timezoneOffset'), # = models.CharField(null=True) # "-0400",
                                'wakeDuration': sleepdetail.get('wakeDuration') # = models.IntegerField(null=True) # 5615410
                            }
                        )
                        sleepdetail_obj.save()
                        print(f"{('found', 'created')[created]} {sleepdetail_obj=}")
        if 'strain' in d and d['strain'] is not None:
            strain, created = Strain.objects.update_or_create(
                day=daily,
                id=d['id'],
                defaults={
                    'averageHeartRate': d['strain'].get('averageHeartRate'), # models.FloatField(null=True) # 75,
                    'kilojoules': d['strain'].get('kilojoules'), # models.FloatField(null=True) # 14218.7,
                    'maxHeartRate': d['strain'].get('maxHeartRate'), # models.FloatField(null=True) # 172,
                    'rawScore': d['strain'].get('rawScore'), # models.FloatField(null=True) # 0.0143161574422024,
                    'score': d['strain'].get('score') # models.FloatField(null=True) # 16.3900610678999,
                }
            )
            strain.save()
            print(f"{('found', 'created')[created]} {strain=}")
            if d['strain'].get('workouts') is not None:
                for workout in d['strain']['workouts']:
                    during = workout.get('during') or dict()
                    zones = workout.get('zones')
                    if zones is None or len(zones) < 6:
                        zones = [0 for i in range(6)]
                    if workout.get('id') is not None:
                        workout_obj, created = Workout.objects.update_or_create(
                            strain=strain, # = models.ForeignKey(Strain, on_delete=models.CASCADE)
                            id=workout['id'], # = models.IntegerField(null=True), # 132062158,
                            defaults={
                                'averageHeartRate': workout.get('averageHeartRate'), # = models.FloatField(null=True), # 131,
                                'cumulativeWorkoutStrain': workout.get('cumulativeWorkoutStrain'), # = models.FloatField(null=True), # 14.9973,
                                'during_bounds': during.get('bounds'), # = models.CharField(null=True) # "[)",
                                'during_lower': get_date(during.get('lower'), tzinfo=pytz.UTC), # = models.DateTimeField(null=True) # "2020-10-25T03:01:00+00:00",
                                'during_upper': get_date(during.get('upper'), tzinfo=pytz.UTC), # = models.DateTimeField(null=True) # "2020-10-25T11:33:00+00:00",
                                'kilojoules': workout.get('kilojoules'), # = models.FloatField(null=True), # 2863.52,
                                'maxHeartRate': workout.get('maxHeartRate'), # = models.FloatField(null=True), # 157,
                                'rawScore': workout.get('rawScore'), # = models.FloatField(null=True), # 0.00579152500086973,
                                'score': workout.get('score'), # = models.FloatField(null=True), # 12.2451922891289,
                                'sportId': workout.get('sportId'), # = models.IntegerField(null=True), # 0,
                                'timezoneOffset': workout.get('timezoneOffset'), # = models.CharField(max_length=10, null=True), # "-0400",
                                'zone_0': zones[0], # = models.IntegerField(null=True)
                                'zone_1': zones[1], # = models.IntegerField(null=True)
                                'zone_2': zones[2], # = models.IntegerField(null=True)
                                'zone_3': zones[3], # = models.IntegerField(null=True)
                                'zone_4': zones[4], # = models.IntegerField(null=True)
                                'zone_5': zones[5] # = models.IntegerField(null=True)
                            }
                        )
                        workout_obj.save()
                        print(f"{('found', 'created')[created]} {workout_obj=}")
        return daily


class Recovery(models.Model):
    day = models.ForeignKey(Daily, on_delete=models.CASCADE)
    # blackoutUntil = models.FloatField(null=True) # null,
    # calibrating = models.FloatField(null=True) # false,
    heartRateVariabilityRmssd = models.FloatField(null=True) # 0.0590787,
    id = models.IntegerField(primary_key=True) # 60322913,
    # responded = models.FloatField(null=True) # false,
    restingHeartRate = models.FloatField(null=True) # 51,
    score = models.FloatField(null=True) # 54,
    # state = models.FloatField(null=True) # "complete",
    # surveyResponseId = models.FloatField(null=True) # null,
    timestamp = models.DateTimeField(null=True) # "2020-10-25T11:33:00+00:00",


class Sleep(models.Model):
    day = models.ForeignKey(Daily, on_delete=models.CASCADE)
    id = models.IntegerField(primary_key=True)
    needBreakdown_baseline = models.IntegerField(null=True) # 27430627,
    needBreakdown_debt = models.IntegerField(null=True) # 3703134,
    needBreakdown_naps = models.IntegerField(null=True) # 0,
    needBreakdown_strain = models.IntegerField(null=True) # 3278169,
    needBreakdown_total = models.IntegerField(null=True) # 34411932,
    qualityDuration = models.IntegerField(null=True) # 25139744,
    score = models.IntegerField(null=True) # 73,


class SleepDetail(models.Model):
    sleep = models.ForeignKey(Sleep, on_delete=models.CASCADE)
    id = models.IntegerField(primary_key=True) # 131838178,
    cyclesCount = models.IntegerField(null=True) # 5,
    disturbanceCount = models.IntegerField(null=True) # 12,
    during_bounds = models.CharField(max_length=20, null=True) # "[)",
    during_lower = models.DateTimeField(null=True) # "2020-10-25T03:01:00+00:00",
    during_upper = models.DateTimeField(null=True) # "2020-10-25T11:33:00+00:00",
    inBedDuration = models.IntegerField(null=True) # 30718493,
    isNap = models.BooleanField(default=False) # false,
    latencyDuration = models.IntegerField(null=True) # 0,
    lightSleepDuration = models.IntegerField(null=True) # 12011596,
    noDataDuration = models.IntegerField(null=True) # 0,
    qualityDuration = models.IntegerField(null=True) # 25139744,
    remSleepDuration = models.IntegerField(null=True) # 5948524,
    respiratoryRate = models.FloatField(null=True) # 14.3262,
    # responded = models.FloatField(null=True) # false,
    score = models.IntegerField(null=True) # 73,
    sleepConsistency = models.IntegerField(null=True) # 89,
    sleepEfficiency = models.FloatField(null=True) # 0.818391,
    slowWaveSleepDuration = models.IntegerField(null=True) # 7179624,
    # source = models.FloatField(null=True) # "user",
    # state = models.FloatField(null=True) # "complete",
    # surveyResponseId = models.FloatField(null=True) # null,
    timezoneOffset = models.CharField(max_length=20, null=True) # "-0400",
    wakeDuration = models.IntegerField(null=True) # 5615410

class Strain(models.Model):
    day = models.ForeignKey(Daily, on_delete=models.CASCADE)
    id = models.IntegerField(primary_key=True) #
    averageHeartRate = models.FloatField(null=True) # 75,
    kilojoules = models.FloatField(null=True) # 14218.7,
    maxHeartRate = models.FloatField(null=True) # 172,
    rawScore = models.FloatField(null=True) # 0.0143161574422024,
    score = models.FloatField(null=True) # 16.3900610678999,
    # state = models.FloatField(null=True) # "complete",


class Workout(models.Model):
    strain = models.ForeignKey(Strain, on_delete=models.CASCADE)
    # "altitudeChange": null,
    # "altitudeGain": null,
    averageHeartRate = models.FloatField(null=True) # 131,
    cumulativeWorkoutStrain = models.FloatField(null=True) # 14.9973,
    # distance = models.FloatField(null=True) # null,
    during_bounds = models.CharField(max_length=20, null=True) # "[)",
    during_lower = models.DateTimeField(null=True) # "2020-10-25T16:53:55.099+00:00",
    during_upper = models.DateTimeField(null=True) # "2020-10-25T18:08:15.545+00:00",
    # gpsEnabled = models.FloatField(null=True) # false,
    id = models.IntegerField(primary_key=True) # 132062158,
    kilojoules = models.FloatField(null=True) # 2863.52,
    maxHeartRate = models.FloatField(null=True) # 157,
    rawScore = models.FloatField(null=True) # 0.00579152500086973,
    # responded = models.FloatField(null=True) # false,
    score = models.FloatField(null=True) # 12.2451922891289,
    # source = models.FloatField(null=True) # "auto",
    sportId = models.IntegerField(null=True) # 0,
    # state = models.FloatField(null=True) # "complete",
    # surveyResponseId = models.FloatField(null=True) # null,
    timezoneOffset = models.CharField(max_length=10, null=True) # "-0400",
    zone_0 = models.IntegerField(null=True)
    zone_1 = models.IntegerField(null=True)
    zone_2 = models.IntegerField(null=True)
    zone_3 = models.IntegerField(null=True)
    zone_4 = models.IntegerField(null=True)
    zone_5 = models.IntegerField(null=True)


class HR(models.Model):
    user = models.ForeignKey(WhoopUser, on_delete=models.CASCADE)
    timestamp = models.DateTimeField()
    value = models.IntegerField()

    @classmethod
    def fill_from_response(cls, user, d, batch_size=500):
        # for point in d:
        #     hr, created = cls.objects.update_or_create(
        #         user = user.whoopuser,
        #         timestamp = datetime.datetime.utcfromtimestamp(point['time']/1000).replace(tzinfo=pytz.UTC),
        #         defaults = {'value': point['data']}
        #     )
        #     hr.save()

        # bulk create:
        objects = [cls(user = user.whoopuser,timestamp = datetime.datetime.utcfromtimestamp(point['time']/1000).replace(tzinfo=pytz.UTC), value=point['data']) for point in d]
        cls.objects.bulk_create(objects, batch_size)


class JournalEntry(models.Model):
    # user = models.ForeignKey(WhoopUser, on_delete=models.CASCADE)
    # it's pulled using the day's id, as "cycle_id"
    day = models.ForeignKey(Daily, on_delete=models.CASCADE)

    tracker_id = models.IntegerField() # "id": 50,
    created_at = models.DateTimeField(null=True, max_length=200) # "created_at": 1583441766.111,
    updated_at = models.DateTimeField(null=True, max_length=200) # "updated_at": 1585669607.122,
    title = models.CharField(max_length=200) # "title": "Intermittent Fasting",
    status = models.CharField(null=True, max_length=200) # "status": "active",
    category = models.CharField(null=True, max_length=200) # "category": "Nutrition",
    # sticky = models.BooleanField(null=True, max_length=200) # "sticky": false,
    description = models.CharField(null=True, max_length=200) # "description": null,
    question_text = models.CharField(null=True, max_length=200) # "question_text": "Follow an intermittent fasting diet?",
    time_label = models.CharField(null=True, max_length=200) # "time_label": "When was your last meal?",
    time_context_label = models.CharField(null=True, max_length=200) # "time_context_label": "stopped eating at",
    magnitude = models.JSONField(null=True) # "magnitude": ...
    default_tracker = models.BooleanField(null=True) # "default_tracker": false

    id = models.IntegerField(primary_key=True) # "id": 434680106,
    journal_entry_id = models.IntegerField(null=True) # "journal_entry_id": 52336709,
    entry_created_at = models.DateTimeField(null=True) # "created_at": 1611923762.42,
    entry_updated_at = models.DateTimeField(null=True) # "updated_at": 1611923762.42,
    answered_yes = models.BooleanField(null=True) # "answered_yes": false,
    behavior_tracker_id = models.IntegerField(null=True) # "behavior_tracker_id": 50,
    time_input_value = models.DateTimeField(null=True) # "time_input_value": null,
    time_input_label = models.CharField(max_length=200, null=True) # "time_input_label": null,
    magnitude_input_label = models.CharField(max_length=200, null=True) # "magnitude_input_label": null,
    magnitude_input_value = models.IntegerField(null=True) # "magnitude_input_value": null

    def __str__(self):
        return f"{str(self.day.day)} {self.title}. answered_yes: {self.answered_yes}"

    @classmethod
    def create_from_response(cls, cycle_id, d):
        behavior_tracker = d.get('behavior_tracker') or dict()
        tracker_input = d.get('tracker_input') or dict()
        journalentry, created = cls.objects.update_or_create(
            day=Daily.objects.get(id=cycle_id),
            id=tracker_input.get('id'),
            defaults={
                'tracker_id': behavior_tracker.get('id'),
                'created_at': get_utc_time(behavior_tracker.get('created_at')),
                'updated_at': get_utc_time(behavior_tracker.get('updated_at')),
                'title': behavior_tracker.get('title'),
                'status': behavior_tracker.get('status'),
                'category': behavior_tracker.get('category'),
                # 'sticky': behavior_tracker.get('sticky'),
                'description': behavior_tracker.get('description'),
                'question_text': behavior_tracker.get('question_text'),
                'time_label': behavior_tracker.get('time_label'),
                'time_context_label': behavior_tracker.get('time_context_label'),
                'magnitude': behavior_tracker.get('magnitude'),
                'default_tracker': behavior_tracker.get('default_tracker'),
                'journal_entry_id': tracker_input.get('journal_entry_id'),
                'entry_created_at': get_utc_time(tracker_input.get('created_at')),
                'entry_updated_at': get_utc_time(tracker_input.get('updated_at')),
                'answered_yes': tracker_input.get('answered_yes'),
                'behavior_tracker_id': tracker_input.get('behavior_tracker_id'),
                'time_input_value': get_utc_time(tracker_input.get('time_input_value')),
                'time_input_label': tracker_input.get('time_input_label'),
                'magnitude_input_label': tracker_input.get('magnitude_input_label'),
                'magnitude_input_value': tracker_input.get('magnitude_input_value')
            }
        )
        print(f"{('found', 'created')[created]} {journalentry=}")
        journalentry.save()
        return journalentry
