import {
    WindowInterface
} from '#core/window/interface';

import {
    Services
} from '#service';

import {
    dev
} from '#utils/log';

import {
    getCookie,
    setCookie
} from '../cookies';
import {
    isProxyOrigin,
    parseUrlDeprecated
} from '../url';

const GOOGLE_API_URL =
    'https://ampcid.google.com/v1/publisher:getClientId?key=';

const TAG = 'GoogleCidApi';
const AMP_TOKEN = 'AMP_TOKEN';

/** @enum {string} */
export const TokenStatus_Enum = {
    RETRIEVING: '$RETRIEVING',
    OPT_OUT: '$OPT_OUT',
    NOT_FOUND: '$NOT_FOUND',
    ERROR: '$ERROR',
};

const TIMEOUT = 30000;
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const YEAR = 365 * DAY;

/**
 * Client impl for Google CID API
 */
export class GoogleCidApi {
    /** @param {!./ampdoc-impl.AmpDoc} ampdoc */
    constructor(ampdoc) {
        /**
         * @private {!Window}
         */
        this.win_ = ampdoc.win;
        /**
         * @private {!./timer-impl.Timer}
         */
        this.timer_ = Services.timerFor(this.win_);

        /**
         * @private {!Object<string, !Promise<?string>>}
         */
        this.cidPromise_ = {};

        const {
            canonicalUrl
        } = Services.documentInfoForDoc(ampdoc);

        /** @private {?string} */
        this.canonicalOrigin_ = canonicalUrl ?
            parseUrlDeprecated(canonicalUrl).origin :
            null;
    }

    /**
     * @param {string} apiKey
     * @param {string} scope
     * @return {!Promise<?string>}
     */
    getScopedCid(apiKey, scope) {
        if (this.cidPromise_[scope]) {
            return this.cidPromise_[scope];
        }
        let token;
        // Block the request if a previous request is on flight
        // Poll every 200ms. Longer interval means longer latency for the 2nd CID.
        return (this.cidPromise_[scope] = this.timer_
            .poll(200, () => {
                token = getCookie(this.win_, AMP_TOKEN);
                return token !== TokenStatus_Enum.RETRIEVING;
            })
            .then(() => {
                if (token === TokenStatus_Enum.OPT_OUT) {
                    return TokenStatus_Enum.OPT_OUT;
                }
                // If the page referrer is proxy origin, we force to use API even the
                // token indicates a previous fetch returned nothing
                const forceFetch =
                    token === TokenStatus_Enum.NOT_FOUND && this.isReferrerProxyOrigin_();

                // Token is in a special state, fallback to existing cookie
                if (!forceFetch && this.isStatusToken_(token)) {
                    return null;
                }

                if (!token || this.isStatusToken_(token)) {
                    this.persistToken_(TokenStatus_Enum.RETRIEVING, TIMEOUT);
                }

                const url = GOOGLE_API_URL + apiKey;
                return this.fetchCid_(dev().assertString(url), scope, token)
                    .then((response) => {
                        const cid = this.handleResponse_(response);
                        if (!cid && response['alternateUrl']) {
                            // If an alternate url is provided, try again with the alternate
                            // url The client is still responsible for appending API keys to
                            // the URL.
                            const altUrl = `${response['alternateUrl']}?key=${apiKey}`;
                            return this.fetchCid_(
                                dev().assertString(altUrl),
                                scope,
                                token
                            ).then(this.handleResponse_.bind(this));
                        }
                        return cid;
                    })
                    .catch((e) => {
                        this.persistToken_(TokenStatus_Enum.ERROR, TIMEOUT);
                        if (e && e.response) {
                            e.response.json().then((res) => {
                                dev().error(TAG, JSON.stringify(res));
                            });
                        } else {
                            dev().error(TAG, e);
                        }
                        return null;
                    });
            }));
    }

    /**
     * @param {string} url
     * @param {string} scope
     * @param {?string} token
     * @return {!Promise<!JsonObject>}
     */
    fetchCid_(url, scope, token) {
        const payload = {
            'originScope': scope,
            'canonicalOrigin': this.canonicalOrigin_,
        };
        if (token) {
            payload['securityToken'] = token;
        }
        return this.timer_.timeoutPromise(
            TIMEOUT,
            Services.xhrFor(this.win_)
            .fetchJson(url, {
                method: 'POST',
                ampCors: false,
                credentials: 'include',
                mode: 'cors',
                body: payload,
            })
            .then((res) => res.json())
        );
    }

    /**
     * @param {!JsonObject} res
     * @return {?string}
     */
    handleResponse_(res) {
        if (res['optOut']) {
            this.persistToken_(TokenStatus_Enum.OPT_OUT, YEAR);
            return TokenStatus_Enum.OPT_OUT;
        }
        if (res['clientId']) {
            this.persistToken_(res['securityToken'], YEAR);
            return res['clientId'];
        }
        if (res['alternateUrl']) {
            return null;
        }
        this.persistToken_(TokenStatus_Enum.NOT_FOUND, HOUR);
        return null;
    }

    /**
     * @param {string|undefined} tokenValue
     * @param {number} expires
     */
    persistToken_(tokenValue, expires) {
        if (tokenValue) {
            setCookie(this.win_, AMP_TOKEN, tokenValue, this.expiresIn_(expires), {
                highestAvailableDomain: true,
            });
        }
    }

    /**
     * @param {number} time
     * @return {number}
     */
    expiresIn_(time) {
        return this.win_.Date.now() + time;
    }

    /**
     * @return {boolean}
     */
    isReferrerProxyOrigin_() {
        return isProxyOrigin(WindowInterface.getDocumentReferrer(this.win_));
    }

    /**
     * @param {?string} token
     * @return {boolean}
     */
    isStatusToken_(token) {
        return /** @type {boolean} */ (token && token[0] === '$');
    }
}