#!/usr/bin/env node

"use strict";

const fs = require("fs");

const Diff = require("diff");
const ExampleValidator = require("openapi-examples-validator");
const Prettier = require("prettier");
const SwaggerParser = require("swagger-parser");
const {Composer, CST, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} = require("yaml");
const yargs = require("yargs");

const {argv} = yargs.option("fix", {
    type: "boolean",
    description: "Automatically fix some problems",
});

async function checkFile(file) {
    const yaml = await fs.promises.readFile(file, "utf8");
    const lineCounter = new LineCounter();
    const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
    const docs = [...new Composer().compose(tokens)];
    if (docs.length !== 1) {
        return;
    }
    const [doc] = docs;
    if (doc.errors.length > 0) {
        for (const error of doc.errors) {
            console.error("%s: %s", file, error.message);
        }
        process.exitCode = 1;
        return;
    }

    const root = doc.contents;
    if (!(root instanceof YAMLMap && root.has("openapi"))) {
        return;
    }

    let ok = true;
    const reformats = new Map();

    visit(doc, {
        Map(key, node) {
            if (node.has("$ref") && node.items.length !== 1) {
                const {line, col} = lineCounter.linePos(node.range[0]);
                console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
                ok = false;
            }
        },

        Pair(key, node) {
            if (
                node.key instanceof Scalar &&
                node.key.value === "allOf" &&
                node.value instanceof YAMLSeq &&
                node.value.items.filter(
                    (subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
                ).length > 1
            ) {
                const {line, col} = lineCounter.linePos(node.value.range[0]);
                console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
                ok = false;
            }

            if (
                node.key instanceof Scalar &&
                node.key.value === "description" &&
                node.value instanceof Scalar &&
                typeof node.value.value === "string"
            ) {
                let formatted = Prettier.format(node.value.value, {parser: "markdown"});
                if (![Scalar.BLOCK_FOLDED, Scalar.BLOCK_LITERAL].includes(node.value.type)) {
                    formatted = formatted.replace(/\n$/, "");
                }
                if (formatted !== node.value.value) {
                    if (argv.fix) {
                        reformats.set(node.value.range[0], {
                            value: formatted,
                            context: {afterKey: true},
                        });
                    } else {
                        ok = false;
                        const {line, col} = lineCounter.linePos(node.value.range[0]);
                        console.error(
                            "%s:%d:%d: Format description with Prettier:",
                            file,
                            line,
                            col,
                        );
                        let diff = "";
                        for (const part of Diff.diffLines(node.value.value, formatted)) {
                            const prefix = part.added
                                ? "\u001B[32m+"
                                : part.removed
                                ? "\u001B[31m-"
                                : "\u001B[34m ";
                            diff += prefix;
                            diff += part.value.replace(/\n$/, "").replace(/\n/g, "\n" + prefix);
                            diff += "\n";
                        }
                        diff += "\u001B[0m";
                        console.error(diff);
                    }
                }
            }
        },
    });

    if (!ok) {
        process.exitCode = 1;
    }
    if (reformats.size > 0) {
        console.log("%s: Fixing problems", file);
        for (const token of tokens) {
            CST.visit(token, ({value}) => {
                if (CST.isScalar(value) && reformats.has(value.offset)) {
                    const reformat = reformats.get(value.offset);
                    CST.setScalarValue(value, reformat.value, reformat.context);
                }
            });
        }
        await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
    }

    try {
        await SwaggerParser.validate(file);
    } catch (error) {
        if (!(error instanceof SyntaxError)) {
            throw error;
        }
        console.error("%s: %s", file, error.message);
        process.exitCode = 1;
    }
    const res = await ExampleValidator.validateFile(file);
    if (!res.valid) {
        for (const error of res.errors) {
            console.error(error);
        }
        process.exitCode = 1;
    }
}

(async () => {
    for (const file of argv._) {
        await checkFile(file);
    }
})().catch((error) => {
    console.error(error);
    process.exit(1);
});
