import { mergeSchema } from "../utils/mergeSchema";
import { JsonSchema, SchemaNode } from "../types";
import { isObject } from "../utils/isObject";
import {
    Keyword,
    JsonSchemaReducerParams,
    JsonSchemaResolverParams,
    JsonSchemaValidatorParams,
    ValidationReturnType,
    ValidationAnnotation
} from "../Keyword";
import { getValue } from "../utils/getValue";
import { validateNode } from "../validateNode";
import settings from "../settings";
import { collectValidationErrors } from "src/utils/collectValidationErrors";

const { REGEX_FLAGS } = settings;

export const patternPropertiesKeyword: Keyword = {
    id: "patternProperties",
    keyword: "patternProperties",
    parse: parsePatternProperties,
    addReduce: (node) => node.patternProperties != null,
    reduce: reducePatternProperties,
    addResolve: (node) => node.patternProperties != null,
    resolve: patternPropertyResolver,
    addValidate: (node) => node.patternProperties != null,
    validate: validatePatternProperties
};

export function parsePatternProperties(node: SchemaNode) {
    const { schema } = node;
    if (!isObject(schema.patternProperties)) {
        return;
    }
    const patterns = Object.keys(schema.patternProperties);
    if (patterns.length === 0) {
        return;
    }

    node.patternProperties = patterns.map((pattern) => ({
        name: pattern,
        pattern: new RegExp(pattern, schema.regexFlags ?? REGEX_FLAGS),
        node: node.compileSchema(
            schema.patternProperties[pattern],
            `${node.evaluationPath}/patternProperties/${pattern}`,
            `${node.schemaLocation}/patternProperties/${pattern}`
        )
    }));

    return collectValidationErrors([], ...node.patternProperties.map(({ node }) => node));
}

function patternPropertyResolver({ node, key }: JsonSchemaResolverParams) {
    return node.patternProperties?.find(({ pattern }) => pattern.test(`${key}`))?.node;
}

function reducePatternProperties({ node, data, key }: JsonSchemaReducerParams) {
    const { patternProperties } = node;
    if (patternProperties == null) {
        return;
    }

    let mergedSchema: JsonSchema | undefined;
    const dataProperties = Object.keys(data ?? {});
    if (key) {
        dataProperties.push(`${key}`);
    }

    let dynamicId = `${node.schemaLocation}(`;
    dataProperties.push(...Object.keys(node.schema.properties ?? {}));
    dataProperties.forEach((propertyName, index, list) => {
        if (list.indexOf(propertyName) !== index) {
            // duplicate
            return;
        }
        // build schema of property
        let propertySchema = node.schema.properties?.[propertyName] ?? {};
        const matchingPatterns = patternProperties.filter((property) => property.pattern.test(propertyName));
        matchingPatterns.forEach((pp) => (propertySchema = mergeSchema(propertySchema, pp.node.schema)));
        if (matchingPatterns.length > 0) {
            mergedSchema = mergedSchema ?? { properties: {} };
            mergedSchema.properties[propertyName] = propertySchema;
            dynamicId += `${matchingPatterns.map(({ name }) => `patternProperties/${name}`).join(",")}`;
        }
    });

    if (mergedSchema == null) {
        return node;
    }

    mergedSchema = mergeSchema(node.schema, mergedSchema, "patternProperties");
    return node.compileSchema(mergedSchema, node.evaluationPath, node.schemaLocation, `${dynamicId})`);
}

function validatePatternProperties({ node, data, pointer, path }: JsonSchemaValidatorParams) {
    if (!isObject(data)) {
        return;
    }
    const { schema, patternProperties } = node;
    const properties = schema.properties || {};
    const patterns = Object.keys(schema.patternProperties).join(",");
    const errors: ValidationReturnType = [];
    const keys = Object.keys(data);

    keys.forEach((key) => {
        const value = getValue(data, key);
        // patternProperties was tested in addValidate
        const matchingPatterns = patternProperties!.filter((property) => property.pattern.test(key));
        matchingPatterns.forEach(({ node }) => errors.push(...validateNode(node, value, `${pointer}/${key}`, path)));

        if (properties[key]) {
            return;
        }

        if (matchingPatterns.length === 0 && schema.additionalProperties === false) {
            // this is an arrangement with additionalProperties
            errors.push(
                node.createError("no-additional-properties-error", {
                    key,
                    pointer: `${pointer}/${key}`,
                    schema,
                    value,
                    patterns
                })
            );
        }
    });

    return errors;
}
