import apps from './apps.js';
import assert from 'node:assert';
import backupSites from './backupsites.js';
import BoxError from './boxerror.js';
import constants from './constants.js';
import dashboard from './dashboard.js';
import logger from './logger.js';
import domains from './domains.js';
import dockerRegistries from './dockerregistries.js';
import directoryServer from './directoryserver.js';
import externalLdap from './externalldap.js';
import groups from './groups.js';
import mail from './mail.js';
import manifestFormat from '@cloudron/manifest-format';
import oidcClients from './oidcclients.js';
import paths from './paths.js';
import promiseRetry from './promise-retry.js';
import safe from 'safetydance';
import semver from 'semver';
import settings from './settings.js';
import superagent from '@cloudron/superagent';
import system from './system.js';
import users from './users.js';
import volumes from './volumes.js';

const { log } = logger('appstore');

// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
const DEFAULT_FEATURES = {
    appUpdates: false,
    appMaxCount: 2,
    userMaxCount: 5,
    domainMaxCount: 1,
    mailboxMaxCount: 5,
    branding: false,
    externalLdap: false,
    privateDockerRegistry: false,
    userGroups: false,
    emailServer: false,
    profileConfig: false,
    multipleBackupTargets: false,
    encryptedBackups: false,

    // TODO how to go about that in the UI?
    userRoles: false,
    appProxy: false,
    eventlogRetention: false,
    hsts: false,

    // TODO remove usage of old features below
    support: false,
    emailPremium: false,
};
let gFeatures = null;

function getFeatures() {
    if (gFeatures === null) {
        gFeatures = Object.assign({}, DEFAULT_FEATURES);

        const tmp = safe.JSON.parse(safe.fs.readFileSync(paths.FEATURES_INFO_FILE, 'utf8'));
        if (!tmp) {
            return DEFAULT_FEATURES;
        }

        for (const f in DEFAULT_FEATURES) {
            if (f in tmp) gFeatures[f] = tmp[f];
            if (tmp[f] === null) gFeatures[f] = 100000; // null essentially means unlimited
        }
    }

    return gFeatures;
}

async function getState() {
    const mailDomains = await mail.listDomains();
    const mailStats = await Promise.all(mailDomains.map(d => mail.getStats(d.domain)));
    const allUsers = await users.list();
    const roleCounts = allUsers.reduce((acc, u) => {
        acc[u.role] = (acc[u.role] || 0) + 1;
        return acc;
    }, {});

    const state = {
        provider: system.getProvider(),
        users: { count: allUsers.length, roleCounts },
        groupCount: (await groups.list()).length,
        domains: (await domains.list()).map(d => d.provider),
        mail: {
            incomingCount: mailDomains.filter(md => md.enabled).length,
            catchAllCount: mailDomains.filter(md => md.catchAll.length).length,
            bannerCount: mailDomains.filter(md => md.banner.text || md.banner.html).length,
            mailboxCount: mailStats.reduce((acc, cur) => acc + cur.mailboxCount, 0),
            mailingListCount: mailStats.reduce((acc, cur) => acc + cur.mailingListCount, 0),
            pop3Count: mailStats.reduce((acc, cur) => acc + cur.pop3Count, 0),
            aliasCount: mailStats.reduce((acc, cur) => acc + cur.aliasCount, 0)
        },
        apps: (await apps.list()).map(a => { return { id: a.manifest.id, community: !!a.versionsUrl }; }),
        dockerRegistries: (await dockerRegistries.list()).map(r => r.provider),
        backupSites: (await backupSites.list()).map(s => { return { provider: s.provider, format: s.format, encryption: !!s.encryption }; }),
        externalLdap: (await externalLdap.getConfig()).provider,
        volumes: (await volumes.list()).map(v => v.mountType),
        directoryServer: (await directoryServer.getConfig()).enabled,
        oidcClientCount: (await oidcClients.list()).length
    };

    return state;
}

async function getApiServerOrigin() {
    return await settings.get(settings.API_SERVER_ORIGIN_KEY) || 'https://api.cloudron.io';
}

async function setApiServerOrigin(origin) {
    assert.strictEqual(typeof origin, 'string');
    await settings.set(settings.API_SERVER_ORIGIN_KEY, origin);
}

async function getWebServerOrigin() {
    return await settings.get(settings.WEB_SERVER_ORIGIN_KEY) || 'https://cloudron.io';
}

async function getConsoleServerOrigin() {
    return await settings.get(settings.CONSOLE_SERVER_ORIGIN_KEY) || 'https://console.cloudron.io';
}

async function getSubscription() {
    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    const [stateError, state] = await safe(getState());
    if (stateError) log('getSubscription: error getting current state', stateError);

    const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/subscription3`)
        .query({ accessToken: token })
        .send({ state })
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status === 502) throw new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${response.status} ${JSON.stringify(response.body)}`);
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${response.status} ${JSON.stringify(response.body)}`);

    // update the features cache
    getFeatures();
    for (const f in gFeatures) {
        if (typeof response.body.features[f] !== 'undefined') gFeatures[f] = response.body.features[f];
        if (response.body.features[f] === null) gFeatures[f] = 100000; // null essentially means unlimited
    }
    safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures, null, 2), 'utf8');

    // { email, emailVerified, cloudronId, cloudronCreatedAt, plan: { id, name }, canceled_at, status, externalCustomer, features: {} }
    return response.body;
}

// cron hook
async function checkSubscription() {
    const [error, result] = await safe(getSubscription());
    if (error) log('checkSubscription error:', error);
    else log(`checkSubscription: Cloudron ${result.cloudronId} is on the ${result.plan.name} plan.`);
}

function isFreePlan(subscription) {
    return !subscription || subscription.plan.id === 'free';
}

async function getBoxUpdate(options) {
    assert.strictEqual(typeof options, 'object');

    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    const query = {
        accessToken: token,
        boxVersion: constants.VERSION,
        stableOnly: options.stableOnly
    };

    const [error, response] = await safe(superagent.get(`${await getApiServerOrigin()}/api/v1/boxupdate`)
        .query(query)
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status === 204) return null; // no update
    if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`);

    const updateInfo = response.body;

    if (!semver.valid(updateInfo.version)) throw new BoxError(BoxError.EXTERNAL_ERROR, `Offered version ${updateInfo.version} is invalid`);
    if (semver.gt(constants.VERSION, updateInfo.version)) throw new BoxError(BoxError.EXTERNAL_ERROR, `Offered version ${updateInfo.version} would be a downgrade`);

    // updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
    if (!updateInfo.version || typeof updateInfo.version !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad version): ${response.status} ${response.text}`);
    if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad changelog): ${response.status} ${response.text}`);
    if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad sourceTarballUrl): ${response.status} ${response.text}`);
    if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad sourceTarballSigUrl): ${response.status} ${response.text}`);
    if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad boxVersionsUrl): ${response.status} ${response.text}`);
    if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad boxVersionsSigUrl): ${response.status} ${response.text}`);
    if (typeof updateInfo.unstable !== 'boolean') throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response (bad unstable): ${response.status} ${response.text}`);

    return updateInfo;
}

async function getAppUpdate(app, options) {
    assert.strictEqual(typeof app, 'object');
    assert.strictEqual(typeof options, 'object');

    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    const query = {
        accessToken: token,
        boxVersion: constants.VERSION,
        appId: app.appStoreId,
        appVersion: app.manifest.version,
        stableOnly: options.stableOnly
    };

    const [error, response] = await safe(superagent.get(`${await getApiServerOrigin()}/api/v1/appupdate`)
        .query(query)
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status === 204) return null; // no update
    if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`);

    const updateInfo = response.body;

    // for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
    const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;

    // do some sanity checks
    if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
        log('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
        throw new BoxError(BoxError.EXTERNAL_ERROR, `Malformed update: ${response.status} ${response.text}`);
    }

    updateInfo.unstable = !!updateInfo.unstable;

    // { id, creationDate, manifest, unstable }
    return updateInfo;
}

async function updateCloudron(data) {
    assert.strictEqual(typeof data, 'object');

    const { domain, version } = data;

    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    const query = {
        accessToken: token
    };

    const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/update_cloudron`)
        .query(query)
        .send({ domain, version })
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`);

    log(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
}

async function registerCloudron3() {
    const { domain } = await dashboard.getLocation();
    const version = constants.VERSION;

    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (token) { // when installed using setupToken, this updates the domain record when called during provisioning
        log('registerCloudron3: already registered. Just updating the record.');
        await getSubscription();
        return await updateCloudron({ domain, version });
    }

    const [error, response] = await safe(superagent.post(`${await getApiServerOrigin()}/api/v1/register_cloudron3`)
        .send({ domain, version })
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error reaching appstore: ${error.message}`);
    if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.status} ${response.text}`);

    if (!response.body.cloudronId) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id');
    if (!response.body.cloudronToken) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token');

    await settings.set(settings.CLOUDRON_ID_KEY, response.body.cloudronId);
    await settings.set(settings.APPSTORE_API_TOKEN_KEY, response.body.cloudronToken);

    log(`registerCloudron3: Cloudron registered with id ${response.body.cloudronId}`);

    await getSubscription();
}

async function unregister() {
    await settings.set(settings.CLOUDRON_ID_KEY, '');
    await settings.set(settings.APPSTORE_API_TOKEN_KEY, '');
}

async function unlinkAccount() {
    log('unlinkAccount: Unlinking existing account.');

    if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');

    await unregister();
    return await registerCloudron3();
}

async function downloadManifest(appStoreId, manifest) {
    if (!appStoreId && !manifest) throw new BoxError(BoxError.BAD_FIELD, 'Neither manifest nor appStoreId provided');

    if (!appStoreId) return { appStoreId: '', manifest };

    const [id, version] = appStoreId.split('@');
    if (!manifestFormat.isId(id)) throw new BoxError(BoxError.BAD_FIELD, 'appStoreId is not valid');
    if (version && !semver.valid(version)) throw new BoxError(BoxError.BAD_FIELD, 'package version is not valid semver');

    const url = await getApiServerOrigin() + '/api/v1/apps/' + id + (version ? '/versions/' + version : '');

    log(`downloading manifest from ${url}`);

    const [error, response] = await safe(superagent.get(url).timeout(60 * 1000).ok(() => true));

    if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message);

    if (response.status !== 200) throw new BoxError(BoxError.NOT_FOUND, `Failed to get app info from store. status: ${response.status} text: ${response.text}`);

    if (!response.body.manifest || typeof response.body.manifest !== 'object') throw new BoxError(BoxError.NOT_FOUND, `Missing manifest. Failed to get app info from store. status: ${response.status} text: ${response.text}`);

    return { appStoreId: id, manifest: response.body.manifest };
}

async function getApps() {
    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    const [error, response] = await safe(superagent.get(`${await getApiServerOrigin()}/api/v1/apps`)
        .query({ accessToken: token, boxVersion: constants.VERSION, unstable: true })
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `App listing failed. ${response.status} ${JSON.stringify(response.body)}`);
    if (!response.body.apps) throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response: ${response.status} ${response.text}`);

    return response.body.apps;
}

async function getAppVersion(appId, version) {
    assert.strictEqual(typeof appId, 'string');
    assert.strictEqual(typeof version, 'string');

    const token = await settings.get(settings.APPSTORE_API_TOKEN_KEY);
    if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');

    let url = `${await getApiServerOrigin()}/api/v1/apps/${appId}`;
    if (version !== 'latest') url += `/versions/${version}`;

    const [error, response] = await safe(superagent.get(url)
        .query({ accessToken: token })
        .timeout(60 * 1000)
        .ok(() => true));

    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.LICENSE_ERROR, 'Invalid appstore token');
    if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, `Could not find app ${appId}`);
    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `App fetch failed. ${response.status} ${JSON.stringify(response.body)}`);

    return response.body; // { id, creationDate, publishState, manifest, iconUrl }
}

async function getApp(appId) {
    assert.strictEqual(typeof appId, 'string');

    return await getAppVersion(appId, 'latest');
}

async function downloadIcon(appStoreId, version) {
    const iconUrl = `${await getApiServerOrigin()}/api/v1/apps/${appStoreId}/versions/${version}/icon`;

    return await promiseRetry({ times: 10, interval: 5000, log }, async function () {
        const [networkError, response] = await safe(superagent.get(iconUrl)
            .timeout(60 * 1000)
            .ok(() => true));

        if (networkError) throw new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon: ${networkError.message}`);
        if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Icon download failed. ${response.status} ${JSON.stringify(response.body)}`);

        const contentType = response.headers['content-type'];
        if (!contentType || contentType.indexOf('image') === -1) throw new BoxError(BoxError.EXTERNAL_ERROR, 'AppStore returned invalid icon for app');

        return response.body;
    });
}

const _setApiServerOrigin = setApiServerOrigin;

export default {
    getFeatures,
    getApiServerOrigin,
    getWebServerOrigin,
    getConsoleServerOrigin,
    downloadManifest,
    getApps,
    getApp,
    getAppVersion,
    downloadIcon,
    registerCloudron3,
    updateCloudron,
    unlinkAccount,
    getSubscription,
    checkSubscription, // cron hook,
    isFreePlan,
    getAppUpdate,
    getBoxUpdate,
    _setApiServerOrigin,
    _unregister: unregister,
};
