import { Netmask } from 'netmask';
import { ReactElement, useCallback, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';

import NETWORK_TYPES from '../../lib/consts/NETWORK_TYPES';

import AnNetworkInputGroup from './AnNetworkInputGroup';
import buildObjectStateSetterCallback from '../../lib/buildObjectStateSetterCallback';
import Grid from '../Grid';
import IconButton from '../IconButton';
import InputWithRef from '../InputWithRef';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import {
  buildIpCsvTestBatch,
  buildNumberTestBatch,
} from '../../lib/test_input';

const INPUT_ID_PREFIX_AN_NETWORK_CONFIG = 'an-network-config-input';

const INPUT_CELL_ID_PREFIX_ANC = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-cell`;

const INPUT_ID_ANC_DNS = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-dns`;
const INPUT_ID_ANC_MTU = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-mtu`;
const INPUT_ID_ANC_NTP = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-ntp`;

const INPUT_LABEL_ANC_DNS = 'DNS';
const INPUT_LABEL_ANC_MTU = 'MTU';
const INPUT_LABEL_ANC_NTP = 'NTP';

const DEFAULT_DNS_CSV = '8.8.8.8,8.8.4.4';

const NETWORK_TYPE_ENTRIES = Object.entries(NETWORK_TYPES);

const MAP_TO_NETWORK_DEFAULTS: Record<string, { base: string; mask: string }> =
  {
    bcn: { base: '10.201.0.0', mask: '255.255.0.0' },
    mn: { base: '10.199.0.0', mask: '255.255.0.0' },
    sn: { base: '10.101.0.0', mask: '255.255.0.0' },
  };

const assertIfn = (type: string) => type === 'ifn';
const assertMn = (type: string) => type === 'mn';

const guessNetworkMinIp = ({
  entries,
  type,
}: {
  entries: [string, ManifestNetwork][];
  type: string;
}): { base?: string; mask?: string } => {
  const last = entries
    .filter(([, { networkType }]) => networkType === type)
    .sort(([, { networkNumber: a }], [, { networkNumber: b }]) =>
      a > b ? 1 : -1,
    )
    .pop();

  if (!last) {
    return MAP_TO_NETWORK_DEFAULTS[type] ?? {};
  }

  const [, { networkMinIp, networkSubnetMask }] = last;

  try {
    const block = new Netmask(`${networkMinIp}/${networkSubnetMask}`);
    const { base, mask } = block.next();

    return { base, mask };
  } catch (error) {
    return {};
  }
};

const AnNetworkConfigInputGroup = <
  M extends MapToInputTestID & {
    [K in
      | typeof INPUT_ID_ANC_DNS
      | typeof INPUT_ID_ANC_MTU
      | typeof INPUT_ID_ANC_NTP]: string;
  },
>({
  formUtils,
  networkListEntries,
  previous: {
    dnsCsv: previousDnsCsv = DEFAULT_DNS_CSV,
    mtu: previousMtu,
    ntpCsv: previousNtpCsv,
  } = {},
  setNetworkList,
}: AnNetworkConfigInputGroupProps<M>): ReactElement => {
  const {
    buildFinishInputTestBatchFunction,
    buildInputFirstRenderFunction,
    setMessage,
    setMessageRe,
  } = formUtils;

  const getNetworkNumber = useCallback(
    (
      type: string,
      {
        input = networkListEntries,
        end = networkListEntries.length,
      }: {
        input?: Array<[string, ManifestNetwork]>;
        end?: number;
      } = {},
    ) => {
      const limit = end - 1;

      let netNum = 0;

      input.every(([, { networkType }], networkIndex) => {
        if (networkType === type) {
          netNum += 1;
        }

        return networkIndex < limit;
      });

      return netNum;
    },
    [networkListEntries],
  );

  const networkTypeOptions = useMemo<SelectItem[]>(
    () =>
      NETWORK_TYPE_ENTRIES.map(([key, value]) => ({
        displayValue: value,
        value: key,
      })),
    [],
  );

  const buildNetwork = useCallback(
    ({
      networkMinIp = '',
      networkSubnetMask = '',
      networkType = networkListEntries.some(([, { networkType: nt }]) =>
        assertMn(nt),
      )
        ? 'ifn'
        : 'mn',
      // Params that depend on others.
      networkGateway = assertIfn(networkType) ? '' : undefined,
      networkNumber = getNetworkNumber(networkType) + 1,
    }: Partial<ManifestNetwork> = {}): {
      network: ManifestNetwork;
      networkId: string;
    } => {
      const { base = networkMinIp, mask = networkSubnetMask } =
        guessNetworkMinIp({
          entries: networkListEntries,
          type: networkType,
        });

      return {
        network: {
          networkGateway,
          networkMinIp: base,
          networkNumber,
          networkSubnetMask: mask,
          networkType,
        },
        networkId: uuidv4(),
      };
    },
    [getNetworkNumber, networkListEntries],
  );

  const setNetwork = useCallback(
    (key: string, value?: ManifestNetwork) =>
      setNetworkList(buildObjectStateSetterCallback(key, value)),
    [setNetworkList],
  );

  const setNetworkProp = useCallback(
    <P extends keyof ManifestNetwork>(
      nkey: string,
      pkey: P,
      value: ManifestNetwork[P],
    ) =>
      setNetworkList((previous) => {
        const nyu = { ...previous };

        const { [nkey]: nw } = nyu;

        if (nw) {
          nw[pkey] = value;
        }

        return nyu;
      }),
    [setNetworkList],
  );

  const handleNetworkTypeChange = useCallback<AnNetworkTypeChangeEventHandler>(
    (
      { networkId: targetId, networkType: previousType },
      { target: { value } },
    ) => {
      const newType = String(value);

      let isIdMatch = false;
      let newTypeNumber = 0;

      const newList = networkListEntries.reduce<ManifestNetworkList>(
        (previous, [networkId, networkValue]) => {
          const {
            networkNumber: initnn,
            networkType: initnt,
            networkMinIp: initbase,
            networkSubnetMask: initmask,
            ...restNetworkValue
          } = networkValue;

          let networkNumber = initnn;
          let networkType = initnt;

          if (networkId === targetId) {
            isIdMatch = true;
            networkType = newType;
            setMessageRe(RegExp(networkId));
          }

          const isTypeMatch = networkType === newType;

          if (isTypeMatch) {
            newTypeNumber += 1;
          }

          if (isIdMatch) {
            if (isTypeMatch) {
              networkNumber = newTypeNumber;
            } else if (networkType === previousType) {
              networkNumber -= 1;
            }

            const {
              base: networkMinIp = initbase,
              mask: networkSubnetMask = initmask,
            } = guessNetworkMinIp({
              entries: networkListEntries,
              type: networkType,
            });

            previous[networkId] = {
              ...restNetworkValue,
              networkMinIp,
              networkSubnetMask,
              networkNumber,
              networkType,
            };
          } else {
            previous[networkId] = networkValue;
          }

          return previous;
        },
        {},
      );

      setNetworkList(newList);
    },
    [networkListEntries, setMessageRe, setNetworkList],
  );

  const handleNetworkRemove = useCallback<AnNetworkCloseEventHandler>(
    ({ networkId: rmId, networkType: rmType }) => {
      let isIdMatch = false;
      let networkNumber = 0;

      const newList = networkListEntries.reduce<ManifestNetworkList>(
        (previous, [networkId, networkValue]) => {
          if (networkId === rmId) {
            isIdMatch = true;
          } else {
            const { networkType } = networkValue;

            if (networkType === rmType) {
              networkNumber += 1;
            }

            previous[networkId] = isIdMatch
              ? { ...networkValue, networkNumber }
              : networkValue;
          }

          return previous;
        },
        {},
      );

      setNetworkList(newList);
    },
    [networkListEntries, setNetworkList],
  );

  const networksGridLayout = useMemo<GridLayout>(() => {
    let result: GridLayout = {};

    result = networkListEntries.reduce<GridLayout>(
      (
        previous,
        [
          networkId,
          {
            networkGateway,
            networkMinIp,
            networkNumber,
            networkSubnetMask,
            networkType,
          },
        ],
      ) => {
        const cellId = `${INPUT_CELL_ID_PREFIX_ANC}-${networkId}`;

        const isFirstNetwork = networkNumber === 1;
        const isIfn = assertIfn(networkType);
        const isMn = assertMn(networkType);
        const isOptional = isMn || !isFirstNetwork;

        previous[cellId] = {
          children: (
            <AnNetworkInputGroup
              formUtils={formUtils}
              networkId={networkId}
              networkNumber={networkNumber}
              networkType={networkType}
              networkTypeOptions={networkTypeOptions}
              onClose={handleNetworkRemove}
              onNetworkMinIpChange={(
                { networkId: nid },
                { target: { value } },
              ) => setNetworkProp(nid, 'networkMinIp', value)}
              onNetworkSubnetMaskChange={(
                { networkId: nid },
                { target: { value } },
              ) => setNetworkProp(nid, 'networkSubnetMask', value)}
              onNetworkTypeChange={handleNetworkTypeChange}
              previous={{
                gateway: networkGateway,
                minIp: networkMinIp,
                subnetMask: networkSubnetMask,
              }}
              readonlyNetworkName={!isOptional}
              showCloseButton={isOptional}
              showGateway={isIfn}
            />
          ),
          md: 3,
          sm: 2,
        };

        return previous;
      },
      result,
    );

    return result;
  }, [
    networkListEntries,
    formUtils,
    networkTypeOptions,
    handleNetworkRemove,
    handleNetworkTypeChange,
    setNetworkProp,
  ]);

  return (
    <Grid
      columns={{ xs: 1, sm: 2, md: 3 }}
      layout={{
        ...networksGridLayout,
        'an-network-config-cell-add-network': {
          children: (
            <IconButton
              mapPreset="add"
              onClick={() => {
                const { network: newNet, networkId: newNetId } = buildNetwork();

                setNetwork(newNetId, newNet);
              }}
            />
          ),
          display: 'flex',
          justifyContent: 'center',
          md: 3,
          sm: 2,
        },
        'an-network-config-input-cell-dns': {
          children: (
            <InputWithRef
              input={
                <OutlinedInputWithLabel
                  id={INPUT_ID_ANC_DNS}
                  label={INPUT_LABEL_ANC_DNS}
                  value={previousDnsCsv}
                />
              }
              inputTestBatch={buildIpCsvTestBatch(
                INPUT_LABEL_ANC_DNS,
                () => {
                  setMessage(INPUT_ID_ANC_DNS);
                },
                {
                  onFinishBatch:
                    buildFinishInputTestBatchFunction(INPUT_ID_ANC_DNS),
                },
                (message) => {
                  setMessage(INPUT_ID_ANC_DNS, { children: message });
                },
              )}
              onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_DNS)}
              required
            />
          ),
        },
        'an-network-config-input-cell-ntp': {
          children: (
            <InputWithRef
              input={
                <OutlinedInputWithLabel
                  id={INPUT_ID_ANC_NTP}
                  label={INPUT_LABEL_ANC_NTP}
                  value={previousNtpCsv}
                />
              }
              inputTestBatch={buildIpCsvTestBatch(
                INPUT_LABEL_ANC_NTP,
                () => {
                  setMessage(INPUT_ID_ANC_NTP);
                },
                {
                  onFinishBatch:
                    buildFinishInputTestBatchFunction(INPUT_ID_ANC_NTP),
                },
                (message) => {
                  setMessage(INPUT_ID_ANC_NTP, { children: message });
                },
              )}
              onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_NTP)}
            />
          ),
        },
        'an-network-config-input-cell-mtu': {
          children: (
            <InputWithRef
              input={
                <OutlinedInputWithLabel
                  id={INPUT_ID_ANC_MTU}
                  inputProps={{ placeholder: '1500' }}
                  label={INPUT_LABEL_ANC_MTU}
                  value={previousMtu}
                />
              }
              inputTestBatch={buildNumberTestBatch(
                INPUT_LABEL_ANC_MTU,
                () => {
                  setMessage(INPUT_ID_ANC_MTU);
                },
                {
                  onFinishBatch:
                    buildFinishInputTestBatchFunction(INPUT_ID_ANC_MTU),
                },
                (message) => {
                  setMessage(INPUT_ID_ANC_MTU, { children: message });
                },
              )}
              onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_MTU)}
              valueType="number"
            />
          ),
        },
      }}
      spacing="1em"
    />
  );
};

export { INPUT_ID_ANC_DNS, INPUT_ID_ANC_MTU, INPUT_ID_ANC_NTP };

export default AnNetworkConfigInputGroup;
