import requests
import json
import six
import os


DOMAIN = 'https://prodtrack.voronezh.studio'
CONFIG_PATH = "$PROJECT_ROOT_PATH/.config/prodtrack.yml"
RESPONSE_ERROR_TEXT = '{status_code} {reason} {text}'


__get_project_config_cache__ = None


class ProdTrackError(Exception):
    pass


def win_getpass(prompt='Password: ', stream=None):
    """Prompt for password with echo off, using Windows getch()."""
    import msvcrt
    for c in prompt:
        msvcrt.putch(six.b(c))
    pw = ""
    while 1:
        c = msvcrt.getwch()
        if c == '\r' or c == '\n':
            break
        if c == '\003':
            raise KeyboardInterrupt
        if c == '\b':
            if pw == '':
                pass
            else:
                pw = pw[:-1]
                msvcrt.putch(six.b('\b'))
                msvcrt.putch(six.b(" "))
                msvcrt.putch(six.b('\b'))
        else:
            pw = pw + c
            msvcrt.putch(six.b("*"))
    msvcrt.putch(six.b('\r'))
    msvcrt.putch(six.b('\n'))
    return pw


def get_project_config():
    global __get_project_config_cache__
    if __get_project_config_cache__ is None:
        import yaml
        import re
        yaml.add_constructor('!regexp', lambda l,
                             n: re.compile(l.construct_scalar(n)))
        path = os.path.expandvars(CONFIG_PATH)
        with open(path, 'r') as f:
            doc = yaml.Loader(f)
            try:
                if not doc.check_data():
                    raise RuntimeError("Cannot load yaml file %s" % path)
                data = doc.get_data()
            finally:
                doc.dispose()
        __get_project_config_cache__ = data
    return __get_project_config_cache__


class Entity(object):
    url_root = None
    list_key = None

    def __init__(self, data):
        super(Entity, self).__init__()
        self.data = data
        if 'links' in self.data:
            self.data.pop('links')

    def __repr__(self):
        class_name = self.__class__.__name__
        if all(key in self.data for key in ['name', 'id']):
            return u"<{0} name:'{1}'>".format(class_name, self.data['name'].encode('ascii', 'ignore'))
        else:
            return "<%s %s>" % (class_name, json.dumps(self.data))

    def __eq__(self, other):
        return self.data['id'] == other.data['id']

    def __ne__(self, other):
        return not self.data['id'] == other.data['id']

    @classmethod
    def _get_error_msg(cls, response):
        return RESPONSE_ERROR_TEXT.format(status_code=response.status_code, reason=response.reason, text=response.text)

    @classmethod
    def _create_request(cls, session=None):
        api_request = requests
        if session:
            api_request = session.get_request_session()
        return api_request

    @classmethod
    def get_list(cls, query=None, session=None):
        query_obj = requests
        if session:
            query_obj = session.get_request_session()
        response = query_obj.get(cls.url_root, params=query)
        if response.status_code == requests.codes.ok:
            items_data = response.json().get(cls.list_key, [])
            return [cls(data) for data in items_data]
        else:
            raise ProdTrackError(cls._get_error_msg(response))

    @classmethod
    def get(cls, item_id, query=None, session=None):
        query_obj = requests
        if session:
            query_obj = session.get_request_session()
        response = query_obj.get(cls.url_root + '/%d' % item_id, params=query)
        if response.status_code == requests.codes.ok:
            item_data = response.json().get(cls.list_key, [])
            return cls(item_data)
        else:
            raise ProdTrackError(cls._get_error_msg(response))

    @classmethod
    def get_related_entity_list(cls, query=None, session=None, entity=None):
        if entity is None:
            entity = cls

        query_obj = requests
        if session:
            query_obj = session.get_request_session()
        response = query_obj.get(cls.url_root, params=query)
        if response.status_code == requests.codes.ok:
            items_data = response.json().get(entity.list_key, [])
            return [entity(data) for data in items_data]
        else:
            raise ProdTrackError(cls._get_error_msg(response))

    def get_result_query(self, default_query, query=None):
        if query is None:
            query = {}
        result_query = dict(default_query, **query)
        return result_query

    @classmethod
    def create(cls, data, session=None):
        api_request = cls._create_request(session)
        data = json.dumps(data)
        response = api_request.post(cls.url_root, data)
        if response.status_code == requests.codes.created:
            item_data = response.json().get(cls.list_key, [])
            return cls(item_data)
        else:
            raise ProdTrackError(cls._get_error_msg(response))

    @ classmethod
    def update(cls, data, session=None):
        api_request = cls._create_request(session)
        data = {cls.list_key: [data]}
        data = json.dumps(data)
        response = api_request.patch(cls.url_root, data)
        if response.status_code == requests.codes.ok:
            item_data = response.json().get(cls.list_key, [])
            return cls(item_data)
        else:
            raise ProdTrackError(cls._get_error_msg(response))


class VisibleToFreelancersMixin(object):

    @classmethod
    def set_visible_to_freelancers(cls, data, session=None):
        query_obj = requests
        if session is not None:
            query_obj = session.get_request_session()
        response = query_obj.patch(cls.url_root, data=data)
        if response.status_code == requests.codes.ok:
            return response
        else:
            error_msg = RESPONSE_ERROR_TEXT.format(
                status_code=response.status_code, reason=response.reason, text=response.text)
            raise ProdTrackError(error_msg)


class Project(Entity):
    url_root = "{domain}/api/v1/projects".format(domain=DOMAIN)
    list_key = 'projects'

    # temporary api fix (wrong list_key)
    @classmethod
    def get(cls, item_id, query=None, session=None):
        query_obj = requests
        if session:
            query_obj = session.get_request_session()
        response = query_obj.get(cls.url_root + '/%d' % item_id, params=query)
        if response.status_code == requests.codes.ok:
            item_data = response.json().get('project', [])
            return cls(item_data)
        else:
            raise ProdTrackError(cls._get_error_msg(response))


class Series(Entity, VisibleToFreelancersMixin):
    url_root = "{domain}/api/v1/series".format(domain=DOMAIN)
    list_key = 'series'

    def get_episodes(self):
        return Episode.get_list(query={"filter{series}": self.data['id']})


class Episode(Entity, VisibleToFreelancersMixin):
    url_root = "{domain}/api/v1/episodes".format(domain=DOMAIN)
    list_key = 'episodes'

    def get_scenes(self, session=None):
        return Scene.get_list(query={"filter{episode}": self.data['id']}, session=session)

    def get_uploaded_files(self, query=None, session=None):
        default_query = {'filter{episode_set}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return UploadedFile.get_list(query=result_query, session=session)

    def get_versions(self, query=None, session=None):
        default_query = {'filter{link_type.model}': 'episode',
                         'filter{link_id}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return Version.get_list(query=result_query, session=session)


class Scene(Entity, VisibleToFreelancersMixin):
    url_root = "{domain}/api/v1/scenes".format(domain=DOMAIN)
    list_key = 'scenes'

    def get_assets(self, query=None, session=None):
        default_query = {'include[]': 'assets.*',
                         'filter{id}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return Scene.get_related_entity_list(query=result_query, session=session, entity=Asset)

    def get_versions(self, query=None, session=None):
        default_query = {'filter{link_type.model}': 'scene',
                         'filter{link_id}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return Version.get_list(query=result_query, session=session)


class Asset(Entity, VisibleToFreelancersMixin):
    url_root = "{domain}/api/v1/assets".format(domain=DOMAIN)
    list_key = 'assets'

    def get_uploaded_files(self, query=None, session=None):
        default_query = {'filter{asset_set}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return UploadedFile.get_list(query=result_query, session=session)

    def get_versions(self, query=None, session=None):
        default_query = {'filter{link_type.model}': 'asset',
                         'filter{link_id}': self.data['id']}
        result_query = self.get_result_query(default_query, query)
        return Version.get_list(query=result_query, session=session)


class AssetType(Entity):
    url_root = "{domain}/api/v1/asset_types".format(domain=DOMAIN)
    list_key = 'types'


class UploadedFile(Entity, VisibleToFreelancersMixin):
    url_root = '{domain}/api/v1/uploaded_files'.format(domain=DOMAIN)
    list_key = 'uploaded_files'


class Version(Entity, VisibleToFreelancersMixin):
    url_root = '{domain}/api/v1/versions'.format(domain=DOMAIN)
    list_key = 'versions'

    @classmethod
    def create(cls, data, session=None):
        raise NotImplementedError


class User(Entity):
    url_root = '{domain}/api/v1/users'.format(domain=DOMAIN)
    list_key = 'users'

    def __repr__(self):
        class_name = self.__class__.__name__
        return u"<{0} username:'{1}'>".format(class_name, self.data['username'].encode('ascii', 'ignore'))


class PublishedFile(Entity):
    url_root = '{domain}/api/v1/published_files'.format(domain=DOMAIN)
    list_key = 'published_files'


class PublishedFileType(Entity):
    url_root = '{domain}/api/v1/published_file_types'.format(domain=DOMAIN)
    list_key = 'published_file_types'


class Note(Entity):
    url_root = "{domain}/api/v1/notes".format(domain=DOMAIN)
    list_key = 'notes'

    def get_uploaded_files(self, query=None, session=None):
        default_query = {'include[]': "uploaded_files.*"}
        result_query = self.get_result_query(default_query, query)
        query_obj = requests
        if session:
            query_obj = session.get_request_session()
        response = query_obj.get(
            "{0}/{1}".format(self.url_root, self.data["id"]), params=result_query)
        if response.status_code == requests.codes.ok:
            items_data = response.json().get(UploadedFile.list_key, [])
            return [UploadedFile(data) for data in items_data]
        else:
            raise ProdTrackError(self._get_error_msg(response))


class Task(Entity):
    url_root = "{domain}/api/v1/tasks".format(domain=DOMAIN)
    list_key = 'tasks'

    @classmethod
    def create(cls, data, session=None):
        api_request = cls._create_request(session)
        data = json.dumps(data)
        response = api_request.post(cls.url_root, data)
        if response.status_code == requests.codes.created:
            response_data = response.json()
            if response_data:
                return response_data[0] if len(response_data) == 1 else response_data
            return []
        else:
            raise ProdTrackError(cls._get_error_msg(response))


class Session(object):
    login_url = '{domain}/api/v1/login/'.format(domain=DOMAIN)
    logout_url = '{domain}/api/v1/logout/'.format(domain=DOMAIN)

    def __init__(self, cookie_jar=None):
        super(Session, self).__init__()
        self._requests_session = requests.Session()
        if cookie_jar:
            self._requests_session.cookies = cookie_jar
            self.__set_headers()

    def get_request_session(self):
        return self._requests_session

    def login(self, username, password):
        response = self._requests_session.post(
            self.login_url, dict(username=username, password=password))
        if response.status_code == requests.codes.ok:
            self.__set_headers()
        else:
            error_msg = RESPONSE_ERROR_TEXT.format(
                status_code=response.status_code, reason=response.reason, text=response.text)
            raise ProdTrackError(error_msg)

    def terminal_login(self):
        login = six.moves.input("login:")
        password = win_getpass()
        self.login(login, password)

    def logout(self):
        self._requests_session.post(
            self.logout_url, cookies=self.cookie_data, headers=self.header_data)
        self._requests_session.cookies.clear()

    def __set_headers(self):
        csrf_token = self._requests_session.cookies['csrftoken']
        self._requests_session.headers.update({'X-CSRFToken': csrf_token})
        self._requests_session.headers.update(
            {'content-type': 'application/json'})
        self._requests_session.headers.update({'Referer': DOMAIN})
