import assert from 'node:assert';
import BoxError from './boxerror.js';
import constants from './constants.js';
import crypto from 'node:crypto';
import database from './database.js';
import Docker from 'dockerode';
import eventlog from './eventlog.js';
import paths from './paths.js';
import safe from 'safetydance';
import tld from 'tldjs';


const REGISTRY_FIELDS = [ 'id', 'provider', 'serverAddress', 'username', 'email', 'password' ].join(',');

function removePrivateFields(registryConfig) {
    assert.strictEqual(typeof registryConfig, 'object');

    delete registryConfig.password;
    return registryConfig;
}

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

async function get(id) {
    assert.strictEqual(typeof id, 'string');

    const result = await database.query(`SELECT ${REGISTRY_FIELDS} FROM dockerRegistries WHERE id=?`, [ id ]);
    if (result.length === 0) return null;
    return result[0];
}

async function list() {
    const result = await database.query(`SELECT ${REGISTRY_FIELDS} FROM dockerRegistries`);
    return result;
}

function validateServerAddress(serverAddress) {
    assert.strictEqual(typeof serverAddress, 'string');

    // workaround https://github.com/oncletom/tld.js/issues/73
    const tmp = serverAddress.replace('_', '-');
    if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name');
    if (tmp.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters');

    return null;
}

async function testRegistryConfig(config) {
    assert.strictEqual(typeof config, 'object');

    if (constants.TEST) return;

    const connection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH, timeout: 3000 });
    const [error] = await safe(connection.checkAuth(config)); // this returns a 500 even for auth errors
    if (error) throw new BoxError(BoxError.BAD_FIELD, `Invalid serverAddress: ${error.message}`);
}

async function add(registry, auditSource) {
    assert.strictEqual(typeof registry, 'object');
    assert(auditSource && typeof auditSource === 'object');

    const error = validateServerAddress(registry.serverAddress);
    if (error) throw error;

    await testRegistryConfig(registry);

    const id = `rc-${crypto.randomUUID()}`;
    await database.query('INSERT INTO dockerRegistries (id, provider, serverAddress, username, email, password) VALUES (?, ?, ?, ?, ?, ?)',
        [ id , registry.provider, registry.serverAddress, registry.username || null, registry.email || null, registry.password || null ]);

    await eventlog.add(eventlog.ACTION_REGISTRY_ADD, auditSource, { registry: removePrivateFields(registry) });

    return id;
}

async function update(oldConfig, newConfig, auditSource) {
    assert.strictEqual(typeof oldConfig, 'object');
    assert.strictEqual(typeof newConfig, 'object');
    assert(auditSource && typeof auditSource === 'object');

    const error = validateServerAddress(newConfig.serverAddress);
    if (error) throw error;

    injectPrivateFields(newConfig, oldConfig);

    await testRegistryConfig(newConfig);

    const args = [], fields = [];
    for (const k in newConfig) {
        fields.push(k + ' = ?');
        args.push(newConfig[k]);
    }
    args.push(oldConfig.id);

    const result = await database.query('UPDATE dockerRegistries SET ' + fields.join(', ') + ' WHERE id=?', args);
    if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Registry not found');

    await eventlog.add(eventlog.ACTION_REGISTRY_UPDATE, auditSource, { oldRegistry: removePrivateFields(oldConfig), newRegistry: removePrivateFields(newConfig) });
}

async function del(registry, auditSource) {
    assert.strictEqual(typeof registry, 'object');
    assert(auditSource && typeof auditSource === 'object');

    const result = await database.query(`DELETE FROM dockerRegistries WHERE id=?`, [ registry.id ]);
    if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Registry not found');

    await eventlog.add(eventlog.ACTION_REGISTRY_DEL, auditSource, { registry: removePrivateFields(registry) });
}

export default {
    removePrivateFields,

    list,
    add,
    get,
    del,
    update,
};
