// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2020-2022 grommunio GmbH

import React, { PureComponent } from 'react';
import { withStyles } from '@mui/styles';
import PropTypes from 'prop-types';
import TopBar from '../components/TopBar';
import { Button, Checkbox, FormControl, FormControlLabel, Grid, IconButton, MenuItem, Paper,
  Typography, Switch, Tooltip, TextField, RadioGroup, Radio } from '@mui/material';
import { withTranslation } from 'react-i18next';
import blue from '../colors/blue';
import { fetchLdapConfig, syncLdapUsers, updateLdapConfig, updateAuthMgr, fetchAuthMgr } from '../actions/ldap';
import { connect } from 'react-redux';
import { cloneObject } from '../utils';
import DeleteConfig from '../components/Dialogs/DeleteConfig';
import Add from '@mui/icons-material/Add';
import Delete from '@mui/icons-material/Close';
import { green, red } from '@mui/material/colors';
import adminConfig from '../config';
import LdapTextfield from '../components/LdapTextfield';
import Help from '@mui/icons-material/HelpOutline';
import Feedback from '../components/Feedback';
import { Autocomplete } from '@mui/lab';
import { SYSTEM_ADMIN_WRITE } from '../constants';
import { CapabilityContext } from '../CapabilityContext';
import TaskCreated from '../components/Dialogs/TaskCreated';

const styles = theme => ({
  root: {
    display: 'flex',
    flex: 1,
    flexDirection: 'column',
  },
  base: {
    flexDirection: 'column',
    padding: theme.spacing(2, 2, 2, 2),
    flex: 1,
    display: 'flex',
    overflow: 'auto',
  }, 
  toolbar: theme.mixins.toolbar,
  pageTitle: {
    margin: theme.spacing(2, 2, 1, 2),
  },
  subtitle: {
    margin: theme.spacing(0, 2, 2, 2),
  },
  homeIcon: {
    color: blue[500],
    position: 'relative',
    top: 4,
    left: 4,
    cursor: 'pointer',
  },
  paper: {
    margin: theme.spacing(3, 2, 1, 2),
    paddingBottom: 16,
  },
  formControl: {
    width: '100%',
  },
  category: {
    margin: theme.spacing(2, 0, 1, 2),
  },
  textfield: {
    margin: theme.spacing(2, 2, 2, 2),
  },
  flexContainer: {
    display: 'flex',
    flex: 1,
    justifyContent: 'flex-end',
    marginRight: 16,
  },
  flexTextfield: {
    flex: 1,
    margin: 8,
    minWidth: 400,
  },
  flexRow: {
    margin: theme.spacing(0, 1, 0, 1),
    flexWrap: 'wrap',
    display: 'flex',
  },
  deleteButton: {
    marginRight: 8,
    backgroundColor: red['500'],
    '&:hover': {
      backgroundColor: red['700'],
    },
  },
  bottomRow: {
    display: 'flex',
    padding: theme.spacing(2, 2, 4, 2),
  },
  spacer: {
    paddingTop: 16,
  },
  mappingTitle: {
    padding: theme.spacing(1, 1, 0, 2),
  },
  addButton: {
    padding: theme.spacing(1, 0, 0, 0),
  },
  removeButton: {
    margin: theme.spacing(1, 2, 0, 0),
  },
  attribute: {
    marginLeft: 8,
  },
  tooltip: {
    marginTop: -2,
  },
  subcaption: {
    margin: theme.spacing(1, 0, 0, 2),
  },
  radioGroup: {
    marginLeft: 16,
  },
});

class LdapConfig extends PureComponent {

  state = {
    baseDn: '',
    objectID: '',
    disabled: true,
    //Connection
    server: '',
    bindUser: '',
    bindPass: '',
    starttls: false,
    // Users
    username: '',
    displayName: '',
    defaultQuota: '',
    filter: '',
    templates: 'none',
    attributes: [],
    searchAttributes: [],
    authBackendSelection: 'externid',
    aliases: '',

    deleting: false,
    taskMessage: '',
    taskID: null,
    force: false,
    snackbar: '',
  }

  /* Formats state to new config object for backend */
  formatData() {
    // Create a deep copy of the object
    const copy = cloneObject(this.state);
    // New, in the end formatted, object
    const formatted = {};
    // Defaults
    formatted.baseDn = copy.baseDn;
    formatted.objectID  = copy.objectID;
    formatted.disabled = copy.disabled;
    // Format connection
    formatted.connection = {};
    formatted.connection.server = copy.server;
    formatted.connection.bindUser = copy.bindUser;
    formatted.connection.bindPass = copy.bindPass;
    formatted.connection.starttls = copy.starttls;

    //Format users
    formatted.users = {};
    formatted.users.username = copy.username;
    formatted.users.displayName = copy.displayName;
    formatted.users.attributes = this.arrayToObject([...this.state.attributes]);
    formatted.users.defaultQuota = parseInt(copy.defaultQuota) || undefined;
    formatted.users.filter = copy.filter; // Put single string in array (necessary)
    formatted.users.templates = copy.templates === 'none' ?
      [] : ['common', copy.templates]; // ['common', 'ActiveDirectory']
    formatted.users.searchAttributes = [...this.state.searchAttributes];
    formatted.users.aliases = copy.aliases;

    return formatted;
  }

  async componentDidMount() {
    const { fetch, fetchAuthMgr } = this.props;
    const resp = await fetch()
      .catch(snackbar => this.setState({ snackbar }));
    const authResp = await fetchAuthMgr()
      .catch(snackbar => this.setState({ snackbar }));
    const config = resp?.data;
    if(!config) return;
    const available = resp?.ldapAvailable || false;
    const connection = config?.connection || {};
    const users = config?.users || {};
    this.setState({
      authBackendSelection: authResp?.data?.authBackendSelection || 'always_mysql',
      available,
      baseDn: config.baseDn || '',
      disabled: config.disabled === undefined ? true : config.disabled,
      objectID: config.objectID || '',
      server: connection.server || '',
      bindUser: connection.bindUser || '',
      bindPass: connection.bindPass || '',
      starttls: connection.starttls || false,
      username: users.username || '',
      displayName: users.displayName || '',
      defaultQuota: users.defaultQuota || '',
      filter: users.filter || '',
      templates: users.templates && users.templates.length > 0 ? users.templates[1] : 'none',
      searchAttributes: users.searchAttributes || [],
      attributes: this.objectToArray(users.attributes || {}),
      aliases: users.aliases || '',
    });
  }

  objectToArray(obj) {
    const arr = [];
    Object.entries(obj).forEach(([key, value]) => arr.push({ key, value }));
    return arr;
  }

  arrayToObject(arr) {
    const obj = {};
    arr.forEach(attr => obj[attr.key] = attr.value);
    return obj;
  }

  handleNavigation = path => event => {
    const { history } = this.props;
    event.preventDefault();
    history.push(`/${path}`);
  }

  handleInput = field => ({ target: t }) => this.setState({
    [field]: t.value,
  });

  handleAutocomplete = (field) => (e, newVal) => {
    this.setState({
      [field]: newVal,
    });
  }

  handleTemplate = ({ target: t }) => {
    const templates = t.value;
    if(templates === 'ActiveDirectory') {
      this.setState({
        templates,
        objectID: 'objectGUID',
        username: 'mail',
        displayName: 'displayName',
        searchAttributes: ["mail", "givenName", "cn", "sn", "name", "displayName"],
        filter: "objectClass=user",
        aliases: 'proxyAddresses',
      });
    } else if(templates === 'OpenLDAP') {
      this.setState({
        templates,
        objectID: 'entryUUID',
        username: 'mail',
        displayName: 'displayName',
        searchAttributes: ["mail", "givenName", "cn", "sn", "displayName", "gecos"],
        filter: "objectClass=posixAccount",
        aliases: 'mailAlternativeAddress',
      });
    } else {
      this.setState({ templates });
    }
  }

  handleAttributeInput = (objectPart, idx) => ({ target: t }) => {
    const copy = [...this.state.attributes];
    copy[idx][objectPart] = t.value;
    this.setState({
      attributes: copy,
    });
  }

  handleNewRow = () => {
    const copy = [...this.state.attributes];
    copy.push({ key: '', value: '' });
    this.setState({
      attributes: copy,
    });
  }

  removeRow = idx => () => {
    const copy = [...this.state.attributes];
    copy.splice(idx, 1);
    this.setState({ attributes: copy });
  }

  handleCheckbox = field => () => this.setState({
    [field]: !this.state[field],
  });

  handleActive = () => {
    const { disabled, authBackendSelection } = this.state;
    this.setState({
      disabled: !disabled,
      authBackendSelection: disabled ? authBackendSelection : 'always_mysql',
    });
  }

  handleSave = e => {
    const { put, authMgr } = this.props;
    const { force, authBackendSelection } = this.state;
    e.preventDefault();
    Promise.all([
      put(this.formatData(), { force: force }),
      authMgr({ authBackendSelection }),
    ])
      .then(msg => this.setState({ snackbar: 'Success! ' + (msg || '') }))
      .catch(snackbar => this.setState({ snackbar }));
  }

  handleDelete = () => this.setState({ deleting: true });

  handleDeleteSuccess = () => {
    this.setState({ deleting: false, snackbar: 'Success!' });
  }

  handleDeleteClose = () => this.setState({ deleting: false });

  handleDeleteError = error => this.setState({ snackbar: error });

  handleSync = importUser => () => this.props.sync({ import: importUser })
    .then(response => {
      if(response?.taskID) {
        this.setState({
          taskMessage: response.message || 'Task created',
          loading: false,
          taskID: response.taskID,
        });
      } else {
        this.setState({ snackbar: 'Success! ' + (response || '') });
      }
    })
    .catch(snackbar => this.setState({ snackbar }));

  handleTaskClose = () => this.setState({
    taskMessage: "",
    taskID: null,
  })

  render() {
    const { classes, t } = this.props;
    const writable = this.context.includes(SYSTEM_ADMIN_WRITE);
    const { available, force, deleting, snackbar, server, bindUser, bindPass, starttls, baseDn, objectID, disabled,
      username, filter, templates, attributes, defaultQuota, displayName, searchAttributes,
      authBackendSelection, aliases, taskMessage, taskID } = this.state;
    return (
      <div className={classes.root}>
        <TopBar />
        <div className={classes.toolbar}></div>
        <form className={classes.base} onSubmit={this.handleSave}>
          <Typography variant="h2" className={classes.pageTitle}>
            {t("Directory")}
            <Tooltip
              className={classes.tooltip}
              title={t("ldap_settingsHelp")}
              placement="top"
            >
              <IconButton
                size="small"
                href="https://docs.grommunio.com/admin/administration.html#ldap"
                target="_blank"
              >
                <Help fontSize="small"/>
              </IconButton>
            </Tooltip>
          </Typography>
          <Typography variant="caption" className={classes.subtitle}>
            {t('ldap_sub')}
          </Typography>
          <Grid container className={classes.category}>
            <FormControlLabel
              control={
                <Switch
                  checked={!disabled}
                  onChange={this.handleActive}
                  name="disabled"
                  color="primary"
                />
              }
              label={<span>
                {t('LDAP enabled')}
                <Tooltip
                  className={classes.tooltip}
                  title={t("Enable LDAP service")}
                  placement="top"
                >
                  <IconButton size="small">
                    <Help fontSize="small"/>
                  </IconButton>
                </Tooltip>
              </span>}
            />
            <div className={classes.flexContainer}>
              <Tooltip placement="top" title={t("Synchronize already imported users")}>
                <Button
                  variant="contained"
                  color="primary"
                  style={{ marginRight: 16 }}
                  onClick={this.handleSync(false)}
                >
                  {t("Sync users")}
                </Button>
              </Tooltip>
              <Tooltip
                placement="top"
                title={t("ldap_import_tooltip")}
              >
                <Button
                  variant="contained"
                  color="primary"
                  style={{ marginRight: 16 }}
                  onClick={this.handleSync(true)}
                >
                  {t("Import users")}
                </Button>
              </Tooltip>
            </div>
          </Grid>
          <Typography
            color="inherit"
            variant="caption"
            style={{
              marginLeft: 16,
              color: available ? green['500'] : red['500'],
            }}
          >
            {!disabled && (available ? t('LDAP connectivity check passed') : t('LDAP connectivity check failed'))}
          </Typography>
          <Paper elevation={1} className={classes.paper}>
            <Typography variant="h6" className={classes.category}>{t('LDAP Server')}</Typography>
            <FormControl className={classes.formControl}>
              <div className={classes.flexRow}>
                <LdapTextfield
                  flex
                  label='LDAP Server'
                  autoFocus
                  placeholder="ldap://[::1]:389/"
                  onChange={this.handleInput('server')}
                  value={server || ''}
                  desc={t("ldap_server_desc")}
                  id="url"
                  name="url"
                  autoComplete="url"
                  InputLabelProps={{
                    shrink: true,
                  }}
                />
                <LdapTextfield
                  flex
                  label="LDAP Bind DN"
                  onChange={this.handleInput('bindUser')}
                  value={bindUser || ''}
                  desc={t("Distinguished Name used for binding")}
                  id="username"
                  name="username"
                  autoComplete="username"
                />
                <LdapTextfield
                  flex
                  label={t('LDAP Bind Password')}
                  onChange={this.handleInput('bindPass')}
                  value={bindPass || ''}
                  desc={t("ldap_password_desc")}
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                />
                <FormControlLabel
                  control={
                    <Checkbox
                      checked={starttls || false}
                      onChange={this.handleCheckbox('starttls')}
                      name="starttls"
                      inputProps={{
                        autoComplete: 'starttls',
                        name: 'starttls',
                        id: 'starttls',
                      }}
                      color="primary"
                    />
                  }
                  label={<span>
                    {'STARTTLS'}
                    <Tooltip
                      className={classes.tooltip}
                      title="Whether to issue a StartTLS extended operation"
                      placement="top"
                    >
                      <IconButton size="small">
                        <Help fontSize="small"/>
                      </IconButton>
                    </Tooltip>
                  </span>}
                />
              </div>
              <LdapTextfield
                label='LDAP Base DN'
                onChange={this.handleInput('baseDn')}
                value={baseDn || ''}
                desc={t("Base DN to use for searches")}
                id="baseDn"
                name="baseDn"
                autoComplete="baseDn"
              />
            </FormControl>
          </Paper>
          <Paper elevation={1} className={classes.paper}>
            <Typography variant="h6" className={classes.category}>
              {t('User authentication mechanism')}
            </Typography>
            <FormControl className={classes.formControl}>
              <RadioGroup
                name="authBackendSelection"
                value={authBackendSelection}
                onChange={this.handleInput("authBackendSelection")}
                row
                className={classes.radioGroup}
                color="primary"
              >
                <FormControlLabel
                  value="externid"
                  control={<Radio color="primary"/>}
                  label={t("Automatic")}
                />
                <FormControlLabel value="always_mysql" control={<Radio color="primary"/>} label={t("Only MySQL")} />
                <FormControlLabel value="always_ldap" control={<Radio color="primary"/>} label={t("Only LDAP")} />
              </RadioGroup>
            </FormControl>
          </Paper>
          <Paper className={classes.paper} elevation={1}>
            <FormControl className={classes.formControl}>
              <Typography variant="h6" className={classes.category}>{t('Attribute Configuration')}</Typography>
              <LdapTextfield
                label={t('LDAP Template')}
                onChange={this.handleTemplate}
                value={templates}
                select
                desc={t("Mapping templates to use")}
                id="templates"
                name="templates"
                autoComplete="templates"
              >
                <MenuItem value='none'>{t('No template')}</MenuItem>
                <MenuItem value="OpenLDAP">OpenLDAP</MenuItem>
                <MenuItem value="ActiveDirectory">ActiveDirectory</MenuItem>
              </LdapTextfield>
              <LdapTextfield
                label={t('LDAP Filter')}
                onChange={this.handleInput('filter')}
                value={filter || ''}
                desc={t("LDAP search filter to apply to user lookup")}
                id="filter"
                name="filter"
                autoComplete="filter"
              />
              <LdapTextfield
                label={t('Unique Identifier Attribute')}
                onChange={this.handleInput('objectID')}
                value={objectID || ''}
                desc={t("ldap_oID_desc")}
                id="objectID"
                name="objectID"
                autoComplete="objectID"
              />
              <LdapTextfield
                label={t('LDAP Username Attribute')}
                onChange={this.handleInput('username')}
                value={username || ''}
                desc={t("ldap_username_desc")}
                id="username"
                name="username"
                autoComplete="username"
              />
              <LdapTextfield
                label={t('LDAP Display Name Attribute')}
                onChange={this.handleInput('displayName')}
                value={displayName || ''}
                desc={t("Name of the attribute that contains the name")}
                id="displayName"
                name="displayName"
                autoComplete="displayName"
              />
              <LdapTextfield
                label={t('LDAP Default Quota')}
                onChange={this.handleInput('defaultQuota')}
                value={defaultQuota}
                desc={t("ldap_defaultQuota_desc")}
                id="defaultQuota"
                name="defaultQuota"
                autoComplete="defaultQuota"
              />
              <LdapTextfield
                label={t('LDAP Aliases')}
                onChange={this.handleInput('aliases')}
                value={aliases}
                desc={t("LDAP alias mapping")}
                id="aliasMapping"
                name="aliasMapping"
                autoComplete="aliasMapping"
              />
            </FormControl>
          </Paper>
          <Paper elevation={1} className={classes.paper}>
            <Typography variant="h6" className={classes.category}>{t('LDAP Search Attributes')}</Typography>
            <Typography variant="caption" className={classes.category}>
              {t('ldap_attribute_desc')}
            </Typography>
            <Autocomplete
              value={searchAttributes || []}
              onChange={this.handleAutocomplete('searchAttributes')}
              className={classes.textfield}
              options={adminConfig.searchAttributes}
              multiple
              renderInput={(params) => (
                <TextField
                  {...params}
                />
              )}
            />
          </Paper>
          <Paper elevation={1} className={classes.paper}>
            <Typography variant="h6" className={classes.category}>
              {t('Custom Mapping')}
              <Tooltip
                className={classes.tooltip}
                title={t('ldap_mapping_desc')}
                placement="top"
              >
                <IconButton size="small">
                  <Help fontSize="small"/>
                </IconButton>
              </Tooltip>
            </Typography>
            {attributes.map((mapping, idx) =>
              <Grid className={classes.attribute} container alignItems="center" key={idx}>
                <LdapTextfield
                  label={t('Name')}
                  flex
                  onChange={this.handleAttributeInput('key', idx)}
                  value={mapping.key || ''}
                  desc={t("LDAP attribute to map")}
                />
                <Typography className={classes.spacer}>:</Typography>
                <LdapTextfield
                  label={t('Value')}
                  flex
                  onChange={this.handleAttributeInput('value', idx)}
                  value={mapping.value || ''}
                  desc={t("Name of the user property to map to")}
                />
                <IconButton
                  onClick={this.removeRow(idx)}
                  className={classes.removeButton}
                  size="large">
                  <Delete color="error" />
                </IconButton>
              </Grid>
            )}
            <Grid container justifyContent="center" className={classes.addButton}>
              <Button size="small" onClick={this.handleNewRow}>
                <Add color="primary" />
              </Button>
            </Grid>
          </Paper>
          <div className={classes.bottomRow}>
            <Button
              variant="contained"
              color="secondary"
              onClick={this.handleDelete}
              className={classes.deleteButton}
            >
              {t('Delete config')}
            </Button>
            <Button
              variant="contained"
              color="primary"
              type="submit"
              onClick={this.handleSave}
              disabled={!writable}
            >
              {t('Save')}
            </Button>
            <FormControlLabel
              className={classes.attribute}
              control={
                <Checkbox
                  checked={force || false}
                  onChange={this.handleCheckbox('force')}
                  name="disabled"
                  color="primary"
                />
              }
              label={<span>
                {t('Force config save')}
                <Tooltip
                  className={classes.tooltip}
                  title={t("Save LDAP configuration even if it's faulty")}
                  placement="top"
                >
                  <IconButton size="small">
                    <Help fontSize="small"/>
                  </IconButton>
                </Tooltip>
              </span>}
            />
          </div>
        </form>
        <DeleteConfig
          open={deleting}
          onSuccess={this.handleDeleteSuccess}
          onError={this.handleDeleteError}
          onClose={this.handleDeleteClose}
        />
        <TaskCreated
          message={taskMessage}
          taskID={taskID}
          onClose={this.handleTaskClose}
        />
        <Feedback
          snackbar={snackbar}
          onClose={() => this.setState({ snackbar: '' })}
        />
      </div>
    );
  }
}

LdapConfig.contextType = CapabilityContext;
LdapConfig.propTypes = {
  classes: PropTypes.object.isRequired,
  t: PropTypes.func.isRequired,
  history: PropTypes.object.isRequired,
  fetch: PropTypes.func.isRequired,
  put: PropTypes.func.isRequired,
  sync: PropTypes.func.isRequired,
  authMgr: PropTypes.func.isRequired,
  fetchAuthMgr: PropTypes.func.isRequired,
};

const mapDispatchToProps = dispatch => {
  return {
    fetch: async () => await dispatch(fetchLdapConfig())
      .then(config => config)
      .catch(message => Promise.reject(message)),
    fetchAuthMgr: async () => await dispatch(fetchAuthMgr())
      .then(config => config)
      .catch(message => Promise.reject(message)),
    put: async (config, params) => await dispatch(updateLdapConfig(config, params))
      .then(msg => msg)
      .catch(message => Promise.reject(message)),
    authMgr: async (config) => await dispatch(updateAuthMgr(config))
      .then(msg => msg)
      .catch(message => Promise.reject(message)),
    sync: async params => await dispatch(syncLdapUsers(params))
      .catch(message => Promise.reject(message)),
  };
};

export default connect(null, mapDispatchToProps)(
  withTranslation()(withStyles(styles)(LdapConfig)));