import React, {FC, ReactNode, useContext, useMemo, useRef} from "react";
import {TabField} from "./tree/fields/TabField";
import {ComponentRuntimeData, ComponentRuntimeDataWithParentType,} from "./tree/components";
import {type ComponentMeta, getComponentMeta, getComponentMetaData} from "./tree/ComponentsMeta";
import {WithLabels} from "./tree/fields/WithLabels";
import {cloneDeep, get, map} from "lodash";
import {CardType, Note} from "./tree/cards";
import {ToolTips} from "./tree/fields/ToolTip";
import {FieldItem, FieldRow, FieldType} from "./tree/fields";
import {MultiSelect} from "./tree/fields/MultiSelect";
import {Number} from "./tree/fields/Number";
import classNames from "classnames";
import {Button} from "./tree/cards/Button";
import {MultiSelectSearch} from "./tree/fields/MultiSelectSearch";
import {convertDictToArrayOfOptions} from "./tree/cards/FieldHelpers";
import {Text} from "./tree/fields/Text";
import {Calendar} from "./tree/fields/Calendar";
import {TimeField} from "./tree/fields/TimeField";
import dayjs from "dayjs";
import {__} from "./globals";
import {ToolTip} from "./ToolTip";
import {PopupWindowStateContext} from "./tree/atoms";
import {getLazyTestables} from "./tree/hooks";
import {TestableData} from "./tree/store";
import {RowOptions} from "./tree/cards/Fields/RowOptions";
import {FieldSection} from "./tree/FieldSection";
import {SuggestedScopeContext} from "./tree/suggestedScopes";


export enum FieldUpdateAction {
    // for single value fields
    Update = 'update',
    // for multi-value fields
    Add = 'add',
    Remove = 'remove',
}

export type FieldsProps<Value = any, Action = undefined | {
    action: FieldUpdateAction,
    merge?: Record<string, any>
}> = {
    componentRuntimeData: ComponentRuntimeDataWithParentType,
    onUpdate: (componentRuntimeData: ComponentRuntimeData, field: FieldItem, value: Value, action: Action, extraNestedFieldPathRelative?: string) => void,
    notes: Note[],
    setNotes: (notes: Note[]) => void,
    scope: PopupWindowStateContext<any>['scope'],
    origin?: string | 'testables'
}

export type UpdateField = <Value = any, Action = {
    action: 'add' | 'remove' | 'update'
}>(value: Value, action?: Action, extraNestedFieldPathRelative?: string) => void

export type RenderFieldContext = {
    componentRuntimeData: ComponentRuntimeDataWithParentType,
    fields: Record<string, FieldItem>,
    options: any,
    onUpdate: FieldsProps['onUpdate'],
    toolTips: ToolTips,
    notes: Note[],
    setNotes: (notes: Note[]) => void,
    lastNoteAddedOnHover: React.MutableRefObject<Note | null>,
    getNoteForFieldValue: (fieldId: string, fieldValue: any) => Note | undefined,
    getNoteForCurrentFieldValue: (fieldId: string) => Note | undefined,
    renderFieldRow?: (fieldRow: FieldRow) => ReactNode,
    renderFieldsGrid?: (fields: FieldRow) => ReactNode,
}

export type RenderFieldProps = {
    field: FieldItem,
    context: RenderFieldContext,
    extraOptionalStructure?: boolean,
}

export const FieldRenderer: FC<RenderFieldProps> = ({field, context, extraOptionalStructure = true}) => {
    return <>{renderField(field, context, undefined, extraOptionalStructure)}</>
}

export type SingleFieldRendererProps = FieldsProps & {
    field: FieldItem,
    extraOptionalStructure?: boolean,
}

/**
 * Hook to create the render context for fields.
 * This contains all the logic needed to render fields and can be used
 * both by the Fields component and SingleFieldRenderer.
 */
export function useFieldRenderContext(
    componentRuntimeData: FieldsProps['componentRuntimeData'],
    onUpdate: FieldsProps['onUpdate'],
    notes: FieldsProps['notes'],
    setNotes: FieldsProps['setNotes'],
    scope: FieldsProps['scope']
): { context: RenderFieldContext, renderFieldRow: (fieldRow: FieldRow) => ReactNode } {
    let {parentType, type, options} = componentRuntimeData
    if (scope === 'tier-subchild') {
        const testables = getLazyTestables()
        const baseTestableData = testables.getParent(componentRuntimeData.id) as TestableData | undefined
        parentType = baseTestableData?.testableType ? `${baseTestableData.testableType}s` : parentType
        type = baseTestableData?.type || type
    }
    const componentMeta = getComponentMeta(parentType, type);
    const cardType = getComponentMetaData(componentMeta)
    const lastNoteAddedOnHover = useRef<Note | null>(null)
    const bottomNoticesPortalRef = useRef<HTMLDivElement>(null)

    const toolTips = useMemo(() => {
        const toolTips = new ToolTips()
        return toolTips
    }, [options])

    const fields = cardType.fields
    const getNoteForFieldValue = (fieldId: string, fieldValue: any) => {
        const notesForThisField = fields[fieldId]?.meta?.notes
        return notesForThisField?.[fieldValue]
    }

    const getNoteForCurrentFieldValue = (fieldId: string) => {
        return getNoteForFieldValue(fieldId, get(options, fieldId));
    }

    // Initialize notes for current field values
    Object.keys(fields).forEach(fieldId => {
        const noteForThisValue = getNoteForCurrentFieldValue(fieldId);
        if (noteForThisValue) {
            const added = toolTips.add(noteForThisValue)
            if (added) {
                setNotes([...notes, noteForThisValue])
            }
        }
    })

    const renderFieldRow = (fieldRow: FieldRow) => {
        function renderFieldsGrid(fields: FieldRow) {
            if (!fields) {
                return [];
            }

            return <div className={classNames("grid grid-cols-2 gap-x-2 gap-y-5 items-center")}>
                {fields.map((field) => {
                    const renderedField = renderField(field, renderContext, undefined, false, bottomNoticesPortalRef)

                    return <>
                        <div className="space-y-1">
                            <h2 className="text-2x text-gray-650">{field.title}</h2>
                            {field.description &&
                                <p className="text-gray-500 text-smaller-1 leading-[18px]]">{field.description}</p>}
                        </div>
                        <div className="w-full flex justify-end">{renderedField}</div>
                    </>
                })}
            </div>;
        }

        const renderContext: RenderFieldContext = {
            componentRuntimeData,
            fields,
            options,
            onUpdate,
            toolTips,
            notes,
            setNotes,
            lastNoteAddedOnHover,
            getNoteForFieldValue,
            getNoteForCurrentFieldValue,
            renderFieldRow: undefined,
            renderFieldsGrid: undefined,
        }
        renderContext.renderFieldRow = renderFieldRow
        renderContext.renderFieldsGrid = renderFieldsGrid

        const actualFieldRow = fieldRow?.filter?.(field => !(field instanceof RowOptions)) as FieldItem[] || []
        const fieldRowOptions = fieldRow?.find?.(field => (field instanceof RowOptions)) as RowOptions | undefined
        const autoWidthCols = fieldRowOptions?.options.colWidth === 'auto'

        if (!actualFieldRow.length) {
            return
        }

        return (
            <>
                <div className={classNames('flex gap-1', {
                    'flex-row': fieldRowOptions?.options.direction === 'horizontal',
                    'flex-col': fieldRowOptions?.options.direction === 'vertical',
                    'px-6 py-10 bg-gray-250/[.13] rounded-[28px]': fieldRowOptions?.options.type === 'featured',
                    'justify-center': autoWidthCols
                })} style={{gap: typeof fieldRowOptions?.options.gap === 'number'? fieldRowOptions?.options.gap : undefined}}>
                    {actualFieldRow.map(field => {
                        if (Array.isArray(field)) {
                            return <div className={classNames('flex flex-col space-y-2', {
                                'w-full': !autoWidthCols
                            })}>
                                {field.map(f => renderField(f, renderContext, undefined, true, bottomNoticesPortalRef))}
                            </div>
                        }

                        return renderField(field, renderContext, undefined, !autoWidthCols, bottomNoticesPortalRef)
                    })}
                </div>
                <div ref={bottomNoticesPortalRef} className={'empty:hidden'}></div>
            </>
        );
    };

    const context: RenderFieldContext = {
        componentRuntimeData,
        fields,
        options,
        onUpdate,
        toolTips,
        notes,
        setNotes,
        lastNoteAddedOnHover,
        getNoteForFieldValue,
        getNoteForCurrentFieldValue,
        renderFieldRow,
        renderFieldsGrid: (fieldsToRender: FieldRow) => {
            if (!fieldsToRender) {
                return [];
            }
            return <div className="grid grid-cols-2 gap-x-2 gap-y-5 items-center">
                {fieldsToRender.map((field) => {
                    const renderedField = renderField(field, context, undefined, false, bottomNoticesPortalRef)
                    return <>
                        <div className="space-y-1">
                            <h2 className="text-2x text-gray-650">{field.title}</h2>
                            {field.description &&
                                <p className="text-gray-500 text-smaller-1 leading-[18px]]">{field.description}</p>}
                        </div>
                        <div className="w-full flex justify-end">{renderedField}</div>
                    </>
                })}
            </div>;
        },
    }

    return {context, renderFieldRow}
}

/**
 * Renders a single field with all the business logic from Fields component.
 * Use this when you need to render an individual field outside of the Fields component.
 */
export const SingleFieldRenderer: FC<SingleFieldRendererProps> = ({
                                                                      componentRuntimeData,
                                                                      onUpdate,
                                                                      notes = [],
                                                                      setNotes = () => {
                                                                      },
                                                                      scope,
                                                                      field,
                                                                      extraOptionalStructure = true
                                                                  }) => {
    const {context} = useFieldRenderContext(componentRuntimeData, onUpdate, notes, setNotes, scope)
    return <>{renderField(field, context, undefined, extraOptionalStructure)}</>
}

function getInternalExtraFieldsMap(componentRuntimeData: ComponentRuntimeDataWithParentType, componentMeta: ComponentMeta, cardType: CardType): FieldRow[] {
    // add the recursive fields map if supported
    if (cardType.supports?.recursiveness) {
        const isRecursiveField = get(cardType.fields, '__BASE__.isRecursive');
        return [
            [
                {
                    id: '__BASE__.isRecursive',
                    renderType: FieldType.DualBooleanTab,
                    fieldOptions: {
                        direction: 'vertical',
                    },
                    title: __('How many times can this be applied?'),
                    fieldOptions: {
                        booleanLabels: isRecursiveField?.meta?.booleanLabels
                    }
                }
            ]
        ]
    }
    return []
}

export const Fields: FC<FieldsProps> = ({componentRuntimeData, onUpdate, notes, setNotes, scope, origin}) => {
    let {parentType, type, options} = componentRuntimeData
    const suggestedScope = useContext(SuggestedScopeContext)

    if (scope === 'tier-subchild') {
        /**
         * Tier subchild is always in 'edit' mode so we know we can get its parent from the store
         */
            // the parent and type we should get it from the parent as this is a testable partial
        const testables = getLazyTestables()
        const baseTestableData = testables.getParent(componentRuntimeData.id) as TestableData | undefined
        parentType = baseTestableData?.testableType ? `${baseTestableData.testableType}s` : parentType
        type = baseTestableData?.type || type
    }
    const componentMeta = getComponentMeta(parentType, type);
    const cardType = getComponentMetaData(componentMeta)
    const fieldsMap = ((typeof componentMeta?.fieldsMap === 'function' ? componentMeta.fieldsMap(componentRuntimeData, scope, suggestedScope) : componentMeta?.fieldsMap) || []).filter(Boolean)
    const fieldsMapLast = ((typeof componentMeta?.fieldsMapLast === 'function' ? componentMeta.fieldsMapLast(componentRuntimeData, scope) : componentMeta?.fieldsMapLast) || []).filter(Boolean)
    const internalExtraFieldsMap = getInternalExtraFieldsMap(componentRuntimeData, componentMeta!, cardType)
    const clientNotes = typeof componentMeta?.fieldsNotes === 'function' ? componentMeta.fieldsNotes(componentRuntimeData, scope) : (componentMeta?.fieldsNotes || [])

    const {renderFieldRow} = useFieldRenderContext(componentRuntimeData, onUpdate, notes, setNotes, scope)

    // one p-1 to account for the scaled items on hover (they would be clipped otherwise because of the overflow-y-scroll situation)
    return <div className="flex flex-col gap-6 w-full overflow-y-scroll p-1 pb-5">
        {[...fieldsMap, internalExtraFieldsMap, ...fieldsMapLast].filter(i => !!i).map(renderFieldRow)}
        {[...(cardType?.notes || []), ...clientNotes]?.map(note => <ToolTip id={'note'}
                                                                            title={''}
                                                                            content={[note]}
                                                                            style={'light'} width={'full'}
                                                                            animate={false}
        />)}
    </div>
}

function renderFieldWithTitle(field: FieldItem, fieldToRender: JSX.Element, fieldOptions) {
    return <FieldSection title={field.title} description={field.description}>
        {fieldToRender}
    </FieldSection>;
}

export const renderField = (field: FieldItem, context: RenderFieldContext, extraOptionalStructure = true, fullWidth: boolean = true, bottomNoticesPortalRef?: React.MutableRefObject<HTMLDivElement | null>): React.ReactNode => {
    const {
        componentRuntimeData,
        fields,
        options,
        onUpdate,
        toolTips,
        setNotes,
        lastNoteAddedOnHover,
        getNoteForFieldValue,
        getNoteForCurrentFieldValue,
    } = context
    const renderFieldRow = context.renderFieldRow || (() => null)
    const renderFieldsGrid = context.renderFieldsGrid || (() => null)
    let renderedField: ReactNode
    const renderType = typeof field.renderType === 'function' ? field.renderType(options) : field.renderType
    const extraFieldProps = (typeof field.fieldProps === 'function' ? field.fieldProps(options, onUpdate, {
        componentRuntimeData,
        field
    }) : field?.fieldProps || {});
    const fieldOptions = (typeof field.fieldOptions === 'function' ? field.fieldOptions(options, onUpdate, {
        componentRuntimeData,
        field
    }) : field?.fieldOptions || {});
    const fieldMeta = get(fields, field.id)?.meta;

    const updateField: UpdateField = (value, action, extraNestedFieldPathRelative?: string) => {
        onUpdate(
            componentRuntimeData,
            field,
            typeof extraFieldProps?.beforeUpdateValue === 'function' ? extraFieldProps.beforeUpdateValue(value) : value,
            // @ts-ignore
            action,
            extraNestedFieldPathRelative
        );
        // this might not get called after the update because of async/queues
        fieldOptions?.onUpdate?.(value, action, extraNestedFieldPathRelative)
    }

    const optionValue = get(options, field.id);

    /**
     *     const options: Option[] = ([
     *         {value: 'one', label: 'Hoddies', id: '7855'},
     *         {
     *             value: {
     *                 type: 'hierarchy',
     *                 value: ['food', 'italian', 'pizza'],
     *             },
     *             label: 'food italian pizza',
     *             id: '5572',
     *         },
     *         {id: 'three', value: 'three', label: 'Jackets'},
     *         {id: 'four', value: 'four', label: 'T-shirts'},
     *         {id: 'five', value: 'five', label: 'Jeans'},
     *         {id: 'six', value: 'six', label: 'Shoes'},
     *         {id: 'seven', value: 'seven', label: 'Seven - a'},
     *         {id: 'eight', value: 'eight', label: 'Eight - a'},
     *         {id: 'nine', value: 'nine', label: 'Nine - a'},
     *         {id: 'ten', value: 'ten', label: 'Ten - a'}
     *     ] as Option[]);
     */

    switch (renderType) {
        case FieldType.Tab:
        case FieldType.BooleanTab:
            let tabOptionValues: string[] = []
            const useTabValues = fieldOptions?.multiple && fieldOptions?.format === 'booleanDictionary';
            if (useTabValues) {
                tabOptionValues = map(optionValue, (value, id) => {
                    return value ? id : false;
                }).filter(value => typeof value == 'string') as unknown as string[]
            }
            const field1 = get(fields, field.id);
            let isBooleanTab = renderType === FieldType.BooleanTab;

            let selectedTabId: string
            if (useTabValues) {
                selectedTabId = tabOptionValues
            } else if (isBooleanTab) {
                selectedTabId = optionValue ? 'true' : 'false'
            } else {
                selectedTabId = optionValue
            }

            renderedField =
                <TabField selectedTabId={selectedTabId}
                          field={field1}
                          format={isBooleanTab ? 'boolean' : fieldOptions?.format}
                          multiple={fieldOptions?.multiple}
                          direction={fieldOptions?.direction}
                          onChange={value => {
                              if (isBooleanTab) {
                                  value = !optionValue;
                              } else if (useTabValues) {
                                  // @ts-ignore
                                  const newBooleanDictionary: Record<string, boolean> = cloneDeep(field1 as Record<string, boolean>)

                                  for (const id in newBooleanDictionary) {
                                      newBooleanDictionary[id] = (value as string[]).includes(id)
                                  }

                                  value = newBooleanDictionary;
                              }

                              updateField(value)
                          }}
                          onTabHover={value => {
                              const note = getNoteForFieldValue(field.id, value);
                              if (note) {
                                  toolTips.set(note);
                                  lastNoteAddedOnHover.current = note;
                                  setNotes([...toolTips.all()])
                              }

                          }}
                          onTabUnhover={() => {
                              const note = getNoteForCurrentFieldValue(field.id)
                              if (note) {
                                  toolTips.set(note);
                                  setNotes([...toolTips.all()])
                              } else {
                                  if (lastNoteAddedOnHover.current) {
                                      toolTips.remove(lastNoteAddedOnHover.current.id);

                                      setNotes([...toolTips.all()])
                                  }
                              }
                          }}
                          {...extraFieldProps}
                />;
            break;
        case FieldType.DualBooleanTab:
            const dualBooleanField = get(fields, field.id);
            const dualBooleanLabels = fieldOptions?.booleanLabels || dualBooleanField?.meta?.booleanLabels || {};
            const dualSelectedTabId = optionValue ? 'true' : 'false';

            renderedField =
                <TabField selectedTabId={dualSelectedTabId}
                          field={{meta: {booleanLabels: dualBooleanLabels}} as any}
                          format={'dualBoolean'}
                          direction={fieldOptions?.direction}
                          onChange={value => {
                              updateField(value === 'true')
                          }}
                          onTabHover={value => {
                              const note = getNoteForFieldValue(field.id, value === 'true');
                              if (note) {
                                  toolTips.set(note);
                                  lastNoteAddedOnHover.current = note;
                                  setNotes([...toolTips.all()])
                              }
                          }}
                          onTabUnhover={() => {
                              const note = getNoteForCurrentFieldValue(field.id)
                              if (note) {
                                  toolTips.set(note);
                                  setNotes([...toolTips.all()])
                              } else {
                                  if (lastNoteAddedOnHover.current) {
                                      toolTips.remove(lastNoteAddedOnHover.current.id);
                                      setNotes([...toolTips.all()])
                                  }
                              }
                          }}
                          {...extraFieldProps}
                />;
            break;
        case FieldType.Select:
            let optionsKey = fieldOptions?.optionsKey || '_allowed';
            const rawOptions = fieldOptions?.options ?? (fieldMeta?.[optionsKey] || {});

            const options = convertDictToArrayOfOptions(rawOptions, false)

            renderedField = <MultiSelectSearch options={options} selectedValue={optionValue}
                                               onSelectedValue={value => {
                                                   updateField(value, {action: 'add'})
                                               }}
                                               {...extraFieldProps}
            />
            break
        case FieldType.MultiSelect:
            // @ts-ignore

            renderedField = <MultiSelect options={
                (
                    fieldOptions?.options ?? (fieldMeta?.[fieldOptions?.optionsKey || '__allowed'] || {})
                )
            }
                                         value={optionValue}
                                         onSelectedValue={value => {
                                             updateField(value, {action: 'add'})
                                         }}
                                         onRemovedValue={value => updateField(value, {action: 'remove'})}
                                         {...(extraFieldProps)}
            />
            break
        case FieldType.Number:
            const numberFieldMeta = get(
                fields,
                field.id // don't try to pass the '${field.id}.amount' as it will break because we wil no longer have the meta of the .type (amount and type (type is a select menu))
            )
            console.log('fieldMeta', numberFieldMeta, fieldMeta, field, get(fields, field.id))
            renderedField = <div className="flex items-center justify-center --mt-4">
                <Number id={componentRuntimeData.id + field.id}
                        fieldMeta={numberFieldMeta}
                        value={optionValue}
                        persistDefaultOnMount={true}
                        onChange={
                            (fieldName, newValue) => {
                                updateField(newValue, {action: 'update'}, fieldName)
                            }
                        }
                        labels={numberFieldMeta.meta?.labels}
                        {...(extraFieldProps)}
                        bottomNoticesPortalRef={bottomNoticesPortalRef}
                />
            </div>;
            break;
        case FieldType.Text:
        case FieldType.MultiLineText:
            renderedField = <div className="----mt-4">
                <Text id={componentRuntimeData.id + fieldMeta?.name}
                      value={optionValue}
                      onChange={newValue => updateField(newValue, {action: 'update'})}
                      name={fieldMeta?.name}
                      type={renderType === FieldType.Text ? 'input' : 'textarea'}
                      placeholder={fieldMeta?.placeholder ?? fieldMeta?._default}
                      {...extraFieldProps}
                />
            </div>
            break
        case FieldType.Button:
            renderedField = <Button {...(extraFieldProps)}
                                    onClick={() => extraFieldProps.onClick(updateField, onUpdate, /*{componentRuntimeData, fieldsMap}*/)}
            />
            break;
        case FieldType.BooleanButton:
            renderedField = <div>
                {fieldOptions?.fieldsMap?.[optionValue ? 'true' : 'false']?.map(renderFieldRow)}
                {!!fieldOptions?.labels?.top?.length &&
                    <div className="text-gray-500 text-smaller-1 space-y-1">
                        {fieldOptions?.labels?.top?.map(label => <p key={label}>{label}</p>)}
                    </div>}
                <Button {...(extraFieldProps)}
                        children={
                            fieldMeta?.labels?.[optionValue ? 'true' : 'false'] || (optionValue ? __('On') : __('Off'))
                        }
                        onClick={() => updateField(!optionValue, {action: 'update'})}
                />
            </div>
            break;
        case FieldType.Date:
            renderedField = <div className="flex justify-center">
                <Calendar source={optionValue || get(fields, field.id)?.meta?._default as string}
                          onChange={newValue => updateField(newValue, {action: 'update'})}
                          {...extraFieldProps}
                />
            </div>
            break;
        case FieldType.Time:
            const originalTime = optionValue || get(fields, field.id)?.meta?._default as string
            let timeToUse: string = originalTime

            if (fieldOptions?.isFullDate) {
                // Extract hours and minutes from date string in format Y-m-d H:i:s
                const parsedDate = dayjs(originalTime, 'YYYY-MM-DD HH:mm:ss');
                timeToUse = parsedDate.format('HH:mm');
            }

            renderedField = <div className="flex justify-center">
                <TimeField source={timeToUse}
                           onChange={newValue => {
                               if (fieldOptions?.isFullDate) {
                                   // Replace the time portion in the original date string with the new time
                                   const originalDate = dayjs(originalTime, 'YYYY-MM-DD HH:mm:ss');
                                   const [hours, minutes] = newValue.split(':');
                                   const updatedDateTime = originalDate
                                       .hour(parseInt(hours))
                                       .minute(parseInt(minutes))
                                       .second(0);
                                   const newDateTimeString = updatedDateTime.format('YYYY-MM-DD HH:mm:ss');
                                   updateField(newDateTimeString, {action: 'update'});
                               } else {
                                   updateField(newValue, {action: 'update'})
                               }
                           }}
                           {...extraFieldProps}
                />
            </div>
            break;
        case FieldType.Custom:
            renderedField = extraFieldProps.component
            break;
        case FieldType.Columns:
            renderedField = renderFieldRow(field.columns)
            break;
        case FieldType.Grid:
            renderedField = renderFieldsGrid(field.fields)
            break;
        case false:
            // unlike default case, this will trigger an error in the console
            renderedField = null
            break;
        default:
            console.error('Fields: Unknown render type', renderType, field);
            renderedField = null
            break;
    }

    if (!renderedField) {
        return null;
    }

    if (!extraOptionalStructure) {
        return renderedField
    }

    let fieldToRender = <WithLabels centered={fieldOptions?.centered} fullWidth={fullWidth}>
        {renderedField}
    </WithLabels>;

    if (field.title) {
        fieldToRender = renderFieldWithTitle(field, fieldToRender, fieldOptions)
    }

    return field.wrap?.(fieldToRender) || fieldToRender;
};
