from datetime import datetime
import hashlib
import json
import os
import requests
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth1
from urllib.parse import urljoin, parse_qs

import fylr_lib_plugin_python3.util as fylr_util


def raise_error(err):
    raise Exception(f'Error: {err}')


def get_event_name(is_scheduled: bool):
    if is_scheduled:
        return 'ASSET_EXPORT_TRANSPORT_COPY_SCHEDULED'
    return 'ASSET_EXPORT_TRANSPORT_COPY'


def handle_wp_response(
    r: requests.Response,
    parse_from_query: bool = False,
) -> tuple[int, dict]:
    js_body = {}
    statuscode = r.status_code

    try:
        if parse_from_query:
            js_body = parse_qs(r.content.decode('utf-8'))
        else:
            js_body = json.loads(r.content)
    except Exception as e:
        js_body = {
            'message': r.url
            + '\n'
            + str(statuscode)
            + '\n'
            + str(r.content.decode('utf-8'))
            + '\n'
            + str(e)
        }

    if not (200 <= statuscode <= 299):
        raise_error(js_body.get('message'))

    return statuscode, js_body


def handle_wp_get(
    wp_url: str,
    headers: dict = {},
    params: dict = {},
    auth=None,
    parse_from_query: bool = False,
    protocol=None,
) -> tuple[int, dict]:
    r = requests.get(
        url=wp_url,
        params=params,
        headers=headers,
        auth=auth,
    )
    if protocol:
        protocol.append(f'GET {r.url}')
    return handle_wp_response(r, parse_from_query)


def handle_wp_post(
    wp_url: str,
    data,
    headers: dict = {},
    params: dict = {},
    auth=None,
    parse_from_query: bool = False,
    protocol=None,
) -> tuple[int, dict]:
    r = requests.post(
        url=wp_url,
        data=data,
        params=params,
        headers=headers,
        auth=auth,
    )
    if protocol:
        protocol.append(f'POST {r.url}')
    return handle_wp_response(r, parse_from_query)


def build_wp_api_url(url: str) -> str:
    # need to do a api discovery to find out at which endpoint the api is registered
    # send a HEAD request to the server and parse the Link headers
    r = requests.head(url)
    if r.status_code != 200:
        return ''

    api_url = ''
    for link in r.links.values():
        if link.get('rel', '').startswith('https://api.w.org'):
            api_url = link.get('url', '')
            if api_url:
                break

    if not api_url:
        return ''

    while api_url.endswith('/'):
        api_url = api_url[:-1]

    api_url += '/wp/v2'
    return api_url


def build_req_auth(auth: dict):
    if auth['auth_type'] == 'http-auth':
        return HTTPBasicAuth(auth['http_auth_login'], auth['http_auth_password'])
    elif auth['auth_type'] == 'oauth-1':
        return OAuth1(
            auth['oauth_client_key'],
            auth['oauth_client_secret'],
            auth['oauth_token'],
            auth['oauth_token_secret'],
        )
    return None


def __error_response(code: str, msg: str, e: Exception, print_stack: bool) -> dict:
    error = {
        'error': {
            'realm': 'user',
            'code': code,
            'parameters': {
                'error': msg,
            },
        }
    }
    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 oauth1(
    query_params: dict,
    plugin_url: str,
    api_callback_token: str = '',
    logger=None,
):

    oauth_callback = urljoin(plugin_url, 'oauth1')

    # this is the callback from WP, we parse this and call ourselves once
    # again for the final step
    verifiers = query_params.get('oauth_verifier', [])
    if verifiers:
        oauth_verifier = verifiers[0]

        if logger:
            logger.debug(f'api callback: oauth1 | oauth_verifier={oauth_verifier}')

        return {
            'headers': {'Content-Type': 'text/html'},
            'body': """
                <html><head><script>
                    var wp_oauth = JSON.parse(window.sessionStorage.getItem("wp_oauth"));
                    wp_oauth.verifier = '"""
            + oauth_verifier
            + """';
                    var params = [];
                    for (k in wp_oauth) {
                        params.push(encodeURIComponent(k)+'='+encodeURIComponent(wp_oauth[k]));
                    }
                    """
            + (
                ("""params.push('access_token=""" + api_callback_token + """');""")
                if api_callback_token is not None
                else ''
            )
            + """
                    url = '"""
            + oauth_callback
            + """?' + params.join('&');
                    document.location.assign(url);
                </script></head></html>
            """,
        }

    wp_inst_url = query_params.get('wp_url', [])
    if not wp_inst_url:
        return __error_response(
            'wordpress.oauth_error', 'Parameter wp_url missing.', Exception(), False
        )
    wp_inst_url = wp_inst_url[0]
    if not wp_inst_url:
        return __error_response(
            'wordpress.oauth_error', 'Parameter wp_url missing.', Exception(), False
        )

    if logger:
        logger.debug(f'api callback: oauth1 | wp_url={wp_inst_url}')

    wp_api_url = build_wp_api_url(wp_inst_url)

    wp_urls = {}
    try:
        _, auth_resp = handle_wp_get(wp_api_url)
        wp_urls = auth_resp.get('authentication', {}).get('oauth1', {})
    except Exception as e:
        return __error_response(
            'wordpress.oauth_error',
            'Authentication \'oauth1\' not allowed',
            e,
            True,
        )

    if logger:
        logger.debug(
            f'api callback: oauth1 | response from {urljoin(wp_inst_url, "wp-json")}: wp_urls={fylr_util.dumpjs(wp_urls)}'
        )

    # this is the callback from ourselves after we got the callback from WP
    verifier = query_params.get('verifier')
    if verifier:

        if logger:
            logger.debug(f'api callback: oauth1 | verifier={verifier}')

        wp_oauth = {}
        for key in [
            'wp_url',
            'client_key',
            'client_secret',
            'resource_owner_key',
            'resource_owner_secret',
            'verifier',
        ]:
            wp_oauth[key] = query_params[key][0]

        oauth = OAuth1(
            wp_oauth['client_key'],
            client_secret=wp_oauth['client_secret'],
            resource_owner_key=wp_oauth['resource_owner_key'],
            resource_owner_secret=wp_oauth['resource_owner_secret'],
            verifier=wp_oauth['verifier'],
        )

        _, credentials = handle_wp_post(
            wp_urls['access'],
            data=None,
            auth=oauth,
            parse_from_query=True,
        )
        if not credentials:
            return __error_response(
                'wordpress.oauth_error',
                'Could not get \'oauth1\' credentials.',
                Exception(),
                False,
            )

        resource_owner_key = credentials.get('oauth_token', [])
        if not resource_owner_key:
            raise Exception('could not get oauth_token')
        resource_owner_key = resource_owner_key[0]
        if not resource_owner_key:
            raise Exception('could not get oauth_token.0')

        resource_owner_secret = credentials.get('oauth_token_secret')
        if not resource_owner_secret:
            raise Exception('could not get oauth_token_secret')
        resource_owner_secret = resource_owner_secret[0]
        if not resource_owner_secret:
            raise Exception('could not get oauth_token_secret.0')

        if logger:
            logger.debug(
                f'api callback: oauth1 | resource_owner_key={resource_owner_key}'
            )
            logger.debug(
                f'api callback: oauth1 | resource_owner_secret={resource_owner_secret}'
            )

        msg = fylr_util.dumpjs(
            {
                "oauth_token": resource_owner_key,
                "oauth_token_secret": resource_owner_secret,
            },
            indent=2,
        )

        return {
            'headers': {'Content-Type': 'text/html'},
            'body': """
                <html><head><script>
                    window.opener.postMessage("""
            + msg
            + """,'*');
                </script></head></html>""",
        }

    # initial request coming from easydb/fylr

    # key is required
    client_key = query_params.get('client_key', [])
    if not client_key:
        return __error_response(
            'wordpress.oauth_error',
            'Required parameter "client_key" missing',
            Exception(),
            False,
        )
    client_key = client_key[0]
    if not client_key:
        return __error_response(
            'wordpress.oauth_error',
            'Required parameter "client_key" missing',
            Exception(),
            False,
        )

    # secret is optional
    client_secret = query_params.get('client_secret', [])
    if client_secret:
        client_secret = client_secret[0]

    if logger:
        logger.debug(f'api callback: oauth1 | client_key={client_key}')
        logger.debug(f'api callback: oauth1 | oauth_callback={oauth_callback}')

    try:
        oauth = OAuth1(client_key, client_secret=client_secret)
        _, resp = handle_wp_post(
            wp_urls['request'],
            data=None,
            params={
                'oauth_callback': oauth_callback
                + (
                    '?access_token=' + api_callback_token
                    if api_callback_token is not None
                    else ''
                )
            },
            auth=oauth,
            parse_from_query=True,
        )

        resource_owner_key = resp.get('oauth_token', [])
        if not resource_owner_key:
            raise Exception('could not get oauth_token')
        resource_owner_key = resource_owner_key[0]
        if not resource_owner_key:
            raise Exception('could not get oauth_token.0')

        resource_owner_secret = resp.get('oauth_token_secret')
        if not resource_owner_secret:
            raise Exception('could not get oauth_token_secret')
        resource_owner_secret = resource_owner_secret[0]
        if not resource_owner_secret:
            raise Exception('could not get oauth_token_secret.0')

        wp_oauth = json.dumps(
            {
                'wp_url': wp_inst_url,
                'client_key': client_key,
                'client_secret': client_secret,
                'resource_owner_key': resource_owner_key,
                'resource_owner_secret': resource_owner_secret,
            }
        )

        authorize_url = f'{wp_urls.get("authorize")}?oauth_token={resource_owner_key}'
        if logger:
            logger.debug(
                f'api callback: oauth1 | resource_owner_key={resource_owner_key}'
            )
            logger.debug(f'api callback: oauth1 | => authorize_url={authorize_url}')

        # redirect browser to wordpress
        # wordpress will redirect back to us

        return {
            'headers': {'Content-Type': 'text/html'},
            'body': """
                <html><head><script>
                    window.sessionStorage.setItem("wp_oauth", '"""
            + wp_oauth
            + """');
                    document.location.assign('"""
            + authorize_url
            + """');
                </script></head></html>
            """,
        }

    except Exception as e:
        return __error_response(
            'wordpress.oauth_error',
            str(e),
            e,
            True,
        )


def __download_export_file(url, f_path):
    resp = requests.get(url)

    if resp.status_code == 200:
        base_dir = os.path.dirname(f_path)
        if not os.path.exists(base_dir):
            os.makedirs(base_dir)

        with open(os.path.abspath(f_path), 'wb') as outf:
            outf.write(resp.content)
        return

    raise_error(
        f'could not get file from fylr: status code {resp.status_code}: {resp.text}'
    )


def transport_files(
    wp_media_url: str,
    fpath: str,
    files: list,
    auth: dict,
    user_generated_displayname: str,
    source_name: str,  # 'easydb' or 'fylr'
    export_id: int = 0,  # only needed for fylr
    transport_uuid: int = 0,  # only needed for fylr
    api_callback_url: str = '',  # only needed for fylr
) -> tuple[list, list, str]:

    events = []
    protocol = []

    if not files:
        protocol.append('No files to upload to wordpress')
        return [], protocol, 'failed'

    req_auth = build_req_auth(auth)
    if not req_auth:
        protocol.append('could not form request authentication')
        return [], protocol, 'failed'

    count_success = 0
    count_skipped = 0
    for fo in files:
        path = os.path.join(fpath, fo['path'])
        filename = os.path.basename(path)
        try:
            needs_file_download = False

            # from easydb5
            fileclass = fo.get('eas_fileclass', '')
            if not fileclass:
                # from fylr
                fileclass = fo.get('export_file_internal', {}).get('content_type', '')
                fileclass = fileclass.split('/')[0]
                needs_file_download = True

            if fileclass != 'image':
                protocol.append(f'{filename} skipped: wrong fileclass {fileclass}')
                count_skipped += 1
                continue

            if needs_file_download:
                # easydb5 exports have already saved the file on disk
                # fylr exports are done on request, so files need to be loaded
                file_url = f'{api_callback_url}/api/v1/export/{export_id}/uuid/{transport_uuid}/file/{path}'
                # protocol.append(f'load file from export: {file_url}')
                __download_export_file(file_url, path)

            headers = {
                'Content-Type': 'application/octet-stream',
                'Content-Disposition': f'attachment; filename="{filename}"',
            }

            title = filename
            description = f'Imported from {source_name}: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}. User: {user_generated_displayname}'

            # produce a unique id for the file we are pushing

            with open(path, 'rb') as f:
                slug = hashlib.md5(f.read()).hexdigest()

            slug_params = {
                'slug': slug,
                'title': title,
                'description': description,
            }

            # let's see if the file exists in WP already
            protocol.append(
                f'{filename} check if this file already exists in Wordpress'
            )
            _, files_by_slug = handle_wp_get(
                wp_media_url,
                params=slug_params,
                auth=req_auth,
            )

            if len(files_by_slug) == 0:
                # file is new, push it
                with open(path, 'rb') as f:
                    _, body_js = handle_wp_post(
                        wp_media_url,
                        data=f,
                        params=slug_params,
                        headers=headers,
                        auth=req_auth,
                    )
                    log_response = body_js

                    protocol.append(f'{filename} uploaded to Wordpress')

                    events.append(
                        {
                            'pollable': False,
                            'base_type': 'asset',
                            'object_id': fo.get('eas_id'),
                            'event_info': {
                                'wordpress': body_js,
                                'version': fo.get('eas_version'),
                                'class': fo.get('eas_fileclass'),
                                'system_object_id': fo.get('system_object_id'),
                            },
                        }
                    )

                    count_success += 1
            else:
                # update file

                # update the first file only
                update_file_id = files_by_slug[0]['id']
                _, body_js = handle_wp_post(
                    # NOTICE: do not use urljoin() here!
                    wp_media_url + '/' + str(update_file_id),
                    data=None,
                    params={
                        'title': title,
                        'description': description,
                    },
                    headers=headers,
                    auth=req_auth,
                )
                log_response = body_js

                protocol.append(
                    f'{filename} updated in Wordpress. ID: {update_file_id}'
                )

                count_success += 1

            if log_response:
                events.append(
                    {
                        'name': 'WORDPRESS_SYNC',
                        'pollable': False,
                        'base_type': 'asset',
                        'object_id': fo.get('eas_id'),
                        'event_info': {
                            'wordpress': log_response,
                            'version': fo.get('eas_version'),
                            'class': fo.get('eas_fileclass'),
                            'system_object_id': fo.get('system_object_id'),
                        },
                    }
                )

        except Exception as e:
            protocol.append(f'{path} failed to upload to Wordpress: {e}')

    count_failed = len(files) - count_success - count_skipped
    if count_failed:
        protocol.append(
            f'{count_failed}/{len(files)} files failed to upload to Wordpress'
        )

    protocol.append(f'{count_skipped}/{len(files)} files skipped')
    protocol.append(f'{count_success}/{len(files)} files uploaded to Wordpress')

    return events, protocol, 'failed' if count_failed else 'done'
