import ast
import json
import httplib
import itertools
from urllib2 import urlopen, URLError
from urlparse import urljoin
from time import time
from copy import deepcopy
from os import path
import logging
import threading

from rauth import OAuth2Service
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError as request_HTTPError

from Model.Project import Project
from Model.Sample import Sample
from Exceptions.ProjectError import ProjectError
from Exceptions.SampleError import SampleError
from Exceptions.SequenceFileError import SequenceFileError
from Exceptions.SampleSheetError import SampleSheetError
from Validation.offlineValidation import validate_URL_form
from API.pubsub import send_message


HTTP_MAX_RETRIES = 5
HTTP_BACKOFF_FACTOR = 1

class ApiCalls(object):

    _instance = None

    def __new__(cls, client_id, client_secret, base_URL, username, password, max_wait_time=20):
        """
            Overriding __new__ to implement a singleton
            This is done instead of a decorator so that mocking still works for class.
            If the instance has not been created yet, or the passed in arguments are different, create a new instance,
                and drop the old (if existing) instance
            If the instance already exists and is valid, return the instance

            arguments:
                client_id -- client_id for creating access token.
                client_secret -- client_secret for creating access  token.
                base_URL -- url of the IRIDA server
                username -- username for server
                password -- password for given username
                max_wait_time -- timeout (seconds), default=20

        """

        if not ApiCalls._instance or ApiCalls._instance.parameters_are_different(
                client_id, client_secret, base_URL,username, password, max_wait_time):

            # Create a new instance of the API
            ApiCalls._instance = object.__new__(cls)

            # initialize API instance variables
            ApiCalls._instance.client_id = client_id
            ApiCalls._instance.client_secret = client_secret
            ApiCalls._instance.base_URL = base_URL
            ApiCalls._instance.username = username
            ApiCalls._instance.password = password
            ApiCalls._instance.max_wait_time = max_wait_time

            # initialize API object
            ApiCalls._instance._session_lock = threading.Lock()
            ApiCalls._instance._session_set_externally = False
            ApiCalls._instance.create_session()
            ApiCalls._instance.cached_projects = None
            ApiCalls._instance.cached_samples = {}

        return ApiCalls._instance

    @classmethod
    def close(cls):
        """
        Close the current session by setting the current instance to None so
        the next call with re-initialize the session
        """
        ApiCalls._instance = None

    def parameters_are_different(self, client_id, client_secret, base_URL, username, password, max_wait_time):
        """
        Compare the current instance variables with a new set of variables
        """

        result = (self.client_id != client_id or
                 self.client_secret != client_secret or
                 self.base_URL != base_URL or
                 self.username != username or
                 self.password != password or
                 self.max_wait_time != max_wait_time)

        if result:
            logging.warning("ApiCalls session instance parameters are different, "
                            "a new session instance will be created")

        return result

    @property
    def session(self):
        if self._session_set_externally:
            return self._session

        try:
            self._session_lock.acquire()
            response = self._session.options(self.base_URL)
            if response.status_code != httplib.OK:
                raise Exception
            else:
                logging.debug("Existing session still works, going to reuse it.")
        except:
            logging.debug("Token is probably expired, going to get a new session.")
            oauth_service = self.get_oauth_service()
            access_token = self.get_access_token(oauth_service)

            new_session = oauth_service.get_session(access_token)
            self._session = self.add_timeout_backoff(new_session)
        finally:
            self._session_lock.release()

        return self._session

    @session.setter
    def session(self, session):
        self._session = session
        self._session_set_externally = True

    def create_session(self):
        """
        create session to be re-used until expiry for get and post calls

        returns session (OAuth2Session object)
        """

        self.cached_projects = None
        # set http backoff option
        self.http_max_retries = HTTP_MAX_RETRIES
        self.http_backoff_factor = HTTP_BACKOFF_FACTOR

        if self.base_URL[-1:] != "/":
            self.base_URL = self.base_URL + "/"

        if validate_URL_form(self.base_URL):
            oauth_service = self.get_oauth_service()
            access_token = self.get_access_token(oauth_service)

            new_session = oauth_service.get_session(access_token)
            self._session = self.add_timeout_backoff(new_session)

            if self.validate_URL_existence(self.base_URL, use_session=True) is False:
                raise Exception("Cannot create session. Verify your credentials are correct.")
        else:
            raise URLError(self.base_URL + " is not a valid URL")

    def add_timeout_backoff(self, new_session):
        # method stolen from https://www.programcreek.com/python/example/102997/requests.adapters example 3
        # Adds a retry counter and backoff to requests that timeout
        try: