// Copyright 2021 Northern.tech AS
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//        http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link as RouterLink } from 'react-router';

import { Block as BlockIcon, CheckCircle as CheckCircleIcon, Error as ErrorIcon, Refresh as RefreshIcon, SaveAlt as SaveAltIcon } from '@mui/icons-material';
import { Button, Checkbox, FormControlLabel, FormHelperText, Typography } from '@mui/material';

import Confirm, { EditButton } from '@northern.tech/common-ui/Confirm';
import { ContentSection } from '@northern.tech/common-ui/ContentSection';
import EnterpriseNotification from '@northern.tech/common-ui/EnterpriseNotification';
import { InfoHintContainer } from '@northern.tech/common-ui/InfoHint';
import Loader from '@northern.tech/common-ui/Loader';
import Time from '@northern.tech/common-ui/Time';
import { TwoColumnData } from '@northern.tech/common-ui/TwoColumnData';
import LogDialog from '@northern.tech/common-ui/dialogs/Log';
import KeyValueEditor from '@northern.tech/common-ui/forms/KeyValueEditor';
import storeActions from '@northern.tech/store/actions';
import { BENEFITS, DEPLOYMENT_ROUTES, DEPLOYMENT_STATES, DEVICE_STATES, TIMEOUTS } from '@northern.tech/store/constants';
import { getDeviceConfigDeployment, getTenantCapabilities, getUserCapabilities } from '@northern.tech/store/selectors';
import { useAppDispatch } from '@northern.tech/store/store';
import {
  abortDeployment,
  applyDeviceConfig,
  getDeviceConfig,
  getDeviceLog,
  getSingleDeployment,
  saveGlobalSettings,
  setDeviceConfig
} from '@northern.tech/store/thunks';
import { groupDeploymentDevicesStats, groupDeploymentStats } from '@northern.tech/store/utils';
import { deepCompare, isEmpty, toggle } from '@northern.tech/utils/helpers';

import Tracking from '../../../tracking';
import { HELPTOOLTIPS } from '../../helptips/HelpTooltips';
import { MenderHelpTooltip } from '../../helptips/MenderTooltip';
import ConfigImportDialog from './ConfigImportDialog';

const { setSnackbar } = storeActions;

const defaultReportTimeStamp = '0001-01-01T00:00:00Z';

const maxDeploymentsChecks = 128;

const configHelpTipsMap = {
  'mender-demo-raspberrypi-led': {
    position: 'right',
    component: ({ anchor, ...props }) => <MenderHelpTooltip style={anchor} id={HELPTOOLTIPS.configureRaspberryLedTip.id} {...props} />
  },
  timezone: {
    position: 'right',
    component: ({ anchor, ...props }) => <MenderHelpTooltip style={anchor} id={HELPTOOLTIPS.configureTimezoneTip.id} {...props} />
  }
};

export const ConfigUpToDateNote = ({ updated_ts = defaultReportTimeStamp }) => (
  <div className="flexbox margin-small align-items-center">
    <CheckCircleIcon className="green margin-right-medium" />
    <div>
      <Typography variant="body2">Configuration Up-To-Date on the device</Typography>
      <Typography color="textSecondary" variant="body2">
        Updated: {<Time value={updated_ts} />}
      </Typography>
    </div>
  </div>
);

export const ConfigEmptyNote = ({ updated_ts = '' }) => (
  <div className="flexbox column margin-small align-items-center full-width">
    <Typography variant="body1" className="margin-bottom-x-small">
      No configurations were found
    </Typography>
    <Typography variant="body2" className="align-center" style={{ width: '500px' }}>
      The device appears to either have an empty configuration or not to have reported a configuration yet. Updated: {<Time value={updated_ts} />}
    </Typography>
  </div>
);

export const ConfigEditingActions = ({ canSetDefault, isSetAsDefault, onSetAsDefaultChange, onSubmit, onCancel }) => (
  <div className="flexbox column">
    {canSetDefault && (
      <div className="flexbox column">
        <FormControlLabel
          control={<Checkbox color="primary" checked={isSetAsDefault} onChange={onSetAsDefaultChange} size="small" />}
          label="Save as default configuration"
        />
        <FormHelperText className="margin-top-none" style={{ marginLeft: 30 }}>
          You can import these key value pairs when configuring other devices
        </FormHelperText>
      </div>
    )}
    <div className="margin-top-small">
      <Button variant="contained" onClick={onSubmit}>
        Save and apply to device
      </Button>
      <Button className="margin-left-small" variant="outlined" color="neutral" onClick={onCancel}>
        Cancel changes
      </Button>
    </div>
  </div>
);

export const ConfigUpdateNote = ({ isUpdatingConfig, isAccepted }) => (
  <div>
    <Typography variant="body2">
      {!isAccepted
        ? 'Configuration will be applied once the device is connected'
        : isUpdatingConfig
          ? 'Updating configuration on device...'
          : 'Configuration could not be updated on device'}
    </Typography>
    <Typography variant="caption">Status: {isUpdatingConfig || !isAccepted ? 'pending' : 'failed'}</Typography>
  </div>
);

export const ConfigUpdateFailureActions = ({ hasLog, onSubmit, onCancel, setShowLog }) => (
  <>
    {hasLog && (
      <Button onClick={setShowLog} className="margin-left-medium">
        View log
      </Button>
    )}
    <Button variant="outlined" className="margin-left-medium" onClick={onSubmit} startIcon={<RefreshIcon fontSize="small" />}>
      Retry
    </Button>
    <Button variant="outlined" color="neutral" className="margin-left-medium" onClick={onCancel}>
      Cancel changes
    </Button>
  </>
);

export const DeviceConfiguration = ({ defaultConfig = {}, device: { id: deviceId } }) => {
  const { device, deviceConfigDeployment: deployment } = useSelector(state => getDeviceConfigDeployment(state, deviceId));
  const { hasDeviceConfig } = useSelector(getTenantCapabilities);
  const { canManageUsers } = useSelector(getUserCapabilities);
  const { config = {}, status } = device;
  const { configured = {}, deployment_id, reported = {}, reported_ts, updated_ts } = config;
  const isRelevantDeployment = deployment.created > updated_ts && (!reported_ts || deployment.finished > reported_ts);
  const [changedConfig, setChangedConfig] = useState();
  const [editableConfig, setEditableConfig] = useState();
  const [isAborting, setIsAborting] = useState(false);
  const [isEditingConfig, setIsEditingConfig] = useState(false);
  const [isSetAsDefault, setIsSetAsDefault] = useState(false);
  const [isUpdatingConfig, setIsUpdatingConfig] = useState(false);
  const [showConfigImport, setShowConfigImport] = useState(false);
  const [showLog, setShowLog] = useState(false);
  const [updateFailed, setUpdateFailed] = useState();
  const [updateLog, setUpdateLog] = useState();
  const [isRefreshing, setIsRefreshing] = useState(false);
  const dispatch = useAppDispatch();
  const deploymentTimer = useRef(null);
  const deploymentCheckCount = useRef(0);

  useEffect(() => {
    if (!isEmpty(config) && !isEmpty(changedConfig) && !isEditingConfig) {
      setIsEditingConfig(isUpdatingConfig || updateFailed);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [JSON.stringify(config), JSON.stringify(changedConfig), isEditingConfig, isUpdatingConfig, updateFailed]);

  useEffect(() => {
    if (deployment.devices && deployment.devices[device.id]?.log) {
      setUpdateLog(deployment.devices[device.id].log);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [JSON.stringify(deployment.devices), device.id]);

  useEffect(() => {
    if (!deployment_id || isRefreshing) {
      return;
    }
    deploymentCheckCount.current = 0;
    deploymentTimer.current = setInterval(() => {
      if (deploymentCheckCount.current > maxDeploymentsChecks) {
        clearInterval(deploymentTimer.current);
        setIsRefreshing(false);
        return;
      }
      setIsRefreshing(true);
      deploymentCheckCount.current++;
      dispatch(getSingleDeployment(deployment_id));
    }, TIMEOUTS.refreshDefault);
    dispatch(getSingleDeployment(deployment_id));
    return () => {
      clearInterval(deploymentTimer.current);
      setIsRefreshing(false);
    };
  }, [deployment_id, dispatch, isRefreshing]);

  useEffect(() => {
    if (deployment.status === DEPLOYMENT_STATES.finished) {
      clearInterval(deploymentTimer.current);
      setIsRefreshing(false);
    }
  }, [deployment.status]);

  useEffect(() => {
    if (deployment.status === DEPLOYMENT_STATES.finished) {
      // we have to rely on the device stats here as the state change might not have propagated to the deployment status
      // leaving all stats at 0 and giving a false impression of deployment success
      const stats = groupDeploymentStats(deployment);
      const deviceStats = groupDeploymentDevicesStats(deployment);
      const updateFailed = !!((stats.failures || deviceStats.failures) && deployment.device_count);
      setUpdateFailed(updateFailed);
      setIsEditingConfig(updateFailed);
      setIsUpdatingConfig(false);
      dispatch(getDeviceConfig(device.id));
    } else if (deployment.status) {
      setChangedConfig(configured);
      setEditableConfig(configured);
      // we can't rely on the deployment.status to be !== 'finished' since `deployment` is initialized as an empty object
      // and thus the undefined status would also point to an ongoing update
      setIsUpdatingConfig(true);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [JSON.stringify(configured), JSON.stringify(deployment.stats), deployment.created, deployment.status, deployment.finished, isRelevantDeployment]);

  useEffect(() => {
    if (!changedConfig && !isEmpty(config) && (!deployment_id || deployment.status)) {
      // let currentConfig = reported;
      const stats = groupDeploymentStats(deployment);
      if (deployment.status !== DEPLOYMENT_STATES.finished || stats.failures) {
        setEditableConfig(configured);
        setChangedConfig(configured);
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dispatch, JSON.stringify(config), deployment.status, !changedConfig, JSON.stringify(deployment), isRelevantDeployment]);

  const onConfigImport = ({ config, importType }) => {
    let updatedConfig = config;
    if (importType === 'default') {
      updatedConfig = defaultConfig.current;
    }
    setChangedConfig(updatedConfig);
    setEditableConfig(updatedConfig);
    setShowConfigImport(false);
  };

  const onSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);

  const onSetAsDefaultChange = () => setIsSetAsDefault(toggle);

  const onShowLog = () =>
    dispatch(getDeviceLog({ deploymentId: deployment_id, deviceId: device.id }))
      .unwrap()
      .then(result => {
        setShowLog(true);
        setUpdateLog(result[1]);
      });

  const onClose = () => {
    setIsEditingConfig(false);
    setUpdateFailed(false);
    setIsAborting(false);
  };

  const onCancel = () => {
    if (!isEmpty(reported)) {
      setEditableConfig(reported);
      setChangedConfig(reported);
    }
    const requests = [];
    if (deployment_id && deployment.status !== DEPLOYMENT_STATES.finished) {
      requests.push(dispatch(abortDeployment(deployment_id)));
    }
    if (deepCompare(reported, changedConfig)) {
      requests.push(Promise.resolve());
    } else {
      requests.push(dispatch(setDeviceConfig({ deviceId: device.id, config: reported })));
      if (isSetAsDefault && canManageUsers) {
        requests.push(dispatch(saveGlobalSettings({ defaultDeviceConfig: { current: defaultConfig.previous } })));
      }
    }
    return Promise.all(requests).then(() => {
      setIsUpdatingConfig(false);
      setUpdateFailed(false);
      setIsAborting(false);
    });
  };

  const onSubmit = () => {
    Tracking.event({ category: 'devices', action: 'apply_configuration' });
    setIsUpdatingConfig(true);
    setUpdateFailed(false);
    return dispatch(setDeviceConfig({ deviceId: device.id, config: changedConfig }))
      .unwrap()
      .then(() =>
        dispatch(applyDeviceConfig({ deviceId: device.id, configDeploymentConfiguration: { retries: 0 }, isDefault: isSetAsDefault, config: changedConfig }))
      )
      .catch(() => {
        setIsEditingConfig(true);
        setUpdateFailed(true);
        setIsUpdatingConfig(false);
      });
  };

  const onStartEdit = e => {
    e.stopPropagation();
    const nextEditableConfig = { ...configured, ...reported };
    setChangedConfig(nextEditableConfig);
    setEditableConfig(nextEditableConfig);
    setIsEditingConfig(true);
  };

  const onStartImportClick = e => {
    e.stopPropagation();
    setShowConfigImport(true);
  };

  const onAbortClick = () => setIsAborting(toggle);

  const hasDeviceConfiguration = !isEmpty(reported);
  let footer = hasDeviceConfiguration ? <ConfigUpToDateNote updated_ts={reported_ts} /> : <ConfigEmptyNote updated_ts={updated_ts} />;
  if (isEditingConfig) {
    footer = (
      <ConfigEditingActions
        canSetDefault={canManageUsers}
        isSetAsDefault={isSetAsDefault}
        onSetAsDefaultChange={onSetAsDefaultChange}
        onSubmit={onSubmit}
        onCancel={onClose}
      />
    );
  }
  if (isUpdatingConfig || updateFailed) {
    const hasLog = deployment.devices && deployment.devices[device.id]?.log;
    footer = (
      <>
        <div className="flexbox align-items-center padding-small">
          {isUpdatingConfig && <Loader show={true} style={{ marginRight: 15, marginTop: -15 }} />}
          {updateFailed && <ErrorIcon className="red margin-right-medium" />}
          <ConfigUpdateNote isUpdatingConfig={isUpdatingConfig} isAccepted={status === DEVICE_STATES.accepted} />
        </div>
        {updateFailed ? (
          <ConfigUpdateFailureActions hasLog={hasLog} setShowLog={onShowLog} onSubmit={onSubmit} onCancel={onCancel} />
        ) : isAborting ? (
          <Confirm cancel={onAbortClick} action={onCancel} type="abort" classes="margin-left-large" />
        ) : (
          <>
            <Button color="secondary" onClick={onAbortClick} startIcon={<BlockIcon fontSize="small" />} className="margin-left-medium">
              Abort update
            </Button>
            <Button
              component={RouterLink}
              to={`/deployments/${deployment.status || DEPLOYMENT_ROUTES.active.key}?open=true&id=${deployment_id}`}
              className="margin-left-medium"
            >
              View deployment
            </Button>
          </>
        )}
      </>
    );
  }

  const helpTipsMap = Object.entries(configHelpTipsMap).reduce((accu, [key, value]) => {
    accu[key] = {
      ...value,
      props: { deviceId: device.id }
    };
    return accu;
  }, {});

  return (
    <ContentSection
      isAddOn
      postTitle={
        <>
          <InfoHintContainer className="flexbox center-aligned">
            <EnterpriseNotification id={BENEFITS.deviceConfiguration.id} />
            <MenderHelpTooltip id={HELPTOOLTIPS.configureAddOnTip.id} small />
          </InfoHintContainer>
          {hasDeviceConfig && !(isEditingConfig || isUpdatingConfig) && <EditButton onClick={onStartEdit} />}
          {isEditingConfig && (
            <Button onClick={onStartImportClick} disabled={isUpdatingConfig} startIcon={<SaveAltIcon />}>
              Import configuration
            </Button>
          )}
        </>
      }
      title="Device configuration"
    >
      <div className="relative">
        {isEditingConfig ? (
          <KeyValueEditor
            disabled={isUpdatingConfig}
            errortext=""
            initialInput={editableConfig}
            inputHelpTipsMap={helpTipsMap}
            onInputChange={setChangedConfig}
          />
        ) : (
          hasDeviceConfig && <TwoColumnData chipLikeKey data={reported} setSnackbar={onSetSnackbar} />
        )}
        {hasDeviceConfig && <div className="flexbox align-items-center margin-bottom margin-top relative">{footer}</div>}
        {showLog && <LogDialog logData={updateLog} onClose={() => setShowLog(false)} type="configUpdateLog" />}
        {showConfigImport && <ConfigImportDialog onCancel={() => setShowConfigImport(false)} onSubmit={onConfigImport} />}
      </div>
    </ContentSection>
  );
};

export default DeviceConfiguration;
