import { Boundary } from '@lib/components/Boundary';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { t } from '@lingui/core/macro';
import {
  Alert,
  Button,
  CloseButton,
  Group,
  List,
  ListItem,
  Overlay,
  Stack,
  Tabs
} from '@mantine/core';
import { openConfirmModal } from '@mantine/modals';
import {
  hideNotification,
  notifications,
  showNotification
} from '@mantine/notifications';
import {
  IconAlertTriangle,
  IconDeviceFloppy,
  IconExclamationCircle,
  IconRefresh
} from '@tabler/icons-react';
import Split from '@uiw/react-split';
import type React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api } from '../../../App';
import type { TemplateI } from '../../../tables/settings/TemplateTable';
import { SplitButton } from '../../buttons/SplitButton';
import { StandaloneField } from '../../forms/StandaloneField';

type EditorProps = {
  template: TemplateI;
};

type EditorRef = {
  setCode: (code: string) => void | Promise<void>;
  getCode: () => (string | undefined) | Promise<string | undefined>;
};

export type EditorComponent = React.ForwardRefExoticComponent<
  EditorProps & React.RefAttributes<EditorRef>
>;

export type Editor = {
  key: string;
  name: string;
  icon?: React.ReactNode;
  component: EditorComponent;
};

type PreviewAreaProps = {};

export type PreviewAreaRef = {
  updatePreview: (
    code: string,
    previewItem: string,
    saveTemplate: boolean,
    templateEditorProps: TemplateEditorProps
  ) => void | Promise<void>;
};

export type PreviewAreaComponent = React.ForwardRefExoticComponent<
  PreviewAreaProps & React.RefAttributes<PreviewAreaRef>
>;

export type PreviewArea = {
  key: string;
  name: string;
  icon: React.ReactNode;
  component: PreviewAreaComponent;
};

export type TemplateEditorProps = {
  templateUrl: string;
  printingUrl?: string;
  editors: Editor[];
  previewAreas: PreviewArea[];
  template: TemplateI;
  // Name of the file field on the template model (e.g. 'template' or 'snippet')
  fileField?: string;
};

export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
  const {
    templateUrl,
    editors,
    previewAreas,
    template,
    fileField = 'template'
  } = props;
  const editorRef = useRef<EditorRef | null>(null);
  const previewRef = useRef<PreviewAreaRef | null>(null);

  // If no preview areas are provided, the template is saved directly
  const hasPreview = previewAreas.length > 0;

  const [hasSaveConfirmed, setHasSaveConfirmed] = useState(false);

  const [previewItem, setPreviewItem] = useState<string>('');
  const [renderingErrors, setRenderingErrors] = useState<string[] | null>(null);
  const [isPreviewLoading, setIsPreviewLoading] = useState(false);
  const [isSaving, setIsSaving] = useState(false);

  const [editorValue, setEditorValue] = useState<null | string>(editors[0].key);
  const [previewValue, setPreviewValue] = useState<null | string>(
    previewAreas[0]?.key ?? null
  );

  const codeRef = useRef<string | undefined>(undefined);

  const loadCodeToEditor = useCallback(async (code: string) => {
    try {
      return await Promise.resolve(editorRef.current?.setCode(code));
    } catch (e: any) {
      showNotification({
        title: t`Error loading template`,
        message: e?.message ?? e,
        color: 'red'
      });
    }
  }, []);

  const getCodeFromEditor = useCallback(async () => {
    try {
      return await Promise.resolve(editorRef.current?.getCode());
    } catch (e: any) {
      showNotification({
        title: t`Error saving template`,
        message: e?.message ?? e,
        color: 'red'
      });
      return undefined;
    }
  }, []);

  useEffect(() => {
    if (!templateUrl) return;

    api.get(templateUrl).then((response: any) => {
      if (response.data?.[fileField]) {
        // Fetch the raw template file from the server
        // Request that it is provided without any caching,
        // to ensure that we always get the latest version
        api
          .get(response.data[fileField], {
            headers: {
              'Cache-Control': 'no-cache',
              Pragma: 'no-cache',
              Expires: '0'
            }
          })
          .then((res) => {
            codeRef.current = res.data;
            loadCodeToEditor(res.data);
          })
          .catch(() => {
            console.error(
              `ERR: Could not load template from ${response.data[fileField]}`
            );
            codeRef.current = undefined;
            hideNotification('template-load-error');
            showNotification({
              id: 'template-load-error',
              title: t`Error`,
              message: t`Could not load the template from the server.`,
              color: 'red'
            });
          });
      }
    });
  }, [templateUrl]);

  useEffect(() => {
    if (codeRef.current === undefined) return;
    loadCodeToEditor(codeRef.current);
  }, [editorValue]);

  const updatePreview = useCallback(
    async (confirmed: boolean, saveTemplate = true) => {
      if (!confirmed) {
        openConfirmModal({
          title: t`Save & Reload Preview`,
          children: (
            <Alert
              color='yellow'
              icon={<IconAlertTriangle />}
              title={t`Are you sure you want to Save & Reload the preview?`}
            >
              {t`To render the preview the current template needs to be replaced on the server with your modifications which may break the label if it is under active use. Do you want to proceed?`}
            </Alert>
          ),
          labels: {
            confirm: t`Save & Reload`,
            cancel: t`Cancel`
          },
          confirmProps: {
            color: 'yellow'
          },
          onConfirm: () => {
            setHasSaveConfirmed(true);
            updatePreview(true);
          }
        });
        return;
      }

      const code = await getCodeFromEditor();
      if (code === undefined || !previewItem) return;

      setIsPreviewLoading(true);
      Promise.resolve(
        previewRef.current?.updatePreview(
          code,
          previewItem,
          saveTemplate,
          props
        )
      )
        .then(() => {
          setRenderingErrors(null);

          notifications.hide('template-preview');

          showNotification({
            title: t`Preview updated`,
            message: t`The preview has been updated successfully.`,
            color: 'green',
            id: 'template-preview'
          });
        })
        .catch((error) => {
          const msg = error?.message || error?.toString();

          if (msg) {
            if (Array.isArray(msg)) {
              setRenderingErrors(msg);
            } else {
              setRenderingErrors([msg]);
            }
          } else {
            setRenderingErrors([
              t`An unknown error occurred while rendering the preview.`
            ]);
          }
        })
        .finally(() => {
          setIsPreviewLoading(false);
        });
    },
    [previewItem]
  );

  // Save the template file directly to the server, without any preview
  const saveTemplate = useCallback(async () => {
    const code = await getCodeFromEditor();

    if (code === undefined) return;

    const filename =
      (template as any)[fileField]?.split('/').pop() ?? 'template.html';

    const formData = new FormData();
    formData.append(fileField, new File([code], filename));

    setIsSaving(true);

    api
      .patch(templateUrl, formData)
      .then(() => {
        hideNotification('template-save');
        showNotification({
          id: 'template-save',
          title: t`Saved`,
          message: t`Template file has been updated`,
          color: 'green'
        });
      })
      .catch(() => {
        hideNotification('template-save');
        showNotification({
          id: 'template-save',
          title: t`Error`,
          message: t`Could not save the template to the server.`,
          color: 'red'
        });
      })
      .finally(() => {
        setIsSaving(false);
      });
  }, [getCodeFromEditor, template, templateUrl, fileField]);

  const previewApiUrl = useMemo(
    () =>
      ModelInformationDict[template.model_type ?? ModelType.stockitem]
        .api_endpoint,
    [template]
  );

  const templateFilters: Record<string, string> = useMemo(() => {
    // TODO: Extract custom filters from template (make this more generic)
    if (template.model_type === ModelType.stockitem) {
      return { part_detail: 'true' } as Record<string, string>;
    }

    return {};
  }, [template]);

  useEffect(() => {
    if (!hasPreview) return;

    api
      .get(apiUrl(previewApiUrl), { params: { limit: 1, ...templateFilters } })
      .then((res) => {
        if (res.data.results.length === 0) return;
        setPreviewItem(res.data.results[0].pk);
      });
  }, [hasPreview, previewApiUrl, templateFilters]);

  return (
    <Boundary label='TemplateEditor'>
      <Stack style={{ height: '100%', flex: '1' }}>
        <Split visible={hasPreview} style={{ flex: 1 }}>
          <Tabs
            value={editorValue}
            onChange={async (v) => {
              codeRef.current = await getCodeFromEditor();
              setEditorValue(v);
            }}
            keepMounted={false}
            style={{
              minWidth: '300px',
              width: hasPreview ? '50%' : '100%',
              display: 'flex',
              flexDirection: 'column'
            }}
          >
            <Tabs.List>
              {editors.map((Editor, index) => {
                return (
                  <Tabs.Tab
                    key={Editor.key}
                    value={Editor.key}
                    leftSection={Editor.icon}
                  >
                    {Editor.name}
                  </Tabs.Tab>
                );
              })}

              <Group justify='right' style={{ flex: '1' }} wrap='nowrap'>
                {hasPreview ? (
                  <SplitButton
                    loading={isPreviewLoading}
                    defaultSelected='preview_save'
                    name='preview-options'
                    options={[
                      {
                        key: 'preview',
                        name: t`Reload preview`,
                        tooltip: t`Use the currently stored template from the server`,
                        icon: IconRefresh,
                        onClick: () => updatePreview(true, false),
                        disabled: !previewItem || isPreviewLoading
                      },
                      {
                        key: 'preview_save',
                        name: t`Save & Reload Preview`,
                        tooltip: t`Save the current template and reload the preview`,
                        icon: IconDeviceFloppy,
                        onClick: () => updatePreview(hasSaveConfirmed),
                        disabled: !previewItem || isPreviewLoading
                      }
                    ]}
                  />
                ) : (
                  <Button
                    leftSection={<IconDeviceFloppy size={18} />}
                    loading={isSaving}
                    onClick={() => saveTemplate()}
                  >
                    {t`Save`}
                  </Button>
                )}
              </Group>
            </Tabs.List>

            {editors.map((Editor) => (
              <Tabs.Panel
                key={Editor.key}
                value={Editor.key}
                style={{
                  display: 'flex',
                  flex: editorValue === Editor.key ? 1 : 0
                }}
              >
                {/* @ts-ignore-next-line */}
                <Editor.component ref={editorRef} template={props.template} />
              </Tabs.Panel>
            ))}
          </Tabs>

          {hasPreview && (
            <Tabs
              value={previewValue}
              onChange={setPreviewValue}
              keepMounted={false}
              style={{
                minWidth: '200px',
                width: '50%',
                display: 'flex',
                flexDirection: 'column'
              }}
            >
              <Tabs.List>
                {previewAreas.map((PreviewArea) => (
                  <Tabs.Tab
                    key={PreviewArea.key}
                    value={PreviewArea.key}
                    leftSection={PreviewArea.icon}
                  >
                    {PreviewArea.name}
                  </Tabs.Tab>
                ))}
              </Tabs.List>

              <div
                style={{
                  minWidth: '100%',
                  paddingBottom: '10px',
                  paddingTop: '10px'
                }}
              >
                <StandaloneField
                  fieldDefinition={{
                    field_type: 'related field',
                    api_url: apiUrl(previewApiUrl),
                    description: '',
                    label: t`Select instance to preview`,
                    model: template.model_type,
                    value: previewItem,
                    filters: templateFilters,
                    onValueChange: (value) => setPreviewItem(value)
                  }}
                />
              </div>

              {previewAreas.map((PreviewArea) => (
                <Tabs.Panel
                  key={PreviewArea.key}
                  value={PreviewArea.key}
                  style={{
                    display: 'flex',
                    flex: previewValue === PreviewArea.key ? 1 : 0
                  }}
                >
                  <div
                    style={{
                      height: '100%',
                      position: 'relative',
                      display: 'flex',
                      flex: '1'
                    }}
                  >
                    {/* @ts-ignore-next-line */}
                    <PreviewArea.component ref={previewRef} />

                    {renderingErrors && (
                      <Overlay color='red' center blur={0.2}>
                        <CloseButton
                          onClick={() => setRenderingErrors(null)}
                          style={{
                            position: 'absolute',
                            top: '10px',
                            right: '10px',
                            color: '#fff'
                          }}
                          variant='filled'
                        />
                        <Alert
                          color='red'
                          icon={<IconExclamationCircle />}
                          title={t`Error rendering template`}
                          mx='10px'
                        >
                          <List>
                            {renderingErrors.map((error, index) => (
                              <ListItem key={index}>{error}</ListItem>
                            ))}
                          </List>
                        </Alert>
                      </Overlay>
                    )}
                  </div>
                </Tabs.Panel>
              ))}
            </Tabs>
          )}
        </Split>
      </Stack>
    </Boundary>
  );
}
