from .routes import _Routes
from .helpers import *
from .http_request_manager import _HttpRequestManager
from .bearer_auth import BearerAuth
from domino._version import __version__

try:
    import urllib2
except ImportError:
    import urllib.request as urllib2

import logging
import requests
from requests.auth import HTTPBasicAuth
import time
import pprint
import re


class Domino:
    def __init__(self, project, api_key=None, host=None, domino_token_file=None):
        self._configure_logging()
        host = get_host_or_throw_exception(host)
        domino_token_file = get_path_to_domino_token_file(domino_token_file)
        api_key = get_api_key(api_key)

        # Initialise request manager
        self.request_manager = self._initialise_request_manager(api_key, domino_token_file)

        owner_username, project_name = project.split("/")
        self._routes = _Routes(host, owner_username, project_name)

        # Get version
        self._version = self.deployment_version().get("version")
        self._logger.info(f"Domino deployment {host} is running version {self._version}")

        # Check version compatibility
        if not is_version_compatible(self._version):
            error_message = f"Domino version: {self._version} is not compatible with " \
                            f"python-domino version: {__version__}"
            self._logger.error(error_message)
            raise Exception(error_message)

    def _configure_logging(self):
        logging.basicConfig(level=logging.INFO)
        self._logger = logging.getLogger(__name__)

    def _initialise_request_manager(self, api_key, domino_token_file):
        if api_key is None and domino_token_file is None:
            raise Exception("Either api_key or path_to_domino_token_file "
                            f"must be provided via class constructor or "
                            f"environment variable")
        elif domino_token_file is not None:
            self._logger.info("Initializing python-domino with bearer token auth")
            return _HttpRequestManager(BearerAuth(domino_token_file))
        else:
            self._logger.info("Fallback: Initializing python-domino with basic auth")
            return _HttpRequestManager(HTTPBasicAuth('', api_key))

    def commits_list(self):
        url = self._routes.commits_list()
        return self._get(url)

    def runs_list(self):
        url = self._routes.runs_list()
        return self._get(url)

    def runs_start(self, command, isDirect=False, commitId=None, title=None,
                   tier=None, publishApiEndpoint=None):

        url = self._routes.runs_start()

        request = {
            "command": command,
            "isDirect": isDirect,
            "commitId": commitId,
            "title": title,
            "tier": tier,
            "publishApiEndpoint": publishApiEndpoint
        }

        response = self.request_manager.post(url, json=request)
        return response.json()

    def runs_start_blocking(self, command, isDirect=False, commitId=None,
                            title=None, tier=None, publishApiEndpoint=None,
                            poll_freq=5, max_poll_time=6000, retry_count=5):
        """
        Run a tasks that runs in a blocking loop that periodically checks to
        see if the task is done.  If the task errors an exception is raised.

        parameters
        ----------
        command : list of strings
                  list that containst the name of the file to run in index 0
                  and args in subsequent positions.
                  example:
                  >> domino.runs_start(["main.py", "arg1", "arg2"])

        isDirect : boolean (Optional)
                   Whether or not this command should be passed directly to
                   a shell.

        commitId : string (Optional)
                   The commitId to launch from. If not provided, will launch
                   from latest commit.

        title    : string (Optional)
                   A title for the run

        tier     : string (Optional)
                   The hardware tier to use for the run. Will use project
                   default tier if not provided.

        publishApiEndpoint : boolean (Optional)
                            Whether or not to publish an API endpoint from the
                            resulting output.

        poll_freq : int (Optional)
                    Number of seconds in between polling of the Domino server
                    for status of the task that is running.

        max_poll_time : int (Optional)
                        Maximum number of seconds to wait for a task to
                        complete. If this threshold is exceeded, an exception
                        is raised.

        retry_count : int (Optional)
                        Maximum number of retry to do while polling
                        (in-case of transient http errors). If this
                        threshold exceeds, an exception is raised.
        """
        run_response = self.runs_start(command, isDirect, commitId, title,
                                       tier, publishApiEndpoint)
        run_id = run_response['runId']

        poll_start = time.time()
        current_retry_count = 0
        while True:
            try:
                run_info = self.get_run_info(run_id)
                current_retry_count = 0
            except requests.exceptions.RequestException as e:
                current_retry_count += 1
                self._logger.warn(f'Failed to get run info for runId: {run_id} : {e}')
                if current_retry_count > retry_count:
                    raise Exception(f'Cannot get run info, max retry {retry_count} exceeded') from None
                else:
                    self._logger.info(f'Retrying ({current_retry_count}/{retry_count}) getting run info ...')
                    time.sleep(poll_freq)
                    continue

            elapsed_time = time.time() - poll_start

            if elapsed_time >= max_poll_time:
                raise Exception('Run \
                                exceeded maximum time of \
                                {} seconds'.format(max_poll_time))

            if run_info is None:
                raise Exception("Tried to access nonexistent run id {}.".
                                format(run_id))

            output_commit_id = run_info.get('outputCommitId')
            if not output_commit_id:
                time.sleep(poll_freq)
                continue

            # once task has finished running check to see if it was successfull
            else:
                stdout_msg = self.runs_stdout(run_id)

                if run_info['status'] != 'Succeeded':
                    header_msg = ("Remote run {0} \
                                  finished but did not succeed.\n"
                                  .format(run_id))
                    raise Exception(header_msg + stdout_msg)

                logging.info(stdout_msg)
                break

        return run_response

    def run_stop(self, runId, saveChanges=True, commitMessage=None):
        """
        :param runId: string
        :param saveChanges: boolean (Optional) Save or discard run results.
        :param commitMessage: string (Optional)
        """
        url = self._routes.run_stop(runId)
        request = {
            "saveChanges": saveChanges,
            "commitMessage": commitMessage,
            "ignoreRepoState": False
        }
        response = self.request_manager.post(url, json=request)

        if response.status_code == 400:
            raise Warning("Run ID:" + runId + " not found.")
        else:
            return response

    def runs_status(self, runId):
        url = self._routes.runs_status(runId)
        return self._get(url)

    def get_run_info(self, run_id):
        for run_info in self.runs_list()['data']:
            if run_info['id'] == run_id:
                return run_info

    def runs_stdout(self, runId):
        """
        Get std out emitted by a particular run.

        parameters
        ----------
        runId : string
                the id associated with the run.
        """
        url = self._routes.runs_stdout(runId)
        # pprint.pformat outputs a string that is ready to be printed
        return pprint.pformat(self._get(url)['stdout'])

    def files_list(self, commitId, path='/'):
        url = self._routes.files_list(commitId, path)
        return self._get(url)

    def files_upload(self, path, file):
        url = self._routes.files_upload(path)
        return self._put_file(url, file)

    def blobs_get(self, key):
        self._validate_blob_key(key)
        url = self._routes.blobs_get(key)
        return self.request_manager.get_raw(url)

    def fork_project(self, target_name):
        url = self._routes.fork_project()
        request = {"overrideProjectName": target_name}
        response = self.request_manager.post(url, data=request)
        return response.status_code

    def endpoint_state(self):
        url = self._routes.endpoint_state()
        return self._get(url)

    def endpoint_unpublish(self):
        url = self._routes.endpoint()
        response = self.request_manager.delete(url)
        return response

    def endpoint_publish(self, file, function, commitId):
        url = self._routes.endpoint_publish()

        request = {
            "commitId": commitId,
            "bindingDefinition": {
                "file": file,
                "function": function
            }
        }

        response = self.request_manager.post(url, json=request)
        return response

    def deployment_version(self):
        url = self._routes.deployment_version()
        return self._get(url)

    def project_create(self, owner_username, project_name):
        self.requires_at_least("1.53.0.0")
        url = self._routes.project_create()
        request = {
            'owner': owner_username,
            'name': project_name
        }
        response = self.request_manager.post(url, data=request, allow_redirects=False)
        disposition = parse_play_flash_cookie(response)
        if disposition.get("error"):
            raise Exception(disposition.get("message"))
        else:
            return disposition

    def collaborators_get(self):
        self.requires_at_least("1.53.0.0")
        url = self._routes.collaborators_get()
        return self._get(url)

    def collaborators_add(self, usernameOrEmail, message=""):
        self.requires_at_least("1.53.0.0")
        url = self._routes.collaborators_add()
        request = {
            'collaboratorUsernameOrEmail': usernameOrEmail,
            'welcomeMessage': message
        }
        response = self.request_manager.post(url, data=request, allow_redirects=False)
        disposition = parse_play_flash_cookie(response)
        if disposition.get("error"):
            raise Exception(disposition.get("message"))
        else:
            return disposition

    # App functions
    def app_publish(self, unpublishRunningApps=True, hardwareTierId=None):
        if unpublishRunningApps is True:
            self.app_unpublish()
        app_id = self._app_id
        if app_id is None:
            # No App Exists creating one
            app_id = self.__app_create(hardware_tier_id=hardwareTierId)
        url = self._routes.app_start(app_id)
        request = {
            'hardwareTierId': hardwareTierId
        }
        response = self.request_manager.post(url, json=request)
        return response

    def app_unpublish(self):
        app_id = self._app_id
        if app_id is None:
            return
        url = self._routes.app_stop(app_id)
        response = self.request_manager.post(url)
        return response

    def __app_create(self, name: str = "", hardware_tier_id: str = None) -> str:
        """
        Private method to create app

        :param name: Optional field to set title of app
        :param hardware_tier_id: Optional field to override hardware tier
        :return: Id of newly created App (Un-Published)
        """
        url = self._routes.app_create()
        request_payload = {
            'modelProductType': 'APP',
            'projectId': self._project_id,
            'name': name,
            'owner': '',
            'created': time.time(),
            'lastUpdated': time.time(),
            'status': '',
            'media': [],
            'openUrl': '',
            'tags': [],
            'stats': {
                'usageCount': 0
            },
            'appExtension': {
                'appType': ''
            },
            'id': '000000000000000000000000',
            'permissionsData': {
                'visibility': 'GRANT_BASED',
                'accessRequestStatuses': {},
                'pendingInvitations': [],
                'discoverable': True,
                'appAccessStatus': 'ALLOWED'
            }
        }
        r = self.request_manager.post(url, json=request_payload)
        response = r.json()
        key = "id"
        if key in response.keys():
            app_id = response[key]
        else:
            raise Exception("Cannot create app")
        return app_id

    # Environment functions
    def environments_list(self):
        self.requires_at_least("2.5.0")
        url = self._routes.environments_list()
        return self._get(url)

    # Model Manager functions
    def models_list(self):
        self.requires_at_least("2.5.0")
        url = self._routes.models_list()
        return self._get(url)

    def model_publish(self, file, function, environment_id, name,
                      description, files_to_exclude=[]):
        self.requires_at_least("2.5.0")

        url = self._routes.model_publish()

        request = {
            "name": name,
            "description": description,
            "projectId": self._project_id,
            "file": file,
            "function": function,
            "excludeFiles": files_to_exclude,
            "environmentId": environment_id
        }

        response = self.request_manager.post(url, json=request)
        return response.json()

    def model_versions_get(self, model_id):
        self.requires_at_least("2.5.0")
        url = self._routes.model_versions_get(model_id)
        return self._get(url)

    def model_version_publish(self, model_id, file, function, environment_id,
                              name, description, files_to_exclude=[]):
        self.requires_at_least("2.5.0")

        url = self._routes.model_version_publish(model_id)

        request = {
            "name": name,
            "description": description,
            "projectId": self._project_id,
            "file": file,
            "function": function,
            "excludeFiles": files_to_exclude,
            "environmentId": environment_id
        }

        response = self.request_manager.post(url, json=request)
        return response.json()

    # Helper methods
    def _get(self, url):
        return self.request_manager.get(url).json()

    def _put_file(self, url, file):
        return self.request_manager.put(url, data=file)

    def _validate_blob_key(self, key):
        regex = re.compile("^\\w{40,40}$")
        if not regex.match(key):
            raise Exception(("Blob key should be 40 alphanumeric characters. "
                             "Perhaps you passed a file path on accident? "
                             "If you have a file path and want to get the "
                             "file, use files_list to get the blob key."))

    def requires_at_least(self, at_least_version):
        if at_least_version > self._version:
            raise Exception("You need at least version {} but your deployment \
                            seems to be running {}".format(
                at_least_version, self._version))

    # Workaround to get project ID which is needed for some model functions
    @property
    def _project_id(self):
        url = self._routes.find_project_by_owner_name_and_project_name_url()
        key = "id"
        response = self._get(url)
        if key in response.keys():
            return response[key]

    # This will fetch app_id of app in current project
    @property
    def _app_id(self):
        url = self._routes.app_list(self._project_id)
        response = self._get(url)
        if len(response) != 0:
            app = response[0]
        else:
            return None
        key = "id"
        if key in app.keys():
            app_id = app[key]
        else:
            app_id = None
        return app_id


def parse_play_flash_cookie(response):
    flash_cookie = response.cookies['PLAY_FLASH']
    messageType, message = flash_cookie.split("=")
    # Format message into user friendly string
    message = urllib2.unquote(message).replace("+", " ")
    # Discern error disposition
    if (messageType == "dominoFlashError"):
        error = True
    else:
        error = False
    return dict(messageType=messageType, message=message, error=error)