import { Keyword, JsonSchemaValidatorParams, ValidationPath } from "../../Keyword";
import { resolveUri } from "../../utils/resolveUri";
import splitRef from "../../utils/splitRef";
import { validateNode } from "../../validateNode";
import { isSchemaNode, JsonError, SchemaNode } from "../../types";
import { get, split } from "@sagold/json-pointer";
import { reduceRef, compileNext } from "../../keywords/$ref";

export const $refKeyword: Keyword = {
    id: "$ref",
    keyword: "$ref",
    parse: parseRef,
    addValidate: ({ schema }) => schema.$ref != null || schema.$recursiveRef != null,
    validate: validateRef,
    addReduce: ({ schema }) => schema.$ref != null || schema.$recursiveRef != null,
    reduce: reduceRef
};

function register(node: SchemaNode, path: string) {
    if (node.context.refs[path] == null) {
        node.context.refs[path] = node;
    }
}

export function parseRef(node: SchemaNode) {
    // @ts-expect-error add ref resolution method to node
    node.resolveRef = resolveRef;

    // get and store current $id of node - this may be the same as parent $id
    const currentId = resolveUri(node.parent?.$id, node.schema?.$id);
    node.$id = currentId;
    node.lastIdPointer = node.parent?.lastIdPointer ?? "#";
    if (currentId !== node.parent?.$id && node.evaluationPath !== "#") {
        node.lastIdPointer = node.evaluationPath;
    }

    // store this node for retrieval by $id + json-pointer from $id
    if (node.lastIdPointer !== "#" && node.evaluationPath.startsWith(node.lastIdPointer)) {
        const localPointer = `#${node.evaluationPath.replace(node.lastIdPointer, "")}`;
        register(node, resolveUri(currentId, localPointer));
    }
    // store $rootId + json-pointer to this node
    register(node, resolveUri(node.context.rootNode.$id, node.evaluationPath));

    // store this node for retrieval by $id + anchor
    if (node.schema.$anchor) {
        node.context.anchors[`${currentId.replace(/#$/, "")}#${node.schema.$anchor}`] = node;
    }

    // precompile reference
    if (node.schema.$ref) {
        node.$ref = resolveUri(currentId, node.schema.$ref);
        if (node.$ref.startsWith("/")) {
            node.$ref = `#${node.$ref}`;
        }
    }
}

// export function reduceRef({ node, data, key, pointer, path }: JsonSchemaReducerParams) {
//     const resolvedNode = node.resolveRef({ pointer, path });
//     if (resolvedNode.schemaLocation === node.schemaLocation) {
//         return resolvedNode;
//     }
//     const result = resolvedNode.reduceNode(data, { key, pointer, path });
//     return result.node ?? result.error;
//     // const merged = mergeNode({ ...node, $ref: undefined, schema: { ...node.schema, $ref: undefined } }, resolvedNode);
//     // const { node: reducedNode, error } = merged.reduceNode(data, { key, pointer, path });
//     // return reducedNode ?? error;
// }

export function resolveRef(this: SchemaNode, { pointer, path }: { pointer?: string; path?: ValidationPath } = {}) {
    if (this.schema.$recursiveRef) {
        const nextNode = resolveRecursiveRef(this, path ?? []);
        if (isSchemaNode(nextNode)) {
            path?.push({ pointer: pointer!, node: nextNode! });
        }
        return nextNode;
    }

    if (this.$ref == null) {
        return this;
    }

    const resolvedNode = getRef(this);
    if (isSchemaNode(resolvedNode)) {
        path?.push({ pointer: pointer!, node: resolvedNode });
    }
    return resolvedNode;
}

function validateRef({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) {
    const nextNode = node.resolveRef({ pointer, path });
    if (nextNode != null) {
        return validateNode(nextNode, data, pointer, path);
    }
    return node.createError("ref-error", {
        ref: node.schema.$ref ?? node.schema.$recursiveRef,
        pointer,
        schema: node.schema,
        value: data
    });
}

// 1. https://json-schema.org/draft/2019-09/json-schema-core#scopes
function resolveRecursiveRef(node: SchemaNode, path: ValidationPath): SchemaNode | JsonError {
    const history = path;

    // RESTRICT BY CHANGE IN BASE-URL
    // go back in history until we have a domain definition and use this as start node to search for an anchor
    let startIndex = 0;
    for (let i = history.length - 1; i >= 0; i--) {
        if (history[i].node.schema.$recursiveAnchor === false) {
            // $recursiveRef with $recursiveAnchor: false works like $ref
            return getRef(node, resolveUri(node.$id, node.schema.$recursiveRef));
        }
        if (/^https?:\/\//.test(history[i].node.schema.$id ?? "") && history[i].node.schema.$recursiveAnchor !== true) {
            startIndex = i;
            break;
        }
    }

    // FROM THERE FIND FIRST OCCURENCE OF AN ANCHOR
    const firstAnchor = history.find((s, index) => index >= startIndex && s.node.schema.$recursiveAnchor === true);
    if (firstAnchor) {
        return firstAnchor.node;
    }

    // $recursiveRef with no $recursiveAnchor works like $ref?
    const nextNode = getRef(node, resolveUri(node.$id, node.schema.$recursiveRef));
    return nextNode;
}

export default function getRef(node: SchemaNode, $ref = node?.$ref): SchemaNode | JsonError {
    if ($ref == null) {
        return node;
    }

    // resolve $ref by json-evaluationPath
    if (node.context.refs[$ref]) {
        return compileNext(node.context.refs[$ref], node);
    }
    // resolve $ref from $anchor
    if (node.context.anchors[$ref]) {
        return compileNext(node.context.anchors[$ref], node);
    }

    // check for remote-host + pointer pair to switch rootSchema
    const fragments = splitRef($ref);
    if (fragments.length === 0) {
        // console.error("REF: INVALID", $ref);
        return node.createError("ref-error", {
            ref: $ref,
            pointer: node.evaluationPath,
            schema: node.schema,
            value: undefined
        });
    }

    // resolve $ref as remote-host
    if (fragments.length === 1) {
        const $ref = fragments[0];
        // this is a reference to remote-host root node
        if (node.context.remotes[$ref]) {
            return compileNext(node.context.remotes[$ref], node);
        }
        if ($ref[0] === "#") {
            // @todo there is a bug joining multiple fragments to e.g. #/base#/examples/0
            // from "$id": "/base" +  $ref "#/examples/0" (in refOfUnknownKeyword spec)
            const ref = $ref.match(/#[^#]*$/)?.pop() as string; // sanitize pointer
            // support refOfUnknownKeyword
            const rootSchema = node.context.rootNode.schema;
            const targetSchema = get(rootSchema, ref);
            if (targetSchema) {
                return node.compileSchema(targetSchema, `${node.evaluationPath}/$ref`, ref);
            }
        }
        // console.error("REF: UNFOUND 1", $ref);
        return node.createError("ref-error", {
            ref: $ref,
            pointer: node.evaluationPath,
            schema: node.schema,
            value: undefined
        });
    }

    if (fragments.length === 2) {
        const $remoteHostRef = fragments[0];
        // this is a reference to remote-host root node (and not a self reference)
        if (node.context.remotes[$remoteHostRef] && node !== node.context.remotes[$remoteHostRef]) {
            const referencedNode = node.context.remotes[$remoteHostRef];
            // resolve full ref on remote schema - we store currently only store ref with domain
            let nextNode = getRef(referencedNode, $ref);
            if (nextNode) {
                return nextNode;
            }
            // @note required for test spec 04
            nextNode = getRef(referencedNode, fragments[1]);
            if (nextNode) {
                return nextNode;
            }
        }

        // resolve by json-pointer (optional dynamicRef)
        if (node.context.refs[$remoteHostRef]) {
            const parentNode = node.context.refs[$remoteHostRef];
            const path = split(fragments[1]);
            // @todo add utility to resolve schema-pointer to schema
            let currentNode = parentNode;
            for (const item of path) {
                const property = item === "definitions" ? "$defs" : item;
                // @ts-expect-error random path
                currentNode = currentNode[property];
                if (currentNode == null) {
                    // console.error("REF: FAILED RESOLVING ref json-pointer", fragments[1]);
                    return node.createError("ref-error", {
                        ref: $ref,
                        pointer: node.evaluationPath,
                        schema: node.schema,
                        value: undefined,
                        host: fragments[0],
                        local: fragments[1]
                    });
                }
            }
            return currentNode;
        }
    }

    return node.createError("ref-error", {
        ref: $ref,
        pointer: node.evaluationPath,
        schema: node.schema,
        value: undefined
    });
}
