import crypto from "node:crypto";
import * as fs from "node:fs";
import { open } from "sqlite";
import sqlite3 from "sqlite3";
import { getIndexSqlitePath } from "../util/paths.js";
export function tagToString(tag) {
    return `${tag.directory}::${tag.branch}::${tag.artifactId}`;
}
export class SqliteDb {
    static db = null;
    static async createTables(db) {
        await db.exec(`CREATE TABLE IF NOT EXISTS tag_catalog (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            dir STRING NOT NULL,
            branch STRING NOT NULL,
            artifactId STRING NOT NULL,
            path STRING NOT NULL,
            cacheKey STRING NOT NULL,
            lastUpdated INTEGER NOT NULL
        )`);
        await db.exec(`CREATE TABLE IF NOT EXISTS global_cache (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            cacheKey STRING NOT NULL,
            dir STRING NOT NULL,
            branch STRING NOT NULL,
            artifactId STRING NOT NULL
        )`);
    }
    static indexSqlitePath = getIndexSqlitePath();
    static async get() {
        if (SqliteDb.db && fs.existsSync(SqliteDb.indexSqlitePath)) {
            return SqliteDb.db;
        }
        SqliteDb.indexSqlitePath = getIndexSqlitePath();
        SqliteDb.db = await open({
            filename: SqliteDb.indexSqlitePath,
            driver: sqlite3.Database,
        });
        await SqliteDb.createTables(SqliteDb.db);
        return SqliteDb.db;
    }
}
async function getSavedItemsForTag(tag) {
    const db = await SqliteDb.get();
    const stmt = await db.prepare(`SELECT path, cacheKey, lastUpdated FROM tag_catalog
    WHERE dir = ? AND branch = ? AND artifactId = ?`, tag.directory, tag.branch, tag.artifactId);
    const rows = await stmt.all();
    return rows;
}
var AddRemoveResultType;
(function (AddRemoveResultType) {
    AddRemoveResultType["Add"] = "add";
    AddRemoveResultType["Remove"] = "remove";
    AddRemoveResultType["UpdateNewVersion"] = "updateNewVersion";
    AddRemoveResultType["UpdateOldVersion"] = "updateOldVersion";
})(AddRemoveResultType || (AddRemoveResultType = {}));
async function getAddRemoveForTag(tag, currentFiles, readFile) {
    const newLastUpdatedTimestamp = Date.now();
    const saved = await getSavedItemsForTag(tag);
    const add = [];
    const updateNewVersion = [];
    const updateOldVersion = [];
    const remove = [];
    for (const item of saved) {
        const { lastUpdated, ...pathAndCacheKey } = item;
        if (currentFiles[item.path] === undefined) {
            // Was indexed, but no longer exists. Remove
            remove.push(pathAndCacheKey);
        }
        else {
            // Exists in old and new, so determine whether it was updated
            if (lastUpdated < currentFiles[item.path]) {
                // Change was made after last update
                updateNewVersion.push({
                    path: pathAndCacheKey.path,
                    cacheKey: calculateHash(await readFile(pathAndCacheKey.path)),
                });
                updateOldVersion.push(pathAndCacheKey);
            }
            else {
                // Already updated, do nothing
            }
            // Remove so we can check leftovers afterward
            delete currentFiles[item.path];
        }
    }
    // Any leftover in current files need to be added
    add.push(...(await Promise.all(Object.keys(currentFiles).map(async (path) => {
        const fileContents = await readFile(path);
        return { path, cacheKey: calculateHash(fileContents) };
    }))));
    // Create the markComplete callback function
    const db = await SqliteDb.get();
    const itemToAction = {};
    async function markComplete(items, _) {
        const actions = items.map((item) => itemToAction[JSON.stringify({ path: item.path, cacheKey: item.cacheKey })]);
        for (const [{ path, cacheKey }, resultType] of actions) {
            switch (resultType) {
                case AddRemoveResultType.Add:
                    await db.run("INSERT INTO tag_catalog (path, cacheKey, lastUpdated, dir, branch, artifactId) VALUES (?, ?, ?, ?, ?, ?)", path, cacheKey, newLastUpdatedTimestamp, tag.directory, tag.branch, tag.artifactId);
                    break;
                case AddRemoveResultType.Remove:
                    await db.run(`DELETE FROM tag_catalog WHERE
              cacheKey = ? AND
              path = ? AND
              dir = ? AND
              branch = ? AND
              artifactId = ?
          `, cacheKey, path, tag.directory, tag.branch, tag.artifactId);
                    break;
                case AddRemoveResultType.UpdateNewVersion:
                    await db.run(`UPDATE tag_catalog SET
                cacheKey = ?,
                lastUpdated = ?
             WHERE
                path = ? AND
                dir = ? AND
                branch = ? AND
                artifactId = ?
            `, cacheKey, newLastUpdatedTimestamp, path, tag.directory, tag.branch, tag.artifactId);
                    break;
                case AddRemoveResultType.UpdateOldVersion:
                    break;
            }
        }
    }
    for (const item of updateNewVersion) {
        itemToAction[JSON.stringify(item)] = [
            item,
            AddRemoveResultType.UpdateNewVersion,
        ];
    }
    for (const item of add) {
        itemToAction[JSON.stringify(item)] = [item, AddRemoveResultType.Add];
    }
    for (const item of updateOldVersion) {
        itemToAction[JSON.stringify(item)] = [
            item,
            AddRemoveResultType.UpdateOldVersion,
        ];
    }
    for (const item of remove) {
        itemToAction[JSON.stringify(item)] = [item, AddRemoveResultType.Remove];
    }
    return [
        [...add, ...updateNewVersion],
        [...remove, ...updateOldVersion],
        markComplete,
    ];
}
/**
 * Check the global cache for items with this cacheKey for the given artifactId.
 * Return all of the tags that it exists under, which could be an empty array
 */
async function getTagsFromGlobalCache(cacheKey, artifactId) {
    const db = await SqliteDb.get();
    const stmt = await db.prepare("SELECT dir, branch, artifactId FROM global_cache WHERE cacheKey = ? AND artifactId = ?");
    const rows = await stmt.all(cacheKey, artifactId);
    return rows;
}
function calculateHash(fileContents) {
    const hash = crypto.createHash("sha256");
    hash.update(fileContents);
    return hash.digest("hex");
}
export async function getComputeDeleteAddRemove(tag, currentFiles, readFile, repoName) {
    const [add, remove, markComplete] = await getAddRemoveForTag(tag, currentFiles, readFile);
    const compute = [];
    const del = [];
    const addTag = [];
    const removeTag = [];
    for (const { path, cacheKey } of add) {
        const existingTags = await getTagsFromGlobalCache(cacheKey, tag.artifactId);
        if (existingTags.length > 0) {
            addTag.push({ path, cacheKey });
        }
        else {
            compute.push({ path, cacheKey });
        }
    }
    for (const { path, cacheKey } of remove) {
        const existingTags = await getTagsFromGlobalCache(cacheKey, tag.artifactId);
        if (existingTags.length > 1) {
            removeTag.push({ path, cacheKey });
        }
        else {
            if (existingTags.length === 0) {
                // console.warn("Existing tags should not be empty when trying to remove");
            }
            del.push({ path, cacheKey });
        }
    }
    const results = {
        compute,
        del,
        addTag,
        removeTag,
    };
    const globalCacheIndex = await GlobalCacheCodeBaseIndex.create();
    return [
        results,
        async (items, resultType) => {
            // Update tag catalog
            markComplete(items, resultType);
            // Update the global cache
            const results = {
                compute: [],
                del: [],
                addTag: [],
                removeTag: [],
            };
            results[resultType] = items;
            for await (const _ of globalCacheIndex.update(tag, results, () => { }, repoName)) {
            }
        },
    ];
}
export class GlobalCacheCodeBaseIndex {
    db;
    constructor(db) {
        this.db = db;
    }
    artifactId = "globalCache";
    static async create() {
        return new GlobalCacheCodeBaseIndex(await SqliteDb.get());
    }
    async *update(tag, results, _, repoName) {
        const add = [...results.compute, ...results.addTag];
        const remove = [...results.del, ...results.removeTag];
        await Promise.all([
            ...add.map(({ cacheKey }) => {
                return this.computeOrAddTag(cacheKey, tag);
            }),
            ...remove.map(({ cacheKey }) => {
                return this.deleteOrRemoveTag(cacheKey, tag);
            }),
        ]);
        yield { progress: 1, desc: "Done updating global cache", status: "done" };
    }
    async computeOrAddTag(cacheKey, tag) {
        await this.db.run("INSERT INTO global_cache (cacheKey, dir, branch, artifactId) VALUES (?, ?, ?, ?)", cacheKey, tag.directory, tag.branch, tag.artifactId);
    }
    async deleteOrRemoveTag(cacheKey, tag) {
        await this.db.run("DELETE FROM global_cache WHERE cacheKey = ? AND dir = ? AND branch = ? AND artifactId = ?", cacheKey, tag.directory, tag.branch, tag.artifactId);
    }
}
