/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
import { createForm, Field } from '@formily/core';
import { toJS } from '@formily/reactive';
import { ISchema, observer, useField, useForm } from '@formily/react';
import { Alert, App, Button, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
import { cloneDeep, get, set } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

import {
  ActionContextProvider,
  FormProvider,
  SchemaComponent,
  SchemaInitializerItemType,
  Variable,
  css,
  cx,
  useAPIClient,
  useActionContext,
  useCancelAction,
  useCompile,
  usePlugin,
  useResourceActionContext,
} from '@nocobase/client';
import { parse, str2moment } from '@nocobase/utils/client';

import WorkflowPlugin from '..';
import { AddButton } from '../AddNodeContext';
import { useFlowContext } from '../FlowContext';
import { DrawerDescription } from '../components/DrawerDescription';
import { StatusButton } from '../components/StatusButton';
import { JobStatusOptionsMap } from '../constants';
import { useGetAriaLabelOfAddButton } from '../hooks/useGetAriaLabelOfAddButton';
import { lang } from '../locale';
import useStyles from '../style';
import { UseVariableOptions, VariableOption, WorkflowVariableInput } from '../variable';

export type NodeAvailableContext = {
  engine: WorkflowPlugin;
  workflow: object;
  upstream: object;
  branchIndex: number;
};

type Config = Record<string, any>;

type Options = { label: string; value: any }[];

export abstract class Instruction {
  title: string;
  type: string;
  group: string;
  description?: string;
  /**
   * @deprecated migrate to `presetFieldset` instead
   */
  options?: { label: string; value: any; key: string }[];
  fieldset: Record<string, ISchema>;
  /**
   * @experimental
   */
  presetFieldset?: Record<string, ISchema>;
  /**
   * To presentation if the instruction is creating a branch
   * @experimental
   */
  branching?: boolean | Options | ((config: Config) => boolean | Options);
  /**
   * @experimental
   */
  view?: ISchema;
  scope?: Record<string, any>;
  components?: Record<string, any>;
  Component?(props): JSX.Element;
  /**
   * @experimental
   */
  createDefaultConfig?(): Config {
    return {};
  }
  useVariables?(node, options?: UseVariableOptions): VariableOption;
  useScopeVariables?(node, options?): VariableOption[];
  useInitializers?(node): SchemaInitializerItemType | null;
  /**
   * @experimental
   */
  isAvailable?(ctx: NodeAvailableContext): boolean;
  end?: boolean | ((node) => boolean);
  testable?: boolean;
}

function useUpdateAction() {
  const form = useForm();
  const api = useAPIClient();
  const ctx = useActionContext();
  const { refresh } = useResourceActionContext();
  const data = useNodeContext();
  const { workflow } = useFlowContext();
  return {
    async run() {
      if (workflow.executed) {
        message.error(lang('Node in executed workflow cannot be modified'));
        return;
      }
      await form.submit();
      await api.resource('flow_nodes').update?.({
        filterByTk: data.id,
        values: {
          config: form.values,
        },
      });
      form.setInitialValues(toJS(form.values));
      ctx.setFormValueChanged(false);
      ctx.setVisible(false);
      refresh();
    },
  };
}

export const NodeContext = React.createContext<any>({});

export function useNodeContext() {
  return useContext(NodeContext);
}

export function useNodeSavedConfig(keys = []) {
  const node = useNodeContext();
  return keys.some((key) => get(node.config, key) != null);
}

/**
 * @experimental
 */
export function useAvailableUpstreams(node, filter?) {
  const stack: any[] = [];
  if (!node) {
    return [];
  }
  for (let current = node.upstream; current; current = current.upstream) {
    if (typeof filter !== 'function' || filter(current)) {
      stack.push(current);
    }
  }

  return stack;
}

/**
 * @experimental
 */
export function useUpstreamScopes(node) {
  const stack: any[] = [];
  if (!node) {
    return [];
  }

  for (let current = node; current; current = current.upstream) {
    if (current.upstream && current.branchIndex != null) {
      stack.push(current.upstream);
    }
  }

  return stack;
}

export function Node({ data }) {
  const { styles } = useStyles();
  const { getAriaLabel } = useGetAriaLabelOfAddButton(data);
  const workflowPlugin = usePlugin(WorkflowPlugin);
  const { Component = NodeDefaultView, end } = workflowPlugin.instructions.get(data.type) ?? {};
  return (
    <NodeContext.Provider value={data}>
      <div className={cx(styles.nodeBlockClass)}>
        <Component data={data} />
        {!end || (typeof end === 'function' && !end(data)) ? (
          <AddButton aria-label={getAriaLabel()} upstream={data} />
        ) : (
          <div className="end-sign">
            <CloseOutlined />
          </div>
        )}
      </div>
    </NodeContext.Provider>
  );
}

export function RemoveButton() {
  const { t } = useTranslation();
  const api = useAPIClient();
  const { workflow, nodes, refresh } = useFlowContext() ?? {};
  const current = useNodeContext();
  const { modal } = App.useApp();

  const resource = api.resource('flow_nodes');

  const onOk = useCallback(async () => {
    await resource.destroy?.({
      filterByTk: current.id,
    });
    refresh();
  }, [current.id, refresh, resource]);

  const onRemove = useCallback(async () => {
    const usingNodes = nodes.filter((node) => {
      if (node === current) {
        return false;
      }

      const template = parse(node.config);
      const refs = template.parameters.filter(
        ({ key }) => key.startsWith(`$jobsMapByNodeKey.${current.key}.`) || key === `$jobsMapByNodeKey.${current.key}`,
      );
      return refs.length;
    });

    if (usingNodes.length) {
      modal.error({
        title: lang('Can not delete'),
        content: lang(
          'The result of this node has been referenced by other nodes ({{nodes}}), please remove the usage before deleting.',
          { nodes: usingNodes.map((item) => `#${item.id}`).join(', ') },
        ),
      });
      return;
    }

    const hasBranches = !nodes.find((item) => item.upstream === current && item.branchIndex != null);
    const message = hasBranches
      ? t('Are you sure you want to delete it?')
      : lang('This node contains branches, deleting will also be preformed to them, are you sure?');

    modal.confirm({
      title: t('Delete'),
      content: message,
      onOk,
    });
  }, [current, modal, nodes, onOk, t]);

  if (!workflow) {
    return null;
  }

  return workflow.executed ? null : (
    <Button
      type="text"
      shape="circle"
      icon={<DeleteOutlined />}
      onClick={onRemove}
      className="workflow-node-remove-button"
      size="small"
    />
  );
}

export function JobButton() {
  const { execution, setViewJob } = useFlowContext();
  const { jobs } = useNodeContext() ?? {};
  const { styles } = useStyles();

  if (!execution) {
    return null;
  }

  if (!jobs.length) {
    return <StatusButton className={styles.nodeJobButtonClass} disabled />;
  }

  function onOpenJob({ key }) {
    const job = jobs.find((item) => item.id == key);
    setViewJob(job);
  }

  return (
    <Tooltip title={lang('View result')}>
      {jobs.length > 1 ? (
        <Dropdown
          menu={{
            items: jobs.map((job) => {
              return {
                key: job.id,
                label: (
                  <>
                    <StatusButton statusMap={JobStatusOptionsMap} status={job.status} />
                    <time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
                  </>
                ),
              };
            }),
            onClick: onOpenJob,
            className: styles.dropdownClass,
          }}
        >
          <StatusButton
            statusMap={JobStatusOptionsMap}
            status={jobs[jobs.length - 1].status}
            className={styles.nodeJobButtonClass}
          />
        </Dropdown>
      ) : (
        <StatusButton
          statusMap={JobStatusOptionsMap}
          status={jobs[0].status}
          onClick={() => setViewJob(jobs[0])}
          className={styles.nodeJobButtonClass}
        />
      )}
    </Tooltip>
  );
}

function useFormProviderProps() {
  return { form: useForm() };
}

const useRunAction = () => {
  const { values, query } = useForm();
  const node = useNodeContext();
  const api = useAPIClient();
  const ctx = useActionContext();
  const field = useField<Field>();
  return {
    async run() {
      const template = parse(node.config);
      const config = template(toJS(values.config));
      const resultField = query('result').take() as Field;
      resultField.setValue(null);
      resultField.setFeedback({});

      field.data = field.data || {};
      field.data.loading = true;

      try {
        const {
          data: { data },
        } = await api.resource('flow_nodes').test({
          values: {
            config,
            type: node.type,
          },
        });

        resultField.setFeedback({
          type: data.status > 0 ? 'success' : 'error',
          messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')],
        });
        resultField.setValue(data.result);
      } catch (err) {
        resultField.setFeedback({
          type: 'error',
          messages: err.message,
        });
      }
      field.data.loading = false;
      ctx.setFormValueChanged(false);
    },
  };
};

const VariableKeysContext = createContext<string[]>([]);

function VariableReplacer({ name, value, onChange }) {
  return (
    <Space>
      <WorkflowVariableInput variableOptions={{}} value={`{{${name}}}`} disabled />
      <Variable.Input useTypedConstant={['string', 'number', 'boolean', 'date']} value={value} onChange={onChange} />
    </Space>
  );
}

function TestFormFieldset({ value, onChange }) {
  const keys = useContext(VariableKeysContext);

  return keys.length ? (
    <>
      {keys.map((key) => (
        <VariableReplacer
          key={key}
          name={key}
          value={get(value, key)}
          onChange={(v) => {
            set(value, key, v);
            onChange(value);
          }}
        />
      ))}
    </>
  ) : (
    <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={lang('No variable')} style={{ margin: '1em' }} />
  );
}

function TestButton() {
  const node = useNodeContext();
  const { values } = useForm();
  const template = parse(values);
  const keys = template.parameters.map((item) => item.key);
  const form = useMemo(() => createForm(), []);

  return (
    <NodeContext.Provider value={{ type: node.type, config: values }}>
      <VariableKeysContext.Provider value={keys}>
        <SchemaComponent
          components={{
            Alert,
          }}
          scope={{
            useCancelAction,
            useRunAction,
          }}
          schema={{
            type: 'void',
            name: 'testButton',
            title: '{{t("Test run")}}',
            'x-component': 'Action',
            'x-component-props': {
              icon: 'CaretRightOutlined',
              // openSize: 'small',
            },
            properties: {
              modal: {
                type: 'void',
                'x-decorator': 'FormV2',
                'x-decorator-props': {
                  form,
                },
                'x-component': 'Action.Modal',
                title: `{{t("Test run", { ns: "workflow" })}}`,
                properties: {
                  alert: {
                    type: 'void',
                    'x-component': 'Alert',
                    'x-component-props': {
                      message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
                      type: 'warning',
                      showIcon: true,
                      className: css`
                        margin-bottom: 1em;
                      `,
                    },
                  },
                  config: {
                    type: 'object',
                    title: '{{t("Replace variables", { ns: "workflow" })}}',
                    'x-decorator': 'FormItem',
                    'x-component': TestFormFieldset,
                  },
                  actions: {
                    type: 'void',
                    'x-component': 'ActionBar',
                    properties: {
                      submit: {
                        type: 'void',
                        title: '{{t("Run")}}',
                        'x-component': 'Action',
                        'x-component-props': {
                          type: 'primary',
                          useAction: '{{ useRunAction }}',
                        },
                      },
                    },
                  },
                  result: {
                    type: 'string',
                    title: `{{t("Result", { ns: "workflow" })}}`,
                    'x-decorator': 'FormItem',
                    'x-component': 'Input.JSON',
                    'x-component-props': {
                      autoSize: {
                        minRows: 5,
                        maxRows: 20,
                      },
                      style: {
                        whiteSpace: 'pre',
                        cursor: 'text',
                      },
                    },
                    'x-pattern': 'disabled',
                  },
                  footer: {
                    type: 'void',
                    'x-component': 'Action.Modal.Footer',
                    properties: {
                      cancel: {
                        type: 'void',
                        title: '{{t("Close")}}',
                        'x-component': 'Action',
                        'x-component-props': {
                          useAction: '{{ useCancelAction }}',
                        },
                      },
                    },
                  },
                },
              },
            },
          }}
        />
      </VariableKeysContext.Provider>
    </NodeContext.Provider>
  );
}

export function NodeDefaultView(props) {
  const { data, children } = props;
  const compile = useCompile();
  const api = useAPIClient();
  const { workflow, refresh } = useFlowContext() ?? {};
  const { styles } = useStyles();
  const workflowPlugin = usePlugin(WorkflowPlugin);
  const instruction = workflowPlugin.instructions.get(data.type);
  const detailText = workflow.executed ? '{{t("View")}}' : '{{t("Configure")}}';

  const [editingTitle, setEditingTitle] = useState<string>(data.title);
  const [editingConfig, setEditingConfig] = useState(false);
  const [formValueChanged, setFormValueChanged] = useState(false);

  const form = useMemo(() => {
    const values = cloneDeep(data.config);
    return createForm({
      initialValues: values,
      disabled: workflow.executed,
    });
  }, [data, workflow]);

  const resetForm = useCallback(
    (editing) => {
      setEditingConfig(editing);
      if (!editing) {
        form.reset();
      }
    },
    [form],
  );

  const onChangeTitle = useCallback(
    async function (next) {
      const title = next || compile(instruction?.title);
      setEditingTitle(title);
      if (title === data.title) {
        return;
      }
      await api.resource('flow_nodes').update?.({
        filterByTk: data.id,
        values: {
          title,
        },
      });
      refresh();
    },
    [data, instruction],
  );

  const onOpenDrawer = useCallback(function (ev) {
    if (ev.target === ev.currentTarget) {
      setEditingConfig(true);
      return;
    }
    const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']);
    for (let el = ev.target; el && el !== ev.currentTarget && el !== document.documentElement; el = el.parentNode) {
      if ((Array.from(el.classList) as string[]).some((name: string) => whiteSet.has(name))) {
        setEditingConfig(true);
        ev.stopPropagation();
        return;
      }
    }
  }, []);

  if (!instruction) {
    return (
      <div className={cx(styles.nodeClass, `workflow-node-type-${data.type}`)}>
        <Tooltip
          title={lang(
            'Node with unknown type will cause error. Please delete it or check plugin which provide this type.',
          )}
        >
          <div role="button" aria-label={`_untyped-${editingTitle}`} className={cx(styles.nodeCardClass, 'invalid')}>
            <div className={styles.nodeHeaderClass}>
              <div className={cx(styles.nodeMetaClass, 'workflow-node-meta')}>
                <Tag color="error">{lang('Unknown node')}</Tag>
                <span className="workflow-node-id">{data.id}</span>
              </div>
              <div className="workflow-node-actions">
                <RemoveButton />
                <JobButton />
              </div>
            </div>
            <Input.TextArea value={editingTitle} disabled autoSize />
          </div>
        </Tooltip>
      </div>
    );
  }

  const typeTitle = compile(instruction.title);

  return (
    <div className={cx(styles.nodeClass, `workflow-node-type-${data.type}`)}>
      <div
        role="button"
        aria-label={`${typeTitle}-${editingTitle}`}
        className={cx(styles.nodeCardClass, { configuring: editingConfig })}
        onClick={onOpenDrawer}
      >
        <div className={styles.nodeHeaderClass}>
          <div className={cx(styles.nodeMetaClass, 'workflow-node-meta')}>
            <Tag>{typeTitle}</Tag>
            <span className="workflow-node-id">{data.id}</span>
          </div>
          <div className="workflow-node-actions">
            <RemoveButton />
            <JobButton />
          </div>
        </div>
        <Input.TextArea
          disabled={workflow.executed}
          value={editingTitle}
          onChange={(ev) => setEditingTitle(ev.target.value)}
          onBlur={(ev) => onChangeTitle(ev.target.value)}
          autoSize
        />
        <ActionContextProvider
          value={{
            visible: editingConfig,
            setVisible: resetForm,
            formValueChanged,
            setFormValueChanged,
          }}
        >
          <FormProvider form={form}>
            <SchemaComponent
              distributed={false}
              scope={{
                ...instruction.scope,
                useFormProviderProps,
                useUpdateAction,
              }}
              components={instruction.components}
              schema={{
                type: 'void',
                properties: {
                  ...(instruction.view ? { view: instruction.view } : {}),
                  button: {
                    type: 'void',
                    'x-content': detailText,
                    'x-component': Button,
                    'x-component-props': {
                      type: 'link',
                      className: 'workflow-node-config-button',
                    },
                  },
                  [data.id]: {
                    type: 'void',
                    title: (
                      <div
                        className={css`
                          display: flex;
                          justify-content: space-between;

                          strong {
                            font-weight: bold;
                          }

                          .ant-tag {
                            margin-inline-end: 0;
                          }

                          code {
                            font-weight: normal;
                          }
                        `}
                      >
                        <strong>{data.title}</strong>
                        <Tooltip title={lang('Variable key of node')}>
                          <Tag>
                            <code>{data.key}</code>
                          </Tag>
                        </Tooltip>
                      </div>
                    ),
                    'x-decorator': 'FormV2',
                    'x-use-decorator-props': 'useFormProviderProps',
                    'x-component': 'Action.Drawer',
                    properties: {
                      ...(instruction.description
                        ? {
                            description: {
                              type: 'void',
                              'x-component': DrawerDescription,
                              'x-component-props': {
                                label: lang('Node type'),
                                title: instruction.title,
                                description: instruction.description,
                              },
                            },
                          }
                        : {}),
                      fieldset: {
                        type: 'void',
                        'x-component': 'fieldset',
                        'x-component-props': {
                          className: css`
                            .ant-input,
                            .ant-select,
                            .ant-cascader-picker,
                            .ant-picker,
                            .ant-input-number,
                            .ant-input-affix-wrapper {
                              &.auto-width {
                                width: auto;
                                min-width: 6em;
                              }
                            }
                          `,
                        },
                        properties: instruction.fieldset,
                      },
                      footer: workflow.executed
                        ? null
                        : {
                            type: 'void',
                            'x-component': 'Action.Drawer.Footer',
                            properties: {
                              actions: {
                                type: 'void',
                                'x-component': 'ActionBar',
                                'x-component-props': {
                                  style: {
                                    flexGrow: 1,
                                  },
                                },
                                properties: {
                                  ...(instruction.testable
                                    ? {
                                        test: {
                                          type: 'void',
                                          'x-component': observer(TestButton),
                                          'x-align': 'left',
                                        },
                                      }
                                    : {}),
                                  cancel: {
                                    title: '{{t("Cancel")}}',
                                    'x-component': 'Action',
                                    'x-component-props': {
                                      useAction: '{{ cm.useCancelAction }}',
                                    },
                                  },
                                  submit: {
                                    title: '{{t("Submit")}}',
                                    'x-component': 'Action',
                                    'x-component-props': {
                                      type: 'primary',
                                      useAction: '{{ useUpdateAction }}',
                                    },
                                  },
                                },
                              },
                            },
                          },
                    },
                  } as ISchema,
                },
              }}
            />
          </FormProvider>
        </ActionContextProvider>
      </div>
      {children}
    </div>
  );
}
