import appstore from './appstore.js';
import assert from 'node:assert';
import backupSites from './backupsites.js';
import backups from './backups.js';
import backuptask from './backuptask.js';
import BoxError from './boxerror.js';
import dashboard from './dashboard.js';
import constants from './constants.js';
import logger from './logger.js';
import dns from './dns.js';
import domains from './domains.js';
import eventlog from './eventlog.js';
import fs from 'node:fs';
import mail from './mail.js';
import mailServer from './mailserver.js';
import network from './network.js';
import oidcClients from './oidcclients.js';
import openssl from './openssl.js';
import platform from './platform.js';
import reverseProxy from './reverseproxy.js';
import safe from 'safetydance';
import semver from 'semver';
import paths from './paths.js';
import system from './system.js';
import tasks from './tasks.js';
import users from './users.js';
import tld from 'tldjs';
import tokens from './tokens.js';

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


// we cannot use tasks since the tasks table gets overwritten when db is imported
const gStatus = {
    setup: {
        active: false,
        message: '',
        errorMessage: null,
        startTime: null
    },
    restore: {
        active: false,
        message: '',
        errorMessage: null,
        startTime: null
    },
    activated: false,
    adminFqdn: null,
    provider: null,
    version: constants.VERSION // for reloading dashboard
};

function setProgress(task, message) {
    log(`setProgress: ${task} - ${message}`);
    gStatus[task].message = message;
}

async function ensureDhparams() {
    if (fs.existsSync(paths.DHPARAMS_FILE)) return;
    log('ensureDhparams: generating dhparams');
    const dhparams = await openssl.generateDhparam();
    if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}

async function unprovision() {
    // TODO: also cancel any existing configureWebadmin task
    await dashboard.clearLocation();
    await mail.clearDomains();
    await domains.clear();
    await backupSites.clear();
}

async function setupTask(domain, auditSource) {
    assert.strictEqual(typeof domain, 'string');
    assert.strictEqual(typeof auditSource, 'object');

    const location = { subdomain: constants.DASHBOARD_SUBDOMAIN, domain };
    try {
        log(`setupTask: subdomain ${location.subdomain} and domain ${location.domain}`);
        await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('setup', progress.message));
        await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message));
        await reverseProxy.ensureCertificate(location, {}, auditSource);
        await ensureDhparams();
        await dashboard.setupLocation(constants.DASHBOARD_SUBDOMAIN, domain, auditSource);
        await backupSites.addDefault(auditSource);
        setProgress('setup', 'Done'),
        await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
    } catch (error) {
        log('setupTask: error. %o', error);
        gStatus.setup.errorMessage = error.message;
    }

    gStatus.setup.active = false;
}

async function setup(domainConfig, ipv4Config, ipv6Config, auditSource) {
    assert.strictEqual(typeof domainConfig, 'object');
    assert.strictEqual(typeof ipv4Config, 'object');
    assert.strictEqual(typeof ipv6Config, 'object');
    assert.strictEqual(typeof auditSource, 'object');

    if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');

    gStatus.setup = { active: true, errorMessage: '', message: 'Adding domain', startTime: (new Date()).toISOString() };

    try {
        const activated = await users.isActivated();
        if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated');

        await unprovision();

        const domain = domainConfig.domain.toLowerCase();
        const zoneName = domainConfig.zoneName ? domainConfig.zoneName : (tld.getDomain(domain) || domain);

        log(`setup: domain ${domain} and zone ${zoneName}`);

        const data = {
            zoneName: zoneName,
            provider: domainConfig.provider,
            config: domainConfig.config,
            fallbackCertificate: domainConfig.fallbackCertificate || null,
            tlsConfig: domainConfig.tlsConfig || { provider: 'letsencrypt-prod' },
            dkimSelector: 'cloudron'
        };

        await mailServer.setLocation(constants.DASHBOARD_SUBDOMAIN, domain); // default mail location. do this before we add the domain for upserting mail DNS
        await domains.add(domain, data, auditSource);
        await network.setIPv4Config(ipv4Config);
        await network.setIPv6Config(ipv6Config);

        safe(setupTask(domain, auditSource), { debug: log }); // now that args are validated run the task in the background
    } catch (error) {
        log('setup: error. %o', error);
        gStatus.setup.active = false;
        gStatus.setup.errorMessage = error.message;
        throw error;
    }
}

async function activate(username, password, email, displayName, ip, auditSource) {
    assert.strictEqual(typeof username, 'string');
    assert.strictEqual(typeof password, 'string');
    assert.strictEqual(typeof email, 'string');
    assert.strictEqual(typeof displayName, 'string');
    assert.strictEqual(typeof ip, 'string');
    assert.strictEqual(typeof auditSource, 'object');

    log(`activate: user: ${username} email:${email}`);

    await appstore.registerCloudron3();

    const [error, ownerId] = await safe(users.createOwner(email, username, password, displayName, auditSource));
    if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated');
    if (error) throw error;

    const token = { clientId: oidcClients.ID_WEBADMIN, identifier: ownerId, allowedIpRanges: '', expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
    const result = await tokens.add(token);

    await eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {});

    safe(platform.onActivated({ skipDnsSetup: false }), { debug: log }); // background

    return {
        userId: ownerId,
        token: result.accessToken,
        expires: result.expires
    };
}

async function restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource) {
    assert.strictEqual(typeof backupSite, 'object');
    assert.strictEqual(typeof remotePath, 'string');
    assert.strictEqual(typeof ipv4Config, 'object');
    assert.strictEqual(typeof ipv6Config, 'object');
    assert.strictEqual(typeof options, 'object'); // { skipDnsSetup }
    assert.strictEqual(typeof auditSource, 'object');

    try {
        setProgress('restore', 'Preparing backup site');
        await backupSites.storageApi(backupSite).setup(backupSite.config);

        setProgress('restore', 'Downloading box backup');
        await backuptask.restore(backupSite, remotePath, (progress) => setProgress('restore', progress.message));

        setProgress('restore', 'Downloading mail backup');
        const mailBackups = await backups.listByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1);
        if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found');
        const mailRemotePath = mailBackups[0].remotePath;
        await backuptask.downloadMail(backupSite, mailRemotePath, (progress) => setProgress('restore', progress.message));

        await ensureDhparams();
        await network.setIPv4Config(ipv4Config);
        await network.setIPv6Config(ipv6Config);
        await network.applyBlocklist();
        await reverseProxy.reprovision();

        await backupSites.reinitAll();

        // when creating a sql dump during a full backup, the task has not completed yet. we mark it as completed here for the Sites UI to not indicate a crash
        // siteId can be missing when restoring manually instead of via the backup config
        const backupTasks = await tasks.list(1, 1, {
            type: options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX + options.siteId : null,
            prefix: !options.siteId ? tasks.TASK_FULL_BACKUP_PREFIX : null // guess that the latest full backup was the site that was used
        });
        await tasks.setCompleted(backupTasks[0].id, { error: null });

        const location = await dashboard.getLocation(); // load this fresh from after the backup.restore
        if (!options.skipDnsSetup) {
            await dns.registerLocations([location], { overwriteDns: true }, (progress) => setProgress('restore', progress.message));
            await dns.waitForLocations([location], (progress) => setProgress('setup', progress.message));
            await reverseProxy.ensureCertificate(location, {}, auditSource);
        }
        await dashboard.setupLocation(location.subdomain, location.domain, auditSource);

        await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { remotePath });

        await appstore.checkSubscription(); // never throws. worst case, user has to visit the Account view to refresh subscription info

        safe(platform.onActivated({ skipDnsSetup: options.skipDnsSetup }), { debug: log }); // background

        await backupSites.storageApi(backupSite).teardown(backupSite.config);
    } catch (error) {
        log('restoreTask: error. %o', error);
        gStatus.restore.errorMessage = error ? error.message : '';
    }
    gStatus.restore.active = false;
}

async function restore(backupConfig, remotePath, version, ipv4Config, ipv6Config, options, auditSource) {
    assert.strictEqual(typeof backupConfig, 'object'); // { format, config, provider, encryptionPassword }
    assert.strictEqual(typeof remotePath, 'string');
    assert.strictEqual(typeof version, 'string');
    assert.strictEqual(typeof ipv4Config, 'object');
    assert.strictEqual(typeof ipv6Config, 'object');
    assert.strictEqual(typeof options, 'object'); // { skipDnsSetup, siteId }
    assert.strictEqual(typeof auditSource, 'object');

    if (!semver.valid(version)) throw new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver');
    if (constants.VERSION !== version) throw new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`);

    if (gStatus.setup.active || gStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');

    gStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config', startTime: (new Date()).toISOString() };

    try {
        const activated = await users.isActivated();
        if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.');

        const backupSite = await backupSites.createPseudo({
            id: `cloudron-restore`,
            provider: backupConfig.provider,
            config: backupConfig.config,
            format: backupConfig.format,
            encryptionPassword: backupConfig.encryptionPassword ?? null,
            encryptedFilenames: !!backupConfig.encryptedFilenames
        });

        const ipv4Error = await network.testIPv4Config(ipv4Config);
        if (ipv4Error) throw ipv4Error;

        const ipv6Error = await network.testIPv6Config(ipv6Config);
        if (ipv6Error) throw ipv6Error;

        safe(restoreTask(backupSite, remotePath, ipv4Config, ipv6Config, options, auditSource), { debug: log }); // now that args are validated run the task in the background
    } catch (error) {
        log('restore: error. %o', error);
        gStatus.restore.active = false;
        gStatus.restore.errorMessage = error.message;
        throw error;
    }
}

async function getStatus() {
    const { fqdn:dashboardFqdn } = await dashboard.getLocation();
    gStatus.adminFqdn =  dashboardFqdn; // indicator for main.js if dnssetup was already done but not activated
    gStatus.activated = await users.isActivated(); // indicator for admin setup
    gStatus.provider = await system.getProvider(); // preselect dns provider and ami token
    return gStatus;
}

export default {
    setup,
    restore,
    activate,
    getStatus,
};
