import { aliases as faIcons } from '@/icons/fa';
import type { IconAliases } from '@/icons/icons';
import { aliases as mdiIcons } from '@/icons/mdi';
import {
  Resolve,
  arrayDefaultTranslations,
  combinatorDefaultTranslations,
  composePaths,
  computeLabel,
  defaultJsonFormsI18nState,
  getArrayTranslations,
  getCombinatorTranslations,
  getCombinedErrorMessage,
  getControlPath,
  getErrorTranslator,
  getFirstPrimitiveProp,
  getTranslator,
  isDescriptionHidden,
  type ControlElement,
  type DispatchPropsOfControl,
  type DispatchPropsOfMultiEnumControl,
  type JsonSchema,
  type UISchemaElement,
} from '@jsonforms/core';
import { useJsonForms } from '@jsonforms/vue';
import type { ErrorObject } from 'ajv';
import cloneDeep from 'lodash/cloneDeep';
import debounce from 'lodash/debounce';
import get from 'lodash/get';
import isPlainObject from 'lodash/isPlainObject';
import merge from 'lodash/merge';
import {
  computed,
  inject,
  provide,
  ref,
  type ComputedRef,
  type InjectionKey,
} from 'vue';
import type { IconOptions } from 'vuetify';
import { useStyles } from '../styles';
import { IsDynamicPropertyContext } from './inject';

export const IconSymbol: InjectionKey<Required<IconOptions>> =
  Symbol.for('vuetify:icons');

export const useControlAppliedOptions = <
  T extends { config: any; uischema: UISchemaElement },
  I extends {
    control: ComputedRef<T>;
  },
>(
  input: I,
) => {
  return computed(() =>
    merge(
      {},
      cloneDeep(input.control.value.config),
      cloneDeep(input.control.value.uischema.options),
    ),
  );
};

export const useLayoutAppliedOptions = <
  T extends { config: any; uischema: UISchemaElement },
  I extends {
    layout: ComputedRef<T>;
  },
>(
  input: I,
) => {
  return computed(() =>
    merge(
      {},
      cloneDeep(input.layout.value.config),
      cloneDeep(input.layout.value.uischema.options),
    ),
  );
};

export const useComputedLabel = <
  T extends { label: string; required: boolean },
  I extends { control: ComputedRef<T> },
>(
  input: I,
  appliedOptions: ReturnType<typeof useControlAppliedOptions>,
) => {
  return computed((): string => {
    return computeLabel(
      input.control.value.label,
      input.control.value.required,
      !!appliedOptions.value?.hideRequiredAsterisk,
    );
  });
};

export const isControlEditable = (control: {
  enabled: boolean;
  readonly: boolean;
}) => control.enabled && !control.readonly;

/**
 * Adds styles, appliedOptions and vuetifyProps
 */
export const useVuetifyLabel = <
  T extends {
    uischema: UISchemaElement;
    config: any;
  },
  I extends {
    label: ComputedRef<T>;
  },
>(
  input: I,
) => {
  const styles = useStyles(input.label.value.uischema);
  const appliedOptions = computed(() =>
    merge(
      {},
      cloneDeep(input.label.value.config),
      cloneDeep(input.label.value.uischema.options),
    ),
  );
  const vuetifyProps = (path: string) => {
    const props = get(appliedOptions.value?.vuetify, path);

    return props && isPlainObject(props) ? props : {};
  };
  return {
    ...input,
    appliedOptions,
    vuetifyProps,
    styles,
  };
};

/**
 * Adds styles, isFocused, appliedOptions and onChange
 */
export const useVuetifyControl = <
  T extends {
    schema: JsonSchema;
    uischema: ControlElement;
    path: string;
    config: any;
    label: string;
    description: string;
    required: boolean;
    errors: string;
    id: string;
    visible: boolean;
    enabled: boolean;
    readonly: boolean;
  },
  I extends {
    control: ComputedRef<T>;
  } & (DispatchPropsOfControl | DispatchPropsOfMultiEnumControl),
>(
  input: I,
  adaptValue: (target: any) => any = (v) => v,
  debounceWait?: number,
) => {
  const touched = ref(false);

  const changeEmitter =
    typeof debounceWait === 'number' &&
    (input as DispatchPropsOfControl).handleChange
      ? debounce((input as DispatchPropsOfControl).handleChange, debounceWait)
      : (input as DispatchPropsOfControl).handleChange;

  const onChange = (value: any) => {
    if (changeEmitter) {
      changeEmitter(input.control.value.path, adaptValue(value));
    }
  };

  const appliedOptions = useControlAppliedOptions(input);
  const isFocused = ref(false);

  const handleFocus = () => {
    isFocused.value = true;
  };

  const handleBlur = () => {
    touched.value = true;
    isFocused.value = false;
    if (changeEmitter && (changeEmitter as any).flush) {
      (changeEmitter as any).flush();
    }
  };

  const jsonforms = useJsonForms();
  const filteredErrors = computed(() => {
    // Always show errors if touched, no errors exist, or filtering is not enabled
    if (
      touched.value ||
      !input.control.value.errors ||
      !appliedOptions.value.enableFilterErrorsBeforeTouch
    ) {
      return input.control.value.errors;
    }

    const filterKeywords = appliedOptions.value.filterErrorKeywordsBeforeTouch;

    // Filtering is enabled - check if specific keywords are configured
    if (Array.isArray(filterKeywords) && filterKeywords.length > 0) {
      // Granular filtering: only hide specific error keywords
      const errorsAtControl =
        jsonforms.core?.errors?.filter(
          (error) => input.control.value.path === getControlPath(error),
        ) ?? [];

      // Filter out errors that match the filterKeywords, keep the rest
      const errorsToShow = errorsAtControl.filter(
        (error) => !error.keyword || !filterKeywords.includes(error.keyword),
      );
      // If no errors were filtered out (all errors remain), return original errors string
      if (errorsToShow.length === errorsAtControl.length) {
        return input.control.value.errors;
      }

      const t = getTranslator()({ jsonforms });
      const te = getErrorTranslator()({ jsonforms });

      return getCombinedErrorMessage(
        errorsToShow,
        te,
        t,
        input.control.value.schema,
        input.control.value.uischema,
        input.control.value.path,
      );
    }

    // default, all errors are filtered
    return '';
  });

  const persistentHint = (): boolean => {
    return !isDescriptionHidden(
      input.control.value.visible,
      input.control.value.description,
      isFocused.value,
      !!appliedOptions.value?.showUnfocusedDescription,
    );
  };

  const computedLabel = useComputedLabel(input, appliedOptions);

  const styles = useStyles(input.control.value.uischema);

  const vuetifyProps = (path: string) => {
    const props = get(appliedOptions.value?.vuetify, path);

    return props && isPlainObject(props) ? props : {};
  };

  const overwrittenControl = computed(() => {
    return {
      ...input.control.value,
      errors: filteredErrors.value,
    };
  });

  const controlWrapper = computed(() => {
    const { id, description, errors, label, visible, required } =
      overwrittenControl.value;
    return { id, description, errors, label, visible, required };
  });

  const rawErrors = computed(() => input.control.value.errors);

  const clearable = computed(() => {
    return appliedOptions.value.clearable !== undefined
      ? appliedOptions.value.clearable && input.control.value.enabled
      : isControlEditable(input.control.value);
  });

  return {
    ...input,
    control: overwrittenControl,
    styles,
    isFocused,
    appliedOptions,
    controlWrapper,
    onChange,
    vuetifyProps,
    persistentHint,
    computedLabel,
    clearable,
    touched,
    handleBlur,
    handleFocus,
    rawErrors,
  };
};

export const useCombinatorTranslations = <
  T extends {
    i18nKeyPrefix: string;
    label: string;
  },
  I extends {
    control: ComputedRef<T>;
  },
>(
  input: I,
) => {
  const jsonforms = useJsonForms();
  const translations = getCombinatorTranslations(
    jsonforms?.i18n?.translate ?? defaultJsonFormsI18nState.translate,
    combinatorDefaultTranslations,
    input.control.value.i18nKeyPrefix,
    input.control.value.label,
  );

  const overwrittenControl = computed(() => {
    return {
      ...input.control.value,
      translations,
    };
  });

  return {
    ...input,
    control: overwrittenControl,
  };
};

/**
 * Adds styles and appliedOptions
 */
export const useVuetifyLayout = <
  T extends { config: any; uischema: UISchemaElement },
  I extends { layout: ComputedRef<T> },
>(
  input: I,
) => {
  const appliedOptions = useLayoutAppliedOptions(input);

  const vuetifyProps = (path: string) => {
    const props = get(appliedOptions.value?.vuetify, path);

    return props && isPlainObject(props) ? props : {};
  };

  return {
    ...input,
    styles: useStyles(input.layout.value.uischema),
    appliedOptions,
    vuetifyProps,
  };
};

/**
 * Adds styles, appliedOptions and childUiSchema
 */
export const useVuetifyArrayControl = <
  T extends {
    label: string;
    required: boolean;
    config: any;
    uischema: UISchemaElement;
    schema: JsonSchema;
    data: any;
    childErrors: ErrorObject[];
    i18nKeyPrefix: string;
  },
  I extends {
    control: ComputedRef<T>;
  },
>(
  input: I,
) => {
  const appliedOptions = useControlAppliedOptions(input);

  const computedLabel = useComputedLabel(input, appliedOptions);

  const vuetifyProps = (path: string) => {
    const props = get(appliedOptions.value?.vuetify, path);

    return props && isPlainObject(props) ? props : {};
  };

  const childLabelForIndex = (index: number | null) => {
    if (index === null) {
      return '';
    }
    const childLabelProp =
      input.control.value.uischema.options?.childLabelProp ??
      getFirstPrimitiveProp(input.control.value.schema);
    if (!childLabelProp) {
      return `${index}`;
    }
    const labelValue = Resolve.data(
      input.control.value.data,
      composePaths(`${index}`, childLabelProp),
    );
    if (
      labelValue === undefined ||
      labelValue === null ||
      Number.isNaN(labelValue)
    ) {
      return '';
    }
    return `${labelValue}`;
  };
  const filteredChildErrors = computed(() => {
    if (
      !input.control.value.childErrors ||
      input.control.value.childErrors.length === 0 ||
      !appliedOptions.value.enableFilterErrorsBeforeTouch
    ) {
      return input.control.value.childErrors;
    }

    // supress childErrors unless touch filtering is disabled
    // otherwise all child errors will show, irrespective of their control touch state

    const filterKeywords = appliedOptions.value.filterErrorKeywordsBeforeTouch;

    // Filtering is enabled - check if specific keywords are configured
    if (Array.isArray(filterKeywords) && filterKeywords.length > 0) {
      // Granular filtering: only hide specific error keywords
      const errorsToShow = input.control.value.childErrors.filter(
        (error) => !error.keyword || !filterKeywords.includes(error.keyword),
      );

      return errorsToShow;
    }

    // default, all child errors are filtered
    return [];
  });

  const jsonforms = useJsonForms();
  const translations = getArrayTranslations(
    jsonforms?.i18n?.translate ?? defaultJsonFormsI18nState.translate,
    arrayDefaultTranslations,
    input.control.value.i18nKeyPrefix,
    input.control.value.label,
  );

  const overwrittenControl = computed(() => {
    return {
      ...input.control.value,
      childErrors: filteredChildErrors.value,
      translations,
    };
  });

  const rawChildErrors = computed(() => input.control.value.childErrors);

  return {
    ...input,
    control: overwrittenControl,
    styles: useStyles(input.control.value.uischema),
    appliedOptions,
    childLabelForIndex,
    computedLabel,
    vuetifyProps,
    rawChildErrors,
  };
};

/**
 * Adds styles and appliedOptions
 */
export const useVuetifyBasicControl = <
  T extends { config: any; uischema: UISchemaElement },
  I extends {
    control: ComputedRef<T>;
  },
>(
  input: I,
) => {
  const appliedOptions = useControlAppliedOptions(input);

  const vuetifyProps = (path: string) => {
    const props = get(appliedOptions.value?.vuetify, path);

    return props && isPlainObject(props) ? props : {};
  };

  return {
    ...input,
    styles: useStyles(input.control.value.uischema),
    appliedOptions,
    vuetifyProps,
  };
};

export interface NestedInfo {
  level: number;
  parentElement?: 'array' | 'object';
}

export const useNested = (element: false | 'array' | 'object'): NestedInfo => {
  const nestedInfo = inject<NestedInfo>('jsonforms.nestedInfo', { level: 0 });
  if (element) {
    provide('jsonforms.nestedInfo', {
      level: nestedInfo.level + 1,
      parentElement: element,
    });
  }
  return nestedInfo;
};

export const useIcons = () => {
  const iconSet = computed<IconAliases>(() => {
    const icons = inject(IconSymbol);
    if (!icons) throw new Error('Missing Vuetify Icons provide!');

    let result = mdiIcons; // default
    const overrides = icons.aliases;

    if (icons.defaultSet === 'fa') {
      result = faIcons;
    }

    return overrides ? { ...result, ...overrides } : result;
  });

  return {
    current: iconSet,
  };
};

export const determineClearValue = (defaultValue: any) => {
  const useDefaultValue = inject<boolean>(IsDynamicPropertyContext, false);

  // undefined will clear the property from the object
  return useDefaultValue ? defaultValue : undefined;
};
