"""
    The Util class is comprised of common code that is used throughout the CLI.
    Refreshing tokens, making calls to the platform, building urls, and looking up
    various lumavate objects are few things implamented here.

    TO DO: This needs to be broken up further. Refreshing tokens and all other things
    auth should be broken out into the auth class. Request handling and the various validation
    of objects sprinkled in this code and the behavior files are two other major chunks of code
    that need a new home.
"""

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)
