# -*- coding: utf8 -*-
import base64
import hashlib
import json
import logging
import os
import random
import socket
import sys

import click
import requests
from requests import HTTPError
from terminaltables import PorcelainTable
from six.moves.urllib import parse
from mali_commands.config import Config
from mali_commands.utilities.tables import format_json_data, dict_to_csv

base_url = '_ah/api/missinglink/v1/'


def print_as_json(result):
    click.echo(json.dumps(result))


def print_as_bad_json(result):
    click.echo(result)


def create_validate_length(min_length, max_length):
    def validate_length(ctx, option, value):
        if value is None:
            return value

        if len(value) > max_length:
            raise click.BadParameter("needs to be shorter than %s characters" % max_length)

        if len(value) < min_length:
            raise click.BadParameter("needs to be longer than %s characters" % min_length)

        return value

    return validate_length


def build_auth0_url(auth0):
    return '{}.auth0.com'.format(auth0)


def add_to_data_if_not_none(data, val, key):
    if val is not None:
        data[key] = val


def get_data_and_remove(kwargs, key):
    key = key.lower()
    result = kwargs.get(key)

    if key in kwargs:
        del kwargs[key]

    return result


def base64url(b):
    return bytearray(base64.b64encode(b).decode('ascii').replace('=', '').replace('+', '-').replace('/', '_'), 'ascii')


def sha256(s):
    h = hashlib.sha256()
    h.update(s)
    return h.digest()


def urljoin(*args):
    base = args[0]
    for u in args[1:]:
        base = parse.urljoin(base, u)

    return base


# noinspection PyUnusedLocal
def handle_api(ctx_or_config, http_method, method_url, data=None):
    config = ctx_or_config if isinstance(ctx_or_config, Config) else ctx_or_config.config

    if config.id_token is None:
        click.echo('Please run: "mali auth init" to setup authorization', err=True)
        exit(1)

    url = urljoin(config.api_host, base_url, method_url)

    id_token = config.id_token
    result = None
    for retries in range(3):
        headers = {'Authorization': 'Bearer {}'.format(id_token)}
        r = http_method(url, headers=headers, json=data)

        if r.status_code == 401:
            try:
                id_token = update_token(config)
            except HTTPError:
                click.echo('Authorization failed, try running "mali auth init" again', err=True)
                exit(1)
                return
            continue

        try:
            r.raise_for_status()
        except requests.exceptions.HTTPError as ex:
            try:
                error_message = ex.response.json().get('error', {}).get('message')
            except ValueError:
                error_message = str(ex)

            if error_message:
                click.echo('\n' + error_message, err=True)
                exit(1)
                return

            raise

        result = r.json()
        break

    if result is None:
        click.echo('failed to refresh the token, rerun auth init again', err=True)
        exit(1)
        return

    return result


def get_prefix_section(config_prefix, section):
    return '%s-%s' % (config_prefix, section) if config_prefix else section


def update_token(config):
    r = requests.post('https://{}/delegation'.format(build_auth0_url(config.auth0)), json={
        'client_id': config.client_id,
        'grant_type': "urn:ietf:params:oauth:grant-type:jwt-bearer",
        'scope': 'openid offline_access user_external_id org orgs email picture name given_name user_metadata',
        'refresh_token': config.refresh_token,
    })

    r.raise_for_status()

    data = r.json()

    config.set(get_prefix_section(config.config_prefix, 'token'), 'id_token', data['id_token'])
    config.save()

    return data['id_token']


class LocalWebServer(object):
    ports = [27017, 41589, 55130, 6617, 1566, 17404, 44288, 6948, 15950, 48216, 3318, 34449, 22082, 22845, 37862, 59304]

    def __init__(self):
        self.app = self.create_app()
        self.code = None
        self.srv = None

        self.disable_logging()
        self.start_server()

    @classmethod
    def disable_logging(cls):
        log = logging.getLogger('werkzeug')
        log.setLevel(logging.ERROR)

    def create_app(self):
        from flask import Flask, request, redirect
        app = Flask(__name__)

        @app.route('/console/auth/init')
        def auth_init():
            self.code = request.args.get('code')
            return redirect("/console/auth/finish")

        @app.route('/console/auth/finish')
        def auth_finish():
            self.srv.shutdown_signal = True
            return "You can go now back to the console"

        return app

    def run(self):
        self.srv.serve_forever()

    @property
    def redirect_uri(self):
        return 'http://localhost:%s/console/auth/init' % self.srv.port

    def start_server(self):
        from werkzeug.serving import make_server

        ports = self.ports[:]
        random.shuffle(ports)

        for port in ports:
            try:
                self.srv = make_server('127.0.0.1', port, self.app)
                break
            except (OSError, socket.error):
                continue

        if self.srv is None:
            click.echo("can't create local server for authentication, consider using the --disable-webserver flag")


def wait_for_input():
    if sys.version_info >= (3, 0):
        code = input('Enter the token ')
    else:
        # noinspection PyUnresolvedReferences
        code = raw_input('Enter the token ')

    return code


def auth0_oauth_token(auth0_url, code, verifier, client_id, redirect_uri):
    params = {
        'code': code,
        'code_verifier': verifier,
        'client_id': client_id,
        'grant_type': 'authorization_code',
        'redirect_uri': redirect_uri,
    }

    r = requests.post('https://{}/oauth/token'.format(auth0_url), json=params)

    r.raise_for_status()

    return r.json()


# noinspection PyUnusedLocal
def pixy_flow(ctx):
    import webbrowser

    verifier = base64url(os.urandom(32))
    verifier_challenge = base64url(sha256(verifier))

    verifier = verifier.decode('ascii')
    verifier_challenge = verifier_challenge.decode('ascii')

    app = LocalWebServer() if ctx.local_web_server else None

    host = ctx.host if 'missinglink.ai' in ctx.host else '{host}.firebaseapp.com'.format(host=ctx.host)

    redirect_uri = app.redirect_uri if app is not None else '{host}/console/auth/init'.format(host=host)

    query = {
        'response_type': 'code',
        'scope': 'openid offline_access user_external_id org orgs email picture name given_name user_metadata',
        'client_id': ctx.client_id,
        'redirect_uri': redirect_uri,
        'code_challenge': verifier_challenge,
        'code_challenge_method': 'S256'
    }

    authorize_url = 'https://{}/authorize?{}'.format(build_auth0_url(ctx.auth0), parse.urlencode(query))

    if ctx.local_web_server:
        click.echo("If the browser doesn't open enter this URL manually\n%s\n" % authorize_url)
        webbrowser.open(authorize_url)
    else:
        click.echo("Enter the following URL in your machine browser\n%s\n" % authorize_url)

    if app is not None:
        app.run()
        code = app.code
    else:
        code = wait_for_input()

    try:
        data = auth0_oauth_token(build_auth0_url(ctx.auth0), code, verifier, ctx.client_id, redirect_uri)
    except HTTPError:
        click.echo("Failed to init authorized (did you enter the current token?).", err=True)
        exit(1)
        return

    click.echo("Success!, you sre authorized to use the command line.")

    return data['access_token'], data['refresh_token'], data['id_token']


def add_options(options):
    def _add_options(func):
        for option in reversed(options):
            func = option(func)
        return func

    return _add_options


def output_result(ctx, result, fields=None, formatters=None):
    format_tables = ctx.obj.output_format == 'tables'

    if result is None:
        return

    if format_tables:
        result = format_json_data(result, formatters)
        table_data = list(dict_to_csv(result, fields))

        click.echo(PorcelainTable(table_data).table)
    elif hasattr(ctx.obj, 'bad_json_format'):
        print_as_bad_json(result)
    else:
        print_as_json(result)
