// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import fs from "fs";
import { Octokit } from "octokit";
import { parseReleases } from "auto-changelog/src/releases.js";
import { fetchTags } from "auto-changelog/src/tags.js";
import { fetchRemote } from "auto-changelog/src/remote.js";
import * as toml from "@iarna/toml";

import "zx/globals";

// if (!process.env.GITHUB_TOKEN) {
//     throw new Error("Missing GITHUB_TOKEN");
// }

const IS_NIGHTLY = process.argv.indexOf("--nightly") > -1;

const PKG_VERSION = JSON.parse(fs.readFileSync("./package.json")).version.match(
    /[0-9]+\.[0-9]+\.[0-9]+/,
)[0];

const GIT_REV = await $`git show --no-patch --format=%ct HEAD`;

console.log();

const NEW_VERSION = IS_NIGHTLY
    ? `${PKG_VERSION}-dev${+new Date(parseInt(GIT_REV.toString()))}`
    : PKG_VERSION;

/**
 * A Github data fetching cache designed to run in parallel with changelog formatting.
 * When a SHA is requested, it is looked up in a local cache, and if it isn't there,
 * the next Github API page is awaited.  Requests are always triggered in advance.
 */
class GithubPRCache {
    constructor() {
        this.prs = new Map();
        this.page = 1;
        this.octokit = new Octokit({
            auth: process.env.GITHUB_TOKEN,
        });
        this.next();
    }
    async next() {
        let ret;
        if (this._next_req) {
            ret = await this._next_req;
        }
        console.log(
            `Fetching Github commits ${(this.page - 1) * 100} - ${
                this.page * 100 - 1
            }`,
        );
        this._next_req = this.octokit.request(
            "GET /repos/{owner}/{repo}/pulls?state=closed&per_page={per_page}&page={page}",
            {
                owner: "perspective-dev",
                repo: "perspective",
                per_page: 100,
                page: this.page++,
            },
        );
        return ret;
    }
    async get(sha) {
        while (!this.prs.has(sha) && !this.failed) {
            const resp = await this.next();
            if (resp.data.length == 0) {
                this.failed = true;
            }
            for (const pr of resp.data) {
                this.prs.set(pr.merge_commit_sha, pr);
            }
        }
        return this.prs.get(sha);
    }
}
function template_group(label, rows, prs) {
    if (rows.length > 0) {
        const group_prs = rows
            .map((merge) => `- ${merge.message} [#${merge.id}](${merge.href})`)
            .join("\n");
        return `${label}\n\n${group_prs}\n`;
    } else {
        return "";
    }
}
function template_release(args) {
    const prs = Object.keys(args.row)
        .map((label) => template_group(label, args.row[label]))
        .filter((x) => x.length > 0)
        .join("\n");
    return `# [${args.release.title}](https://github.com/perspective-dev/perspective/releases/tag/${args.release.title})

_${args.release.niceDate}_ ([Full changelog](${args.release.href}))

${prs}
`;
}

/**
 * Generate the changelog text from the output of `auto-changelog`, iteratively calling
 * Github's API when necessary to get label information for grouping.
 *
 * @param {*} json the output data from `auto-changelog`
 * @returns
 */
async function template(json) {
    const cache = new GithubPRCache();
    let changelog = "";
    for (const release of json) {
        const row = {
            "**Breaking**": [],
            Features: [],
            Fixes: [],
            Misc: [],
        };
        for (const merge of release.merges) {
            const pr = await cache.get(merge.commit.hash);
            merge.pr = pr;
            if (pr) {
                const labels = pr.labels.map((x) => x.name);
                if (labels.indexOf("breaking") > -1) {
                    row["**Breaking**"].push(merge);
                } else if (labels.indexOf("enhancement") > -1) {
                    row.Features.push(merge);
                } else if (labels.indexOf("bug") > -1) {
                    row.Fixes.push(merge);
                } else {
                    row.Misc.push(merge);
                }
            } else {
                row.Misc.push(merge);
            }
        }
        changelog += template_release({
            row,
            release,
        });
    }
    return changelog;
}

/**
 * Update the `CHANGELOG.md` file from Github API.
 */
async function update_changelog() {
    let options = {
        remote: "origin",
        commitLimit: 10000,
        backfillLimit: 10000,
        tagPrefix: "",
        sortCommits: "relevance",
        appendGitLog: "",
        appendGitTag: "",
        latestVersion: NEW_VERSION,
    };
    const remote = await fetchRemote(options);
    options = {
        ...options,
        ...remote,
    };
    console.log("Fetching tags…");
    const tags = await fetchTags(options);
    console.log(`${tags.length} version tags found…`);
    const onParsed = ({ title }) => console.log(`Fetched ${title}…`);
    const json = await parseReleases(tags, options, onParsed);
    const changelog = await template(json);
    fs.writeFileSync("./CHANGELOG.md", changelog);
    $.sync`git add CHANGELOG.md`;
}

/**
 * Update all project `package.json` files.
 */
async function update_package_jsons() {
    const pkg = JSON.parse(fs.readFileSync("./package.json"));
    pkg.version = NEW_VERSION;
    const pkg_json = `${JSON.stringify(pkg, undefined, 4)}\n`;
    fs.writeFileSync("./package.json", pkg_json);
    const packages = {};
    for (const ws of pkg.workspaces) {
        if (ws === "packages/jupyterlab" && IS_NIGHTLY) {
            continue;
        }

        for (const path of glob.sync(`${ws}/package.json`, {
            sync: true,
        })) {
            const pkg = JSON.parse(fs.readFileSync(path));
            pkg.version = NEW_VERSION;
            fs.writeFileSync(path, JSON.stringify(pkg, undefined, 4) + "\n");
            packages[pkg.name] = {
                pkg,
                path,
            };
        }
    }

    const cargo = toml.parse(fs.readFileSync("./Cargo.toml"));
    for (const pkg of cargo.workspace.members) {
        const crate = toml.parse(fs.readFileSync(`./${pkg}/Cargo.toml`));
        if (crate?.package.version) {
            crate.package.version = NEW_VERSION;
        }

        for (const key of Object.keys(crate?.dependencies)) {
            if (key.startsWith("perspective")) {
                crate.dependencies[key].version = NEW_VERSION;
            }
        }
        fs.writeFileSync(`./${pkg}/Cargo.toml`, toml.stringify(crate));
    }

    // if (!IS_NIGHTLY) {
    const pyproject = toml.parse(
        fs.readFileSync(`./rust/perspective-python/pyproject.toml`),
    );

    pyproject.tool.maturin.data = `perspective_python-${NEW_VERSION}.data`;
    fs.writeFileSync(
        `./rust/perspective-python/pyproject.toml`,
        toml.stringify(pyproject),
    );
    // }
}

if (!IS_NIGHTLY) {
    if (!process.env.GITHUB_TOKEN) {
        throw new Error("Missing GITHUB_TOKEN");
    }

    await update_changelog();
}

await update_package_jsons();
