/**
 * 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 { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react';
import {
  PopupContextProvider,
  RecordProvider,
  getLabelFormatValue,
  useCollection,
  useCollectionParentRecordData,
  usePopupUtils,
  useProps,
  useToken,
  withDynamicSchemaProps,
  useACLRoleContext,
} from '@nocobase/client';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { get } from 'lodash-es';
import React, { useMemo, useState, useEffect } from 'react';
import type { View } from 'react-big-calendar';
import { i18nt, useTranslation } from '../../locale';
import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer';
import Header from './components/Header';
import { CalendarToolbarContext } from './context';
import GlobalStyle from './global.style';
import { useCalenderHeight } from './hook';
import useStyle from './style';
import type { ToolbarProps } from './types';
import { formatDate } from './utils';
import { useLazy } from '@nocobase/client';

interface Event {
  id: string;
  colorFieldValue: string;
  title: string;
  start: Date;
  end: Date;
}

const Weeks = ['month', 'week', 'day'] as View[];

const getColorString = (
  colorFieldValue: string,
  enumList: {
    color: string;
    label: string;
    value: string;
    rawLabel: string;
  }[],
) => {
  for (const item of enumList) {
    if (item.value === colorFieldValue) {
      return item.color;
    }
  }

  return '';
};

export const DeleteEventContext = React.createContext({
  close: () => {},
  allowDeleteEvent: false,
});

function Toolbar(props: ToolbarProps) {
  const fieldSchema = useFieldSchema();
  const toolBarSchema: Schema = useMemo(
    () =>
      fieldSchema.reduceProperties((buf, current) => {
        if (current['x-component'].endsWith('.ActionBar')) {
          return current;
        }
        return buf;
      }, null),
    [],
  );
  return (
    <CalendarToolbarContext.Provider value={props}>
      <RecursionField name={toolBarSchema.name} schema={toolBarSchema} />
    </CalendarToolbarContext.Provider>
  );
}

const useEvents = (
  dataSource: any,
  fieldNames: {
    colorFieldName: string;
    start: string;
    end: string;
    id: string;
    title: string;
  },
  date: Date,
  view: (typeof Weeks)[number],
) => {
  const parseExpression = useLazy<typeof import('cron-parser').parseExpression>(
    () => import('cron-parser'),
    'parseExpression',
  );
  const { t } = useTranslation();
  const { fields } = useCollection();
  const labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema;
  const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName);
  return useMemo(() => {
    if (!Array.isArray(dataSource)) return { events: [], enumList: [] };
    const enumList = enumUiSchema?.uiSchema?.enum || [];
    const events: Event[] = [];

    dataSource.forEach((item) => {
      const { cron, exclude = [] } = item;
      const start = dayjs(get(item, fieldNames.start) || dayjs());
      const end = dayjs(get(item, fieldNames.end) || start);
      const intervalTime = end.diff(start, 'millisecond', true);

      const dateM = dayjs(date);
      let startDate = dateM.clone().startOf('month');
      let endDate = startDate.clone().endOf('month');

      /**
       * view === month 时，会显示当月日程
       * 但这个日历一般情况下需要展示 6w * 7d 的日程，总共 42 天
       * 假设 10.1 号是星期六，我们需要将日程的开始时间调整为这一周的星期一，也就是 9.25 号
       * 而结束时间需要调整为 10.31 号这一周的星期日，也就是 10.5 号
       */
      if (view === 'month') {
        startDate = startDate.startOf('week');
        endDate = endDate.endOf('week');
      }

      const push = (eventStart: Dayjs = start.clone()) => {
        // 必须在这个月的开始时间和结束时间，且在日程的开始时间之后
        if (
          eventStart.isBefore(start) || // 开始时间早于 start
          (!eventStart.isBetween(startDate, endDate) && !end.isBetween(startDate, endDate)) // 开始时间和结束时间不在月份范围内
        ) {
          return;
        }
        let out = false;
        const res = exclude?.some((d) => {
          if (d.endsWith('_after')) {
            d = d.replace(/_after$/, '');
            out = true;
            return eventStart.isSameOrAfter(d);
          } else {
            return eventStart.isSame(d);
          }
        });

        if (res) return out;
        const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true);
        const event: Event = {
          id: get(item, fieldNames.id || 'id'),
          colorFieldValue: item[fieldNames.colorFieldName],
          title: title || t('Untitle'),
          start: eventStart.toDate(),
          end: eventStart.add(intervalTime, 'millisecond').toDate(),
        };

        events.push(event);
      };

      if (cron === 'every_week') {
        let nextStart = start
          .clone()
          .year(startDate.year())
          .month(startDate.month())
          .date(startDate.date())
          .day(start.day());
        while (nextStart.isBefore(endDate)) {
          if (push(nextStart.clone())) {
            break;
          }
          nextStart = nextStart.add(1, 'week');
        }
      } else if (cron === 'every_month') {
        push(start.clone().year(dateM.year()).month(dateM.month()));
      } else if (cron === 'every_year') {
        push(start.clone().year(dateM.year()));
      } else {
        push();
        if (!cron) return;
        try {
          const interval = parseExpression(cron, {
            startDate: startDate.toDate(),
            endDate: endDate.toDate(),
            iterator: true,
            currentDate: start.toDate(),
            utc: true,
          });
          while (interval.hasNext()) {
            const { value } = interval.next();
            if (push(dayjs(value.toDate()))) break;
          }
        } catch (err) {
          console.error(err);
        }
      }
    });
    return { events, enumList };
  }, [
    labelUiSchema,
    dataSource,
    fieldNames.colorFieldName,
    fieldNames.start,
    fieldNames.end,
    fieldNames.id,
    fieldNames.title,
    date,
    view,
    t,
    enumUiSchema?.uiSchema?.enum,
    parseExpression,
  ]);
};

function findCreateSchema(schema): Schema {
  return schema.reduceProperties((buf, current) => {
    if (current['x-component'].endsWith('Action') && current['x-action'] === 'create') {
      return current;
    }
    if (current['x-component'].endsWith('.ActionBar')) {
      return findCreateSchema(current);
    }
    return buf;
  }, null);
}

export const Calendar: any = withDynamicSchemaProps(
  observer(
    (props: any) => {
      const [visible, setVisible] = useState(false);
      const { openPopup } = usePopupUtils({
        setVisible,
      });
      const reactBigCalendar = useLazy(
        () => import('react-big-calendar'),
        (module) => ({
          BigCalendar: module.Calendar,
          dayjsLocalizer: module.dayjsLocalizer,
        }),
      );

      const eq = useLazy<typeof import('react-big-calendar/lib/utils/dates').eq>(
        () => import('react-big-calendar/lib/utils/dates'),
        'eq',
      );

      const localizer = useMemo(() => {
        return reactBigCalendar.dayjsLocalizer(dayjs);
      }, [reactBigCalendar]);

      // 新版 UISchema（1.0 之后）中已经废弃了 useProps，这里之所以继续保留是为了兼容旧版的 UISchema
      const { dataSource, fieldNames, showLunar, defaultView } = useProps(props);
      const height = useCalenderHeight();
      const [date, setDate] = useState<Date>(new Date());
      const [view, setView] = useState<View>(props.defaultView || 'month');
      const { events, enumList } = useEvents(dataSource, fieldNames, date, view);
      const [record, setRecord] = useState<any>({});
      const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
      const parentRecordData = useCollectionParentRecordData();
      const fieldSchema = useFieldSchema();
      const { token } = useToken();
      //nint deal with slot select to show create popup
      const { parseAction } = useACLRoleContext();
      const collection = useCollection();
      const canCreate = parseAction(`${collection.name}:create`);
      const createActionSchema: Schema = useMemo(() => findCreateSchema(fieldSchema), [fieldSchema]);
      const startFieldName = fieldNames?.start?.[0];
      const endFieldName = fieldNames?.end?.[0];

      useEffect(() => {
        setView(props.defaultView);
      }, [props.defaultView]);

      const components = useMemo(() => {
        return {
          toolbar: (props) => <Toolbar {...props} showLunar={showLunar}></Toolbar>,
          // week: {
          //   header: (props) => <Header {...props} type="week" showLunar={showLunar}></Header>,
          // },
          month: {
            dateHeader: (props) => <Header {...props} showLunar={showLunar}></Header>,
          },
        };
      }, [showLunar]);

      const messages: any = {
        allDay: '',
        previous: (
          <div>
            <LeftOutlined />
          </div>
        ),
        next: (
          <div>
            <RightOutlined />
          </div>
        ),
        today: i18nt('Today'),
        month: i18nt('Month'),
        week: i18nt('Week'),
        work_week: i18nt('Work week'),
        day: i18nt('Day'),
        agenda: i18nt('Agenda'),
        date: i18nt('Date'),
        time: i18nt('Time'),
        event: i18nt('Event'),
        noEventsInRange: i18nt('None'),
        showMore: (count) => i18nt('{{count}} more items', { count }),
      };

      const eventPropGetter = (event: Event) => {
        if (event.colorFieldValue) {
          const fontColor = token[`${getColorString(event.colorFieldValue, enumList)}7`];
          const backgroundColor = token[`${getColorString(event.colorFieldValue, enumList)}1`];
          return {
            style: { color: fontColor, backgroundColor, border: 'none' },
          };
        }
      };

      const BigCalendar = reactBigCalendar?.BigCalendar;

      return wrapSSR(
        <div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
          <PopupContextProvider visible={visible} setVisible={setVisible}>
            <GlobalStyle />
            <RecordProvider record={record} parent={parentRecordData}>
              <CalendarRecordViewer />
            </RecordProvider>
            <BigCalendar
              popup
              selectable
              events={events}
              eventPropGetter={eventPropGetter}
              view={view}
              views={Weeks}
              date={date}
              step={60}
              showMultiDayTimes
              messages={messages}
              onNavigate={setDate}
              onView={setView}
              onSelectSlot={(slotInfo) => {
                //nint show create popup
                if (canCreate && createActionSchema) {
                  const record = {};
                  record[startFieldName] = slotInfo.start;
                  record[endFieldName] = slotInfo.end;
                  openPopup({
                    recordData: record,
                    customActionSchema: createActionSchema,
                  });
                }
              }}
              onDoubleClickEvent={() => {
                console.log('onDoubleClickEvent');
              }}
              onSelectEvent={(event) => {
                const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
                if (!record) {
                  return;
                }
                record.__event = {
                  ...event,
                  start: formatDate(dayjs(event.start)),
                  end: formatDate(dayjs(event.end)),
                };

                setRecord(record);
                openPopup({
                  recordData: record,
                  customActionSchema: findEventSchema(fieldSchema),
                });
              }}
              formats={{
                monthHeaderFormat: 'YYYY-M',
                agendaDateFormat: 'M-DD',
                dayHeaderFormat: 'YYYY-M-DD',
                dayRangeHeaderFormat: ({ start, end }, culture, local) => {
                  if (eq(start, end, 'month')) {
                    return local.format(start, 'YYYY-M', culture);
                  }
                  return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`;
                },
              }}
              components={components}
              localizer={localizer}
            />
          </PopupContextProvider>
        </div>,
      );
    },
    { displayName: 'Calendar' },
  ),
);
