// Webex REST client wrappers. Every function that hits webexapis.com lives
// here so index.js doesn't have to juggle URLs, auth headers, or the 429
// retry loop. State (token getters, logger) is injected via
// createWebexClient() so the module itself stays free of module-level
// mutable state — that keeps it importable from anywhere without
// initialization-order landmines.
//
// Usage:
//   import { createWebexClient } from './lib/webex.js';
//   const webex = createWebexClient({
//       getBotToken: (app) => botTokens[app] && botTokens[app].token,
//       getServiceAccountAccessToken: () => serviceAccountToken.access_token,
//       getServiceAccountRefreshToken: () => serviceAccountToken.refresh_token,
//       saveServiceAccountToken: (t) => { /* persist to disk */ },
//       env: process.env,
//       logger,
//   });

import fs from 'fs';

export function createWebexClient(deps) {
    var getBotToken = deps.getBotToken;
    var getServiceAccountAccessToken = deps.getServiceAccountAccessToken;
    var getServiceAccountRefreshToken = deps.getServiceAccountRefreshToken;
    var saveServiceAccountToken = deps.saveServiceAccountToken;
    var env = deps.env || {};
    var logger = deps.logger || function () {};

    // ---- low-level fetch helpers -----------------------------------------

    async function fetchWithRateLimit(url, requestOptions) {
        const response = await fetch(url, requestOptions);
        if (response.status === 429) {
            const secondsToWait = Number(response.headers.get('retry-after'));
            logger('fetchWithRateLimit',
                'Waiting ' + secondsToWait + 's on 429 for ' + url);
            await new Promise(resolve => setTimeout(resolve, secondsToWait * 1000));
            return await fetchWithRateLimit(url, requestOptions);
        }
        return response;
    }

    function getMillisToSleep(retryHeaderString) {
        let millisToSleep = Math.round(parseFloat(retryHeaderString) * 1000);
        if (isNaN(millisToSleep)) {
            millisToSleep = Math.max(0, new Date(retryHeaderString) - new Date());
        }
        return millisToSleep;
    }

    function sleep(milliseconds) {
        return new Promise((resolve) => setTimeout(resolve, milliseconds));
    }

    // Variant of fetchWithRateLimit that takes a callable so the caller can
    // rebuild a fresh request per retry (needed for POSTs whose body is a
    // stream — the body is consumed on the first attempt).
    async function fetchAndRetryIfNecessary(callAPIFn) {
        const response = await callAPIFn();
        if (response.status === 429) {
            const retryAfter = response.headers.get('retry-after');
            const millisToSleep = getMillisToSleep(retryAfter);
            await sleep(millisToSleep);
            return fetchAndRetryIfNecessary(callAPIFn);
        }
        return response;
    }

    // ---- people ----------------------------------------------------------

    // GET /people/me — resolves the caller identified by `bearerToken`.
    // Used by loadBotProfile (with the bot token) and the OAuth callback
    // (with the freshly issued user access token).
    function whoAmI(bearerToken) {
        return new Promise(function (resolve, reject) {
            logger('whoAmI', 'Fetching /people/me.');
            var requestOptions = {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + bearerToken,
                    'Content-Type': 'application/json'
                },
                redirect: 'follow'
            };
            fetchWithRateLimit('https://webexapis.com/v1/people/me', requestOptions)
                .then(async function (response) {
                    resolve(await response.json());
                })
                .catch(reject);
        });
    }

    // GET /people?email= via the service account. Resolves to the raw
    // Webex person record (id, displayName, emails, avatar, ...) or
    // rejects on !ok. Used by the CSV recipient parser.
    function getPersonInfo(email) {
        return new Promise(function (resolve, reject) {
            var url = 'https://webexapis.com/v1/people?email=' + encodeURIComponent(email) + '&max=100';
            var requestOptions = {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + getServiceAccountAccessToken(),
                    'Content-Type': 'application/json'
                },
                redirect: 'follow'
            };
            fetchWithRateLimit(url, requestOptions)
                .then(async function (response) {
                    logger('getPersonInfo', email + ': ' + response.status + ' ' + response.statusText);
                    if (!response.ok) return reject(new Error('HTTP ' + response.status));
                    var userInfo = await response.json();
                    resolve(userInfo.items && userInfo.items[0]);
                })
                .catch(reject);
        });
    }

    // GET /people?email= via the service account, normalized for the admin
    // UI (id/displayName/email/avatar or null when no match). Wraps
    // getPersonInfo behavior in the shape the /admin/users route wants.
    function findPersonByEmail(email) {
        return new Promise(function (resolve, reject) {
            var url = 'https://webexapis.com/v1/people?email=' + encodeURIComponent(email);
            var requestOptions = {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + getServiceAccountAccessToken(),
                    'Content-Type': 'application/json'
                }
            };
            fetchWithRateLimit(url, requestOptions)
                .then(function (response) {
                    if (!response.ok) {
                        return reject(new Error('HTTP ' + response.status + ' from Webex /people'));
                    }
                    return response.json();
                })
                .then(function (body) {
                    var items = body && body.items;
                    if (!items || !items.length) return resolve(null);
                    var person = items[0];
                    resolve({
                        id: person.id,
                        displayName: person.displayName,
                        email: (person.emails && person.emails[0]) || email,
                        avatar: person.avatar || null
                    });
                })
                .catch(reject);
        });
    }

    // ---- groups ----------------------------------------------------------

    // Paginated GET /groups. Returns the flat list of every group in the
    // org (typically ~10k). Uses the service account token because bot
    // tokens can't read the SCIM group directory.
    function findWebexGroup() {
        return new Promise(async function (resolve, reject) {
            var groups = [];
            var startIndex = 1;
            var count = 500;
            var groupSize = 10000;

            var url = new URL('https://webexapis.com/v1/groups');
            url.searchParams.append('attributes', 'displayName');
            url.searchParams.append('sortBy', 'displayName');
            url.searchParams.append('sortOrder', 'ascending');
            url.searchParams.append('includeMembers', 'false');
            url.searchParams.append('startIndex', startIndex);
            url.searchParams.append('count', count);

            var requestOptions = {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + getServiceAccountAccessToken(),
                    'Content-Type': 'application/json'
                }
            };
            while (groupSize > groups.length) {
                try {
                    var fetchedGroups = await fetchWithRateLimit(url, requestOptions);
                    if (fetchedGroups.ok) {
                        var groupData = await fetchedGroups.json();
                        groupSize = groupData.totalResults;
                        for (var group of groupData.groups) groups.push(group);
                        startIndex = startIndex + count;
                        url.searchParams.delete('startIndex');
                        url.searchParams.append('startIndex', startIndex);
                    } else {
                        return reject(fetchedGroups.status + ': ' + fetchedGroups.statusText);
                    }
                } catch (error) {
                    logger('findWebexGroup', 'page fetch failed: ' + (error && error.message || error));
                    return reject(error);
                }
            }
            resolve(groups);
        });
    }

    // Paginated GET /groups/:id/members. Returns { members: [...], groups:
    // [...] } separated by whether the child is a person or a nested
    // group. Callers must recurse into nested groups themselves.
    function getGroupMembers(groupId) {
        return new Promise(async function (resolve, reject) {
            var groupMembers = { members: [], groups: [] };
            var startIndex = 1;
            var count = 500;
            var memberSize = 10000;

            var requestOptions = {
                method: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + getServiceAccountAccessToken(),
                    'Content-Type': 'application/json'
                }
            };

            while (memberSize > (groupMembers.members.length + groupMembers.groups.length)) {
                var pageUrl = 'https://webexapis.com/v1/groups/' + groupId + '/members?startIndex=' + startIndex + '&count=' + count;
                try {
                    var groupResponse = await fetchWithRateLimit(pageUrl, requestOptions);
                    if (groupResponse.ok) {
                        var groupData = await groupResponse.json();
                        groupMembers.displayName = groupData.displayName;
                        memberSize = groupData.memberSize;
                        for (var member of groupData.members) {
                            if (member.type == 'group') {
                                groupMembers.groups.push({ id: member.id, name: member.displayName });
                            } else {
                                groupMembers.members.push(member);
                            }
                        }
                        startIndex = startIndex + count;
                    } else {
                        return reject(groupResponse.status + ': ' + groupResponse.statusText);
                    }
                } catch (error) {
                    logger('getGroupMembers', 'page fetch failed: ' + (error && error.message || error));
                    return reject(error);
                }
            }
            resolve(groupMembers);
        });
    }

    // ---- messages --------------------------------------------------------

    // POST /messages as a bot. Attaches an image from ./uploads/<imageName>
    // when supplied. Resolves with the parsed Webex message record
    // (including id) so callers can retract later via deleteMessage.
    function sendDirectMessage(toPersonId, message, imageName, appName) {
        return new Promise(function (resolve, reject) {
            var myHeaders = new Headers();
            myHeaders.append('Authorization', 'Bearer ' + getBotToken(appName));
            var formdata = new FormData();
            formdata.append('toPersonId', toPersonId);
            formdata.append('markdown', message);

            if (imageName) {
                var stream = fs.createReadStream('./uploads/' + imageName);
                formdata.append('files', stream, imageName);
            }

            var requestOptions = {
                method: 'POST',
                headers: myHeaders,
                body: formdata,
                redirect: 'follow'
            };
            fetchWithRateLimit('https://webexapis.com/v1/messages', requestOptions)
                .then(response => response.text())
                .then(result => {
                    try { resolve(JSON.parse(result)); }
                    catch (err) { reject(err); }
                })
                .catch(reject);
        });
    }

    // POST /messages carrying an Adaptive Card attachment. Fire-and-forget
    // for job-completion cards.
    function sendDirectCard(toPersonId, cardInfo, appName) {
        return new Promise(function (resolve, reject) {
            var body = {
                toPersonId: toPersonId,
                markdown: cardInfo.message,
                attachments: [
                    {
                        contentType: 'application/vnd.microsoft.card.adaptive',
                        content: cardInfo.card
                    }
                ]
            };
            var requestOptions = {
                method: 'POST',
                headers: {
                    'Authorization': 'Bearer ' + getBotToken(appName),
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body),
                redirect: 'follow'
            };
            fetchWithRateLimit('https://webexapis.com/v1/messages', requestOptions)
                .then(async function (response) {
                    await response.text();
                    resolve(response);
                })
                .catch(reject);
        });
    }

    // POST /messages via the retry-friendly path (streamed body). Used by
    // the send queue where transient 429s are worth retrying.
    function sendMessageWithRetry(toPersonId, message, imageName, appName) {
        var url = 'https://webexapis.com/v1/messages';
        return fetchAndRetryIfNecessary(async () => {
            var myHeaders = new Headers();
            myHeaders.append('Authorization', 'Bearer ' + getBotToken(appName));
            var formdata = new FormData();
            formdata.append('toPersonId', toPersonId);
            formdata.append('markdown', message);
            if (imageName) {
                var stream = fs.createReadStream('./uploads/' + imageName);
                formdata.append('files', stream, imageName);
            }
            return fetch(url, {
                method: 'POST',
                headers: myHeaders,
                body: formdata,
                redirect: 'follow'
            });
        });
    }

    // Best-effort DELETE /messages/:id. Never rejects — the callers (right
    // now: preview-DM retraction) always want the flow to continue whether
    // this succeeds or not. Resolves to true on 2xx, false otherwise.
    function deleteMessage(messageId, appName) {
        return new Promise(function (resolve) {
            var url = 'https://webexapis.com/v1/messages/' + encodeURIComponent(messageId);
            var requestOptions = {
                method: 'DELETE',
                headers: { 'Authorization': 'Bearer ' + getBotToken(appName) }
            };
            fetchWithRateLimit(url, requestOptions)
                .then(function (response) {
                    if (!response.ok) {
                        logger('deleteMessage', 'HTTP ' + response.status + ' for ' + messageId);
                    }
                    resolve(response.ok);
                })
                .catch(function (err) {
                    logger('deleteMessage', 'error for ' + messageId + ': ' + (err && err.message || err));
                    resolve(false);
                });
        });
    }

    // ---- OAuth refresh ---------------------------------------------------

    // Renews the service-account access token using its refresh token.
    // Also computes / persists renewBy + refreshBy timestamps in
    // America/New_York for the cron that reads them.
    function refreshToken() {
        return new Promise(function (resolve, reject) {
            var params = new URLSearchParams();
            params.append('grant_type', 'refresh_token');
            params.append('client_id', env.WEBEX_SERVICE_ACCOUNT_CLIENT_ID || '');
            params.append('client_secret', env.WEBEX_SERVICE_ACCOUNT_CLIENT_SECRET || '');
            params.append('refresh_token', getServiceAccountRefreshToken());

            fetchWithRateLimit('https://webexapis.com/v1/access_token', { method: 'post', body: params })
                .then(function (res) {
                    logger('refreshToken', 'HTTP ' + res.status);
                    return res.json();
                })
                .then(function (json) {
                    var opts = { timeZoneName: 'short', timeZone: 'America/New_York' };
                    var updated = Object.assign({}, json, {
                        created: new Date().toLocaleString('en-US', opts),
                        renewBy: new Date(Date.now() + (json.expires_in * 1000)).toLocaleString('en-US', opts),
                        refreshBy: new Date(Date.now() + (json.refresh_token_expires_in * 1000)).toLocaleString('en-US', opts)
                    });
                    saveServiceAccountToken(updated);
                    resolve('Refresh Success!\nExpires: ' + updated.renewBy + '\nRefreshBy: ' + updated.refreshBy);
                })
                .catch(reject);
        });
    }

    return {
        fetchWithRateLimit,
        fetchAndRetryIfNecessary,
        getMillisToSleep,
        sleep,
        whoAmI,
        getPersonInfo,
        findPersonByEmail,
        findWebexGroup,
        getGroupMembers,
        sendDirectMessage,
        sendDirectCard,
        sendMessageWithRetry,
        deleteMessage,
        refreshToken,
    };
}
