import functools
import requests
import json
from hashlib import md5
from .models import *
from .exceptions import *
from cachecontrol import CacheControl
from datetime import datetime
import time


class TBA:
    """
    Main library class.

    Contains methods for interacting with The Blue Alliance.
    """

    READ_URL_PRE = 'https://www.thebluealliance.com/api/v3/'
    WRITE_URL_PRE = 'https://www.thebluealliance.com/api/trusted/v1/'
    session = CacheControl(requests.Session())
    auth_secret = ''
    event_key = ''

    def __init__(self, auth_key, auth_id='', auth_secret='', event_key=''):
        """
        Store auth key so we can reuse it as many times as we make a request.

        :param auth_key: Your application authorization key, obtainable at https://www.thebluealliance.com/account.
        :param auth_id: Your event authorization ID, obtainable at https://www.thebluealliance.com/request/apiwrite
        :param auth_secret: Your event authorization secret, obtainable at https://www.thebluealliance.com/request/apiwrite
        :param event_key: The event key that is linked to the ID and secret provided.
        """
        self.auth_secret = auth_secret
        self.event_key = event_key
        self.session.headers.update({'X-TBA-Auth-Key': auth_key, 'X-TBA-Auth-Id': auth_id})
        self._if_modified_since = None
        self._last_modified = False

    def _get(self, url):
        """
        Helper method: GET data from given URL on TBA's API.

        :param url: URL string to get data from.
        :return: Requested data in JSON format.
        """
        extra_headers = {}
        if self._if_modified_since is not None:
            extra_headers['If-Modified-Since'] = self._if_modified_since

        response = self.session.get(self.READ_URL_PRE + url, headers=extra_headers)
        last_modified = response.headers.get('Last-Modified')
        
        if last_modified is not None:
            if response.status_code == 304:
                raise NotModifiedException(response.headers['Last-Modified'])

            if self._last_modified:
                self._last_modified = LastModifiedDate(last_modified)

        raw = response.json()
        self._detect_errors(raw)
        return raw

    def _post(self, url, data):
        """
        Helper method: POST data to a given URL on TBA's API.

        :param url: URL string to post data to and hash.
        :pararm data: JSON data to post and hash.
        :return: Requests Response object.

        """
        raw = self.session.post(
            self.WRITE_URL_PRE + url % self.event_key, 
            data=data, 
            headers={
                'X-TBA-Auth-Sig': md5((self.auth_secret + '/api/trusted/v1/' + url % self.event_key + data).encode('utf-8')).hexdigest()
            }
        )

        self._detect_errors(raw)
        return raw

    def _detect_errors(self, json):
        if not isinstance(json, dict):
            return

        errors = json.get('Errors')
        if errors is not None:
            raise TBAErrorList([error.popitem() for error in errors])

    def _check_modified(func):
        @functools.wraps(func)
        def wrapper(self, *args, last_modified=False, if_modified_since=None, silent=True, **kwargs):
            self._if_modified_since = if_modified_since and datetime.strftime(if_modified_since, '%a, %d %b %Y %H:%M:%S GMT')
            self._last_modified = last_modified

            if last_modified or if_modified_since:
                self.cache = False

            try:
                output = func(self, *args, **kwargs)
                if last_modified:
                    return output, self._last_modified
                return output
            except NotModifiedException as e:
                if not silent:
                    raise e from None
                return e.last_modified
            finally:
                self.cache = True

        return wrapper

    @property
    def cache(self):
        for adapter in self.session.adapters.values():
            if 'GET' in adapter.cacheable_methods:
                return True

        return False

    @cache.setter
    def cache(self, on):
        for adapter in self.session.adapters.values():
            if on:
                adapter.cacheable_methods = ('GET',)
            else:
                adapter.cacheable_methods = ()

    @staticmethod
    def team_key(identifier):
        """
        Take raw team number or string key and return string key.

        Used by all team-related methods to support either an integer team number or team key being passed.

        (We recommend passing an integer, just because it's cleaner. But whatever works.)

        :param identifier: int team number or str 'frc####'
        :return: string team key in format 'frc####'
        """
        return identifier if type(identifier) == str else 'frc%s' % identifier

    @_check_modified
    def status(self):
        """
        Get TBA API status information.

        :return: Data on current status of the TBA API as APIStatus object.
        """
        return APIStatus(self._get('status'))

    @_check_modified
    def teams(self, page=None, year=None, simple=False, keys=False):
        """
        Get list of teams.

        :param page: Page of teams to view. Each page contains 500 teams.
        :param year: View teams from a specific year.
        :param simple: Get only vital data.
        :param keys: Set to true if you only want the teams' keys rather than full data on them.
        :return: List of Team objects or string keys.
        """
        # If the user has requested a specific page, get that page.
        if page is not None:
            if year:
                if keys:
                    return self._get('teams/%s/%s/keys' % (year, page))
                else:
                    return [Team(raw) for raw in self._get('teams/%s/%s%s' % (year, page, '/simple' if simple else ''))]
            else:
                if keys:
                    return self._get('teams/%s/keys' % page)
                else:
                    return [Team(raw) for raw in self._get('teams/%s%s' % (page, '/simple' if simple else ''))]
        # If no page was specified, get all of them and combine.
        else:
            teams = []
            target = 0
            while True:
                page_teams = self.teams(page=target, year=year, simple=simple, keys=keys)
                if page_teams:
                    teams.extend(page_teams)
                else:
                    break
                target += 1
            return teams

    @_check_modified
    def team(self, team, simple=False):
        """
        Get data on a single specified team.

        :param team: Team to get data for.
        :param simple: Get only vital data.
        :return: Team object with data on specified team.
        """
        return Team(self._get('team/%s%s' % (self.team_key(team), '/simple' if simple else '')))

    @_check_modified
    def team_events(self, team, year=None, simple=False, keys=False):
        """
        Get team events a team has participated in.

        :param team: Team to get events for.
        :param year: Year to get events from.
        :param simple: Get only vital data.
        :param keys: Get just the keys of the events. Set to True if you only need the keys of each event and not their full data.
        :return: List of strings or Teams
        """
        if year:
            if keys:
                return self._get('team/%s/events/%s/keys' % (self.team_key(team), year))
            else:
                return [Event(raw) for raw in self._get('team/%s/events/%s%s' % (self.team_key(team), year, '/simple' if simple else ''))]
        else:
            if keys:
                return self._get('team/%s/events/keys' % self.team_key(team))
            else:
                return [Event(raw) for raw in self._get('team/%s/events%s' % (self.team_key(team), '/simple' if simple else ''))]

    @_check_modified
    def team_awards(self, team, year=None, event=None):
        """
        Get list of awards team has recieved.

        :param team: Team to get awards of.
        :param year: Year to get awards from.
        :param event: Event to get awards from.
        :return: List of Award objects
        """
        if event:
            return [Award(raw) for raw in self._get('team/%s/event/%s/awards' % (self.team_key(team), event))]
        else:
            if year:
                return [Award(raw) for raw in self._get('team/%s/awards/%s' % (self.team_key(team), year))]
            else:
                return [Award(raw) for raw in self._get('team/%s/awards' % self.team_key(team))]

    @_check_modified
    def team_matches(self, team, event=None, year=None, simple=False, keys=False):
        """
        Get list of matches team has participated in.

        :param team: Team to get matches of.
        :param year: Year to get matches from.
        :param event: Event to get matches from.
        :param simple: Get only vital data.
        :param keys: Only get match keys rather than their full data.
        :return: List of string keys or Match objects.
        """
        if event:
            if keys:
                return self._get('team/%s/event/%s/matches/keys' % (self.team_key(team), event))
            else:
                return [Match(raw) for raw in self._get('team/%s/event/%s/matches%s' % (self.team_key(team), event, '/simple' if simple else ''))]
        elif year:
            if keys:
                return self._get('team/%s/matches/%s/keys' % (self.team_key(team), year))
            else:
                return [Match(raw) for raw in self._get('team/%s/matches/%s%s' % (self.team_key(team), year, '/simple' if simple else ''))]

    @_check_modified
    def team_years(self, team):
        """
        Get years during which a team participated in FRC.

        :param team: Key for team to get data about.
        :return: List of integer years in which team participated.
        """
        return self._get('team/%s/years_participated' % self.team_key(team))

    @_check_modified
    def team_media(self, team, year=None, tag=None):
        """
        Get media for a given team.

        :param team: Team to get media of.
        :param year: Year to get media from.
        :param tag: Get only media with a given tag.
        :return: List of Media objects.
        """
        return [Media(raw) for raw in self._get('team/%s/media%s%s' % (self.team_key(team), ('/tag/%s' % tag) if tag else '', ('/%s' % year) if year else ''))]

    @_check_modified
    def team_robots(self, team):
        """
        Get data about a team's robots.

        :param team: Key for team whose robots you want data on.
        :return: List of Robot objects
        """
        return [Robot(raw) for raw in self._get('team/%s/robots' % self.team_key(team))]

    @_check_modified
    def team_districts(self, team):
        """
        Get districts a team has competed in.

        :param team: Team to get data on.
        :return: List of District objects.
        """
        return [District(raw) for raw in self._get('team/%s/districts' % self.team_key(team))]

    @_check_modified
    def team_profiles(self, team):
        """
        Get team's social media profiles linked on their TBA page.

        :param team: Team to get data on.
        :return: List of Profile objects.
        """
        return [Profile(raw) for raw in self._get('team/%s/social_media' % self.team_key(team))]

    @_check_modified
    def team_status(self, team, event):
        """
        Get status of a team at an event.

        :param team: Team whose status to get.
        :param event: Event team is at.
        :return: Status object.
        """
        return Status(self._get('team/%s/event/%s/status' % (self.team_key(team), event)))

    @_check_modified
    def events(self, year, simple=False, keys=False):
        """
        Get a list of events in a given year.

        :param year: Year to get events from.
        :param keys: Get only keys of the events rather than full data.
        :param simple: Get only vital data.
        :return: List of string event keys or Event objects.
        """
        if keys:
            return self._get('events/%s/keys' % year)
        else:
            return [Event(raw) for raw in self._get('events/%s%s' % (year, '/simple' if simple else ''))]

    @_check_modified
    def event(self, event, simple=False):
        """
        Get basic information about an event.

        More specific data (typically obtained with the detail_type URL parameter) can be obtained with event_alliances(), event_district_points(), event_insights(), event_oprs(), event_predictions(), and event_rankings().

        :param event: Key of event for which you desire data.
        :param simple: Get only vital data.
        :return: A single Event object.
        """
        return Event(self._get('event/%s%s' % (event, '/simple' if simple else '')))

    @_check_modified
    def event_alliances(self, event):
        """
        Get information about alliances at event.

        :param event: Key of event to get data on.
        :return: List of Alliance objects.
        """
        return [Alliance(raw) for raw in self._get('event/%s/alliances' % event)]

    @_check_modified
    def event_district_points(self, event):
        """
        Get district point information about an event.

        :param event: Key of event to get data on.
        :return: Single DistrictPoints object.
        """
        return DistrictPoints(self._get('event/%s/district_points' % event))

    @_check_modified
    def event_insights(self, event):
        """
        Get insights about an event.

        :param event: Key of event to get data on.
        :return: Single Insights object.
        """
        return Insights(self._get('event/%s/insights' % event))

    @_check_modified
    def event_oprs(self, event):
        """
        Get OPRs from an event.

        :param event: Key of event to get data on.
        :return: Single OPRs object.
        """
        return OPRs(self._get('event/%s/oprs' % event))

    @_check_modified
    def event_predictions(self, event):
        """
        Get predictions for matches during an event.

        :param event: Key of event to get data on.
        :return: Single Predictions object.
        """
        return Predictions(self._get('event/%s/predictions' % event))

    @_check_modified
    def event_rankings(self, event):
        """
        Get rankings from an event.

        :param event: Key of event to get data on.
        :return: Single Rankings object.
        """
        return Rankings(self._get('event/%s/rankings' % event))

    @_check_modified
    def event_teams(self, event, simple=False, keys=False):
        """
        Get list of teams at an event.

        :param event: Event key to get data on.
        :param simple: Get only vital data.
        :param keys: Return list of team keys only rather than full data on every team.
        :return: List of string keys or Team objects.
        """
        if keys:
            return self._get('event/%s/teams/keys' % event)
        else:
            return [Team(raw) for raw in self._get('event/%s/teams%s' % (event, '/simple' if simple else ''))]

    @_check_modified
    def event_awards(self, event):
        """
        Get list of awards presented at an event.

        :param event: Event key to get data on.
        :return: List of Award objects.
        """
        return [Award(raw) for raw in self._get('event/%s/awards' % event)]

    @_check_modified
    def event_matches(self, event, simple=False, keys=False):
        """
        Get list of matches played at an event.

        :param event: Event key to get data on.
        :param keys: Return list of match keys only rather than full data on every match.
        :param simple: Get only vital data.
        :return: List of string keys or Match objects.
        """
        if keys:
            return self._get('event/%s/matches/keys' % event)
        else:
            return [Match(raw) for raw in self._get('event/%s/matches%s' % (event, '/simple' if simple else ''))]

    @_check_modified
    def match(self, key=None, year=None, event=None, type='qm', number=None, round=None, simple=False):
        """
        Get data on a match.

        You may either pass the match's key directly, or pass `year`, `event`, `type`, `match` (the match number), and `round` if applicable (playoffs only). The event year may be specified as part of the event key or specified in the `year` parameter.

        :param key: Key of match to get data on. First option for specifying a match (see above).
        :param year: Year in which match took place. Optional; if excluded then must be included in event key.
        :param event: Key of event in which match took place. Including year is optional; if excluded then must be specified in `year` parameter.
        :param type: One of 'qm' (qualifier match), 'qf' (quarterfinal), 'sf' (semifinal), 'f' (final). If unspecified, 'qm' will be assumed.
        :param number: Match number. For example, for qualifier 32, you'd pass 32. For Semifinal 2 round 3, you'd pass 2.
        :param round: For playoff matches, you will need to specify a round.
        :param simple: Get only vital data.
        :return: A single Match object.
        """
        if key:
            return Match(self._get('match/%s%s' % (key, '/simple' if simple else '')))
        else:
            return Match(self._get('match/{year}{event}_{type}{number}{round}{simple}'.format(year=year if not event[0].isdigit() else '',
                                                                                              event=event,
                                                                                              type=type,
                                                                                              number=number,
                                                                                              round=('m%s' % round) if not type == 'qm' else '',
                                                                                              simple='/simple' if simple else '')))

    @_check_modified
    def districts(self, year):
        """
        Return a list of districts active.

        :param year: Year from which you want to get active districts.
        :return: A list of District objects.
        """
        return [District(raw) for raw in self._get('districts/%s' % year)]

    @_check_modified
    def district_events(self, district, simple=False, keys=False):
        """
        Return list of events in a given district.

        :param district: Key of district whose events you want.
        :param simple: Get only vital data.
        :param keys: Return list of event keys only rather than full data on every event.
        :return: List of string keys or Event objects.
        """
        if keys:
            return self._get('district/%s/events/keys' % district)
        else:
            return [Event(raw) for raw in self._get('district/%s/events%s' % (district, '/simple' if simple else ''))]

    @_check_modified
    def district_rankings(self, district):
        """
        Return data about rankings in a given district.

        :param district: Key of district to get rankings of.
        :return: List of DistrictRanking objects.
        """
        return [DistrictRanking(raw) for raw in self._get('district/%s/rankings' % district)]

    @_check_modified
    def district_teams(self, district, simple=False, keys=False):
        """
        Get list of teams in the given district.

        :param district: Key for the district to get teams in.
        :param simple: Get only vital data.
        :param keys: Return list of team keys only rather than full data on every team.
        :return: List of string keys or Team objects.
        """
        if keys:
            return self._get('district/%s/teams/keys' % district)
        else:
            return [Team(raw) for raw in self._get('district/%s/teams' % district)]

    def update_trusted(self, auth_id, auth_secret, event_key):
        """
        Set Trusted API ID and Secret and the event key they are assigned to.

        :param auth_id: Your event authorization ID, obtainable at https://www.thebluealliance.com/request/apiwrite
        :param auth_secret: Your event authorization secret, obtainable at https://www.thebluealliance.com/request/apiwrite
        :param event_key: The event key that is linked to the ID and secret provided.
        """
        self.session.headers.update({'X-TBA-Auth-Id': auth_id})
        self.auth_secret = auth_secret
        self.event_key = event_key

    def update_event_info(self, data):
        """
        Update an event's info on The Blue Alliance.

        :param data: Dictionary of data to update the event with.
        """
        return self._post('event/%s/info/update', json.dumps(data))

    def update_event_alliances(self, data):
        """
        Update an event's alliances on The Blue Alliance.

        :param data: List of lists of alliances in frc#### string format.
        """
        return self._post('event/%s/alliance_selections/update', json.dumps(data))

    def update_event_awards(self, data):
        """
        Update an event's awards on The Blue Alliance.

        :param data: List of Dictionaries of award winners. Each dictionary should have a name_str for the award name, team_key in frc#### string format, and the awardee for any awards given to individuals. The last two can be null
        """
        return self._post('event/%s/awards/update', json.dumps(data))

    def update_event_matches(self, data):
        """
        Update an event's matches on The Blue Alliance.

        :param data: List of Dictionaries. More info about the match data can be found in the API docs.
        """
        return self._post('event/%s/matches/update', json.dumps(data))

    def delete_event_matches(self, data=None):
        """
        Delete an event's matches on The Blue Alliance.

        :param data: List of match keys to delete, can be ommited if you would like to delete all matches.
        """
        return self._post('event/%s/matches/delete_all' if data is None else 'event/%s/matches/delete', json.dumps(self.event_key) if data is None else json.dumps(data))

    def update_event_rankings(self, data):
        """
        Update an event's rankings on The Blue Alliance.

        :param data: Dictionary of breakdowns and rankings. Rankings are a list of dictionaries.
        """
        return self._post('event/%s/rankings/update', json.dumps(data))

    def update_event_team_list(self, data):
        """
        Update an event's team list on The Blue Alliance.

        :param data: a list of team keys in frc#### string format.
        """
        return self._post('event/%s/team_list/update', json.dumps(data))

    def add_match_videos(self, data):
        """
        Add match videos to the respective match pages of an event on The Blue Alliance.

        :param data: Dictionary of partial match keys to youtube video ids.
        """
        return self._post('event/%s/match_videos/add', json.dumps(data))

    def add_event_videos(self, data):
        """
        Add videos to an event's media tab on The Blue Alliance.

        :param data: List of youtube video ids.
        """
        return self._post('event/%s/media/add', json.dumps(data))