import {
    ArrayProperty,
    AuthController,
    CMSType,
    CustomizationController,
    EntityCollection,
    EntityCustomView,
    EntityValues,
    EnumValueConfig,
    EnumValues,
    NumberProperty,
    Properties,
    PropertiesOrBuilders,
    Property,
    PropertyConfig,
    PropertyOrBuilder,
    ResolvedArrayProperty,
    ResolvedEntityCollection,
    ResolvedNumberProperty,
    ResolvedProperties,
    ResolvedProperty,
    ResolvedStringProperty,
    StringProperty,
    UserConfigurationPersistence
} from "../types";
import { getValueInPath, mergeDeep } from "./objects";
import { getDefaultValuesFor, isPropertyBuilder } from "./entities";
import { DEFAULT_ONE_OF_TYPE } from "./common";
import { getIn } from "@firecms/formex";
import { enumToObjectEntries } from "./enums";
import { isDefaultFieldConfigId } from "../core";

export const resolveCollection = <M extends Record<string, any>, >
({
     collection,
     path,
     entityId,
     values,
     previousValues,
     userConfigPersistence,
     propertyConfigs,
     ignoreMissingFields = false,
     authController
 }: {
    collection: EntityCollection<M> | ResolvedEntityCollection<M>;
    path: string,
    entityId?: string,
    values?: Partial<EntityValues<M>>,
    previousValues?: Partial<EntityValues<M>>,
    userConfigPersistence?: UserConfigurationPersistence;
    propertyConfigs?: Record<string, PropertyConfig>;
    ignoreMissingFields?: boolean;
    authController: AuthController;
}): ResolvedEntityCollection<M> => {

    const collectionOverride = userConfigPersistence?.getCollectionConfig<M>(path);
    const storedProperties = getValueInPath(collectionOverride, "properties");

    const defaultValues = getDefaultValuesFor(collection.properties);
    const usedValues = values ?? defaultValues;
    const usedPreviousValues = previousValues ?? values ?? defaultValues;

    const resolvedProperties = Object.entries(collection.properties)
        .map(([key, propertyOrBuilder]) => {
            const childResolvedProperty = resolveProperty({
                propertyKey: key,
                propertyOrBuilder: propertyOrBuilder as PropertyOrBuilder | ResolvedProperty,
                values: usedValues,
                previousValues: usedPreviousValues,
                path,
                entityId,
                propertyConfigs: propertyConfigs,
                ignoreMissingFields,
                authController
            });
            if (!childResolvedProperty) return {};
            return ({
                [key]: childResolvedProperty
            });
        })
        .filter((a) => a !== null)
        .reduce((a, b) => ({ ...a, ...b }), {}) as ResolvedProperties<M>;

    const properties: Properties = mergeDeep(resolvedProperties, storedProperties);
    const cleanedProperties = Object.entries(properties)
        .filter(([_, property]) => Boolean(property?.dataType))
        .map(([id, property]) => ({ [id]: property }))
        .reduce((a, b) => ({ ...a, ...b }), {});

    return {
        ...collection,
        properties: cleanedProperties,
        originalCollection: collection
    } as ResolvedEntityCollection<M>;

};

/**
 * Resolve property builders, enums and arrays.
 * @param propertyOrBuilder
 * @param propertyValue
 */
export function resolveProperty<T extends CMSType = CMSType, M extends Record<string, any> = any>({
                                                                                                      propertyOrBuilder,
                                                                                                      fromBuilder = false,
                                                                                                      ignoreMissingFields = false,
                                                                                                      ...props
                                                                                                  }: {
    propertyKey?: string,
    propertyOrBuilder: PropertyOrBuilder<T, M> | ResolvedProperty<T>,
    values?: Partial<M>,
    previousValues?: Partial<M>,
    path?: string,
    entityId?: string,
    index?: number,
    fromBuilder?: boolean;
    propertyConfigs?: Record<string, PropertyConfig<any>>;
    ignoreMissingFields?: boolean;
    authController: AuthController;
}): ResolvedProperty<T> | null {

    if (typeof propertyOrBuilder === "object" && "resolved" in propertyOrBuilder) {
        return propertyOrBuilder as ResolvedProperty<T>;
    }

    let resolvedProperty: ResolvedProperty<T> | null = null;

    if (!propertyOrBuilder) {
        return null;
    } else if (isPropertyBuilder(propertyOrBuilder)) {
        const path = props.path;
        if (!path)
            throw Error("Trying to resolve a property builder without specifying the entity path");

        const usedPropertyValue = props.propertyKey ? getIn(props.values, props.propertyKey) : undefined;
        const result: Property<T> | null = propertyOrBuilder({
            ...props,
            path,
            propertyValue: usedPropertyValue,
            values: props.values ?? {},
            previousValues: props.previousValues ?? props.values ?? {}
        });

        if (!result) {
            return null;
        }

        resolvedProperty = resolveProperty({
            ...props,
            propertyOrBuilder: result,
            fromBuilder: true,
            ignoreMissingFields
        });
    } else {
        const property = propertyOrBuilder as Property<T>;
        if (property.dataType === "map" && property.properties) {
            const properties = resolveProperties({
                ignoreMissingFields,
                ...props,
                properties: property.properties,
            });
            resolvedProperty = {
                ...property,
                resolved: true,
                fromBuilder,
                properties
            } as ResolvedProperty<T>;
        } else if (property.dataType === "array") {
            resolvedProperty = resolveArrayProperty({
                property,
                fromBuilder,
                ignoreMissingFields,
                ...props
            }) as ResolvedProperty<any>;
        } else if ((property.dataType === "string" || property.dataType === "number") && property.enumValues) {
            resolvedProperty = resolvePropertyEnum(property, fromBuilder) as ResolvedProperty<any>;
        }
    }

    if (!resolvedProperty) {
        resolvedProperty = {
            ...propertyOrBuilder,
            resolved: true,
            fromBuilder
        } as ResolvedProperty<T>;
    }

    if (resolvedProperty.propertyConfig && !isDefaultFieldConfigId(resolvedProperty.propertyConfig)) {
        const cmsFields = props.propertyConfigs;
        if (!cmsFields && !ignoreMissingFields) {
            throw Error(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property configs were provided. Use the property 'propertyConfigs' in your app config to provide them`);
        }
        const customField: PropertyConfig | undefined = cmsFields?.[resolvedProperty.propertyConfig];
        if (!customField) {
            console.warn(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property config with that key was found. Check the 'propertyConfigs' in your app config`)
            console.warn("Available property configs", cmsFields);
            return null;
        }
        if (customField.property) {
            const configPropertyOrBuilder = customField.property;
            if ("propertyConfig" in configPropertyOrBuilder) {
                delete configPropertyOrBuilder.propertyConfig;
            }
            const customFieldProperty = resolveProperty<any>({
                propertyOrBuilder: configPropertyOrBuilder,
                ignoreMissingFields,
                ...props
            });
            if (customFieldProperty) {
                resolvedProperty = mergeDeep(customFieldProperty, resolvedProperty);
            }
        }

    }

    return resolvedProperty
        ? {
            ...resolvedProperty,
            resolved: true
        }
        : null;
}

export function getArrayResolvedProperties<M>({
                                                  propertyKey,
                                                  propertyValue,
                                                  property,
                                                  ...props
                                              }: {
    propertyValue: any,
    propertyKey?: string,
    property: ArrayProperty<any> | ResolvedArrayProperty<any>,
    ignoreMissingFields: boolean,
    values?: Partial<M>;
    previousValues?: Partial<M>;
    path?: string;
    entityId?: string;
    index?: number;
    fromBuilder?: boolean;
    propertyConfigs?: Record<string, PropertyConfig>;
    authController: AuthController;
}) {

    const of = property.of;
    return Array.isArray(propertyValue)
        ? propertyValue.map((v: any, index: number) => {
            return resolveProperty({
                propertyKey: `${propertyKey}.${index}`,
                propertyOrBuilder: of,
                ...props,
                index
            });
        }).filter(e => Boolean(e)) as ResolvedProperty[]
        : [];
}

export function resolveArrayProperty<T extends any[], M>({
                                                             propertyKey,
                                                             property,
                                                             ignoreMissingFields = false,
                                                             ...props
                                                         }: {
    propertyKey?: string,
    property: ArrayProperty<T> | ResolvedArrayProperty<T>,
    values?: Partial<M>,
    previousValues?: Partial<M>,
    path?: string,
    entityId?: string,
    index?: number,
    fromBuilder?: boolean;
    propertyConfigs?: Record<string, PropertyConfig>;
    ignoreMissingFields?: boolean;
    authController: AuthController;
}): ResolvedArrayProperty {
    const propertyValue = propertyKey ? getIn(props.values, propertyKey) : undefined;

    if (property.of) {
        if (Array.isArray(property.of)) {
            return {
                ...property,
                resolved: true,
                fromBuilder: props.fromBuilder,
                resolvedProperties: property.of.map((p, index) => {
                    return resolveProperty({
                        propertyKey: `${propertyKey}.${index}`,
                        propertyOrBuilder: p as Property<any>,
                        ignoreMissingFields,
                        ...props,
                        index,
                    });
                })
            } as ResolvedArrayProperty;
        } else {
            const of = property.of;
            const resolvedProperties = getArrayResolvedProperties({
                propertyValue,
                propertyKey,
                property,
                ignoreMissingFields,
                ...props
            });
            const ofProperty = resolveProperty({
                propertyOrBuilder: of,
                ignoreMissingFields,
                ...props
            });
            if (!ofProperty && !ignoreMissingFields)
                throw Error("When using a property builder as the 'of' prop of an ArrayProperty, you must return a valid child property")
            return {
                ...property,
                resolved: true,
                fromBuilder: props.fromBuilder,
                of: ofProperty,
                resolvedProperties
            } as ResolvedArrayProperty;
        }
    } else if (property.oneOf) {
        const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE;
        const resolvedProperties: ResolvedProperty[] = Array.isArray(propertyValue)
            ? propertyValue.map((v, index) => {
                const type = v && v[typeField];
                const childProperty = property.oneOf?.properties[type];
                if (!type || !childProperty) return null;
                return resolveProperty({
                    propertyKey: `${propertyKey}.${index}`,
                    propertyOrBuilder: childProperty,
                    ignoreMissingFields,
                    ...props
                });
            }).filter(e => Boolean(e)) as ResolvedProperty[]
            : [];
        const properties = resolveProperties<any>({
            propertyKey,
            properties: property.oneOf.properties,
            ignoreMissingFields,
            ...props
        });
        return {
            ...property,
            resolved: true,
            oneOf: {
                ...property.oneOf,
                properties
            },
            fromBuilder: props.fromBuilder,
            resolvedProperties
        } as ResolvedArrayProperty;
    } else if (!property.Field) {
        throw Error("The array property needs to declare an 'of' or a 'oneOf' property, or provide a custom `Field`")
    } else {
        return {
            ...property,
            resolved: true,
            fromBuilder: props.fromBuilder
        } as ResolvedArrayProperty;
    }

}

/**
 * Resolve enums and arrays for properties
 * @param properties
 * @param value
 */
export function resolveProperties<M extends Record<string, any>>({
                                                                     propertyKey,
                                                                     properties,
                                                                     ignoreMissingFields,
                                                                     ...props
                                                                 }: {
    propertyKey?: string,
    properties: PropertiesOrBuilders<M>,
    values?: Partial<M>,
    previousValues?: Partial<M>,
    path?: string,
    entityId?: string,
    index?: number,
    fromBuilder?: boolean;
    propertyConfigs?: Record<string, PropertyConfig>;
    ignoreMissingFields?: boolean;
    authController: AuthController;
}): ResolvedProperties<M> {
    return Object.entries<PropertyOrBuilder>(properties as Record<string, PropertyOrBuilder>)
        .map(([key, property]) => {
            const childResolvedProperty = resolveProperty({
                propertyKey: propertyKey ? `${propertyKey}.${key}` : undefined,
                propertyOrBuilder: property,
                ignoreMissingFields,
                ...props
            });
            if (!childResolvedProperty) return {};
            return {
                [key]: childResolvedProperty
            };
        })
        .filter((a) => a !== null)
        .reduce((a, b) => ({ ...a, ...b }), {}) as ResolvedProperties<M>;
}

/**
 * Resolve enum aliases for a string or number property
 * @param property
 * @param fromBuilder
 */
export function resolvePropertyEnum(property: StringProperty | NumberProperty, fromBuilder?: boolean): ResolvedStringProperty | ResolvedNumberProperty {
    if (typeof property.enumValues === "object") {
        return {
            ...property,
            resolved: true,
            enumValues: enumToObjectEntries(property.enumValues)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? [],
            fromBuilder: fromBuilder ?? false
        }
    }
    return property as ResolvedStringProperty | ResolvedNumberProperty;
}

export function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined {
    if (typeof input === "object") {
        return Object.entries(input).map(([id, value]) =>
            (typeof value === "string"
                ? {
                    id,
                    label: value
                }
                : value));
    } else if (Array.isArray(input)) {
        return input as EnumValueConfig[];
    } else {
        return undefined;
    }
}

export function resolveEntityView(entityView: string | EntityCustomView<any>, contextEntityViews?: EntityCustomView<any>[]): EntityCustomView<any> | undefined {
    if (typeof entityView === "string") {
        return contextEntityViews?.find((entry) => entry.key === entityView);
    } else {
        return entityView;
    }
}

export function resolvedSelectedEntityView<M extends Record<string, any>>(
    customViews: (string | EntityCustomView<M>)[] | undefined,
    customizationController: CustomizationController,
    selectedTab?: string,
    canEdit?: boolean,
) {
    const resolvedEntityViews = customViews ? customViews
            .map(e => resolveEntityView(e, customizationController.entityViews))
            .filter((e): e is EntityCustomView<M> => Boolean(e))
            // .filter((e) => canEdit || !e.includeActions)
        : [];

    const selectedEntityView = resolvedEntityViews.find(e => e.key === selectedTab);
    const selectedSecondaryForm = customViews
        && resolvedEntityViews.filter(e => e.includeActions).find(e => e.key === selectedTab);
    return {
        resolvedEntityViews,
        selectedEntityView,
        selectedSecondaryForm
    };
}
