import { useEffect, useMemo, useState } from 'react';

import { Box, Button, IconButton, TextField, Typography } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CloseIcon from '@mui/icons-material/Close';
import { makeStyles } from 'tss-react/mui';

import { notify } from '../../common/notify';
import { CollapsibleSection } from './CollapsibleSection';
import { useAppDispatch } from '../store';
import { fetchModuleInput, saveModuleInput } from '../store/modulesSlice/thunks';

interface InputSubtype {
  key: string;
  label?: string;
  question?: string;
  type: string;
}

interface InputItem {
  label?: string;
  question?: string;
  response?: unknown;
  subtype?: InputSubtype | InputSubtype[];
  type: string;
}

interface Props {
  encodedName?: boolean;
  moduleAdded: boolean;
  moduleName: string;
  onSaved: () => void;
  projectId: number | string;
}

const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value));

const isSubtypeSupported = (subtype: InputSubtype) => subtype.type === 'string';

const isItemSupported = (item: InputItem) => {
  if (item.type === 'string') return true;
  if (item.type === 'list') {
    return Array.isArray(item.subtype)
      ? item.subtype.every(isSubtypeSupported)
      : item.subtype != null && isSubtypeSupported(item.subtype);
  }
  return false;
};

const useStyles = makeStyles()(theme => ({
  item: { marginTop: 16 },
  listLabel: { display: 'inline-block', marginBottom: 8 },
  fieldRow: { display: 'flex', alignItems: 'center', marginTop: 8 },
  fieldLabel: { display: 'inline-block', minWidth: 100, marginRight: 12 },
  responseRow: { display: 'flex', alignItems: 'baseline' },
  responseRowMulti: { marginBottom: 36 },
  bulletValue: { display: 'list-item', listStylePosition: 'inside' },
  muted: { color: theme.palette.text.secondary },
  buttons: { display: 'flex', gap: theme.spacing(1), marginTop: 16 }
}));

export const ModuleInputData = ({ encodedName, moduleAdded, moduleName, onSaved, projectId }: Props) => {
  const { classes, cx } = useStyles();
  const dispatch = useAppDispatch();

  const [input, setInput] = useState<InputItem[] | null>(null);
  const [original, setOriginal] = useState<InputItem[] | null>(null);
  const [loaded, setLoaded] = useState(false);
  const [editing, setEditing] = useState(false);

  const unsupported = useMemo(() => input?.some(item => !isItemSupported(item)) ?? false, [input]);

  useEffect(() => {
    let cancelled = false;
    // Reset per-module state so data from a previously viewed module never leaks
    // into a module that has no input spec of its own.
    setLoaded(false);
    setInput(null);
    setOriginal(null);
    setEditing(false);
    dispatch(fetchModuleInput({ projectId, name: moduleName, encodedName }))
      .unwrap()
      .then(data => {
        if (cancelled) return;
        const spec = (data as { input_spec?: InputItem[] })?.input_spec;
        if (spec && spec.length > 0) {
          setInput(spec);
          setOriginal(clone(spec));
        }
        setLoaded(true);
      })
      .catch(() => {
        if (cancelled) return;
        setLoaded(true);
        notify.error('Unable to load input data for this module.');
      });
    return () => {
      cancelled = true;
    };
  }, [projectId, moduleName, encodedName, dispatch]);

  const updateItem = (index: number, updater: (item: InputItem) => InputItem) => {
    setInput(prev => (prev ? prev.map((item, i) => (i === index ? updater(item) : item)) : prev));
  };

  const addResponse = (index: number) => {
    updateItem(index, item => {
      const response = Array.isArray(item.response) ? [...(item.response as unknown[])] : [];
      if (Array.isArray(item.subtype)) {
        response.push(item.subtype.reduce<Record<string, string>>((acc, sub) => ({ ...acc, [sub.key]: '' }), {}));
      } else {
        response.push('');
      }
      return { ...item, response };
    });
  };

  const removeResponse = (index: number, responseIndex: number) => {
    updateItem(index, item => ({
      ...item,
      response: (item.response as unknown[]).filter((_, i) => i !== responseIndex)
    }));
  };

  const onSave = () => {
    dispatch(saveModuleInput({ projectId, name: moduleName, input, encodedName }))
      .unwrap()
      .then(() => {
        setOriginal(clone(input as InputItem[]));
        setEditing(false);
        notify.success('Input data successfully saved.');
        onSaved();
      })
      .catch((err: { data?: { msg?: string } }) => notify.error(err?.data?.msg ?? 'Unable to save input data.'));
  };

  const onCancel = () => {
    setEditing(false);
    setInput(original ? clone(original) : null);
  };

  const renderStringField = (
    value: string,
    question: string | undefined,
    onChange: (v: string) => void,
    name?: string
  ) =>
    editing ? (
      <TextField
        variant="outlined"
        size="small"
        fullWidth
        name={name}
        placeholder={question}
        value={value ?? ''}
        onChange={e => onChange(e.target.value)}
      />
    ) : (
      <Typography component="span" variant="subtitle1" className={cx(!value && classes.muted)}>
        {value || question}
      </Typography>
    );

  return (
    <CollapsibleSection
      title="Input data"
      action={
        <Button
          variant="outlined"
          color="inherit"
          disabled={editing || !input || !moduleAdded || unsupported}
          onClick={() => setEditing(true)}
        >
          Edit
        </Button>
      }
    >
      {input?.map((item, index) => {
        if (item.type === 'string') {
          return (
            <Box key={index} className={classes.item}>
              <Box className={classes.fieldRow}>
                <Typography component="span" variant="subtitle1" className={classes.fieldLabel}>
                  {item.label}:
                </Typography>
                {renderStringField(
                  item.response as string,
                  item.question,
                  value => updateItem(index, it => ({ ...it, response: value })),
                  item.label
                )}
              </Box>
            </Box>
          );
        }
        if (item.type === 'list') {
          const multi = Array.isArray(item.subtype);
          const responses = (item.response as unknown[]) ?? [];
          return (
            <Box key={index} className={classes.item}>
              <Typography component="span" variant="h6" className={classes.listLabel}>
                {item.label}
              </Typography>
              {responses.map((response, responseIndex) => (
                <Box key={responseIndex} className={cx(classes.responseRow, multi && classes.responseRowMulti)}>
                  {multi ? (
                    <Box>
                      {(item.subtype as InputSubtype[]).map(sub =>
                        isSubtypeSupported(sub) ? (
                          <Box key={sub.key} className={classes.fieldRow}>
                            <Typography component="span" variant="subtitle1" className={classes.fieldLabel}>
                              {sub.label}:
                            </Typography>
                            {renderStringField(
                              (response as Record<string, string>)[sub.key],
                              sub.question,
                              value =>
                                updateItem(index, it => {
                                  const next = [...(it.response as Record<string, string>[])];
                                  next[responseIndex] = { ...next[responseIndex], [sub.key]: value };
                                  return { ...it, response: next };
                                }),
                              sub.key
                            )}
                          </Box>
                        ) : (
                          <UnsupportedNotice key={sub.key} />
                        )
                      )}
                    </Box>
                  ) : isSubtypeSupported(item.subtype as InputSubtype) ? (
                    editing ? (
                      renderStringField((response as string) ?? '', (item.subtype as InputSubtype).question, value =>
                        updateItem(index, it => {
                          const next = [...(it.response as string[])];
                          next[responseIndex] = value;
                          return { ...it, response: next };
                        })
                      )
                    ) : (
                      <Typography component="span" variant="subtitle1" className={classes.bulletValue}>
                        {(response as string) ?? ''}
                      </Typography>
                    )
                  ) : null}
                  {editing && (
                    <IconButton size="small" aria-label="Remove" onClick={() => removeResponse(index, responseIndex)}>
                      <CloseIcon fontSize="small" />
                    </IconButton>
                  )}
                </Box>
              ))}
              {responses.length === 0 && !editing && <Typography className={classes.muted}>(Not set)</Typography>}
              {editing && (
                <Button variant="text" startIcon={<AddIcon />} onClick={() => addResponse(index)} sx={{ mt: 1 }}>
                  Add
                </Button>
              )}
            </Box>
          );
        }
        return <UnsupportedNotice key={index} />;
      })}

      {editing && (
        <Box className={classes.buttons}>
          <Button variant="outlined" color="inherit" onClick={onCancel}>
            Cancel
          </Button>
          <Button variant="contained" onClick={onSave}>
            Save
          </Button>
        </Box>
      )}

      {!input && loaded && <Typography sx={{ mt: 2 }}>This module does not support input data</Typography>}
    </CollapsibleSection>
  );
};

const UnsupportedNotice = () => (
  <Typography sx={{ mt: 2 }}>This module uses input type(s) which are not yet supported in Mission Portal</Typography>
);
