import {
  DataTable,
  DataTableExpandedRows,
  DataTableFilterMeta,
  DataTableFilterMetaData,
  DataTableRowData,
  DataTableRowEvent,
  DataTableRowExpansionTemplate,
  DataTableRowToggleEvent,
  DataTableValueArray,
  SortOrder,
} from "primereact/datatable";
import React, {
  useState,
  useEffect,
  useCallback,
  ReactNode,
  CSSProperties,
  useRef,
  ReactElement,
  Fragment,
} from "react";
import {
  ColumnBodyOptions,
  ColumnFilterElementTemplateOptions,
  ColumnProps,
} from "primereact/column";
import { InputNumber, InputNumberChangeEvent } from "primereact/inputnumber";
import {
  Dropdown,
  DropdownChangeEvent,
  DropdownProps,
} from "primereact/dropdown";
import { InputText } from "primereact/inputtext";
import { FilterMatchMode } from "primereact/api";
import { Skeleton } from "primereact/skeleton";
import { Dialog } from "primereact/dialog";
import { goGet } from "../../utils/goFetch";
import { Column } from "primereact/column";
import { OSLabel } from "../label";
import { OSIcon } from "../icon";
import { FilterType } from "./FilterType";
import { Calendar } from "primereact/calendar";
import { useDebounce } from "primereact/hooks";
import { OSButton } from "../button/OSButton";

interface DataItem {
  id: number;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any;
}

interface Yii2DataProviderResponse {
  models: DataItem[];
  totalCount: number;
  page: number;
  pageSize: number;
}

export interface FilterDropDownOptions {
  value: string | number;
  label: string;
}

export interface TSColumnTemplate extends ColumnProps {
  field?: string;
  header?: string | React.ReactNode;
  sortable?: boolean;
  filter?: boolean;
  filterType?: FilterType;
  filterDropDownOptions?: FilterDropDownOptions[];
  dropdownFilter?: boolean;
  dropDownStatic?: boolean;
  dropDownValueTemplate?:
    | React.ReactNode
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    | ((option: any, props: DropdownProps) => React.ReactNode)
    | undefined;
  dropDownItemTemplate?:
    | React.ReactNode
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    | ((option: any) => React.ReactNode)
    | undefined;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  body?: (rowData: any) => ReactNode;
  style?: CSSProperties;
  /**
   * Displays an icon to toggle row expansion.
   * @defaultValue false
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  expander?: boolean | ((data: any, options: ColumnBodyOptions) => boolean);

  /**
   * Loading state for dropdown filter
   */
  dropDownLoading?: boolean;

  /** Execute this after a filter is applied */
  afterFilter?: (value: unknown) => void;
}

interface Yii2DataTableProps {
  endpoint: string;
  columns: TSColumnTemplate[];
  model: string;
  primaryKey?: string | number;
  pagination: boolean;
  rowCount?: number;
  noHeader?: boolean;
  /**
   * Overwrite the normal header with some custom react
   */
  customHeader?: ReactElement;
  removableSort?: boolean | undefined;
  sortMode?: "single" | "multiple" | undefined;
  rowsPerPageOptions?: number[] | undefined;
  /**
   * Need to force a refresh of the table data without any other value changing? Simply increase this number.
   */
  refreshTrigger?: number;

  /**
   * A collection of rows or a map object row data keys that are expanded.
   */
  expandedRows?: DataTableValueArray | DataTableExpandedRows | undefined;
  /**
   * Callback to invoke when a row is toggled or collapsed.
   * @param {DataTableRowToggleEvent} event - Custom row toggle event.
   */
  onRowToggle?(event: DataTableRowToggleEvent): void;
  /**
   * Callback to invoke when a row is expanded.
   * @param {DataTableRowEvent} event - Custom row event.
   */
  onRowExpand?(event: DataTableRowEvent): void;
  /**
   * Callback to invoke when a row is collapsed.
   * @param {DataTableRowEvent} event - Custom row event.
   */
  onRowCollapse?(event: DataTableRowEvent): void;
  /**
   * Function that receives the row data as the parameter and returns the expanded row content. You can override the rendering of the content by setting options.customRendering = true.
   * @param {DataTableRowData<TValue>} data - Editing row data.
   * @param {DataTableRowExpansionTemplate} options - Options for the row expansion template.
   */
  rowExpansionTemplate?(
    // @ts-expect-error TValue not defined
    data: DataTableRowData<TValue>,
    options: DataTableRowExpansionTemplate,
  ): React.ReactNode;

  /**
   * Allows parent to hook into table loading state
   * @param loading Table loading state
   */
  onLoadingChange?(loading: boolean): void;

  /**
   * Show maximize button in table header
   * @defaultValue true
   */
  showMaximizeButton?: boolean;

  /**
   * Custom title for the maximized modal
   */
  modalTitle?: string;

  /**
   * Table internally scro
   */
  scrollable?: boolean;

  /**
   * Input delay in ms.
   * @defaultValue 300ms.
   */
  debounce?: number;

  /**
   * Whether to hide the generated skeleton on fetch
   * Used for tables that are simulating live updates
   * @defaultValue false
   */
  hideFetchSkeleton?: boolean;

  /**
   * Execute code after the goGet() has completed successfully. Useful to detect if the table has loaded or not
   */  
  afterFind?: () => void;
}

const OSDataTable: React.FC<Yii2DataTableProps> = ({
  endpoint,
  columns,
  model,
  primaryKey,
  pagination,
  rowCount = 20,
  noHeader,
  customHeader = undefined,
  removableSort = undefined,
  sortMode = "single",
  rowsPerPageOptions = undefined,
  refreshTrigger = 0,
  expandedRows = undefined,
  onRowToggle = undefined,
  onRowExpand = undefined,
  onRowCollapse = undefined,
  rowExpansionTemplate = undefined,
  onLoadingChange = undefined,
  showMaximizeButton = true,
  modalTitle = "Table View",
  scrollable = undefined,
  debounce = 500,
  hideFetchSkeleton = false,
  afterFind = undefined,
}) => {
  /**
   *  Flag to determine whether current data is placeholder data or real data - Should never display placeholder data
   *  Standard variable as stays false after first render of component
   */
  const isDummyData = useRef(false);

  const [isMaximized, setIsMaximized] = useState<boolean>(false);

  // Populates table with placeholder data to allow skeleton to display on initial load
  function generateSkeletonData(): DataItem[] {
    const data: DataItem[] = [];
    for (let index = 0; index < rowCount; index++) {
      data.push({ id: index });
    }
    isDummyData.current = true;
    return data;
  }

  //Memory of most recent network request
  const latestRequest = useRef<string>(undefined);
  const latestRefresh = useRef<number>(undefined);

  const [data, setData] = useState<DataItem[]>(generateSkeletonData);
  const [loading, setLoading] = useState<boolean>(false);
  const [totalRecords, setTotalRecords] = useState<number>(0);
  const [first, setFirst] = useState<number>(0);
  const [rows, setRows] = useState<number>(rowCount);
  const [sortField, setSortField] = useState<string | undefined>(undefined);
  const [sortOrder, setSortOrder] = useState<SortOrder>(1);

  const [filters, debouncedFilters, setFilters] =
    useDebounce<DataTableFilterMeta>({}, debounce);

  //Init filters
  useEffect(() => {
    const initialFilters: DataTableFilterMeta = {};

    columns.forEach((column) => {
      if (column.filter && column.field) {
        initialFilters[column.field] = {
          value: null,
          matchMode:
            column.filterType === FilterType.Number //Todo: Improve filter functionality
              ? FilterMatchMode.EQUALS
              : FilterMatchMode.CONTAINS,
        };
      }
    });

    setFilters(initialFilters);
  }, []);

  useEffect(() => {
    if (onLoadingChange !== undefined) {
      onLoadingChange(loading);
    }
  }, [loading]);

  const fetchData = useCallback(
    async (refreshTrigger: number) => {
      setLoading(true);

      if (hideFetchSkeleton === false) {
        setData(generateSkeletonData);
      }

      try {
        const params = new URLSearchParams();

        if (pagination) {
          params.append("page", String(Math.floor(first / rows) + 1));
          params.append("pageSize", String(rows));
        }

        if (sortField && sortOrder) {
          params.append("sort", `${sortOrder === -1 ? "-" : ""}${sortField}`);
        }

        for (const [field, filter] of Object.entries(debouncedFilters)) {
          const filterValue = (filter as DataTableFilterMetaData).value;
          if (filterValue !== undefined && filterValue !== null) {
            // recreate GET params as Yii would expect
            params.append(`${model}[${field}]`, filterValue.toString());
            // } else {
            //   console.log("Ignoring filtervalue...");
          }
        }

        //   console.log("Making request with params:", params.toString());

        const sendEndpoint = endpoint.includes("?")
          ? `${endpoint}&${params.toString()}`
          : `${endpoint}?${params.toString()}`;

        if (
          sendEndpoint === latestRequest.current &&
          refreshTrigger === latestRefresh.current
        ) {
          //If endpoint hasn't changed and refresh wasn't prompted, abort fetch.
          return;
        }

        //Fetch is going forward - store state to prevent unnecessary requests.
        latestRequest.current = sendEndpoint;
        latestRefresh.current = refreshTrigger;

        //Perform request
        await goGet(sendEndpoint)
          .then((response) => {
            if (sendEndpoint !== latestRequest.current) {
              //Ignore if not latest request
              return;
            }
            const responseData = response as {
              dataProvider: Yii2DataProviderResponse;
            };
            setData(responseData.dataProvider.models);
            setTotalRecords(responseData.dataProvider.totalCount);
            if (afterFind !== undefined) {
              afterFind();
            }
          })
          .catch(() => {
            if (isDummyData.current) {
              setData([]);
            }
          })
          .finally(() => (isDummyData.current = false)); // Not necessary, but calls anyway to be safe
      } catch (error) {
        console.error("Error fetching data:", error);
      } finally {
        setLoading(false);
      }
    },
    [
      endpoint,
      first,
      rows,
      sortField,
      sortOrder,
      debouncedFilters,
      model,
      pagination,
    ],
  );

  const fetch = useRef<boolean>(false);

  useEffect(() => {
    /**
     * Prevents a double fetch on load
     */
    if (fetch.current !== false) {
      fetchData(refreshTrigger);
    } else {
      fetch.current = true;
    }
  }, [fetchData, refreshTrigger]);

  const onPage = (event: { first: number; rows: number }) => {
    setFirst(event.first);
    setRows(event.rows);
  };

  const onSort = (event: { sortField: string; sortOrder: SortOrder }) => {
    setSortField(event.sortField);
    setSortOrder(event.sortOrder);
  };

  const onFilter = (event: { filters: DataTableFilterMeta }) => {
    // console.log("Filter event:", event.filters);
    setFilters(event.filters);
    setFirst(0);
  };

  const getFilterTemplate = (
    options: ColumnFilterElementTemplateOptions,
    column: TSColumnTemplate,
    filterDropDownOptions?: FilterDropDownOptions[],
  ) => {
    switch (column.filterType) {
      case FilterType.Number: {
        return (
          <InputNumber
            value={options.value || null}
            onChange={(e: InputNumberChangeEvent) => {
              options.filterApplyCallback(e.value);
              if (column.afterFilter !== undefined) {
                column.afterFilter(e.value);
              }
            }}
            placeholder={`Filter`}
            className="p-column-filter w-full"
            showButtons
            style={{ fontFamily: "Poppins" }}
          />
        );
      }

      case FilterType.Text: {
        return (
          <InputText
            value={options.value || ""}
            onChange={(e) => {
              options.filterApplyCallback(e.target.value);
              if (column.afterFilter !== undefined) {
                column.afterFilter(e.target.value);
              }
            }}
            placeholder={`Filter`}
            className="p-column-filter w-full"
            style={{ fontFamily: "Poppins" }}
          />
        );
      }

      case FilterType.Dropdown: {
        return (
          <Dropdown
            value={options.value || null}
            onChange={(e: DropdownChangeEvent) => {
              options.filterApplyCallback(e.value);
              if (column.afterFilter !== undefined) {
                column.afterFilter(e.value);
              }
            }}
            options={filterDropDownOptions}
            optionLabel="label"
            placeholder={`Filter`}
            className="p-column-filter"
            checkmark
            editable={
              column.dropDownStatic === true ||
              column.dropDownValueTemplate !== undefined
                ? false
                : true
            }
            filter={column.dropdownFilter}
            valueTemplate={column.dropDownValueTemplate}
            itemTemplate={column.dropDownItemTemplate}
            loading={column.dropDownLoading}
          />
        );
      }

      case FilterType.Date: {
        return (
          <Calendar
            value={options.value || null}
            onChange={(e) => {
              options.filterApplyCallback(e.value);
              if (column.afterFilter !== undefined) {
                column.afterFilter(e.value);
              }
            }}
            placeholder={`Filter`}
            className="p-column-filter w-full"
            dateFormat="yy/mm/dd"
            readOnlyInput
            showButtonBar
          />
        );
      }
    }
  };

  const getHeaderTemplate = () => {
    let end = (Math.floor(first / rows) + 1) * rows;
    if (end > totalRecords) {
      end = totalRecords;
    }

    return (
      <div className="flex justify-between items-center w-full p-2">
        <div>
          {data ? (
            <OSLabel
              className="p-1"
              label={
                loading && !hideFetchSkeleton
                  ? ""
                  : end !== 0
                    ? pagination
                      ? `Showing ${
                          first + 1
                        } to ${end} out of ${totalRecords} total records`
                      : `${totalRecords} records found`
                    : "" //Table itself shows 'No Records found', so no need for duplication.
              }
              tag={"small"}
            />
          ) : (
            <Skeleton height="5rem" />
          )}
        </div>
        {showMaximizeButton && !isMaximized && (
          <OSButton
            id="maximize-table-icon"
            icon={{
              identifier: "arrow-up-right-and-arrow-down-left-from-center",
              colour: "var(--gray-400)",
            }}
            onClick={() => setIsMaximized(true)}
            className="text-sm"
          />
        )}
      </div>
    );
  };

  const renderDataTable = (maximized: boolean = false) => (
    <DataTable
      id={maximized ? "os-data-table-maximized" : "os-data-table"}
      value={data}
      lazy
      paginator={pagination && totalRecords > rows}
      rows={pagination ? rows : undefined}
      rowsPerPageOptions={rowsPerPageOptions}
      totalRecords={totalRecords}
      first={first}
      onPage={onPage}
      onSort={onSort}
      sortField={sortField}
      sortOrder={sortOrder}
      filters={filters}
      filterDisplay="row"
      filterClearIcon={
        <OSIcon
          identifier="filter-circle-xmark"
          colour="var(--danger-red)"
          tooltip="Clear filter"
          size={"lg"}
        />
      }
      onFilter={onFilter}
      key={primaryKey}
      emptyMessage="No records found"
      stripedRows
      header={() => {
        if (noHeader) {
          return undefined;
        }
        if (customHeader !== undefined) {
          return <>{customHeader}</>;
        }
        return getHeaderTemplate();
      }}
      className="w-full"
      sortIcon={(args) => {
        return (
          <a
            className={
              args.sortOrder === 1
                ? "asc"
                : args.sortOrder === -1
                  ? "desc"
                  : "sort"
            }
          />
        );
      }}
      removableSort={removableSort}
      sortMode={sortMode}
      expandedRows={expandedRows}
      onRowToggle={onRowToggle}
      onRowExpand={onRowExpand}
      onRowCollapse={onRowCollapse}
      resizableColumns
      columnResizeMode="fit"
      rowExpansionTemplate={rowExpansionTemplate}
      scrollable={scrollable}
      scrollHeight={scrollable ? "80vh" : undefined} // soz if breaks something
    >
      {columns.map((column, i) => (
        <Column
          key={i}
          field={column.field}
          header={column.header || <></>}
          sortable={column.sortable}
          showFilterMenu={false}
          filter={column.filter}
          filterField={column.field}
          filterElement={(options) =>
            getFilterTemplate(options, column, column.filterDropDownOptions)
          }
          onFilterClear={() => {
            if (column.afterFilter !== undefined) {
              column.afterFilter(undefined);
            }
          }}
          body={
            loading || isDummyData.current ? (
              <>
                {" "}
                <Skeleton height="2rem" />{" "}
              </>
            ) : column.body ? (
              (rowData) => column.body!(rowData)
            ) : undefined
          }
          style={column.style}
          expander={column.expander}
        />
      ))}
    </DataTable>
  );

  return (
    <Fragment>
      <div id="table-wrapper" className="flex w-full">
        {renderDataTable(false)}
      </div>
      <Dialog
        header={modalTitle}
        visible={isMaximized}
        blockScroll
        maximized
        draggable={false}
        modal
        onHide={() => setIsMaximized(false)}
      >
        <div className="w-full h-full">{renderDataTable(true)}</div>
      </Dialog>
    </Fragment>
  );
};

export default OSDataTable;
