import assert from 'node:assert';
import BoxError from './boxerror.js';
import constants from './constants.js';
import cron from './cron.js';
import fs from 'node:fs';
import ipaddr from './ipaddr.js';
import path from 'node:path';
import paths from './paths.js';
import safe from 'safetydance';
import settings from './settings.js';
import shellModule from './shell.js';
import noopProvider from './network/noop.js';
import fixedProvider from './network/fixed.js';
import networkInterfaceProvider from './network/network-interface.js';
import genericProvider from './network/generic.js';

const shell = shellModule('network');


const SET_BLOCKLIST_CMD = path.join(import.meta.dirname, 'scripts/setblocklist.sh');

let gDefaultIface = null; // cache

const NETWORK_PROVIDERS = {
    'noop': noopProvider,
    'fixed': fixedProvider,
    'network-interface': networkInterfaceProvider
};

function api(provider) {
    assert.strictEqual(typeof provider, 'string');

    return NETWORK_PROVIDERS[provider] || genericProvider;
}

function hasIPv6() {
    const IPV6_PROC_FILE = '/proc/net/if_inet6';
    // on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
    return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}

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

    return await api(config.provider).testIPv4Config(config);
}

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

    return await api(config.provider).testIPv6Config(config);
}

async function getBlocklist() {
    const value = await settings.getBlob(settings.FIREWALL_BLOCKLIST_KEY);
    return value ? value.toString('utf8') : '';
}

async function applyBlocklist() {
    const blocklist = await getBlocklist();

    if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message);

    const [error] = await safe(shell.sudo([ SET_BLOCKLIST_CMD ], {}));
    if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`);
}

async function setBlocklist(blocklist, auditSource) {
    assert.strictEqual(typeof blocklist, 'string');
    assert.strictEqual(typeof auditSource, 'object');

    let count = 0;
    for (const line of blocklist.split('\n')) {
        if (!line || line.startsWith('#')) continue;
        const rangeOrIP = line.trim();
        if (!ipaddr.isValid(rangeOrIP) && !ipaddr.isValidCIDR(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `'${rangeOrIP}' is not a valid IP or range`);

        if (rangeOrIP.indexOf('/') === -1) {
            if (ipaddr.isEqual(rangeOrIP, auditSource.ip)) throw new BoxError(BoxError.BAD_FIELD, `IP '${rangeOrIP}' is the client IP. Cannot block yourself`);
        } else {
            if (ipaddr.includes(rangeOrIP, auditSource.ip)) throw new BoxError(BoxError.BAD_FIELD, `range '${rangeOrIP}' includes client IP. Cannot block yourself`);
        }

        // this won't work in cases where it's a bigger subnet
        if (rangeOrIP.startsWith('172.18.') || rangeOrIP.toLowerCase().startsWith('fd00:c107:d509:')) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes internal docker network. This cannot be blocked`);

        ++count;
    }

    if (count >= 262144) throw new BoxError(BoxError.CONFLICT, 'Blocklist is too large. Max 262144 entries are allowed'); // see the cloudron-firewall.sh
    if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');

    // store in blob since the value field is TEXT and has 16kb size limit
    await settings.setBlob(settings.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist));

    await applyBlocklist();
}

async function getDynamicDns() {
    const enabled = await settings.get(settings.DYNAMIC_DNS_KEY);
    return enabled ? !!enabled : false; // db holds string values only
}

async function setDynamicDns(enabled) {
    assert.strictEqual(typeof enabled, 'boolean');

    await settings.set(settings.DYNAMIC_DNS_KEY, enabled ? 'enabled' : ''); // db holds string values only
    await cron.handleDynamicDnsChanged(enabled);
}

async function getIPv4Config() {
    const value = await settings.getJson(settings.IPV4_CONFIG_KEY);
    return value || { provider: 'generic' };
}

async function setIPv4Config(ipv4Config) {
    assert.strictEqual(typeof ipv4Config, 'object');

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

    const error = await testIPv4Config(ipv4Config);
    if (error) throw error;

    await settings.setJson(settings.IPV4_CONFIG_KEY, ipv4Config);
}

async function getIPv6Config() {
    const value = await settings.getJson(settings.IPV6_CONFIG_KEY);
    return value || { provider: 'noop' };
}

async function setIPv6Config(ipv6Config) {
    assert.strictEqual(typeof ipv6Config, 'object');

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

    const error = await testIPv6Config(ipv6Config);
    if (error) throw error;

    await settings.setJson(settings.IPV6_CONFIG_KEY, ipv6Config);
}

async function getIPv4() {
    const config = await getIPv4Config();

    return await api(config.provider).getIPv4(config);
}

async function getIPv6() {
    const config = await getIPv6Config();
    return await api(config.provider).getIPv6(config);
}

async function detectIP() {

    const [error4, ipv4] = await safe(genericProvider.getIPv4({}));
    const [error6, ipv6] = await safe(genericProvider.getIPv6({}));

    return {
        ipv4: error4 ? null : ipv4,
        ipv6: error6 ? null : ipv6
    };
}

async function getDefaultInterface() {
    if (gDefaultIface) return gDefaultIface;

    const contents4 = await fs.promises.readFile('/proc/net/route', { encoding: 'utf8' });
    const lines4 = contents4.trim().split('\n').slice(1); // skip header

    for (const line of lines4) {
        const cols = line.trim().split(/\s+/); // Iface, dest, gw, flags, refcount, use, metric, mask, mtu, window, irtt
        if (cols[1] === '00000000') { // && cols[7] === '00000000'
            gDefaultIface = cols[0];
            return gDefaultIface;
        }
    }

    const contents6 = await fs.promises.readFile('/proc/net/ipv6_route', { encoding: 'utf8' });
    const lines6 = contents6.trim().split('\n'); // no header!

    for (const line of lines6) {
        const cols = line.trim().split(/\s+/); // dest, dest_prefix_len, src, src_prefix_len, next_hop, metric, refcount, use, flags, iface
        if (cols[0] === '00000000000000000000000000000000' && cols[1] === '00') {
            gDefaultIface = cols.at(-1);
            return gDefaultIface;
        }
    }

    throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not detect default interface');
}

export default {
    testIPv4Config,
    testIPv6Config,

    applyBlocklist,
    getBlocklist,
    setBlocklist,

    getDynamicDns,
    setDynamicDns,

    getIPv4Config,
    setIPv4Config,

    getIPv6Config,
    setIPv6Config,

    getIPv4,
    hasIPv6,
    getIPv6,
    detectIP,

    getDefaultInterface
};
