from pathlib import Path
from pyparsing import *
import dateutil.parser
from cli import pyke
import requests
import platform
import urllib
import click
import copy
import json
import sys
import os


ESC = Literal('\x1b')
integer = Word(nums)
escapeSeq = Combine(ESC + '[' + Optional(delimitedList(integer,';')) +
                oneOf(list(alphas)))

uncolor = lambda s : Suppress(escapeSeq).transformString(s)

class Util:
    def __init__(self, context=None, profile=None):
        self.context = pyke.auth.load_cache()
        if profile is None:
            self.profile = self.context.get('profileName')
        else:
            self.profile = profile

    def login_token(self, renew=False):
        if renew:
            self.context['accessToken'] = self.login()['access_token']
            return self.context['accessToken']

        if self.context is not None:
            return self.context.get('accessToken')

    def login_without_context(self, env, config_data=None):
        if config_data is None:
            config_data = pyke.auth.load_config()
        else:
            config_data = config_data

        env = config_data['envs'][env]

        data = {
          'grant_type': 'client_credentials',
          'audience': env['audience'],
          'client_id': env['clientId'],
          'client_secret': env['clientSecret']
        }

        req_data = pyke.auth.sanatize_data(data)

        token_url = env['token'].replace(" ", "")
        resp = self.cli_request('POST', 'https://' + token_url+ '/oauth/token', json=req_data, external=True)

        if resp.status_code != 200:
            click.echo(json.dumps(env))
            click.echo(' ')

            raise click.ClickException(click.style("Env not configured correctly. Make sure your Client ID and Client Secret are correct.", fg='red'))

        return resp.json()['access_token']

    def login(self):
        data = {
          'grant_type': 'client_credentials',
          'audience': self.context['audience'].replace(" ", ""),
          'client_id': self.context['clientId'].replace(" ", ""),
          'client_secret': self.context['clientSecret'].replace(" ", "")
        }

        res = self.cli_request('POST', 'https://{}/oauth/token'.format(self.context['token']), json=data, external=True)

        if res.status_code != 200:
            raise click.ClickException(click.style("Env not configured correctly. Make sure your Client ID and Client Secret are correct.", fg='red'))

        resp = res.json()

        if self.context.get('orgId') is not None:
            url = '{}/auth/v1/session'.format(self.context.get('app'))
            data = {'companyId': self.context.get('orgId')}
            r = requests.post(url, json=data, headers={'Authorization': 'Bearer ' + resp['access_token']})
            if r.status_code != 200:
                raise click.BadParameter(click.style("Not authorized for company id {}".format(self.context.get('orgId')), fg='red'))

        pyke.auth.set_cache_token(resp.get('access_token'))
        pyke.auth.set_profile_token(self.context.get('profileName'), resp.get('access_token'))

        return resp

    def print_record_count(self, data):
        d = data['payload']
        if d['totalItems'] > d['currentItemCount']:
            d['startItem'] = (d['page'] - 1) * d['pageSize'] + 1
            d['endItem'] = d['startItem'] - 1 + d['currentItemCount']
            count_str = 'showing {startItem} - {endItem} of {totalItems} total items (page {page} / {totalPages})'
            click.echo(click.style(count_str.format(**d), fg='yellow'))

    def load_users(self):
        resp = self.cli_request('GET', self.build_url('{app}/auth/v1/users'))

        users = {}
        for u in resp['payload']['data']:
            users[u['id']] = u['emailAddress']

        return users

    def print_table(self, data, format, titles={}, handlers={}):
        headers = {}
        stats = {}
        users = {}

        if 'createdBy' in format or 'lastModifiedBy' in format:
            users = self.load_users()

        for c in data:
            child_records = {}
            for k in c:
                if isinstance(c[k], dict):
                    for sk in c[k]:
                        child_records[k + '__' + sk] = c[k][sk]

            c.update(child_records)

        if len(data) > 0:
            for k in data[0]:
                headers[k] = titles.get(k, k)
        else:
            click.echo('No Records Found')
            return

        for k in headers:
            stats[k] = len(uncolor(headers[k]))

        # TO DO: Cache users and reload when we can't find one
        for c in data:
            for k in c:
                if k in ['createdAt', 'lastModifiedAt']:
                    cd = dateutil.parser.parse(c[k])
                    c[k] = cd.strftime('%x %X')
                if k in ['createdBy', 'lastModifiedBy']:
                    c[k] = users.get(c[k], 'Not Found ({})'.format(c[k]))
                if stats.get(k, 0) < len(str(uncolor(handlers[k](c[k])) if k in handlers else c[k])):
                    stats[k] = len(str(uncolor(handlers[k](c[k])) if k in handlers else c[k]))

        for k in headers:
            headers[k] = titles.get(k, k).ljust(stats[k])

        for c in data:
            for k in c:
                c[k] = str(handlers[k](c[k]) if k in handlers else c[k])
                c[k] = c[k] + ' ' * (stats[k] - len(uncolor(c[k])))

        click.echo(format.format(**headers))

        for c in data:
            print(format.format(**c))

    def check_response(self, resp):
        if resp.status_code == 500:
            raise click.ClickException(str(click.style('Unhandled Exception: ', fg='red')) + str(resp.text))
        elif resp.status_code == 404:
            raise click.ClickException(click.style('Not Found', fg='red'))
        elif resp.status_code == 400:
            message = resp.json()['payload']['error']
            if 'apiField' in resp.json()['payload']:
                message += ' (' + resp.json()['payload']['apiField'] + ')'
            raise click.ClickException(click.style(message, fg='red'))

        return resp

    def get_headers(self):
        return {
            'Authorization': 'Bearer ' + self.login_token(),
            'Content-Type': 'application/json'
        }

    def build_url(self, format, extra_data = {}):
        data = pyke.auth.sanatize_data(self.context)
        env = copy.deepcopy(data)
        env.update(self.context)
        env.update(extra_data)
        return format.format(**env).replace(" ", "+")

    def cli_request(self, method, url, data=None, json=None, headers=None, params=None, external=False):
        req_args = self.build_req_args(data, json, headers, params, external)
        req = self.get_request_func(method)
        url = url.replace(" ", "")
        if data is not None:
            resp = self.check_response(req(url, data=data, **req_args))
        else:
            resp = self.check_response(req(url, **req_args))

        if resp.status_code != 200 and external is False:
            self.login_token(renew=True)
            req_args = self.build_req_args(data, json, headers, params, external)
            resp = self.check_response(req(url, **req_args))
            if resp.status_code != 200:
                raise click.BadParameter(click.style('Invalid Credentials', fg='red'))

        if not external:
            return resp.json()
        else:
            return resp

    def build_req_args(self, data, json_data, headers, params, external=False):
        if external is False:
            req_headers = self.get_headers()
            if headers is not None:
                req_headers.update(headers)
        elif headers is not None:
            req_headers = headers
        else:
            req_headers = None

        req_args = {}
        if json_data is not None:
            req_args['json'] = json_data
        if req_headers is not None:
            req_args['headers'] = req_headers
        if params is not None:
            req_args['params'] = params

        return req_args

    def get_request_func(self, method):
        method = method.lower()
        methods = {
            'get': requests.get,
            'head': requests.head,
            'post': requests.post,
            'put': requests.put,
            'patch': requests.patch,
            'delete': requests.delete
        }

        return methods[method]

    def list_companies(self, env, print_table=True):
        conf = pyke.auth.load_config()

        handlers = {
            'instanceType': lambda x: 'dev' if x == 'cc' else x,
        }

        headers = {
            'Authorization': 'Bearer ' + self.login_without_context(env),
            'Content-Type': 'application/json'
        }

        app_url = conf['envs'][env]['app'].replace(" ", "")
        res = requests.get('{}/auth/v1/me/companies?sort=name'.format(app_url), headers=headers)
        resp = res.json()['payload']['data']

        titles = {
        'id': 'id',
        'name': 'Org Name',
        'instanceType': 'Org Type',
        'isTest': 'Test Org'
        }

        if print_table is True:
            self.print_table(resp, '{id} {name} {instanceType} {isTest}', handlers=handlers, titles=titles)

        return resp

    def get_company_id_from_resp(self, id, resp):
        try:
            company_id = next(int(rec.get('id')) for rec in resp if rec.get('name').lower() == id.lower())
            return company_id
        except:
            raise click.ClickException(click.style("Company not found. You can only share/unshare with companies you have provisioned.", fg='red'))

    def get_company_id(self, name_or_id, resp):
        try:
            company_id = int(name_or_id)
            return company_id
        except:
            return self.get_company_id_from_resp(name_or_id, resp)

    def get_company_name(self, id, resp):
        return next(rec.get('name') for rec in resp if int(rec.get('id')) == id)

    def upload_ephemeral(self, filename, content_type):
        resp = self.cli_request('GET', self.build_url('{app}/iot/v1/files/ephemeral?contentType=' + urllib.parse.quote(content_type)))
        if '/' in filename:
            file_name = filename.rsplit('/', 1)[-1]
        else:
            file_name = filename

        ephemeral_url = resp['payload']['data']['url']
        ephemeral_data = resp['payload']['data']['fields']
        ephemeral_data['key'] = ephemeral_data['key'].replace('${filename}', file_name)
        ephemeral_data['Content-Type'] = content_type

        post_res = requests.post(
            ephemeral_url,
            data=ephemeral_data,
            files={'file': open(filename, 'rb')})

        return ephemeral_data['key']

    def get_current_access_records(self, object, object_id):
        path = '{}s/{}/access'.format(object, object_id)
        resp = self.cli_request('GET', self.build_url('{app}/iot/v1/' + path))
        return resp['payload']['data']

    def lookup_object_id(self, object_type, id, abort_if_null=True):
        if object_type == 'component-set':
            try:
                object_filter = int(id)
                filter_type = 'id'
            except:
                object_filter = id
                filter_type = 'urlRef'

            resp = self.cli_request('GET',
                self.build_url('{app}/iot/v1/component-sets?{filter_type}={object_filter}',
                    extra_data={'filter_type': filter_type, 'object_filter': object_filter}))


            if len(resp.get('payload').get('data', [])) > 0:
                return resp['payload']['data'][0]['id']

            if abort_if_null:
                click.echo(click.style('{} not found'.format(object_type), fg='red'))
                sys.exit()

        else:
            try:
                object_filter = int(id)
                filter_type = 'id'
            except:
                object_filter = id
                filter_type = 'urlRef'

            resp = self.cli_request('GET',
                self.build_url('{app}/iot/v1/containers?{filter_type}={object_filter}',
                    extra_data={'filter_type': filter_type, 'object_filter': object_filter}))

            if len(resp.get('payload').get('data', [])) > 0:
                return resp['payload']['data'][0]['id']

            if abort_if_null:
                click.echo(click.style('{} not found'.format(object_type), fg='red'))
                sys.exit()

    def save_config(self, data):
        with open(self.get_config_file(), 'w+') as outfile:
            json.dump(data, outfile, indent=4, sort_keys=True)

    def get_size(self, path, image=True):
        stats = os.stat(path)
        byte_size = stats.st_size
        if byte_size < 1048576:
            return '{} KB'.format(round(byte_size / 1024, 2))
        elif byte_size < 1073741824:
            return '{} MB'.format(round(byte_size / 1048576, 2))
        else:
            if image is True:
                click.echo('Hey! Thanks for being an awesome Lumavate customer.')
                click.echo('While you are waiting for this image to upload to the platform you should check this out!')
                click.echo('https://hackernoon.com/tips-to-reduce-docker-image-sizes-876095da3b34')
            return '{} GB'.format(round(byte_size / 1073741824, 2))

    def parse_version_number(self, version_number):
        try:
            major, minor, patch = version_number.split('.')
            for x in [major, minor, patch]:
                if x is None:
                    raise click.ClickException(click.style("Version numbers must be in the format '<major: int>.<minor: int>.<patch: int>' ", fg='red'))
                if int(x) < 0:
                    raise click.ClickException(click.style("Version numbers must be in the format '<major: int>.<minor: int>.<patch: int>' ", fg='red'))

            return major, minor, patch
        except:
            raise click.ClickException(click.style("Version numbers must be in the format '<major: int>.<minor: int>.<patch: int>' ", fg='red'))

    def get_config_file(self):
        config_path = str(Path.home()) + '/.luma_cli_config'
        if not os.path.exists(config_path):
            with open(config_path, 'w') as config:
                json.dump({ "envs": {}, "profiles": {} }, config)

        return config_path

    def get_version_id(self, object_type, parent_id, version):
        try:
            version_id = int(version)
        except:
            _version = self.get_version_from_version_number(object_type, parent_id, version)
            if _version is not None:
                version_id = _version.get('id')

        return version_id

    def get_versions_from_mask(self, object_type, parent_id, version):
        if object_type == 'component-set':
            versions = self.cli_request('GET', self.build_url('{app}/iot/v1/component-sets/{parent_id}/versions',\
                {'parent_id': parent_id}))['payload']['data']
        else:
            versions = self.cli_request('GET', self.build_url('{app}/iot/v1/containers/{parent_id}/versions',\
                {'parent_id': parent_id}))['payload']['data']

        major, minor, patch = version.split('.')

        return sorted([x for x in versions if
            (str(x['major']) == major or major == '*') and
            (str(x['minor']) == minor or minor == '*') and
            (str(x['patch']) == patch or patch == '*')], key=lambda x: (x['major'], x['minor'], x['patch']), reverse=True)

    def get_version_from_version_number(self, object_type, parent_id, version_number):
        major, minor, patch = version_number.split('.')

        if object_type == 'component-set':
            version = self.cli_request('GET', self.build_url('{app}/iot/v1/component-sets/{parent_id}/versions?major={major}&minor={minor}&patch={patch}',\
                { 'parent_id': parent_id, 'major': major, 'minor': minor, 'patch': patch }))['payload']['data']
        else:
            version = self.cli_request('GET', self.build_url('{app}/iot/v1/containers/{parent_id}/versions?major={major}&minor={minor}&patch={patch}',\
                { 'parent_id': parent_id, 'major': major, 'minor': minor, 'patch': patch }))['payload']['data']

        if version is not None and len(version) > 0:
            return version[0]

        else:
            raise click.ClickException(click.style('Version Not Found', fg='red'))

    def get_version(self, object_type, parent_id, version):
        versions = self.get_versions_from_mask(object_type, parent_id, version)
        if versions:
            return versions[0]

        return None

    def resolve_default_icon_path(self):
        pwd = os.path.dirname(os.path.realpath(__file__))
        if platform.system() == 'Windows':
            icon_path = pwd.rsplit('\\', 1)[0]
            return '{}\\images\\default-icon.svg'.format(icon_path)
        else:
            icon_path = pwd.rsplit('/', 1)[0]
            return '{}/images/default-icon.svg'.format(icon_path)
