import {
    CMSType,
    EntityReference,
    GeoPoint,
    ResolvedArrayProperty,
    ResolvedMapProperty,
    ResolvedProperties,
    ResolvedProperty
} from "../types";
import * as yup from "yup";
import { AnySchema, ArraySchema, BooleanSchema, DateSchema, NumberSchema, ObjectSchema, StringSchema } from "yup";
import { enumToObjectEntries, getValueInPath, hydrateRegExp, isPropertyBuilder } from "../util";

// Add custom unique function for array values
declare module "yup" {
    // tslint:disable-next-line
    interface ArraySchema<TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends yup.Flags = ""> {
        uniqueInArray(mapper: (a: any) => any, message: string): ArraySchema<TIn, TContext, TDefault, TFlags>;
    }
}
yup.addMethod(yup.array, "uniqueInArray", function (
    mapper = (a: any) => a,
    message: string
) {
    return this.test("uniqueInArray", message, values => {
        return !values || values.length === new Set(values.map(mapper)).size;
    });
});

export type CustomFieldValidator = (props: {
    name: string,
    value: any,
    property: ResolvedProperty,
    entityId?: string,
    parentProperty?: ResolvedMapProperty | ResolvedArrayProperty,
}) => Promise<boolean>;

interface PropertyContext<T extends CMSType> {
    property: ResolvedProperty<T>,
    parentProperty?: ResolvedMapProperty | ResolvedArrayProperty,
    entityId: string,
    customFieldValidator?: CustomFieldValidator,
    name?: any
}

export function getYupEntitySchema<M extends Record<string, any>>(
    entityId: string,
    properties: ResolvedProperties<M>,
    customFieldValidator?: CustomFieldValidator): ObjectSchema<any> {
    const objectSchema: any = {};
    Object.entries(properties as Record<string, ResolvedProperty>)
        .forEach(([name, property]) => {
            try {
                objectSchema[name] = mapPropertyToYup({
                    property: property as ResolvedProperty<any>,
                    customFieldValidator,
                    name,
                    entityId
                });
            } catch (e: any) {
                console.error(`Error creating validation schema for property ${name}:`, e);
                objectSchema[name] = yup.mixed().test(
                    "validation-error",
                    `Validation error: ${e?.message ?? "Unknown error"}`,
                    () => false
                );
            }
        });
    return yup.object().shape(objectSchema);
}

export function mapPropertyToYup<T extends CMSType>(propertyContext: PropertyContext<T>): AnySchema<unknown> {

    const property = propertyContext.property;
    if (isPropertyBuilder(property)) {
        console.error("Error in property", propertyContext);
        // Return a permissive schema with an error message instead of crashing
        return yup.mixed().test(
            "property-builder-error",
            "Invalid property configuration: property builder should be resolved",
            () => false
        );
    }

    if (property.dataType === "string") {
        return getYupStringSchema(propertyContext as PropertyContext<string>);
    } else if (property.dataType === "number") {
        return getYupNumberSchema(propertyContext as PropertyContext<number>);
    } else if (property.dataType === "boolean") {
        return getYupBooleanSchema(propertyContext as PropertyContext<boolean>);
    } else if (property.dataType === "map") {
        return getYupMapObjectSchema(propertyContext as PropertyContext<object>);
    } else if (property.dataType === "array") {
        return getYupArraySchema(propertyContext as PropertyContext<any[]>);
    } else if (property.dataType === "date") {
        return getYupDateSchema(propertyContext as PropertyContext<Date>);
    } else if (property.dataType === "geopoint") {
        return getYupGeoPointSchema(propertyContext as PropertyContext<GeoPoint>);
    } else if (property.dataType === "reference") {
        return getYupReferenceSchema(propertyContext as PropertyContext<EntityReference>);
    }

    // Log the error but don't crash the form - return a permissive schema with an error message
    console.error("Unsupported data type in yup mapping", property);
    const dataType = (property as any).dataType ?? "unknown";
    return yup.mixed().test(
        "unsupported-data-type",
        `Unsupported data type: ${dataType}`,
        () => false
    );
}

export function getYupMapObjectSchema({
    property,
    entityId,
    customFieldValidator,
    name
}: PropertyContext<Record<string, any>>): ObjectSchema<any> {
    const objectSchema: any = {};
    const validation = property.validation;
    if (property.properties)
        Object.entries(property.properties).forEach(([childName, childProperty]: [string, ResolvedProperty]) => {
            try {
                objectSchema[childName] = mapPropertyToYup<any>({
                    property: childProperty,
                    parentProperty: property as ResolvedMapProperty,
                    customFieldValidator,
                    name: `${name}[${childName}]`,
                    entityId
                });
            } catch (e: any) {
                console.error(`Error creating validation schema for property ${childName}:`, e);
                objectSchema[childName] = yup.mixed().test(
                    "validation-error",
                    `Validation error: ${e?.message ?? "Unknown error"}`,
                    () => false
                );
            }
        });

    const shape = yup.object().shape(objectSchema);
    if (validation?.required) {
        // In yup v0.x, .required().nullable(true) allowed null values
        // To match this behavior: reject undefined but allow null
        return shape.nullable().test(
            "required",
            validation?.requiredMessage ? validation.requiredMessage : "Required",
            (value) => value !== undefined
        ) as any;
    }
    return yup.object().shape(shape.fields).default(undefined).nullable().optional() as any;
}

function getYupStringSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<string>): StringSchema {
    let schema: StringSchema<any> = yup.string().nullable();
    const validation = property.validation;

    if (property.enumValues) {
        if (validation?.required) {
            schema = schema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null && value !== ""
            );
        }
        const entries = enumToObjectEntries(property.enumValues);
        schema = schema.oneOf(
            (validation?.required ? entries : [...entries, null])
                .map((enumValueConfig) => enumValueConfig?.id ?? null)
        );
    }

    if (validation) {
        if (validation.required) {
            schema = schema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null && value !== ""
            );
        }
        if (validation.unique && customFieldValidator && name)
            schema = schema.test("unique", "This value already exists and should be unique",
                (value, context) =>
                    customFieldValidator({
                        name,
                        property,
                        parentProperty,
                        value,
                        entityId
                    }));
        if (validation.min || validation.min === 0) schema = schema.min(validation.min, `${property.name} must be min ${validation.min} characters long`);
        if (validation.max || validation.max === 0) schema = schema.max(validation.max, `${property.name} must be max ${validation.max} characters long`);
        if (validation.matches) {
            const regExp = typeof validation.matches === "string" ? hydrateRegExp(validation.matches) : validation.matches;
            if (regExp) {
                schema = schema.matches(regExp, validation.matchesMessage ? { message: validation.matchesMessage } : undefined)
            }
        }
        if (validation.trim) schema = schema.trim();
        if (validation.lowercase) schema = schema.lowercase();
        if (validation.uppercase) schema = schema.uppercase();
        if (property.email) schema = schema.email(`${property.name} must be an email`);
        if (property.url) {
            if (!property.storage || property.storage?.storeUrl) {
                schema = schema.url(`${property.name} must be a url`);
            } else {
                console.warn(`Property ${property.name} has a url validation but its storage configuration is not set to store urls`);
            }
        }
    }
    return schema;
}

function getYupNumberSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<number>): NumberSchema {
    const validation = property.validation;
    let schema: NumberSchema<any> = yup.number().nullable().typeError("Must be a number");

    if (validation) {
        if (validation.required) {
            schema = schema.test(
                "required",
                validation.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null
            );
        }
        if (validation.unique && customFieldValidator && name)
            schema = schema.test("unique",
                "This value already exists and should be unique",
                (value) => customFieldValidator({
                    name,
                    property,
                    parentProperty,
                    value,
                    entityId
                }));
        if (validation.min || validation.min === 0) schema = schema.min(validation.min, `${property.name} must be higher or equal to ${validation.min}`);
        if (validation.max || validation.max === 0) schema = schema.max(validation.max, `${property.name} must be lower or equal to ${validation.max}`);
        if (validation.lessThan || validation.lessThan === 0) schema = schema.lessThan(validation.lessThan, `${property.name} must be higher than ${validation.lessThan}`);
        if (validation.moreThan || validation.moreThan === 0) schema = schema.moreThan(validation.moreThan, `${property.name} must be lower than ${validation.moreThan}`);
        if (validation.positive) schema = schema.positive(`${property.name} must be positive`);
        if (validation.negative) schema = schema.negative(`${property.name} must be negative`);
        if (validation.integer) schema = schema.integer(`${property.name} must be an integer`);
    }
    return schema;
}

function getYupGeoPointSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<GeoPoint>): AnySchema {
    let schema: ObjectSchema<any> = yup.object().nullable() as ObjectSchema<any>;
    const validation = property.validation;

    if (validation?.unique && customFieldValidator && name)
        schema = schema.test("unique",
            "This value already exists and should be unique",
            (value) => customFieldValidator({
                name,
                property,
                parentProperty,
                value,
                entityId
            }));
    if (validation?.required) {
        schema = schema.test(
            "required",
            validation.requiredMessage ? validation.requiredMessage : "Required",
            (value) => value !== undefined && value !== null
        );
    }
    return schema;
}

function getYupDateSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<Date>): AnySchema | DateSchema {
    if (property.autoValue) {
        return yup.date().nullable();
    }
    let schema: DateSchema<any> = yup.date().nullable();
    const validation = property.validation;

    if (validation) {
        if (validation.required) {
            schema = schema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null
            );
        }
        if (validation.unique && customFieldValidator && name)
            schema = schema.test("unique",
                "This value already exists and should be unique",
                (value) => customFieldValidator({
                    name,
                    property,
                    parentProperty,
                    value,
                    entityId
                }));
        if (validation.min) schema = schema.min(validation.min, `${property.name} must be after ${validation.min}`);
        if (validation.max) schema = schema.max(validation.max, `${property.name} must be before ${validation.min}`);
    }
    return schema.transform((v: any) => (v instanceof Date ? v : null));
}

function getYupReferenceSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<EntityReference>): AnySchema {
    let schema: ObjectSchema<any> = yup.object().nullable() as ObjectSchema<any>;
    const validation = property.validation;

    if (validation) {
        if (validation.required) {
            schema = schema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null
            );
        }
        if (validation.unique && customFieldValidator && name)
            schema = schema.test("unique",
                "This value already exists and should be unique",
                (value) => customFieldValidator({
                    name,
                    property,
                    parentProperty,
                    value,
                    entityId
                }));
    }
    return schema;
}

function getYupBooleanSchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<boolean>): BooleanSchema {
    let schema: BooleanSchema<any> = yup.boolean().nullable();
    const validation = property.validation;

    if (validation) {
        if (validation.required) {
            schema = schema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value) => value !== undefined && value !== null
            );
        }
        if (validation.unique && customFieldValidator && name)
            schema = schema.test("unique",
                "This value already exists and should be unique",
                (value) => customFieldValidator({
                    name,
                    property,
                    parentProperty,
                    value,
                    entityId
                }));
    }
    return schema;
}

function hasUniqueInArrayModifier(property: ResolvedProperty): boolean | [string, ResolvedProperty][] {
    if (property.validation?.uniqueInArray) {
        return true;
    } else if (property.dataType === "map" && property.properties) {
        return Object.entries(property.properties)
            .filter(([key, childProperty]) => childProperty.validation?.uniqueInArray);
    }
    return false;
}

function getYupArraySchema({
    property,
    parentProperty,
    customFieldValidator,
    name,
    entityId
}: PropertyContext<any[]>): AnySchema<any> {

    let arraySchema: any = yup.array().nullable();

    if (property.of) {
        if (Array.isArray(property.of)) {
            const yupProperties = (property.of as ResolvedProperty[]).map((p, index) => {
                try {
                    return {
                        [`${name}[${index}]`]: mapPropertyToYup({
                            property: p as ResolvedProperty<any>,
                            parentProperty: property,
                            entityId
                        })
                    };
                } catch (e: any) {
                    console.error(`Error creating validation schema for array item ${index}:`, e);
                    return {
                        [`${name}[${index}]`]: yup.mixed().test(
                            "validation-error",
                            `Validation error: ${e?.message ?? "Unknown error"}`,
                            () => false
                        )
                    };
                }
            }).reduce((a, b) => ({ ...a, ...b }), {});
            return yup.array().nullable().of(
                yup.mixed().test(
                    "Dynamic object validation",
                    "Dynamic object validation error",
                    (object, context) => {
                        const yupProperty = getValueInPath(yupProperties, context.path);
                        return yupProperty.validate(object);
                    }
                )
            );
        } else {
            try {
                arraySchema = arraySchema.of(mapPropertyToYup({
                    property: property.of,
                    parentProperty: property,
                    entityId
                }));
            } catch (e: any) {
                console.error(`Error creating validation schema for array of property:`, e);
                arraySchema = arraySchema.of(yup.mixed().test(
                    "validation-error",
                    `Validation error: ${e?.message ?? "Unknown error"}`,
                    () => false
                ));
            }
            const arrayUniqueFields = hasUniqueInArrayModifier(property.of);
            if (arrayUniqueFields) {
                if (typeof arrayUniqueFields === "boolean") {
                    arraySchema = arraySchema.uniqueInArray((v: any) => v, `${property.name} should have unique values within the array`);
                } else if (Array.isArray(arrayUniqueFields)) {
                    arrayUniqueFields.forEach(([name, childProperty]) => {
                        arraySchema = arraySchema.uniqueInArray((v: any) => v && v[name], `${property.name} → ${childProperty.name ?? name}: should have unique values within the array`);
                    });
                }
            }
        }
    }
    const validation = property.validation;

    if (validation) {
        if (validation.required) {
            arraySchema = arraySchema.test(
                "required",
                validation?.requiredMessage ? validation.requiredMessage : "Required",
                (value: any) => value !== undefined && value !== null && value.length > 0
            );
        }
        if (validation.min || validation.min === 0) arraySchema = arraySchema.min(validation.min, `${property.name} should be min ${validation.min} entries long`);
        if (validation.max) arraySchema = arraySchema.max(validation.max, `${property.name} should be max ${validation.max} entries long`);
        // Handle uniqueInArray at the array level (in addition to the of.validation check above)
        if (validation.uniqueInArray) {
            arraySchema = arraySchema.uniqueInArray((v: any) => v, `${property.name} should have unique values within the array`);
        }
    }
    return arraySchema;
}
