import base64
import json
from urllib.parse import urljoin
import requests

import wordpress_util
import fylr_lib_plugin_python3.util as fylr_util


def __error_response(code: str, msg: str, e: Exception, print_stack: bool) -> dict:
    error = {
        'error': {
            'realm': 'user',
            'code': code,
            'parameters': {
                'error': f'{msg}: {e}',
            },
        }
    }
    if print_stack:
        error['error']['parameters']['trace'] = fylr_util.get_exception_traceback(e)
    msg = fylr_util.dumpjs(error)
    return {
        'headers': {'Content-Type': 'text/html'},
        'body': """
            <html><head><script>
                window.opener.postMessage("""
        + msg
        + """,'*');
            </script></head></html>
        """,
    }


def __get_query_param(
    query_params: dict,
    param_name: str,
    required: bool = False,
) -> str:
    param_values = query_params.get(param_name, [])
    if not param_values:
        if required:
            raise Exception(f'query parameter {param_name} missing')
        return ''

    param_value = param_values[0]
    if not param_value:
        if required:
            raise Exception(f'query parameter {param_name} missing/empty')

    return param_value


def __load_oauth2_urls(wp_url: str) -> tuple[str, str]:
    try:
        resp = requests.get(urljoin(wp_url, 'wp-json'))
        wordpress_util.check_response(resp)

        urls = resp.json().get('authentication', {}).get('oauth2', {})
        authorize_url = urls.get('authorize', '')
        token_url = urls.get('token', '')

        if not authorize_url:
            raise Exception('no oauth2 authorize url')
        if not token_url:
            raise Exception('no oauth2 token url')

        return authorize_url, token_url

    except Exception as e:
        raise Exception(f'could not get oauth2 endpoints: {e}')


def __encode_state(wp_inst_uuid: str, step: str) -> str:
    return base64.encodebytes(
        json.dumps(
            {
                'uuid': wp_inst_uuid,
                'step': step,
            },
            indent=None,
        ).encode()
    ).decode()


def __decode_state(state: str) -> tuple[str, str]:
    # sometimes necessary if the padding is invalid
    while len(state) % 4 != 0:
        state += '='
    dec = json.loads(base64.decodebytes(state.encode()))
    return dec.get('uuid', ''), dec.get('step', '')


def __build_redirect_uri(plugin_url: str, api_callback_token: str = '') -> str:
    if not api_callback_token:
        return plugin_url
    return f'{plugin_url}?access_token={api_callback_token}'


# (1) state: "initial" -> next state: "authorize"
def __step_initial(
    plugin_url: str,
    wp_url: str,
    wp_auth: dict,
    api_callback_token: str = '',
) -> dict:
    """
    call the OAuth2 endpoint of fylr and authorize the given client
    * https://wp-oauth.com/docs/general/grant-types/authorization-code/
    * needs the client id (passed into this plugin api call as url parameter)
    * needs the redirect uri to fylr
      * must be built from the current plugin extension url
      * must include the access token so wordpress can call fylr (passed as url parameter)
    """

    # discover oauth2 related urls
    oauth2_authorize_url, _ = __load_oauth2_urls(wp_url)

    # build redirect uri
    oauth2_redirect_uri = __build_redirect_uri(plugin_url, api_callback_token)
    if not oauth2_redirect_uri:
        raise Exception('could not build redirect uri')

    client_id = wp_auth.get('oauth2_client_id')
    if not client_id:
        raise Exception('auth.oauth2_client_id invalid')

    wp_inst_uuid = wp_auth.get('uuid')
    if not wp_inst_uuid:
        raise Exception('auth.uuid invalid')

    resp = requests.post(
        url=oauth2_authorize_url,
        params={
            'response_type': 'code',
            'client_id': client_id,
            'redirect_uri': oauth2_redirect_uri,
            'scope': 'basic',
            'state': __encode_state(wp_inst_uuid, 'authorize'),
        },
    )
    wordpress_util.check_response(resp)

    return {
        'headers': {
            'Content-Type': 'text/html',
        },
        'body': resp.content.decode(),
    }


# (2) state: "authorize" -> next state: "token"
def __step_authorize(
    query_params: dict,
    plugin_url: str,
    wp_url: str,
    wp_auth: dict,
    api_callback_token: str = '',
) -> dict:

    # check if there is a code in the response
    oauth2_code = __get_query_param(
        query_params,
        'code',
        required=True,
    )

    # discover oauth2 related urls
    _, oauth2_token_url = __load_oauth2_urls(wp_url)

    # build redirect uri
    oauth2_redirect_uri = __build_redirect_uri(plugin_url, api_callback_token)
    if not oauth2_redirect_uri:
        raise Exception('could not build redirect uri')

    client_id = wp_auth.get('oauth2_client_id')
    if not client_id:
        raise Exception('auth.oauth2_client_id invalid')

    client_secret = wp_auth.get('oauth2_client_secret')
    if not client_secret:
        raise Exception('auth.oauth2_client_secret invalid')

    resp = requests.post(
        url=oauth2_token_url,
        auth=(client_id, client_secret),
        headers={
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        data={
            'grant_type': 'authorization_code',
            'redirect_uri': oauth2_redirect_uri,
            'code': oauth2_code,
        },
    )
    wordpress_util.check_response(resp)

    # send the access token info back to the frontend plugin config
    token_info = fylr_util.dumpjs(
        {
            "oauth2_access_token": resp.json().get('access_token', ''),
            "oauth2_token_type": resp.json().get('token_type', ''),
        }
    )
    return {
        'headers': {
            'Content-Type': 'text/html',
        },
        'body': f"""
            <html><head><script>
                window.opener.postMessage({token_info},'*');
            </script></head></html>
        """,
    }


def authenticate(
    query_params: dict,
    plugin_url: str,
    config_js: dict,
    api_callback_token: str = '',  # only relevant for fylr
):
    # check if there are errors in parameters from the OAuth2 endpoints
    # e.g. 'error=invalid_request&error_description=Invalid or missing response type'
    oauth2_error = __get_query_param(query_params, 'error')
    if oauth2_error:
        oauth2_error_description = __get_query_param(query_params, 'error_description')
        if oauth2_error_description:
            oauth2_error += ': ' + oauth2_error_description

        return __error_response(
            'wordpress.oauth2_error',
            'OAuth2 authentication failed',
            Exception(oauth2_error),
            print_stack=False,
        )

    try:
        # parse the state, get the wordpress server url.
        # get the step, this is required and tells this endpoint which step is current and next

        wp_inst_uuid, step = __decode_state(
            __get_query_param(query_params, 'state', required=True)
        )

        if not wp_inst_uuid:
            raise Exception(f'invalid state: invalid uuid')

        wp_inst, wp_auth = wordpress_util.get_wp_instance_and_auth_by_uuid(
            config_js,
            wp_inst_uuid,
        )
        wp_url = wp_inst.get('url')
        if not wp_url:
            raise Exception(f'wordpress url not set')

        # 1. step: initial
        #   * set by the frontend (when "generate" button is clicked in frontend)
        #   * next: call authorize endpoint
        if step == 'initial':
            return __step_initial(
                plugin_url,
                wp_url,
                wp_auth,
                api_callback_token,
            )

        # 2. step: authorize
        #   * returned by OAuth2 endpoint
        #   * next: get code from parameters, call token endpoint
        if step == 'authorize':
            return __step_authorize(
                query_params,
                plugin_url,
                wp_url,
                wp_auth,
                api_callback_token,
            )

        raise Exception(
            f'invalid state: invalid step {step}. expect [initial, authorize]'
        )

    except Exception as e:
        return __error_response(
            'wordpress.oauth2_error',
            'OAuth2 authentication failed',
            e,
            print_stack=False,  # enable for better debugging
        )
