import assert from 'node:assert';
import BoxError from '../boxerror.js';
import constants from '../constants.js';
import logger from '../logger.js';
import dig from '../dig.js';
import dns from '../dns.js';
import ovhClient from 'ovh';
import safe from 'safetydance';
import waitForDns from './waitfordns.js';

const { log } = logger('dns/ovh');


function formatError(error) {
    return `OVH DNS error ${error.error} ${error.message}`; // error.error is the status
}

function removePrivateFields(domainObject) {
    delete domainObject.config.appSecret;
    return domainObject;
}

function injectPrivateFields(newConfig, currentConfig) {
    if (!Object.hasOwn(newConfig, 'appSecret')) newConfig.appSecret = currentConfig.appSecret;
}

function createClient(domainConfig) {
    return ovhClient({
        endpoint: domainConfig.endpoint,
        appKey: domainConfig.appKey,
        appSecret: domainConfig.appSecret,
        consumerKey: domainConfig.consumerKey,
    });
}

async function getDnsRecordIds(domainConfig, zoneName, name, type) {
    assert.strictEqual(typeof domainConfig, 'object');
    assert.strictEqual(typeof zoneName, 'string');
    assert.strictEqual(typeof name, 'string');
    assert.strictEqual(typeof type, 'string');

    log(`get: ${name} in zone ${zoneName} of type ${type}`);

    const client = createClient(domainConfig);
    const [error, data] = await safe(client.requestPromised('GET', `/domain/zone/${zoneName}/record`, { fieldType: type, subDomain: name }));
    if (error) {
        if (error.error === 401 || error.error === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
        throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
    }
    return data || []; // array of numbers. data is undefined when no entries
}

async function refreshZone(domainConfig, zoneName) {
    assert.strictEqual(typeof domainConfig, 'object');
    assert.strictEqual(typeof zoneName, 'string');

    log(`refresh: zone ${zoneName}`);

    const client = createClient(domainConfig);
    const [error] = await safe(client.requestPromised('POST', `/domain/zone/${zoneName}/refresh`));
    if (error) {
        if (error.error === 401 || error.error === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
        throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
    }
}

async function upsert(domainObject, location, type, values) {
    assert.strictEqual(typeof domainObject, 'object');
    assert.strictEqual(typeof location, 'string');
    assert.strictEqual(typeof type, 'string');
    assert(Array.isArray(values));

    const domainConfig = domainObject.config,
        zoneName = domainObject.zoneName,
        name = dns.getName(domainObject, location, type) || '';

    log(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);

    const recordIds = await getDnsRecordIds(domainConfig, zoneName, name, type);

    const client = createClient(domainConfig);

    // used to track available records to update instead of create
    let i = 0;

    for (const value of values) {
        const data = {
            subDomain: name,
            target: value,
            ttl: 60
        };

        let error;
        if (i >= recordIds.length) {
            data.fieldType = type;
            [error] = await safe(client.requestPromised('POST', `/domain/zone/${zoneName}/record`, data));
        } else {
            [error] = await safe(client.requestPromised('PUT', `/domain/zone/${zoneName}/record/${recordIds[i]}`, data));
            ++i;
        }

        if (error) {
            if (error.error === 401 || error.error === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
            throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
        }
    }

    for (let j = values.length + 1; j < recordIds.length; j++) {
        const [error] = await safe(client.requestPromised('DELETE', `/domain/zone/${zoneName}/record/${recordIds[j]}`));
        if (error) {
            if (error.error === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
            if (error.error === 404) continue; // not found
            throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
        }
    }

    await refreshZone(domainConfig, zoneName);
    log('upsert: completed');
}

async function get(domainObject, location, type) {
    assert.strictEqual(typeof domainObject, 'object');
    assert.strictEqual(typeof location, 'string');
    assert.strictEqual(typeof type, 'string');

    const domainConfig = domainObject.config,
        zoneName = domainObject.zoneName,
        name = dns.getName(domainObject, location, type) || '';

    const recordIds = await getDnsRecordIds(domainConfig, zoneName, name, type);
    const client = createClient(domainConfig);
    const result = [];
    for (const id of recordIds) {
        const [error, data] = await safe(client.requestPromised('GET', `/domain/zone/${zoneName}/record/${id}`));
        if (error) {
            if (error.error === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
            throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
        }
        result.push(data.target);
    }

    return result;
}

async function del(domainObject, location, type, values) {
    assert.strictEqual(typeof domainObject, 'object');
    assert.strictEqual(typeof location, 'string');
    assert.strictEqual(typeof type, 'string');
    assert(Array.isArray(values));

    const domainConfig = domainObject.config,
        zoneName = domainObject.zoneName,
        name = dns.getName(domainObject, location, type) || '';

    log(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);

    const recordIds = await getDnsRecordIds(domainConfig, zoneName, name, type);

    const client = createClient(domainConfig);
    for (const id of recordIds) {
        const [error] = await safe(client.requestPromised('DELETE', `/domain/zone/${zoneName}/record/${id}`));
        if (error) {
            if (error.error === 401) throw new BoxError(BoxError.ACCESS_DENIED, formatError(error));
            if (error.error === 404) continue; // not found
            throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(error));
        }
    }

    await refreshZone(domainConfig, zoneName);
}

async function wait(domainObject, subdomain, type, value, options) {
    assert.strictEqual(typeof domainObject, 'object');
    assert.strictEqual(typeof subdomain, 'string');
    assert.strictEqual(typeof type, 'string');
    assert.strictEqual(typeof value, 'string');
    assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }

    const fqdn = dns.fqdn(subdomain, domainObject.domain);

    await waitForDns(fqdn, domainObject.zoneName, type, value, options);
}

async function verifyDomainConfig(domainObject) {
    assert.strictEqual(typeof domainObject, 'object');

    const domainConfig = domainObject.config,
        zoneName = domainObject.zoneName;

    if (!domainConfig.endpoint || typeof domainConfig.endpoint !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'endpoint must be a non-empty string');
    if (!domainConfig.appKey || typeof domainConfig.appKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'appKey must be a non-empty string');
    if (!domainConfig.appSecret || typeof domainConfig.appSecret !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'appSecret must be a non-empty string');
    if (!domainConfig.consumerKey || typeof domainConfig.consumerKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'consumerKey must be a non-empty string');
    if ('customNameservers' in domainConfig && typeof domainConfig.customNameservers !== 'boolean') throw new BoxError(BoxError.BAD_FIELD, 'customNameservers must be a boolean');

    const ip = '127.0.0.1';

    const credentials = {
        endpoint: domainConfig.endpoint, // https://github.com/ovh/node-ovh#2-authorize-your-application-to-access-to-a-customer-account
        appKey: domainConfig.appKey,
        appSecret: domainConfig.appSecret,
        consumerKey: domainConfig.consumerKey,
        customNameservers: !!domainConfig.customNameservers
    };

    if (constants.TEST) return credentials; // this shouldn't be here

    const [error, nameservers] = await safe(dig.resolve(zoneName, 'NS', { timeout: 5000 }));
    if (error && error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain');
    if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers');

    //  ovh.net, ovh.ca or anycast.me
    if (!nameservers.every(function (n) { return n.toLowerCase().search(/ovh|kimsufi|anycast/) !== -1; })) {
        log('verifyDomainConfig: %j does not contain OVH NS', nameservers);
        if (!domainConfig.customNameservers) throw new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to OVH');
    }

    const location = 'cloudrontestdns';

    await upsert(domainObject, location, 'A', [ ip ]);
    log('verifyDomainConfig: Test A record added');

    await del(domainObject, location, 'A', [ ip ]);
    log('verifyDomainConfig: Test A record removed again');

    return credentials;
}

export default {
    removePrivateFields,
    injectPrivateFields,
    upsert,
    get,
    del,
    wait,
    verifyDomainConfig
};
