import base64
import binascii
import configparser
import inspect
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import webbrowser

import click
import jwt
import pyotp
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError
from requests.packages.urllib3.util.retry import Retry

from apigee import (APIGEE_CLI_ACCESS_TOKEN_FILE, APIGEE_CLI_CREDENTIALS_FILE,
                    APIGEE_CLI_DIRECTORY, APIGEE_CLI_IS_MACHINE_USER,
                    APIGEE_OAUTH_URL, APIGEE_SAML_LOGIN_URL,
                    APIGEE_ZONENAME_OAUTH_URL, console)
from apigee.cls import AliasedGroup
# from apigee.prefix import common_prefix_options
from apigee.prefix import auth_with_prefix as with_prefix
from apigee.silent import common_silent_options
from apigee.types import Struct
from apigee.utils import make_dirs
from apigee.verbose import common_verbose_options


def attach_username_option(func, profile):
    username = get_credential(profile, 'username')
    username_envvar = os.environ.get(f'APIGEE_USERNAME', '')
    if username:
        func = click.option('-u', '--username', default=username, show_default='current username')(
            func
        )
    elif username_envvar:
        func = click.option(
            '-u', '--username', default=username_envvar, show_default='current username'
        )(func)
    else:
        func = click.option('-u', '--username', required=True)(func)
    return func


def attach_password_option(func, profile):
    password = get_credential(profile, 'password')
    password_envvar = os.environ.get(f'APIGEE_PASSWORD', '')
    if password:
        func = click.option('-p', '--password', default=password, show_default='current password')(
            func
        )
    elif password_envvar:
        func = click.option(
            '-p', '--password', default=password_envvar, show_default='current password'
        )(func)
    else:
        func = click.option('-p', '--password', required=True)(func)
    return func


def attach_mfa_secret_option(func, profile):
    mfa_secret = get_credential(profile, 'mfa_secret')
    mfa_envvar = os.environ.get(f'APIGEE_MFA_SECRET', '')
    if mfa_secret:
        func = click.option(
            '-mfa', '--mfa-secret', default=mfa_secret, show_default='current mfa key'
        )(func)
    elif mfa_envvar:
        func = click.option(
            '-mfa', '--mfa-secret', default=mfa_envvar, show_default='current mfa key'
        )(func)
    else:
        func = click.option('-mfa', '--mfa-secret')(func)
    return func


def attach_is_token_option(func, profile):
    is_token = get_credential(profile, 'is_token')
    is_token_envvar = os.environ.get(f'APIGEE_IS_TOKEN', '')
    if is_token in (True, 'True', 'true', '1'):
        func = click.option(
            '--token/--no-token',
            default=is_token,
            show_default=True,
            help='specify to use oauth without MFA',
        )(func)
    elif is_token_envvar in (True, 'True', 'true', '1'):
        func = click.option(
            '--token/--no-token',
            default=is_token_envvar,
            show_default=True,
            help='specify to use oauth without MFA',
        )(func)
    else:
        func = click.option(
            '--token/--no-token',
            default=False,
            show_default=True,
            help='specify to use oauth without MFA',
        )(func)
    return func


def attach_zonename_option(func, profile):
    zonename = get_credential(profile, 'zonename')
    zonename_envvar = os.environ.get('APIGEE_ZONENAME', '')
    if zonename:
        func = click.option(
            '-z', '--zonename', default=zonename, show_default='current identity zone name'
        )(func)
    elif zonename_envvar:
        func = click.option(
            '-z', '--zonename', default=zonename_envvar, show_default='current identity zone name'
        )(func)
    else:
        func = click.option('-z', '--zonename', help='identity zone name')(func)
    return func


def attach_org_option(func, profile):
    org = get_credential(profile, 'org')
    org_envvar = os.environ.get(f'APIGEE_ORG', '')
    if org:
        func = click.option('-o', '--org', default=org, show_default='current org')(func)
    elif org_envvar:
        func = click.option('-o', '--org', default=org_envvar, show_default='current org')(func)
    else:
        func = click.option('-o', '--org', required=True)(func)
    return func


def common_auth_options(func):
    profile = 'default'
    for i, arg in enumerate(sys.argv):
        if arg == '-P' or arg == '--profile':
            try:
                profile = sys.argv[i + 1]
            except IndexError:
                pass
    attach_username_option(func, profile)
    attach_password_option(func, profile)
    attach_mfa_secret_option(func, profile)
    attach_is_token_option(func, profile)
    attach_zonename_option(func, profile)
    attach_org_option(func, profile)
    func = click.option(
        '-P',
        '--profile',
        help='name of the user profile to authenticate with',
        default=profile,
        show_default=True,
    )(func)
    return func


def gen_auth(username=None, password=None, mfa_secret=None, token=None, zonename=None):
    return Struct(
        username=username, password=password, mfa_secret=mfa_secret, token=token, zonename=zonename
    )


def get_access_token_for_token(auth, username, password, oauth_url, post_headers, session):
    if auth.zonename:
        oauth_url = APIGEE_ZONENAME_OAUTH_URL.format(zonename=auth.zonename)
    post_body = f'username={urllib.parse.quote(username)}&password={urllib.parse.quote(password)}&grant_type=password&response_type=token'
    try:
        return session.post(f'{oauth_url}', headers=post_headers, data=post_body)
    except ConnectionError as ce:
        console.echo(ce)


def get_access_token_for_mfa(auth, username, password, oauth_url, post_headers, session):
    mfa_secret = auth.mfa_secret
    totp = pyotp.TOTP(mfa_secret)
    try:
        totp.now()
    except binascii.Error as e:
        sys.exit(f'{type(e).__name__}: {e}: Not a valid MFA key')
    post_body = f'username={urllib.parse.quote(username)}&password={urllib.parse.quote(password)}&grant_type=password'
    try:
        response_post = session.post(
            f'{oauth_url}?mfa_token={totp.now()}', headers=post_headers, data=post_body
        )
    except ConnectionError as ce:
        console.echo(ce)
    try:
        response_post.json()['access_token']
        return response_post
    except KeyError:
        return session.post(
            f'{oauth_url}?mfa_token={totp.now()}', headers=post_headers, data=post_body
        )


def get_access_token_for_sso(auth, username, password, oauth_url, post_headers, session):
    oauth_url = APIGEE_ZONENAME_OAUTH_URL.format(zonename=auth.zonename)
    passcode_url = APIGEE_SAML_LOGIN_URL.format(zonename=auth.zonename)
    webbrowser.open(passcode_url)
    console.echo('SSO authorization page has automatically been opened in your default browser.')
    console.echo('Follow the instructions in the browser to complete this authorization request.')
    console.echo(
        f"""\nIf your browser did not automatically open, go to the following URL and sign in:\n\n{passcode_url}\n\nthen copy the Temporary Authentication Code.\n"""
    )
    try:
        passcode = click.prompt('Please enter the Temporary Authentication Code')
        post_body = f'passcode={passcode}&grant_type=password&response_type=token'
        try:
            response_post = session.post(f'{oauth_url}', headers=post_headers, data=post_body)
            response_post.json()['access_token']
            return response_post
        except KeyError:
            sys.exit('Temporary Authentication Code is invalid. Please try again.')
    except ConnectionError as ce:
        console.echo(ce)
    except KeyError:
        pass


def generate_auth_error_message(error):
    error_message = f'An exception of type {type(error).__name__} occurred. Arguments:\n{error}\nDouble check your credentials and try again.'
    if APIGEE_CLI_IS_MACHINE_USER:
        return f'{error_message} \nWARNING: APIGEE_CLI_IS_MACHINE_USER={APIGEE_CLI_IS_MACHINE_USER}'
    return error_message


def get_access_token(
    auth, retries=4, backoff_factor=0.3, status_forcelist=(500, 502, 504), session=None
):
    oauth_url = APIGEE_OAUTH_URL
    username = auth.username
    password = auth.password
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session = requests.Session()
    session.mount('https://', adapter)
    post_headers = {
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
        'Accept': 'application/json;charset=utf-8',
        'Authorization': 'Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0',
    }
    if auth.token or APIGEE_CLI_IS_MACHINE_USER:
        response_post = get_access_token_for_token(
            auth, username, password, oauth_url, post_headers, session
        )
    elif auth.mfa_secret:
        response_post = get_access_token_for_mfa(
            auth, username, password, oauth_url, post_headers, session
        )
    elif auth.zonename:
        response_post = get_access_token_for_sso(
            auth, username, password, oauth_url, post_headers, session
        )
    else:
        return
    try:
        return response_post.json()['access_token']
    except KeyError as ke:
        sys.exit(generate_auth_error_message(ke))


def get_credential(section, key):
    try:
        config = configparser.ConfigParser()
        config.read(APIGEE_CLI_CREDENTIALS_FILE)
        if section in config:
            return config[section][key]
    except:
        return


def set_header(auth_obj, headers={}):
    if auth_obj.mfa_secret or auth_obj.token or auth_obj.zonename:
        access_token = ""
        make_dirs(APIGEE_CLI_DIRECTORY)
        try:
            with open(APIGEE_CLI_ACCESS_TOKEN_FILE, 'r') as f:
                access_token = f.read().strip()
        except (IOError, OSError):
            pass
        if access_token:
            decoded = jwt.decode(
                access_token,
                options={'verify_exp': False, 'verify_signature': False, 'verify_aud': False},
            )
            if (
                decoded['exp'] < int(time.time())
                or decoded['email'].lower() != auth_obj.username.lower()
            ):
                access_token = ""
        if not access_token:
            access_token = get_access_token(auth_obj)
            with open(APIGEE_CLI_ACCESS_TOKEN_FILE, 'w') as f:
                f.write(access_token)
        headers['Authorization'] = f'Bearer {access_token}'
    else:
        headers['Authorization'] = (
            'Basic '
            + base64.b64encode((f'{auth_obj.username}:{auth_obj.password}').encode()).decode()
        )
    return headers


def auth_with_prefix(auth_obj, org, name, file=None, key='name'):
    if file:
        with open(file) as f:
            attr = json.loads(f.read())[key]
        return with_prefix(auth_obj, org, attr)
    return with_prefix(auth_obj, org, name)


@click.command(
    help='Custom authorization commands. More information on the use cases for these commands are yet to be documented.',
    cls=AliasedGroup,
)
def auth():
    pass


@auth.command(name='get-access-token', help='request a fresh access token')
@common_auth_options
@common_verbose_options
@common_silent_options
def get_access_token_command(
    username, password, mfa_secret, token, zonename, org, profile, **kwargs
):
    console.echo(get_access_token(gen_auth(username, password, mfa_secret, token, zonename)))


@auth.command(help='check if user (developer) is authorized to access resource with prefix in file')
@common_auth_options
@common_verbose_options
@common_silent_options
@click.option(
    '-f',
    '--file',
    type=click.Path(exists=True, dir_okay=False, file_okay=True, resolve_path=False),
    required=True,
)
@click.option('-k', '--key', help='name of the attribute/key to check', default='name')
def file(username, password, mfa_secret, token, zonename, org, profile, file, key, **kwargs):
    console.echo(
        auth_with_prefix(
            gen_auth(username, password, mfa_secret, token, zonename), org, None, file=file, key=key
        )
    )


@auth.command(help='check if user (developer) is authorized to access resource with prefix in name')
@common_auth_options
@common_verbose_options
@common_silent_options
@click.option('-n', '--name', help='name of the resource to check', required=True)
def name(username, password, mfa_secret, token, zonename, org, profile, name, **kwargs):
    console.echo(
        auth_with_prefix(
            gen_auth(username, password, mfa_secret, token, zonename),
            org,
            name,
            file=None,
            key='name',
        )
    )
