import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import LinkOffIcon from "@mui/icons-material/LinkOff";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh";
import SearchIcon from "@mui/icons-material/Search";
import {
  Checkbox,
  FormControlLabel,
  IconButton,
  ListItemIcon,
  ListItemText,
  Menu,
  MenuItem,
  TextField,
  Tooltip,
} from "@mui/material";
import {
  DataGrid,
  GridActionsCellItem,
  GridColDef,
  GridRowSelectionModel,
} from "@mui/x-data-grid";
import React from "react";
import { InboxApi, InboxEntry } from "../api/InboxApi";
import { Movement, MovementApi } from "../api/MovementsApi";
import { AttachInboxEntryToMovementDialog } from "../dialogs/AttachInboxEntryToMovementDialog";
import { AttachMultipleInboxEntriesDialog } from "../dialogs/AttachMultipleInboxEntriesDialog";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useSelectAccount } from "../hooks/context_providers/ChooseAccountDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../hooks/context_providers/LoadingMessageProvider";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
import { useUnmatchedInboxEntriesCount } from "../hooks/UnmatchedInboxEntriesCountProvider";
import { AmountWidget } from "../widgets/AmountWidget";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { DateWidget } from "../widgets/DateWidget";
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
import { AsyncMovementWidget } from "../widgets/MovementWidget";
import { NewMovementWidget } from "../widgets/NewMovementWidget";
import { UploadedFileWidget } from "../widgets/UploadedFileWidget";

export function InboxRoute(): React.ReactElement {
  const loadingMessage = useLoadingMessage();
  const alert = useAlert();

  const unmatched = useUnmatchedInboxEntriesCount();

  const [entries, setEntries] = React.useState<InboxEntry[] | undefined>();
  const [includeAttached, setIncludeAttached] = React.useState(false);

  const loadKey = React.useRef(1);

  const load = async () => {
    setEntries(await InboxApi.GetList(includeAttached));
  };

  const reload = async (skipEntries: boolean) => {
    try {
      loadingMessage.show("Refreshing the list of inbox entries...");
      unmatched.reload();
      if (!skipEntries) await load();
    } catch (e) {
      console.error("Failed to load list of inbox entries!", e);
      alert(`Failed to refresh the list of inbox entries! ${e}`);
    } finally {
      loadingMessage.hide();
    }
  };

  return (
    <MoneyMgrWebRouteContainer
      label={"Inbox"}
      actions={
        <span>
          <FormControlLabel
            checked={includeAttached}
            control={
              <Checkbox
                onChange={(_e, value) => {
                  setIncludeAttached(value);
                }}
              />
            }
            label="Include attached"
          />
        </span>
      }
    >
      <div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
        <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
          <AsyncWidget
            loadKey={`${loadKey.current}/${includeAttached}`}
            load={load}
            errMsg="Failed to load the content of inbox!"
            build={() => (
              <InboxTable
                entries={entries!}
                onDeleteEntry={(del) => {
                  setEntries((entries) => {
                    return entries?.filter((m) => m.id !== del.id);
                  });
                }}
                onReload={reload}
                showMovements={includeAttached}
              />
            )}
          />
          <NewMovementWidget isInbox onCreated={() => reload(false)} />
        </div>
      </div>
    </MoneyMgrWebRouteContainer>
  );
}

function InboxTable(p: {
  entries: InboxEntry[];
  onDeleteEntry: (entry: InboxEntry) => void;
  onReload: (skipEntries: boolean) => void;
  showMovements?: boolean;
}): React.ReactElement {
  const alert = useAlert();
  const confirm = useConfirm();
  const snackbar = useSnackbar();
  const loadingMessage = useLoadingMessage();
  const selectAccount = useSelectAccount();

  const [labelFilter, setLabelFilter] = React.useState("");

  const filteredList = React.useMemo(() => {
    return p.entries.filter((m) =>
      (m.label ?? "").toLowerCase().includes(labelFilter.toLowerCase())
    );
  }, [p.entries, labelFilter]);

  const [rowSelectionModel, setRowSelectionModel] =
    React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });

  const [attaching, setAttaching] = React.useState<InboxEntry | undefined>();

  // Request to attach entry to movement
  const handleAttachClick = (entry: InboxEntry) => {
    setAttaching(entry);
  };

  const handleCloseAttachDialog = () => {
    setAttaching(undefined);
  };

  // Finish attach entry to movement
  const performAttach = async (movement: Movement) => {
    try {
      loadingMessage.show("Attaching inbox entry to movement...");

      await Promise.all([
        // Update movement
        MovementApi.Update({
          ...movement,
          file_id: attaching?.file_id,
        }),

        // Update inbox entries
        InboxApi.Update({
          ...attaching!,
          movement_id: movement.id,
        }),
      ]);

      snackbar(
        "The inbox entry has been successfully attached to the movement!"
      );

      setAttaching(undefined);

      p.onReload(false);
    } catch (e) {
      console.error("Failed to attach inbox entry to movement!", e);
      alert(`Failed to attach inbox entry to movement! ${e}`);
    } finally {
      loadingMessage.hide();
    }
  };

  // Delete inbox entry
  const handleDeleteClick = async (entry: InboxEntry) => {
    try {
      if (
        !(await confirm(
          `Do you really want to delete this inbox entry ${
            entry.label ?? ""
          } (${entry.amount ?? 0} €)?`
        ))
      )
        return;

      await InboxApi.Delete(entry);

      p.onDeleteEntry(entry);

      p.onReload(true);
    } catch (e) {
      console.error("Failed to delete entry!", e);
      alert(`Failed to delete entry! ${e}`);
    }
  };

  // Detach inbox entry from movement
  const handleDetachClick = async (entry: InboxEntry) => {
    try {
      if (
        !(await confirm(
          `Do you really want to detach this inbox entry ${
            entry.label ?? ""
          } (${entry.amount ?? 0} €) from its movement?`,
          "Detach inbox entry from movement",
          "Detach"
        ))
      )
        return;

      entry.movement_id = undefined;

      await InboxApi.Update(entry);

      p.onReload(true);
    } catch (e) {
      console.error("Failed to detach movement from entry!", e);
      alert(`Failed to detach movement from entry! ${e}`);
    }
  };

  // Attach multiple entries to movements
  const [attachMultipleEntries, setAttachMultipleEntries] =
    React.useState<InboxEntry[]>();
  const [attachMultipleMovements, setAttachMultipleMovements] =
    React.useState<Movement[][]>();
  const attachMultiple = async () => {
    try {
      const targetAccount = await selectAccount(
        "Attach multiple entries",
        "Select the target account for automatic attaching",
        "Start search"
      );
      if (!targetAccount) return;

      // Find the entry to map
      const entries = p.entries.filter(
        (m) => rowSelectionModel.ids.has(m.id) && !m.movement_id
      );
      const movements: Movement[][] = [];

      // Search for applicable movements
      for (const [num, e] of entries.entries()) {
        loadingMessage.show(
          `Searching for proper movements ${num}/${entries.length}`
        );

        movements.push(
          await MovementApi.GetAccountMovements(targetAccount.id, {
            amount_min: e.amount ? e.amount - 0.5 : undefined,
            amount_max: e.amount ? e.amount + 0.5 : undefined,
            label: e.label,
            limit: 5,
            time_min: e.time ? e.time - 3600 * 24 : undefined,
            time_max: e.time ? e.time + 3600 * 24 : undefined,
          })
        );
      }

      setAttachMultipleEntries(entries);
      setAttachMultipleMovements(movements);
    } catch (e) {
      console.error("Failed to attach multiple accounts to movements!", e);
      alert(`Failed to attach multiple accounts to movements! ${e}`);
    } finally {
      loadingMessage.hide();
    }
  };

  const handleCancelAttachMultiple = () => {
    setAttachMultipleEntries(undefined);
    setAttachMultipleMovements(undefined);
  };

  const handlePerformAttachMultiple = async (
    mapping: (Movement | undefined)[]
  ) => {
    try {
      for (const [num, entry] of attachMultipleEntries!.entries()) {
        loadingMessage.show(`Attaching ${num} of ${mapping.length}`);

        const movement = mapping[num];
        if (!movement) continue;

        await Promise.all([
          // Update movement
          MovementApi.Update({
            ...movement,
            file_id: entry.file_id,
          }),

          // Update inbox entries
          InboxApi.Update({
            ...entry,
            movement_id: movement.id,
          }),
        ]);
      }

      setAttachMultipleEntries(undefined);
      setAttachMultipleMovements(undefined);

      snackbar("Mapping successfully performed!");

      p.onReload(false);
    } catch (e) {
      console.error("Performing mapping...", e);
      alert(`Failed to perform mapping! ${e}`);
    } finally {
      loadingMessage.hide();
    }
  };

  // Delete multiple inbox entries
  const deleteMultiple = async () => {
    try {
      const deletedEntries = p.entries.filter((m) =>
        rowSelectionModel.ids.has(m.id)
      );

      if (
        !(await confirm(
          <>
            Do you really want to delete the following inbox entries:
            <ul>
              {deletedEntries.map((m) => (
                <li key={m.id}>
                  {m.label ?? "Label unspecified"} ({m.amount ?? 0} €)
                </li>
              ))}
            </ul>
          </>
        ))
      )
        return;

      for (const [num, e] of deletedEntries.entries()) {
        loadingMessage.show(
          `Deleting inbox entry ${num}/${deletedEntries.length}`
        );

        await InboxApi.Delete(e);

        p.onDeleteEntry(e);
      }

      snackbar("The inbox entries have been successfully deleted!");

      p.onReload(false);
    } catch (e) {
      console.error("Failed to delete multiple inbox entries!", e);
      alert(`Failed to delete multiple inbox entries! ${e}`);
    } finally {
      loadingMessage.hide();
    }
  };

  const columns: GridColDef<(typeof p.entries)[number]>[] = [
    {
      field: "time",
      headerName: "Date",
      width: 98 + 80,
      editable: true,
      type: "dateTime",
      valueGetter(_, m) {
        return new Date(m.time * 1000);
      },
      valueSetter(v, row) {
        row.time = Math.floor(v.getTime() / 1000);
        return row;
      },
      renderCell: (params) => {
        return <DateWidget time={params.row.time} />;
      },
    },
    {
      field: "label",
      headerName: "Label",
      flex: 3,
      editable: true,
      type: "string",
    },
    {
      field: "amount",
      headerName: "Amount",
      width: 110,
      editable: true,
      type: "number",
      align: "left",
      headerAlign: "left",
      renderCell: (params) => {
        if (params.row.amount)
          return <AmountWidget amount={params.row.amount} />;
        else return <i>Unspecified</i>;
      },
    },
    {
      field: "file",
      headerName: "File",
      editable: false,
      flex: 3,
      renderCell: (params) => {
        return <UploadedFileWidget file_id={params.row.file_id} />;
      },
    },
    {
      field: "movement_id",
      headerName: "Movement",
      editable: false,
      flex: 3,
      renderCell: (params) => {
        if (params.row.movement_id)
          return <AsyncMovementWidget id={params.row.movement_id} />;
      },
    },
    {
      field: "actions",
      type: "actions",
      headerName: "",
      width: 82,
      cellClassName: "actions",
      editable: false,
      getActions: (params) => {
        return [
          <Tooltip key="attach" title="Attach entry to movement">
            <GridActionsCellItem
              key={`attach-${params.row.id}`}
              icon={<SearchIcon />}
              label="Attach entry to movement"
              color="inherit"
              onClick={() => {
                handleAttachClick(params.row);
              }}
              disabled={!!params.row.movement_id}
            />
          </Tooltip>,
          <InboxEntryActionMenu
            key="menu"
            entry={params.row}
            onDelete={handleDeleteClick}
            onDetach={handleDetachClick}
          />,
        ];
      },
    },
  ];

  return (
    <>
      {attaching && (
        <AttachInboxEntryToMovementDialog
          open
          entry={attaching}
          onSelected={performAttach}
          onClose={handleCloseAttachDialog}
        />
      )}

      {attachMultipleEntries && attachMultipleMovements && (
        <AttachMultipleInboxEntriesDialog
          open
          entries={attachMultipleEntries}
          movements={attachMultipleMovements}
          onClose={handleCancelAttachMultiple}
          onSelected={handlePerformAttachMultiple}
        />
      )}

      <div style={{ display: "flex" }}>
        <TextField
          placeholder="Filter by label"
          variant="standard"
          size="small"
          value={labelFilter}
          onChange={(e) => {
            setLabelFilter(e.target.value);
          }}
          style={{ padding: "0px", flex: 1 }}
        />
        <span style={{ flex: 1 }}></span>
        <Tooltip title="Refresh table">
          <IconButton
            onClick={() => {
              p.onReload(false);
            }}
          >
            <RefreshIcon />
          </IconButton>
        </Tooltip>
        <Tooltip title="Attach all the selected inbox entries to movements">
          <IconButton
            disabled={rowSelectionModel.ids.size === 0}
            onClick={attachMultiple}
          >
            <SearchIcon />
          </IconButton>
        </Tooltip>
        <Tooltip title="Delete all the selected inbox entries">
          <IconButton
            disabled={
              rowSelectionModel.ids.size === 0 ||
              rowSelectionModel.ids.size === p.entries.length
            }
            onClick={deleteMultiple}
          >
            <DeleteIcon />
          </IconButton>
        </Tooltip>
      </div>
      <div style={{ flex: 1 }}>
        <DataGrid<InboxEntry>
          columns={columns}
          rows={filteredList}
          autoPageSize
          checkboxSelection
          initialState={{
            sorting: {
              sortModel: [{ field: "time", sort: "desc" }],
            },
            columns: {
              columnVisibilityModel: {
                movement_id: p.showMovements!,
              },
            },
          }}
          onRowSelectionModelChange={(newRowSelectionModel) => {
            setRowSelectionModel(newRowSelectionModel);
          }}
          rowSelectionModel={rowSelectionModel}
          processRowUpdate={async (n) => {
            try {
              return await InboxApi.Update(n);
            } catch (e) {
              console.error("Failed to update inbox entry information!", e);
              alert(`Failed to update row! ${e}`);
              throw e;
            } finally {
              p.onReload(true);
            }
          }}
        />
      </div>
    </>
  );
}

function InboxEntryActionMenu(p: {
  entry: InboxEntry;
  onDelete: (entry: InboxEntry) => void;
  onDetach: (entry: InboxEntry) => void;
}): React.ReactElement {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };
  return (
    <>
      <IconButton
        aria-label="Actions"
        aria-haspopup="true"
        onClick={handleClick}
      >
        <MoreVertIcon />
      </IconButton>
      <Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
        {/* Unlink entry */}
        {p.entry.movement_id && (
          <MenuItem
            onClick={() => {
              handleClose();
              p.onDetach(p.entry);
            }}
          >
            <ListItemIcon>
              <LinkOffIcon />
            </ListItemIcon>
            <ListItemText secondary={"Detach the entry from its movement"}>
              Detach from movement
            </ListItemText>
          </MenuItem>
        )}

        {/* Delete */}
        <MenuItem
          onClick={() => {
            handleClose();
            p.onDelete(p.entry);
          }}
        >
          <ListItemIcon>
            <DeleteIcon color="error" />
          </ListItemIcon>
          <ListItemText secondary={"Delete the entry"}>Delete</ListItemText>
        </MenuItem>
      </Menu>
    </>
  );
}
