import { faker } from '@faker-js/faker';
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import { Field } from 'formik';
import React, { useContext } from 'react';
import { MemoryRouter } from 'react-router';
import * as Yup from 'yup';
import {
  createGroups,
  enumToObj,
  generateItemArray,
  randomDate,
} from '../../helpers/storybook';
import { initializeUi } from '../../initialize';
import { FilterTypes } from '../Filters/Filters.model';
import { ReadOnlyField, SingleLineTextField } from '../FormElements';
import {
  FormStation,
  InitialFormData,
  ObjectSchemaDefinition,
} from '../FormStation';
import { IconName } from '../Icons';
import { ListSelectMode } from '../List';
import { createConnectionRenderer } from '../Utils';
import { BulkEditConfig } from './BulkEdit';
import { generateBulkEditMutation } from './BulkEdit/GenerateMutation';
import { Explorer } from './Explorer';
import { QuickEditContext } from './QuickEdit/QuickEditContext';
import {
  createInMemoryDataProvider,
  findAnywhereInStringCaseInsensitive,
} from './helpers/InMemoryDataProvider';

interface ExplorerStoryData {
  id: number;
  title: string;
  date?: Date;
  desc: string;
  tags?: { nodes: { name: string }[] };
}

type ExplorerStoryType = typeof Explorer<ExplorerStoryData>;

const selectionModeOptions = enumToObj(ListSelectMode);

const groups = createGroups({
  Content: [
    'dataProvider',
    'columns',
    'title',
    'stationKey',
    'actions',
    'bulkActions',
    'defaultFilterValues',
    'defaultSortOrder',
    'filterOptions',
    'generateItemLink',
    'inlineMenuActions',
    'keyProperty',
  ],
  Behavior: [
    'selectionMode',
    'enableSelectAll',
    'loadingTriggerOffset',
    'modalMode',
    'onBulkActionsToggled',
    'onItemClicked',
    'openBulkActionsOnStart',
    'persistExplorerStates',
    'setTabTitle',
  ],
  Styling: [
    'className',
    'columnGap',
    'headerRowHeight',
    'listRowHeight',
    'minimumWidth',
    'rowGap',
    'verticalTextAlign',
    'horizontalTextAlign',
    'textWrap',
    'headerRowActionSize',
    'listRowActionSize',
  ],
});

const meta: Meta<ExplorerStoryType> = {
  title: 'Primary Components/Station Controls/Explorer',
  component: Explorer,
  decorators: [
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    (Story) => (
      <MemoryRouter>
        <Story />
      </MemoryRouter>
    ),
  ],
  argTypes: {
    ...groups,
    selectionMode: {
      ...groups.selectionMode,
      control: {
        type: 'radio',
      },
      options: selectionModeOptions,
    },
  },
};
export default meta;

const generateData = (
  amount: number,
  { startIndex = 1, usePrefix = true } = {},
): ExplorerStoryData[] =>
  generateItemArray(amount, (i) => {
    const index = i + startIndex;
    return {
      id: index,
      desc: `${usePrefix ? `Description ${index}: ` : ''}${faker.lorem.words(
        faker.datatype.number({ min: 10, max: 20 }),
      )}`,
      date: randomDate(new Date(2019, 0, 1)),
      title: `${usePrefix ? `Index ${index}: ` : ''}${faker.random.words(
        faker.datatype.number({ min: 1, max: 3 }),
      )}`,
      tags: {
        nodes: Array.from(
          { length: faker.datatype.number({ min: 0, max: 10 }) },
          () => ({
            name: faker.random.word(),
          }),
        ),
      },
    };
  });

const defaultData = generateData(20, { usePrefix: false });

export const Default: StoryObj<ExplorerStoryType> = {
  argTypes: {
    dataProvider: { control: false },
  },
  args: {
    columns: [
      {
        propertyName: 'id',
        label: 'Id',
        size: '50px',
      },
      {
        propertyName: 'title',
        label: 'Title',
      },
      {
        propertyName: 'tags',
        label: 'Tags',
        render: createConnectionRenderer<{ nodes: { name: string }[] }>(
          (tag) => tag.name,
        ),
      },
      {
        propertyName: 'date',
        label: 'Date',
      },
      {
        propertyName: 'desc',
        label: 'Description',
        size: '2fr',
      },
    ],
    dataProvider: createInMemoryDataProvider<ExplorerStoryData>(defaultData, {
      filterFunctions: {
        title: findAnywhereInStringCaseInsensitive,
        desc: findAnywhereInStringCaseInsensitive,
      },
    }),
    filterOptions: [
      {
        label: 'Title',
        property: 'title',
        type: FilterTypes.FreeText,
      },
      {
        label: 'Description',
        property: 'desc',
        type: FilterTypes.Options,
        options: [
          {
            label: "Containing 'ax'",
            value: 'ax',
          },
          {
            label: "Containing 'en'",
            value: 'en',
          },
        ],
      },
    ],
    title: 'Explorer',
    stationKey: 'StoryBookExplorer_Default',
  },
};

export const Empty: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    dataProvider: createInMemoryDataProvider<ExplorerStoryData>([]),
    stationKey: 'StoryBookExplorer_Empty',
  },
};

export const NoFilterNoBulkOperations: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    filterOptions: undefined,
    stationKey: 'StoryBookExplorer_NoFilterNoBulkOperations',
  },
};

export const LoadingState: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    dataProvider: {
      loadData: (config) => {
        action('loadData')(config);
        return new Promise(() => {
          // never resolving
        });
      },
    },
    stationKey: 'StoryBookExplorer_LoadingState',
  },
};

export const ErrorState: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    dataProvider: {
      loadData: (config) => {
        action('loadData')(config);
        return Promise.reject({
          title: 'There was an error',
          body: 'Error body',
        });
      },
    },
    stationKey: 'StoryBookExplorer_ErrorState',
  },
};

const actualProvider = createInMemoryDataProvider<ExplorerStoryData>(
  defaultData,
  {
    filterFunctions: {
      title: findAnywhereInStringCaseInsensitive,
      desc: findAnywhereInStringCaseInsensitive,
    },
  },
);

export const TalkativeInMemoryDataProvider: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    dataProvider: {
      loadData: (config) => {
        action('loadData')(config);
        return actualProvider.loadData(config);
      },
    },
    stationKey: 'StoryBookExplorer_TalkativeInMemoryDataProvider',
  },
};

initializeUi({
  showNotification: (args) => {
    action('showNotification')(args);
    return faker.random.numeric();
  },
  addIndicator: (args) => {
    action('addIndicator')(args);
    return Math.random();
  },
  removeIndicator: (args) => {
    action('removeIndicator')(args);
  },
  on: (event, callback) => {
    action('on')(event, callback);
  },
  setTitle: (args) => {
    action('setTitle')(args);
  },
  setSaveIndicator: (args) => {
    action('setSaveIndicator')(args);
  },
});

export const ActionErrors: StoryObj<ExplorerStoryType> = {
  parameters: {
    docs: {
      description: {
        story: `This story demonstrates how errors raised by actions are handled. Since Bulk Actions are executed asynchonously, 
        errors are reported using notification messages. This story is configured to report an action on the action panel whenever
        a notification would be raised.`,
      },
    },
  },
  args: {
    ...NoFilterNoBulkOperations.args,
    stationKey: 'StoryBookExplorer_ActionErrors',
    actions: [
      {
        label: 'Returning Action',
        icon: IconName.Delete,
        onClick: () => ({ title: 'action error' }),
      },
      {
        label: 'Throwing Action',
        icon: IconName.Delete,
        onClick: (): void => {
          throw { title: 'action error' };
        },
      },
      {
        label: 'Throwing Action 2',
        icon: IconName.Delete,
        onClick: (): void => {
          throw new Error('action error');
        },
      },
    ],
    bulkActions: [
      {
        label: 'Test Bulk Action',
        onClick: () => ({ title: 'bulk error' }),
      },
    ],
    inlineMenuActions: () => {
      return [
        {
          label: 'Returning Action',
          icon: IconName.Delete,
          onActionSelected: () => ({ title: 'action error' }),
        },
        {
          label: 'Throwing Action',
          icon: IconName.Delete,
          onActionSelected: () => {
            throw { title: 'action error' };
          },
        },
        {
          label: 'Throwing Action 2',
          icon: IconName.Delete,
          onActionSelected: () => {
            throw new Error('action error');
          },
        },
      ];
    },
  },
  render: (args) => {
    return <Explorer {...args} />;
  },
};

export const InlineMenuActions: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    stationKey: 'StoryBookExplorer_InlineMenuActions',
    inlineMenuActions: () => {
      return [
        {
          label: 'Action One',
          path: '#',
          icon: IconName.NavigateRight,
        },
        {
          label: 'Action Two',
          onActionSelected: action('onActionTwoSelected'),
          icon: IconName.Copy,
        },
        {
          label: 'Action Three',
          onActionSelected: action('onActionThreeSelected'),
          icon: IconName.Publish,
          confirmationMode: 'Simple',
        },
      ];
    },
  },
  render: (args) => {
    return (
      <MemoryRouter>
        <Explorer {...args} />
      </MemoryRouter>
    );
  },
};

export const DataProviderConnection: StoryObj<ExplorerStoryType> = {
  parameters: {
    docs: {
      description: {
        story: `This story demonstrates how to integrate with a data provider that can push changes to the explorer.
        The story uses a data provider that every second updates the description of a random item.
        The changes are also reported in the action logger.`,
      },
    },
  },
  args: {
    ...Default.args,
    stationKey: 'StoryBookExplorer_DataProviderConnection',
    dataProvider: {
      ...createInMemoryDataProvider<ExplorerStoryData>(defaultData, {
        filterFunctions: {
          title: findAnywhereInStringCaseInsensitive,
          desc: findAnywhereInStringCaseInsensitive,
        },
      }),
      connect: ({ change }) => {
        const interval = setInterval(() => {
          const itemId = Math.floor(Math.random() * defaultData.length);
          const newDesc = faker.lorem.words(
            faker.datatype.number({ min: 10, max: 20 }),
          );
          change(itemId, {
            desc: newDesc,
          });
          action('change')(itemId, newDesc);
        }, 1000);
        return () => {
          clearInterval(interval);
        };
      },
    },
  },
};

const QuickEditComponent: React.FC = () => {
  const { selectedItem } = useContext(QuickEditContext);

  return (
    <FormStation<ExplorerStoryData>
      initialData={{ loading: false, data: selectedItem }}
      titleProperty={'title'}
      saveData={function (
        values: ExplorerStoryData,
        initialData: InitialFormData<ExplorerStoryData>,
      ): void | Promise<unknown> {
        action('Save')(values, initialData);
      }}
      validationSchema={Yup.object<ObjectSchemaDefinition<ExplorerStoryData>>({
        title: Yup.string()
          .max(10, 'Title can only be 10 characters')
          .required('Title is required'),
        id: Yup.number().optional(),
        desc: Yup.string().optional(),
      })}
    >
      <>
        <Field name="id" label="Id" as={ReadOnlyField} />
        <Field name="title" label="Title" as={SingleLineTextField} />
      </>
    </FormStation>
  );
};

const QuickEditComponent2: React.FC = () => {
  const { selectedItem } = useContext(QuickEditContext);

  return (
    <FormStation<ExplorerStoryData>
      initialData={{ loading: false, data: selectedItem }}
      titleProperty={'title'}
      saveData={function (
        values: ExplorerStoryData,
        initialData: InitialFormData<ExplorerStoryData>,
      ): void | Promise<unknown> {
        action('Save')(values, initialData);
      }}
    >
      <>
        <Field name="id" label="Id" as={ReadOnlyField} />
        <Field name="desc" label="Description" as={SingleLineTextField} />
      </>
    </FormStation>
  );
};

export const QuickEdit: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    generateItemLink: (item) => `/details/${item.id}`,
    stationKey: 'StoryBookExplorer_QuickEdit',
    quickEditRegistrations: [
      {
        label: 'Quick Edit (Title)',
        component: <QuickEditComponent />,
      },
      {
        label: 'Quick Edit (Description)',
        component: <QuickEditComponent2 />,
      },
    ],
  },
};

const BulkEditImagesAsyncFormFieldsConfig: BulkEditConfig = {
  mutation: 'bulkEditImagesAsync',
  keys: {
    add: 'relatedEntitiesToAdd',
    remove: 'relatedEntitiesToRemove',
    clear: 'relatedEntitiesToClear',
    set: 'set',
    filter: 'filter',
  },
  fields: {
    imagesTagsAdd: {
      type: [
        {
          name: 'String!',
        },
      ],
      label: 'Images Tags ( Add )',
      originalFieldName: 'imagesTags',
      action: 'relatedEntitiesToAdd',
    },
    imagesTagsRemove: {
      type: [
        {
          name: 'String!',
        },
      ],
      label: 'Images Tags ( Remove )',
      originalFieldName: 'imagesTags',
      action: 'relatedEntitiesToRemove',
      clearable: true,
      clearValue: [],
    },
    altText: {
      type: 'String',
      label: 'Alt Text',
      originalFieldName: 'altText',
      action: 'set',
      clearable: true,
      clearValue: '',
    },
    focalX: {
      type: 'BigFloat',
      label: 'Focal X',
      originalFieldName: 'focalX',
      action: 'set',
    },
    focalY: {
      type: 'BigFloat',
      label: 'Focal Y',
      originalFieldName: 'focalY',
      action: 'set',
    },
    isArchived: {
      type: 'Boolean',
      label: 'Is Archived',
      originalFieldName: 'isArchived',
      action: 'set',
    },
    title: {
      type: 'String',
      label: 'Title',
      originalFieldName: 'title',
      action: 'set',
    },
    email: {
      type: 'String',
      label: 'Email',
      originalFieldName: 'email',
      action: 'set',
      customValidation: Yup.string().email('Invalid email address'),
    },
  },
};

export const BulkEdit: StoryObj<ExplorerStoryType> = {
  args: {
    ...Default.args,
    stationKey: 'StoryBookExplorer_QuickEdit',
    bulkEditRegistration: {
      config: BulkEditImagesAsyncFormFieldsConfig,
      saveData: async (data, items) => {
        // eslint-disable-next-line no-console
        console.log(
          generateBulkEditMutation(BulkEditImagesAsyncFormFieldsConfig, data),
        );
        action('Save')(data, items);
      },
    },
  },
};
