import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';

import { removeNewlines, iptablesTemplate } from '#server/utils/template';
import { exec } from '#server/utils/cmd';
import { WG_ENV } from '#server/utils/config';
import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types';
import type { UserConfigType } from '#db/repositories/userConfig/types';
import type { HooksType } from '#db/repositories/hooks/types';

type Options = {
  enableIpv6?: boolean;
};

// needed to support cli
const wgExecutable =
  typeof WG_ENV !== 'undefined' ? WG_ENV.WG_EXECUTABLE : 'dev';

export const wg = {
  generateServerPeer: (
    client: Omit<ClientType, 'createdAt' | 'updatedAt'>,
    options: Options = {}
  ) => {
    const { enableIpv6 = true } = options;

    const allowedIps = [
      `${client.ipv4Address}/32`,
      ...(enableIpv6 ? [`${client.ipv6Address}/128`] : []),
      ...(client.serverAllowedIps ?? []),
    ];

    const extraLines = [];
    if (client.serverEndpoint) {
      extraLines.push(`Endpoint = ${client.serverEndpoint}`);
    }

    return `# Client: ${client.name} (${client.id})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join('\n')}` : ''}`;
  },

  generateServerInterface: (
    wgInterface: InterfaceType,
    hooks: HooksType,
    options: Options = {}
  ) => {
    const { enableIpv6 = true } = options;

    const cidr4 = parseCidr(wgInterface.ipv4Cidr);
    const cidr6 = parseCidr(wgInterface.ipv6Cidr);
    const ipv4Addr = stringifyIp({ number: cidr4.start + 1n, version: 4 });
    const ipv6Addr = stringifyIp({ number: cidr6.start + 1n, version: 6 });

    const address =
      `${ipv4Addr}/${cidr4.prefix}` +
      (enableIpv6 ? `, ${ipv6Addr}/${cidr6.prefix}` : '');

    let awgLines: string[] = [];

    if (wgExecutable === 'awg') {
      const parameters = {
        Jc: wgInterface.jC,
        Jmin: wgInterface.jMin,
        Jmax: wgInterface.jMax,
        S1: wgInterface.s1,
        S2: wgInterface.s2,
        S3: wgInterface.s3,
        S4: wgInterface.s4,
        H1: wgInterface.h1,
        H2: wgInterface.h2,
        H3: wgInterface.h3,
        H4: wgInterface.h4,
        I1: wgInterface.i1,
        I2: wgInterface.i2,
        I3: wgInterface.i3,
        I4: wgInterface.i4,
        I5: wgInterface.i5,
      } as const;

      awgLines = Object.entries(parameters)
        .filter(([_, value]) => !!value)
        .map(([key, value]) => `${key} = ${value}`);
    }

    const extraLines = [...awgLines].filter((v) => v !== null);

    return `# Note: Do not edit this file directly.
# Your changes will be overwritten!

# Server
[Interface]
PrivateKey = ${wgInterface.privateKey}
Address = ${address}
ListenPort = ${wgInterface.port}
MTU = ${wgInterface.mtu}
${extraLines.length ? `${extraLines.join('\n')}\n` : ''}
PreUp = ${iptablesTemplate(hooks.preUp, wgInterface)}
PostUp = ${iptablesTemplate(hooks.postUp, wgInterface)}
PreDown = ${iptablesTemplate(hooks.preDown, wgInterface)}
PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
  },

  generateClientConfig: (
    wgInterface: InterfaceType,
    userConfig: UserConfigType,
    client: ClientType,
    options: Options = {}
  ) => {
    const { enableIpv6 = true } = options;

    const address =
      `${client.ipv4Address}/32` +
      (enableIpv6 ? `, ${client.ipv6Address}/128` : '');

    const hookLines = [
      client.preUp ? `PreUp = ${removeNewlines(client.preUp)}` : null,
      client.postUp ? `PostUp = ${removeNewlines(client.postUp)}` : null,
      client.preDown ? `PreDown = ${removeNewlines(client.preDown)}` : null,
      client.postDown ? `PostDown = ${removeNewlines(client.postDown)}` : null,
    ];

    const dnsServers = client.dns ?? userConfig.defaultDns;
    const dnsLine =
      dnsServers.length > 0 ? `DNS = ${dnsServers.join(', ')}` : null;

    let awgLines: string[] = [];

    if (wgExecutable === 'awg') {
      const parameters = {
        Jc: client.jC,
        Jmin: client.jMin,
        Jmax: client.jMax,
        S1: wgInterface.s1,
        S2: wgInterface.s2,
        S3: wgInterface.s3,
        S4: wgInterface.s4,
        H1: wgInterface.h1,
        H2: wgInterface.h2,
        H3: wgInterface.h3,
        H4: wgInterface.h4,
        I1: client.i1,
        I2: client.i2,
        I3: client.i3,
        I4: client.i4,
        I5: client.i5,
      } as const;

      awgLines = Object.entries(parameters)
        .filter(([_, value]) => !!value)
        .map(([key, value]) => `${key} = ${value}`);
    }

    const extraLines = [dnsLine, ...hookLines, ...awgLines].filter(
      (v) => v !== null
    );

    return `[Interface]
PrivateKey = ${client.privateKey}
Address = ${address}
MTU = ${client.mtu}
${extraLines.length ? `${extraLines.join('\n')}\n` : ''}
[Peer]
PublicKey = ${wgInterface.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${(client.allowedIps ?? userConfig.defaultAllowedIps).join(', ')}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${userConfig.host}:${userConfig.port}`;
  },

  generatePrivateKey: () => {
    return exec(`${wgExecutable} genkey`);
  },

  getPublicKey: (privateKey: string) => {
    return exec(`echo ${privateKey} | ${wgExecutable} pubkey`, {
      log: `echo ***hidden*** | ${wgExecutable} pubkey`,
    });
  },

  generatePreSharedKey: () => {
    return exec(`${wgExecutable} genpsk`);
  },

  up: (infName: string) => {
    return exec(`${wgExecutable}-quick up ${infName}`);
  },

  down: (infName: string) => {
    return exec(`${wgExecutable}-quick down ${infName}`);
  },

  restart: (infName: string) => {
    return exec(
      `${wgExecutable}-quick down ${infName}; ${wgExecutable}-quick up ${infName}`
    );
  },

  sync: (infName: string) => {
    return exec(
      `${wgExecutable} syncconf ${infName} <(${wgExecutable}-quick strip ${infName})`
    );
  },

  dump: async (infName: string) => {
    const rawDump = await exec(`${wgExecutable} show ${infName} dump`, {
      log: false,
    });

    type wgDumpLine = [
      string,
      string,
      string,
      string,
      string,
      string,
      string,
      string,
    ];

    return rawDump
      .trim()
      .split('\n')
      .slice(1)
      .map((line) => {
        const splitLines = line.split('\t');
        const [
          publicKey,
          preSharedKey,
          endpoint,
          allowedIps,
          latestHandshakeAt,
          transferRx,
          transferTx,
          persistentKeepalive,
        ] = splitLines as wgDumpLine;

        return {
          publicKey,
          preSharedKey,
          endpoint: endpoint === '(none)' ? null : endpoint,
          allowedIps,
          latestHandshakeAt:
            latestHandshakeAt === '0'
              ? null
              : new Date(Number.parseInt(`${latestHandshakeAt}000`)),
          transferRx: Number.parseInt(transferRx),
          transferTx: Number.parseInt(transferTx),
          persistentKeepalive: persistentKeepalive,
        };
      });
  },
};
