/*
 * Copyright © 2024 Hexastack. All rights reserved.
 *
 * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
 * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
 * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
 */

import { Add, MoveUp } from "@mui/icons-material";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import FitScreenIcon from "@mui/icons-material/FitScreen";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import {
  Box,
  Button,
  ButtonGroup,
  Grid,
  Tab,
  Tabs,
  Tooltip,
  tabsClasses,
} from "@mui/material";
import {
  DefaultPortModel,
  DiagramEngine,
  DiagramModel,
  DiagramModelGenerics,
} from "@projectstorm/react-diagrams";
import { useRouter } from "next/router";
import {
  SyntheticEvent,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { useQueryClient } from "react-query";

import { DeleteDialog } from "@/app-components/dialogs";
import { MoveDialog } from "@/app-components/dialogs/MoveDialog";
import { CategoryDialog } from "@/components/categories/CategoryDialog";
import { isSameEntity } from "@/hooks/crud/helpers";
import { useDeleteFromCache } from "@/hooks/crud/useDelete";
import { useDeleteMany } from "@/hooks/crud/useDeleteMany";
import { useFind } from "@/hooks/crud/useFind";
import { useGetFromCache } from "@/hooks/crud/useGet";
import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate";
import { useUpdateMany } from "@/hooks/crud/useUpdateMany";
import useDebouncedUpdate from "@/hooks/useDebouncedUpdate";
import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
import { useSearch } from "@/hooks/useSearch";
import { useTranslate } from "@/hooks/useTranslate";
import { EntityType, Format, QueryType, RouterType } from "@/services/types";
import { IBlock } from "@/types/block.types";
import { ICategory } from "@/types/category.types";
import { BlockPorts } from "@/types/visual-editor.types";

import BlockDialog from "../BlockDialog";
import { ZOOM_LEVEL } from "../constants";
import { useVisualEditor } from "../hooks/useVisualEditor";

import { AdvancedLinkModel } from "./AdvancedLink/AdvancedLinkModel";

const Diagrams = () => {
  const { t } = useTranslate();
  const router = useRouter();
  const flowId = router.query.id?.toString();
  const [model, setModel] = useState<
    DiagramModel<DiagramModelGenerics> | undefined
  >();
  const [engine, setEngine] = useState<DiagramEngine | undefined>();
  const [canvas, setCanvas] = useState<JSX.Element | undefined>();
  const [selectedBlockId, setSelectedBlockId] = useState<string | undefined>();
  const deleteDialogCtl = useDialog<string[]>(false);
  const moveDialogCtl = useDialog<string[] | string>(false);
  const addCategoryDialogCtl = useDialog<ICategory>(false);
  const { mutateAsync: updateBlocks } = useUpdateMany(EntityType.BLOCK);
  const {
    buildDiagram,
    setViewerZoom,
    setViewerOffset,
    setSelectedCategoryId,
    selectedCategoryId,
    createNode,
  } = useVisualEditor();
  const editDialogCtl = useDialog<IBlock>(false);
  const { searchPayload } = useSearch<IBlock>({
    $eq: [{ category: selectedCategoryId }],
  });
  const { data: categories } = useFind(
    { entity: EntityType.CATEGORY },
    {
      hasCount: false,
      initialSortState: [{ field: "createdAt", sort: "asc" }],
    },
    {
      onSuccess([{ id, zoom, offset }]) {
        if (flowId) {
          setSelectedCategoryId?.(flowId);
        } else if (id) {
          setSelectedCategoryId?.(id);
          if (engine?.getModel()) {
            setViewerOffset(offset || [0, 0]);
            setViewerZoom(zoom || 100);
          }
        }
      },
    },
  );
  const currentCategory = categories.find(
    ({ id }) => id === selectedCategoryId,
  );
  const { mutateAsync: updateCategory } = useUpdate(EntityType.CATEGORY, {
    invalidate: false,
  });
  const { mutateAsync: deleteBlocks } = useDeleteMany(EntityType.BLOCK, {
    onSuccess: () => {
      deleteDialogCtl.closeDialog();
      setSelectedBlockId(undefined);
    },
  });
  const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, {
    invalidate: false,
  });
  const debouncedUpdateCategory = useDebouncedUpdate(updateCategory, 300);
  const debouncedZoomEvent = useCallback(
    (event: any) => {
      if (selectedCategoryId) {
        engine?.repaintCanvas();
        debouncedUpdateCategory({
          id: selectedCategoryId,
          params: {
            zoom: event.zoom,
          },
        });
      }
      event.stopPropagation();
    },
    [selectedCategoryId, engine, debouncedUpdateCategory],
  );
  const debouncedOffsetEvent = useCallback(
    (event: any) => {
      if (selectedCategoryId) {
        debouncedUpdateCategory({
          id: selectedCategoryId,
          params: {
            offset: [event.offsetX, event.offsetY],
          },
        });
      }
      event.stopPropagation();
    },
    [selectedCategoryId, debouncedUpdateCategory],
  );
  const queryClient = useQueryClient();
  const getBlockFromCache = useGetFromCache(EntityType.BLOCK);
  const updateCachedBlock = useUpdateCache(EntityType.BLOCK);
  const deleteCachedBlock = useDeleteFromCache(EntityType.BLOCK);
  const handleChange = (_event: SyntheticEvent, newValue: number) => {
    if (categories) {
      const { id } = categories[newValue];

      if (id) {
        setSelectedCategoryId?.(id);
        setSelectedBlockId(undefined); // Reset selected block when switching categories, resetting edit & remove buttons

        router.push(`/${RouterType.VISUAL_EDITOR}/flows/${id}`);
      }
    }
  };
  const { data: blocks } = useFind(
    { entity: EntityType.BLOCK, format: Format.FULL },
    { hasCount: false, params: searchPayload },
    {
      enabled: !!selectedCategoryId,
    },
  );
  const deleteCallbackRef = useRef<() => void | null>(() => {});

  useEffect(() => {
    // Case when categories are already cached
    if (categories?.length > 0 && !selectedCategoryId) {
      setSelectedCategoryId(categories[0].id);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (flowId) setSelectedCategoryId(flowId);
    else if (categories?.length) setSelectedCategoryId(categories[0].id);

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [flowId]);

  useEffect(() => {
    const { canvas, model, engine } = buildDiagram({
      zoom: currentCategory?.zoom || 100,
      offset: currentCategory?.offset || [0, 0],
      data: blocks,
      setter: setSelectedBlockId,
      updateFn: updateBlock,
      onRemoveNode: (ids, next) => {
        deleteDialogCtl.openDialog(ids);
        deleteCallbackRef.current = next;
      },
      onDbClickNode: (event, id) => {
        if (id) {
          const block = getBlockFromCache(id);

          editDialogCtl.openDialog(block);
        }
      },
      targetPortChanged: ({
        entity,
        port,
      }: {
        entity: AdvancedLinkModel;
        port: DefaultPortModel;
      }) => {
        const link = model.getLink(entity.getOptions().id as string);

        if (!link) return;

        if (
          !port.getOptions().in ||
          [BlockPorts.nextBlocksOutPort, BlockPorts.attachmentOutPort].includes(
            // @ts-expect-error protected attr
            entity.targetPort.getOptions().label,
          ) ||
          (link.getSourcePort().getType() === "attached" &&
            link.getSourcePort().getParent().getOptions().id ===
              link.getTargetPort().getParent().getOptions().id)
        ) {
          model.removeLink(link);

          return;
        }

        link.setLocked(true);
        link.registerListener({
          selectionChanged(event: any) {
            const { entity, isSelected } = event;

            setSelectedBlockId(isSelected === true && entity.options.id);
          },
        });

        Object.entries(port.links).map(([, val]) => {
          // @ts-expect-error protected attr
          if (val.targetPort?.options?.locked) {
            model.removeLink(val);
          }
        });

        const sourceId = entity.getSourcePort().getParent().getOptions()
          .id as string;
        const targetId = entity.getTargetPort().getParent().getOptions()
          .id as string;
        const previousData = getBlockFromCache(sourceId!);

        if (
          // @ts-expect-error undefined attr
          entity.getSourcePort().getOptions()?.label ===
          BlockPorts.nextBlocksOutPort
        ) {
          const nextBlocks = [
            ...(previousData?.nextBlocks || []),
            ...(targetId ? [targetId] : []),
          ];

          updateBlock(
            {
              id: sourceId,
              params: {
                nextBlocks,
              },
            },
            {
              onSuccess(data) {
                if (data.id)
                  updateCachedBlock({
                    id: targetId,
                    payload: {
                      previousBlocks: [data.id as any],
                    },
                  });
              },
            },
          );
        } else if (
          // @ts-expect-error undefined attr
          entity.getSourcePort().getOptions().label ===
          BlockPorts.attachmentOutPort
        ) {
          updateBlock({
            id: sourceId,
            params: {
              attachedBlock: targetId,
            },
          });
        }
      },
    });

    setModel(model);
    setEngine(engine);
    setCanvas(canvas);

    model.registerListener({
      zoomUpdated: debouncedZoomEvent,
      offsetUpdated: debouncedOffsetEvent,
    });
  }, [
    selectedCategoryId,
    JSON.stringify(
      blocks.map((b) => {
        return { ...b, position: undefined, updatedAt: undefined };
      }),
    ),
  ]);

  const handleLinkDeletion = async (linkId: string) => {
    const link = model?.getLink(linkId) as any;
    const sourceId = link?.sourcePort.parent.options.id;
    const targetId = link?.targetPort.parent.options.id;

    if (link?.sourcePort.options.label === BlockPorts.nextBlocksOutPort) {
      await removeNextBlockLink(sourceId, targetId);
    } else if (
      link?.sourcePort.options.label === BlockPorts.attachmentOutPort
    ) {
      await removeAttachmentLink(sourceId, targetId);
    }
  };
  const removeNextBlockLink = async (sourceId: string, targetId: string) => {
    const previousData = getBlockFromCache(sourceId);
    const nextBlocks = [...(previousData?.nextBlocks || [])];

    await updateBlock(
      {
        id: sourceId,
        params: {
          nextBlocks: nextBlocks.filter((block) => block !== targetId),
        },
      },
      {
        onSuccess() {
          updateCachedBlock({
            id: targetId,
            preprocess: ({ previousBlocks = [], ...rest }) => ({
              ...rest,
              previousBlocks: previousBlocks.filter(
                (block) => block !== sourceId,
              ),
            }),
          });
        },
      },
    );
  };
  const removeAttachmentLink = async (sourceId: string, targetId: string) => {
    await updateBlock(
      {
        id: sourceId,
        params: { attachedBlock: null },
      },
      {
        onSuccess() {
          updateCachedBlock({
            id: targetId,
            preprocess: (oldData) => ({ ...oldData, attachedToBlock: null }),
          });
        },
      },
    );
  };
  const handleBlocksDeletion = async (blockIds: string[]) => {
    await deleteBlocks(blockIds, {
      onSuccess: () => {
        blockIds.forEach((blockId) => {
          const block = getBlockFromCache(blockId);

          if (block) {
            updateLinkedBlocks(block, blockIds);
            deleteCachedBlock(blockId);
          }
        });
      },
    });
  };
  const getLinkedBlockIds = (block: IBlock): string[] => [
    ...(block?.nextBlocks || []),
    ...(block?.previousBlocks || []),
    ...(block?.attachedBlock ? [block.attachedBlock] : []),
    ...(block?.attachedToBlock ? [block.attachedToBlock] : []),
  ];
  const updateLinkedBlocks = (block: IBlock, deletedIds: string[]) => {
    const linkedBlockIds = getLinkedBlockIds(block);

    linkedBlockIds.forEach((linkedBlockId) => {
      const linkedBlock = getBlockFromCache(linkedBlockId);

      if (linkedBlock) {
        updateCachedBlock({
          id: linkedBlock.id,
          payload: {
            ...linkedBlock,
            nextBlocks: linkedBlock.nextBlocks?.filter(
              (nextBlockId) => !deletedIds.includes(nextBlockId),
            ),
            previousBlocks: linkedBlock.previousBlocks?.filter(
              (previousBlockId) => !deletedIds.includes(previousBlockId),
            ),
            attachedBlock: deletedIds.includes(linkedBlock.attachedBlock || "")
              ? undefined
              : linkedBlock.attachedBlock,
            attachedToBlock: deletedIds.includes(
              linkedBlock.attachedToBlock || "",
            )
              ? undefined
              : linkedBlock.attachedToBlock,
          },
          strategy: "overwrite",
        });
      }
    });
  };
  const cleanupAfterDeletion = () => {
    deleteCallbackRef.current?.();
    deleteCallbackRef.current = () => {};
    deleteDialogCtl.closeDialog();
  };
  const handleDeleteButton = () => {
    const selectedEntities = engine?.getModel().getSelectedEntities();
    const ids = selectedEntities?.map((model) => model.getID());

    if (ids && selectedEntities && ids.length > 0) {
      deleteCallbackRef.current = () => {
        selectedEntities.forEach((model) => {
          model.setLocked(false);
          model.remove();
        });
        engine?.repaintCanvas();
      };
      deleteDialogCtl.openDialog(ids);
    }
  };
  const handleMoveButton = () => {
    const selectedEntities = engine?.getModel().getSelectedEntities().reverse();
    const ids = selectedEntities?.map((model) => model.getID());

    if (ids && selectedEntities) {
      moveDialogCtl.openDialog(ids);
    }
  };
  const onDelete = async () => {
    const ids = deleteDialogCtl?.data;

    if (!ids || ids?.length === 0) {
      return;
    }
    const isLink = ids[0].length === 36;

    if (isLink) {
      await handleLinkDeletion(ids[0]);
    } else {
      await handleBlocksDeletion(ids);
    }

    cleanupAfterDeletion();
  };
  const onMove = async (newCategoryId?: string) => {
    if (!newCategoryId) {
      return;
    }

    const ids = moveDialogCtl?.data;

    if (ids?.length && Array.isArray(ids)) {
      await updateBlocks({ ids, payload: { category: newCategoryId } });

      queryClient.invalidateQueries({
        predicate: ({ queryKey }) => {
          const [qType, qEntity] = queryKey;

          return (
            qType === QueryType.collection &&
            isSameEntity(qEntity, EntityType.BLOCK)
          );
        },
      });

      setSelectedCategoryId(newCategoryId);
      setSelectedBlockId(undefined);
      moveDialogCtl.closeDialog();
    }
  };

  return (
    <div
      className="visual-editor"
      id="visual-editor"
      onDrop={(event) => {
        const data = JSON.parse(
          event.dataTransfer.getData("storm-diagram-node"),
        );

        if (!data) {
          // eslint-disable-next-line no-console
          console.warn("Unable to handle the drop event");

          return;
        }

        const payload = {
          ...data,
          category: selectedCategoryId || "",
          position: engine?.getRelativeMousePoint(event),
        };

        createNode(payload);
      }}
      onDragOver={(event) => {
        event.preventDefault();
      }}
    >
      <Box sx={{ width: "100%" }}>
        <CategoryDialog {...getDisplayDialogs(addCategoryDialogCtl)} />
        <BlockDialog {...getDisplayDialogs(editDialogCtl)} />
        <DeleteDialog<string[]> {...deleteDialogCtl} callback={onDelete} />
        <MoveDialog
          open={moveDialogCtl.open}
          openDialog={moveDialogCtl.openDialog}
          callback={onMove}
          closeDialog={moveDialogCtl.closeDialog}
          categories={categories}
        />
        <Grid sx={{ bgcolor: "#fff", padding: "0" }}>
          <Grid
            sx={{
              display: "flex",
              position: "relative",
              flexDirection: "row",
              borderBottom: "1.5px solid #DDDDDD",
            }}
          >
            <Tabs
              value={
                currentCategory
                  ? categories?.findIndex(({ id }) => id === selectedCategoryId)
                  : 0
              }
              onChange={handleChange}
              sx={{
                backgroundColor: "#fff",
                [`& .${tabsClasses.indicator}`]: {
                  display: "none",
                },
                "& .MuiTabs-scrollButtons": {
                  opacity: 0.8,
                  backgroundColor: "#1ca089",
                  borderTop: "1px solid #137261",
                  marginTop: "7px",
                  color: "#FFF",
                  overflow: "visible",
                  boxShadow:
                    "-20px 0px 20px -20px rgba(0, 0, 0, 0.5), 0px 2px 9px 0px rgba(0, 0, 0, 0.5)",
                  zIndex: 10,
                  "&:hover": {
                    opacity: 1,
                  },
                },
              }}
              variant="scrollable"
              allowScrollButtonsMobile
            >
              {categories?.map(({ id, label }) => (
                <Tab
                  sx={{
                    mt: "7px",
                    ml: "5px",
                    border: "1px solid #DDDDDD",
                    backgroundColor: "#F8F8F8",
                    borderBottom: "none",
                    minHeight: "30px",

                    "&.Mui-selected": {
                      backgroundColor: "#EAF1F1",
                      zIndex: 1,
                      color: "#000",

                      backgroundSize: "20px 20px",
                      backgroundAttachment: "fixed",
                      backgroundPosition: "-1px -1px",
                    },
                  }}
                  key={id}
                  label={
                    <Grid
                      sx={{
                        alignItems: "center",
                        justifyContent: "center",
                        display: "flex",
                      }}
                    >
                      {label}
                    </Grid>
                  }
                />
              ))}
            </Tabs>
            <Button
              sx={{
                mt: "7px",
                ml: "5px",
                borderRadius: "0",
                minHeight: "30px",

                border: "1px solid #DDDDDD",
                backgroundColor: "#F8F8F8",
                borderBottom: "none",
                width: "42px",
                minWidth: "42px",
              }}
              onClick={(e) => {
                addCategoryDialogCtl.openDialog();
                e.preventDefault();
              }}
            >
              <Add />
            </Button>
          </Grid>
          <Grid container>
            <Grid
              sx={{
                left: 240,
                top: 140,
                zIndex: 6,
                position: "absolute",
                display: "flex",
                flexDirection: "row",
                gap: "8px",
              }}
            >
              <Button
                sx={{}}
                size="small"
                variant="contained"
                startIcon={<EditIcon />}
                onClick={() => {
                  if (selectedBlockId) {
                    const block = getBlockFromCache(selectedBlockId);

                    editDialogCtl.openDialog(block);
                  }
                }}
                disabled={!selectedBlockId || selectedBlockId.length !== 24}
              >
                {t("button.edit")}
              </Button>
              <Button
                size="small"
                variant="contained"
                startIcon={<MoveUp />}
                onClick={handleMoveButton}
                disabled={!selectedBlockId || selectedBlockId.length !== 24}
              >
                {t("button.move")}
              </Button>
              <Button
                sx={{}}
                size="small"
                variant="contained"
                color="secondary"
                startIcon={<DeleteIcon />}
                onClick={handleDeleteButton}
                disabled={!selectedBlockId}
              >
                {t("button.remove")}
              </Button>
            </Grid>
            <Grid container item justifyContent="right" xs alignSelf="center">
              <ButtonGroup
                orientation="vertical"
                color="inherit"
                variant="contained"
                sx={{
                  left: 240,
                  bottom: 20,
                  zIndex: 6,
                  position: "absolute",
                  transition: "all 0.3s",
                  boxShadow: "0 0 8px #0003",
                  borderRadius: "20px",
                  backgroundColor: "#fffc",
                  "&.MuiButtonGroup-contained:hover": {
                    boxShadow: "0 0 8px #0005",
                  },

                  "& .MuiButton-root": {
                    backgroundColor: "background.paper",
                  },
                }}
              >
                <Tooltip
                  title={t("visual_editor.zoom.reset")}
                  arrow
                  placement="left"
                >
                  <span>
                    <Button
                      sx={{ p: "8px 2px 6px 2px" }}
                      disabled={
                        !!currentCategory?.zoom && currentCategory?.zoom === 100
                      }
                      onClick={() => {
                        setViewerZoom(100);
                      }}
                    >
                      <RestartAltIcon />
                    </Button>
                  </span>
                </Tooltip>
                <Tooltip
                  title={t("visual_editor.zoom.in")}
                  arrow
                  placement="left"
                >
                  <span>
                    <Button
                      sx={{ p: "6px 2px" }}
                      disabled={
                        !!currentCategory?.zoom &&
                        currentCategory.zoom >= ZOOM_LEVEL.maximum
                      }
                      onClick={() => {
                        if (model) {
                          const currentZoom = model.getZoomLevel();

                          setViewerZoom(currentZoom + ZOOM_LEVEL.step);
                        }
                      }}
                    >
                      <ZoomInIcon />
                    </Button>
                  </span>
                </Tooltip>
                <Tooltip
                  title={t("visual_editor.zoom.out")}
                  arrow
                  placement="left"
                >
                  <span>
                    <Button
                      sx={{ p: "6px 2px" }}
                      disabled={
                        !!currentCategory?.zoom &&
                        currentCategory?.zoom <= ZOOM_LEVEL.minimum
                      }
                      onClick={() => {
                        if (model) {
                          const currentZoom = model.getZoomLevel();

                          setViewerZoom(currentZoom - ZOOM_LEVEL.step);
                        }
                      }}
                    >
                      <ZoomOutIcon />
                    </Button>
                  </span>
                </Tooltip>
                <Tooltip
                  title={t("visual_editor.zoom.fitScreen")}
                  arrow
                  placement="left"
                >
                  <span>
                    <Button
                      sx={{ p: "6px 2px 8px 2px" }}
                      onClick={() => {
                        engine?.zoomToFitSelectedNodes({ margin: 0 });
                      }}
                    >
                      <FitScreenIcon />
                    </Button>
                  </span>
                </Tooltip>
              </ButtonGroup>
            </Grid>
          </Grid>
        </Grid>
      </Box>
      {canvas}
    </div>
  );
};

export default Diagrams;
