import {
  Stack,
  Button,
  Grid,
  Dialog,
  DialogTitle,
  DialogContent,
  List,
  ListItem,
  DialogActions,
  Checkbox,
  FormControlLabel,
  TextField,
  InputAdornment,
  Divider,
  Typography,
  Toolbar,
} from '@mui/material';
import * as React from 'react';
import AddIcon from '@mui/icons-material/Add';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { BindableAttrValue, BindableAttrValues, LiveBinding } from '@mui/toolpad-core';
import { evaluateBindable } from '@mui/toolpad-core/runtime';
import { LoadingButton } from '@mui/lab';
import useLatest from '../../../utils/useLatest';
import { useDom, useDomApi } from '../../DomLoader';
import { usePageEditorState } from './PageEditorProvider';
import * as appDom from '../../../appDom';
import { NodeId } from '../../../types';
import dataSources from '../../../toolpadDataSources/client';
import NodeNameEditor from '../NodeNameEditor';
import JsonView from '../../JsonView';
import { ConnectionSelect } from '../HierarchyExplorer/CreateApiNodeDialog';
import { omit, update } from '../../../utils/immutability';
import client from '../../../api';
import ParametersEditor from './ParametersEditor';
import ErrorAlert from './ErrorAlert';
import { JsExpressionEditor } from './JsExpressionEditor';

function refetchIntervalInSeconds(maybeInterval?: number) {
  if (typeof maybeInterval !== 'number') {
    return undefined;
  }
  const seconds = Math.floor(maybeInterval / 1000);
  return seconds > 0 ? seconds : undefined;
}

interface DataSourceSelectorProps<Q> {
  open: boolean;
  onClose: () => void;
  onCreated: (newNode: appDom.QueryNode<Q>) => void;
}

function ConnectionSelectorDialog<Q>({ open, onCreated, onClose }: DataSourceSelectorProps<Q>) {
  const dom = useDom();

  const [input, setInput] = React.useState<NodeId | null>(null);

  const handleClick = React.useCallback(() => {
    const connectionId = input;
    const connection = connectionId && appDom.getMaybeNode(dom, connectionId, 'connection');

    if (!connection) {
      throw new Error(`Invariant: Selected non-existing connection "${connectionId}"`);
    }

    const dataSourceId = connection.attributes.dataSource.value;
    const dataSource = dataSources[dataSourceId];
    if (!dataSource) {
      throw new Error(`Invariant: Selected non-existing dataSource "${dataSourceId}"`);
    }

    const queryNode = appDom.createNode(dom, 'query', {
      attributes: {
        query: appDom.createConst(dataSource.getInitialQueryValue()),
        connectionId: appDom.createConst(connectionId),
        dataSource: appDom.createConst(dataSourceId),
      },
    });

    onCreated(queryNode);
  }, [dom, input, onCreated]);

  return (
    <Dialog open={open} onClose={onClose} scroll="body">
      <DialogTitle>Create Query</DialogTitle>
      <DialogContent>
        <ConnectionSelect value={input} onChange={setInput} />
      </DialogContent>
      <DialogActions>
        <Button color="inherit" variant="text" onClick={onClose}>
          Cancel
        </Button>
        <Button disabled={!input} onClick={handleClick}>
          Create query
        </Button>
      </DialogActions>
    </Dialog>
  );
}

interface QueryNodeEditorProps<Q, P> {
  open: boolean;
  onClose: () => void;
  onSave: (newNode: appDom.QueryNode) => void;
  onRemove: (newNode: appDom.QueryNode) => void;
  node: appDom.QueryNode<Q, P>;
}

function QueryNodeEditorDialog<Q, P>({
  open,
  node,
  onClose,
  onRemove,
  onSave,
}: QueryNodeEditorProps<Q, P>) {
  const { appId } = usePageEditorState();

  const [input, setInput] = React.useState(node);
  React.useEffect(() => setInput(node), [node]);

  const connectionId = input.attributes.connectionId.value;
  const dataSourceId = input.attributes.dataSource?.value;
  const dataSource = (dataSourceId && dataSources[dataSourceId]) || null;

  const handleConnectionChange = React.useCallback((newConnectionId) => {
    setInput((existing) =>
      update(existing, {
        attributes: update(existing.attributes, {
          connectionId: appDom.createConst(newConnectionId),
        }),
      }),
    );
  }, []);

  const handleQueryChange = React.useCallback((newQuery: Q) => {
    setInput((existing) =>
      update(existing, {
        attributes: update(existing.attributes, {
          query: appDom.createConst(newQuery),
        }),
      }),
    );
  }, []);

  const handleTransformFnChange = React.useCallback((newValue: string) => {
    setInput((existing) =>
      update(existing, {
        attributes: update(existing.attributes, {
          transform: appDom.createConst(newValue),
        }),
      }),
    );
  }, []);

  const handleTransformEnabledChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setInput((existing) =>
        update(existing, {
          attributes: update(existing.attributes, {
            transformEnabled: appDom.createConst(event.target.checked),
          }),
        }),
      );
    },
    [],
  );

  const handleRefetchOnWindowFocusChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setInput((existing) =>
        update(existing, {
          attributes: update(existing.attributes, {
            refetchOnWindowFocus: appDom.createConst(event.target.checked),
          }),
        }),
      );
    },
    [],
  );

  const handleRefetchOnReconnectChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setInput((existing) =>
        update(existing, {
          attributes: update(existing.attributes, {
            refetchOnReconnect: appDom.createConst(event.target.checked),
          }),
        }),
      );
    },
    [],
  );

  const handleRefetchIntervalChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const interval = Number(event.target.value);

      setInput((existing) =>
        update(existing, {
          attributes:
            Number.isNaN(interval) || interval <= 0
              ? omit(existing.attributes, 'refetchInterval')
              : update(existing.attributes, {
                  refetchInterval: appDom.createConst(interval * 1000),
                }),
        }),
      );
    },
    [],
  );

  const [params, setParams] = React.useState<[string, BindableAttrValue<any>][]>(
    Object.entries(input.params || {}),
  );
  React.useEffect(() => setParams(Object.entries(input.params || {})), [input.params]);

  const { pageState } = usePageEditorState();
  const liveParams: [string, LiveBinding][] = React.useMemo(() => {
    return params.map(([name, bindable]) => [name, evaluateBindable(bindable, pageState)]);
  }, [params, pageState]);

  const handleParamsChange = React.useCallback((newParams: [string, BindableAttrValue<any>][]) => {
    setParams(newParams);
    const paramsObj: BindableAttrValues<any> = Object.fromEntries(newParams);
    setInput((existing) =>
      update(existing, {
        params: paramsObj,
      }),
    );
  }, []);

  const handleSave = React.useCallback(() => {
    onSave(input);
  }, [onSave, input]);

  const handleRemove = React.useCallback(() => {
    onRemove(node);
    onClose();
  }, [onRemove, node, onClose]);

  const paramsObject: Record<string, any> = React.useMemo(() => {
    const liveParamValues: [string, any][] = liveParams.map(([name, result]) => [
      name,
      result?.value,
    ]);
    return Object.fromEntries(liveParamValues);
  }, [liveParams]);

  const [previewQuery, setPreviewQuery] = React.useState<appDom.QueryNode<Q, P> | null>(null);
  const [previewParams, setPreviewParams] = React.useState(paramsObject);
  const queryPreview = client.useQuery(
    'execQuery',
    previewQuery ? [appId, previewQuery, previewParams] : null,
    { retry: false },
  );

  const handleUpdatePreview = React.useCallback(() => {
    setPreviewQuery(input);
    setPreviewParams(paramsObject);
  }, [input, paramsObject]);

  const isInputSaved = node === input;

  const handleClose = React.useCallback(() => {
    const ok = isInputSaved
      ? true
      : // eslint-disable-next-line no-alert
        window.confirm(
          'Are you sure you want to close the editor. All unsaved progress will be lost.',
        );

    if (ok) {
      onClose();
    }
  }, [onClose, isInputSaved]);

  if (!dataSourceId || !dataSource) {
    throw new Error(`DataSource "${dataSourceId}" not found`);
  }

  return (
    <Dialog fullWidth maxWidth="lg" open={open} onClose={handleClose} scroll="body">
      <DialogTitle>Edit Query ({node.id})</DialogTitle>
      <DialogContent>
        <Stack spacing={1} py={1} gap={2}>
          <Stack direction="row" gap={2}>
            <NodeNameEditor node={node} />
            <ConnectionSelect
              dataSource={dataSourceId}
              value={input.attributes.connectionId.value || null}
              onChange={handleConnectionChange}
            />
          </Stack>

          <Divider />
          <Typography>Parameters</Typography>
          <ParametersEditor
            value={params}
            onChange={handleParamsChange}
            globalScope={pageState}
            liveValue={liveParams}
          />
          <Divider />
          <Typography>Build query:</Typography>
          <dataSource.QueryEditor
            appId={appId}
            connectionId={connectionId}
            value={input.attributes.query.value}
            onChange={handleQueryChange}
            globalScope={{ query: paramsObject }}
          />
          <Divider />
          <Typography>Options:</Typography>
          <Grid container direction="row" spacing={1}>
            <Grid item xs={4}>
              <Stack direction="column">
                <FormControlLabel
                  control={
                    <Checkbox
                      checked={input.attributes.refetchOnWindowFocus?.value ?? true}
                      onChange={handleRefetchOnWindowFocusChange}
                    />
                  }
                  label="Refetch on window focus"
                />
                <FormControlLabel
                  control={
                    <Checkbox
                      checked={input.attributes.refetchOnReconnect?.value ?? true}
                      onChange={handleRefetchOnReconnectChange}
                    />
                  }
                  label="Refetch on network reconnect"
                />
                <TextField
                  InputProps={{
                    startAdornment: <InputAdornment position="start">s</InputAdornment>,
                  }}
                  sx={{ maxWidth: 300 }}
                  type="number"
                  label="Refetch interval"
                  value={refetchIntervalInSeconds(input.attributes.refetchInterval?.value) ?? ''}
                  onChange={handleRefetchIntervalChange}
                />
              </Stack>
            </Grid>
            <Grid item xs={6}>
              <Stack>
                <FormControlLabel
                  label="Transform API response"
                  control={
                    <Checkbox
                      checked={input.attributes.transformEnabled?.value ?? false}
                      onChange={handleTransformEnabledChange}
                      inputProps={{ 'aria-label': 'controlled' }}
                    />
                  }
                />

                <JsExpressionEditor
                  globalScope={{}}
                  value={input.attributes.transform?.value ?? '(data) => {\n  return data;\n}'}
                  onChange={handleTransformFnChange}
                  disabled={!input.attributes.transformEnabled?.value}
                />
              </Stack>
            </Grid>
          </Grid>
          <Divider />
          <Toolbar disableGutters>
            preview
            <LoadingButton
              sx={{ ml: 2 }}
              disabled={previewParams === paramsObject && previewQuery === input}
              loading={queryPreview.isLoading}
              loadingPosition="start"
              onClick={handleUpdatePreview}
              startIcon={<PlayArrowIcon />}
            >
              Run
            </LoadingButton>
          </Toolbar>
          {queryPreview.error ? <ErrorAlert error={queryPreview.error} /> : null}
          {queryPreview.isSuccess ? <JsonView src={queryPreview.data} /> : null}
        </Stack>
      </DialogContent>
      <DialogActions>
        <Button color="inherit" variant="text" onClick={handleClose}>
          Cancel
        </Button>
        <Button onClick={handleRemove}>Remove</Button>
        <Button disabled={isInputSaved} onClick={handleSave}>
          Save
        </Button>
      </DialogActions>
    </Dialog>
  );
}

type DialogState = {
  nodeId?: NodeId;
};

export default function QueryEditor() {
  const dom = useDom();
  const state = usePageEditorState();
  const domApi = useDomApi();

  const [dialogState, setDialogState] = React.useState<DialogState | null>(null);

  const handleEditStateDialogClose = React.useCallback(() => setDialogState(null), []);

  const page = appDom.getNode(dom, state.nodeId, 'page');
  const { queries = [] } = appDom.getChildNodes(dom, page) ?? [];

  const handleCreate = React.useCallback(() => {
    setDialogState({});
  }, []);

  const handleCreated = React.useCallback(
    (node) => {
      domApi.addNode(node, page, 'queries');
      setDialogState({ nodeId: node.id });
    },
    [domApi, page],
  );

  const handleSave = React.useCallback(
    (node: appDom.QueryNode) => {
      domApi.saveNode(node);
    },
    [domApi],
  );

  const handleRemove = React.useCallback(
    (node: appDom.QueryNode) => {
      domApi.removeNode(node.id);
    },
    [domApi],
  );

  const editedNode = dialogState?.nodeId
    ? appDom.getMaybeNode(dom, dialogState.nodeId, 'query')
    : null;

  // To keep it around during closing animation
  const lastEditednode = useLatest(editedNode);

  return (
    <Stack spacing={1} alignItems="start">
      <Button color="inherit" startIcon={<AddIcon />} onClick={handleCreate}>
        Add query
      </Button>
      <List>
        {queries.map((queryNode) => {
          return (
            <ListItem
              key={queryNode.id}
              button
              onClick={() => setDialogState({ nodeId: queryNode.id })}
            >
              {queryNode.name}
            </ListItem>
          );
        })}
      </List>
      {/* eslint-disable-next-line no-nested-ternary */}
      {dialogState?.nodeId && lastEditednode ? (
        <QueryNodeEditorDialog
          open={!!dialogState}
          node={lastEditednode}
          onSave={handleSave}
          onRemove={handleRemove}
          onClose={handleEditStateDialogClose}
        />
      ) : (
        <ConnectionSelectorDialog
          open={!!dialogState}
          onCreated={handleCreated}
          onClose={handleEditStateDialogClose}
        />
      )}
    </Stack>
  );
}