module app;

import std.stdio;
import std.file;
import std.path;
import std.json;
import std.datetime;
import std.string;
import std.array;
import std.algorithm;
import std.conv;
import std.regex;

enum BONSAI_DIR = ".bonsai";
enum SNAPSHOTS_DIR = buildPath(BONSAI_DIR, "snapshots");
enum JSON_FILE = buildPath(BONSAI_DIR, "bonsai.json");
enum CURRENT_FILE = buildPath(BONSAI_DIR, "CURRENT");
enum IGNORE_FILE = ".bonsaiignore";

enum DEFAULT_IGNORE = ".bonsai\n*.exe\n*.obj\n*.o\n*.so\n*.dll\n.DS_Store\n";

struct Snapshot {
    string id;
    string name;
    string timestamp;
}

int main(string[] args) {
    if (args.length < 2) {
        printUsage();
        return 1;
    }

    string command = args[1];

    try {
        switch (command) {
            case "init":
                cmdInit();
                break;
            case "add":
                if (args.length < 3) {
                    writeln("Error: Please specify snapshot name. Example: bonsai add \"Initial commit\"");
                    return 1;
                }
                cmdAdd(args[2]);
                break;
            case "revert":
                string targetId = "";
                bool removeFuture = false;

                for (size_t i = 2; i < args.length; i++) {
                    if (args[i] == "-r" || args[i] == "--remove") {
                        removeFuture = true;
                    } else if (targetId.length == 0) {
                        targetId = args[i];
                    }
                }

                cmdRevert(targetId, removeFuture);
                break;
            case "history":
                cmdHistory();
                break;
            case "check":
                cmdCheck();
                break;
            default:
                writeln("Unknown command: ", command);
                printUsage();
                return 1;
        }
    } catch (Exception e) {
        writeln("Error: ", e.msg);
        return 1;
    }

    return 0;
}

void printUsage() {
    writeln("Bonsai version control system");
    writeln("Usage:");
    writeln("  bonsai init               - Initialize a new repository");
    writeln("  bonsai add \"<name>\"       - Create a new snapshot");
    writeln("  bonsai revert [id] [-r]   - Revert to specified/previous snapshot (-r deletes subsequent snapshots)");
    writeln("  bonsai history            - Display snapshot history");
    writeln("  bonsai check              - Check repository integrity");
}

void ensureRepositoryExists() {
    if (!exists(BONSAI_DIR) || !isDir(BONSAI_DIR)) {
        throw new Exception("Repository .bonsai not found. Run 'bonsai init' first.");
    }
}

void cmdInit() {
    if (!exists(BONSAI_DIR)) {
        mkdir(BONSAI_DIR);
    }
    if (!exists(SNAPSHOTS_DIR)) {
        mkdirRecurse(SNAPSHOTS_DIR);
    }

    if (!exists(JSON_FILE)) {
        JSONValue root = JSONValue(["snapshots": JSONValue(JSONValue[].init)]);
        std.file.write(JSON_FILE, root.toJSON(true));
    }

    if (!exists(CURRENT_FILE)) {
        std.file.write(CURRENT_FILE, "-1");
    }

    if (!exists(IGNORE_FILE)) {
        std.file.write(IGNORE_FILE, DEFAULT_IGNORE);
    }

    writeln("Initialized empty Bonsai repository in ", buildNormalizedPath(absolutePath(BONSAI_DIR)));
}

string[] loadIgnorePatterns() {
    string[] patterns;
    if (exists(IGNORE_FILE)) {
        string content = readText(IGNORE_FILE);
        foreach (line; content.splitLines()) {
            string trimmed = line.strip();
            if (trimmed.length > 0 && !trimmed.startsWith("#")) {
                patterns ~= trimmed;
            }
        }
    }
    return patterns;
}

bool isIgnored(string path, string[] patterns) {
    string name = baseName(path);
    string normPath = buildNormalizedPath(path);

    if (name == ".bonsai" || normPath.startsWith(buildNormalizedPath(BONSAI_DIR))) {
        return true;
    }

    foreach (pattern; patterns) {
        if (globMatch(name, pattern) || globMatch(normPath, pattern)) {
            return true;
        }
    }
    return false;
}

Snapshot[] loadSnapshots() {
    Snapshot[] list;
    if (!exists(JSON_FILE)) return list;

    string content = readText(JSON_FILE);
    JSONValue json = parseJSON(content);

    if ("snapshots" in json && json["snapshots"].type == JSONType.array) {
        foreach (item; json["snapshots"].array) {
            Snapshot s;
            s.id = item["id"].str;
            s.name = item["name"].str;
            s.timestamp = item["timestamp"].str;
            list ~= s;
        }
    }
    return list;
}

void saveSnapshots(Snapshot[] snapshots) {
    JSONValue[] arr;
    foreach (s; snapshots) {
        JSONValue obj = JSONValue([
            "id": JSONValue(s.id),
            "name": JSONValue(s.name),
            "timestamp": JSONValue(s.timestamp)
        ]);
        arr ~= obj;
    }
    JSONValue root = JSONValue(["snapshots": JSONValue(arr)]);
    std.file.write(JSON_FILE, root.toJSON(true));
}

string getCurrentId() {
    if (!exists(CURRENT_FILE)) return "-1";
    return readText(CURRENT_FILE).strip();
}

void setCurrentId(string id) {
    std.file.write(CURRENT_FILE, id);
}

void copyDirectoryRecursive(string srcDir, string destDir, string projectRoot, string[] ignorePatterns) {
    if (!exists(destDir)) {
        mkdirRecurse(destDir);
    }

    foreach (DirEntry entry; dirEntries(srcDir, SpanMode.shallow, false)) {
        string relPath = relativePath(entry.name, projectRoot);

        if (isIgnored(entry.name, ignorePatterns) || isIgnored(relPath, ignorePatterns)) {
            continue;
        }

        string targetPath = buildPath(destDir, baseName(entry.name));

        if (entry.isDir) {
            copyDirectoryRecursive(entry.name, targetPath, projectRoot, ignorePatterns);
        } else if (entry.isFile) {
            copy(entry.name, targetPath);
        }
    }
}

void cmdAdd(string snapshotName) {
    ensureRepositoryExists();

    Snapshot[] snapshots = loadSnapshots();

    int nextIdInt = 0;
    if (snapshots.length > 0) {
        int maxId = -1;
        foreach (s; snapshots) {
            try {
                int val = to!int(s.id);
                if (val > maxId) maxId = val;
            } catch (Exception) {}
        }
        nextIdInt = maxId + 1;
    }
    string newId = to!string(nextIdInt);

    string snapshotPath = buildPath(SNAPSHOTS_DIR, newId);
    if (exists(snapshotPath)) {
        rmdirRecurse(snapshotPath);
    }

    string[] ignorePatterns = loadIgnorePatterns();
    copyDirectoryRecursive(".", snapshotPath, ".", ignorePatterns);

    SysTime now = Clock.currTime();
    string timestampStr = format("%02d.%02d.%d-%02d:%02d",
        now.day, cast(int)now.month, now.year, now.hour, now.minute);

    Snapshot newSnapshot = Snapshot(newId, snapshotName, timestampStr);
    snapshots ~= newSnapshot;

    saveSnapshots(snapshots);
    setCurrentId(newId);

    writeln("Created snapshot [", newId, "] \"", snapshotName, "\"");
}

void cmdRevert(string targetId, bool removeFuture) {
    ensureRepositoryExists();

    string currentId = getCurrentId();

    if (targetId.length == 0) {
        if (currentId == "-1") {
            throw new Exception("No current snapshot to revert from.");
        }
        int cur = to!int(currentId);
        if (cur <= 0) {
            throw new Exception("Cannot revert further. Current snapshot is already the initial one.");
        }
        targetId = to!string(cur - 1);
    }

    string targetSnapshotPath = buildPath(SNAPSHOTS_DIR, targetId);

    if (!exists(targetSnapshotPath) || !isDir(targetSnapshotPath)) {
        throw new Exception("Snapshot with ID '" ~ targetId ~ "' does not exist in " ~ SNAPSHOTS_DIR ~ ". Revert cancelled, files were not modified.");
    }

    cleanWorkingDirectory(".");
    restoreSnapshotRecursive(targetSnapshotPath, ".");
    setCurrentId(targetId);

    if (removeFuture) {
        int targetInt = to!int(targetId);
        Snapshot[] snapshots = loadSnapshots();
        Snapshot[] keptSnapshots;

        foreach (s; snapshots) {
            int sInt = to!int(s.id);
            if (sInt > targetInt) {
                string pathToRemove = buildPath(SNAPSHOTS_DIR, s.id);
                if (exists(pathToRemove)) {
                    rmdirRecurse(pathToRemove);
                }
            } else {
                keptSnapshots ~= s;
            }
        }

        saveSnapshots(keptSnapshots);
        writeln("Successfully reverted to snapshot [", targetId, "] and deleted all subsequent snapshots.");
    } else {
        writeln("Successfully reverted to snapshot [", targetId, "]");
    }
}

void cleanWorkingDirectory(string dirPath) {
    foreach (DirEntry entry; dirEntries(dirPath, SpanMode.shallow, false)) {
        string name = baseName(entry.name);
        string ext = extension(name).toLower();

        if (name == BONSAI_DIR ||
            name == IGNORE_FILE ||
            name == "bonsai.exe" ||
            name == "bonsai" ||
            ext == ".obj" ||
            ext == ".o") {
            continue;
        }

        if (entry.isDir) {
            rmdirRecurse(entry.name);
        } else if (entry.isFile) {
            remove(entry.name);
        }
    }
}

void restoreSnapshotRecursive(string srcDir, string destDir) {
    if (!exists(destDir)) {
        mkdirRecurse(destDir);
    }

    foreach (DirEntry entry; dirEntries(srcDir, SpanMode.shallow, false)) {
        string targetPath = buildPath(destDir, baseName(entry.name));

        if (entry.isDir) {
            restoreSnapshotRecursive(entry.name, targetPath);
        } else if (entry.isFile) {
            copy(entry.name, targetPath);
        }
    }
}

void cmdHistory() {
    ensureRepositoryExists();

    Snapshot[] snapshots = loadSnapshots();
    string currentId = getCurrentId();

    if (snapshots.length == 0) {
        writeln("Snapshot history is empty.");
        return;
    }

    writeln("Now");

    for (long i = cast(long)snapshots.length - 1; i >= 0; i--) {
        auto s = snapshots[cast(size_t)i];

        string prefix = " * ";

        writeln(" |");
        writeln(prefix, s.name, "  [", s.id, "]");
    }

    writeln(" |");
    writeln("Created repo");
}

bool isDirEmpty(string dirPath) {
    foreach (DirEntry e; dirEntries(dirPath, SpanMode.shallow, false)) {
        return false;
    }
    return true;
}

void cmdCheck() {
    int critCount = 0;
    int discCount = 0;

    string[] critBonsai;
    string[] critSnapshots;
    string[] critJson;
    string[] critCurrent;
    string[] critIgnore;

    string[] discJson;
    string[] discRepo;

    string formatError(string base, string[] details) {
        if (details.length == 0) return base;
        return base ~ " (" ~ details.join(", ") ~ ")";
    }

    // --- 1. Check .bonsai ---
    if (!exists(BONSAI_DIR) || !isDir(BONSAI_DIR)) {
        critBonsai ~= ".bonsai is not a directory.";
    } else if (isDirEmpty(BONSAI_DIR)) {
        critBonsai ~= "The folder is empty.";
    }

    // --- 2. Check .bonsai/snapshots ---
    string[] diskSnapshotDirs;
    string[] emptySnapFoldersDetails;
    if (!exists(SNAPSHOTS_DIR) || !isDir(SNAPSHOTS_DIR)) {
        critSnapshots ~= ".bonsai/snapshots is not a directory.";
    } else {
        foreach (entry; dirEntries(SNAPSHOTS_DIR, SpanMode.shallow, false)) {
            if (entry.isDir) {
                diskSnapshotDirs ~= baseName(entry.name);
            }
        }
    }

    // --- 3. Check bonsai.json ---
    Snapshot[] jsonSnapshots;
    bool jsonParsed = false;
    string[] identicalIdDetails;
    string[] missingOnDiskDetails;
    string[] badTimestampDetails;
    string[] badIdDetails;

    if (!exists(JSON_FILE)) {
        critJson ~= "Reading the file is not possible. (File does not exist)";
    } else if (getSize(JSON_FILE) == 0) {
        critJson ~= "The file is empty.";
    } else {
        try {
            string content = readText(JSON_FILE);
            JSONValue j = parseJSON(content);

            if ("snapshots" in j && j["snapshots"].type == JSONType.array) {
                string[][string] idNames;

                foreach (item; j["snapshots"].array) {
                    Snapshot s;
                    s.id = item["id"].str;
                    s.name = item["name"].str;
                    s.timestamp = item["timestamp"].str;
                    jsonSnapshots ~= s;

                    idNames[s.id] ~= s.name;

                    string expectedPath = buildPath(SNAPSHOTS_DIR, s.id);
                    if (!exists(expectedPath) || !isDir(expectedPath)) {
                        missingOnDiskDetails ~= s.id ~ ": " ~ s.name;
                    }

                    if (!matchFirst(s.timestamp, r"^\d{2}\.\d{2}\.\d{4}-\d{2}:\d{2}$")) {
                        badTimestampDetails ~= s.id ~ ": " ~ s.name;
                    }

                    try {
                        to!int(s.id);
                    } catch (Exception e) {
                        badIdDetails ~= s.id ~ ": " ~ s.name;
                    }
                }

                foreach (id, names; idNames) {
                    if (names.length > 1) {
                        identicalIdDetails ~= id ~ ": " ~ names.join(", ");
                    }
                }

                jsonParsed = true;
            }
        } catch (Exception e) {
            critJson ~= "Reading the file is not possible. (" ~ e.msg ~ ")";
        }
    }

    if (identicalIdDetails.length > 0) {
        critJson ~= formatError("Snapshots with identical IDs have been detected.", identicalIdDetails);
    }
    if (missingOnDiskDetails.length > 0) {
        critJson ~= formatError("The file contains a snapshot, but in fact, it doesn't exist.", missingOnDiskDetails);
    }

    // --- Validate Disk against JSON ---
    string[] untrackedDiskDetails;
    foreach (diskDir; diskSnapshotDirs) {
        string expectedPath = buildPath(SNAPSHOTS_DIR, diskDir);
        bool inJson = false;
        string snapName = "";

        foreach (s; jsonSnapshots) {
            if (s.id == diskDir) {
                inJson = true;
                snapName = s.name;
                break;
            }
        }

        if (!inJson) {
            untrackedDiskDetails ~= "[" ~ expectedPath ~ "]";
        }

        if (exists(expectedPath) && isDirEmpty(expectedPath)) {
            if (inJson) {
                emptySnapFoldersDetails ~= diskDir ~ ": " ~ snapName ~ " [" ~ expectedPath ~ "]";
            } else {
                emptySnapFoldersDetails ~= diskDir ~ " [" ~ expectedPath ~ "]";
            }
        }
    }

    if (emptySnapFoldersDetails.length > 0) {
        critSnapshots ~= formatError("The snapshot folder is empty.", emptySnapFoldersDetails);
    }
    if (untrackedDiskDetails.length > 0) {
        critSnapshots ~= formatError("There is a snapshot that actually exists on the disk but is missing from bonsai.json.", untrackedDiskDetails);
    }

    // --- 4. Check CURRENT ---
    if (exists(CURRENT_FILE)) {
        if (getSize(CURRENT_FILE) == 0) {
            critCurrent ~= "The file is empty.";
        } else {
            string cur = readText(CURRENT_FILE).strip();
            if (cur == "-1") {
                discRepo ~= "The current snapshot is marked as -1.";
            } else {
                bool found = false;
                foreach (s; jsonSnapshots) {
                    if (s.id == cur) {
                        found = true;
                        break;
                    }
                }
                if (!found && jsonParsed) {
                    critCurrent ~= "CURRENT has the meaning of a non-existent snapshot. (CURRENT: " ~ cur ~ ")";
                }
            }
        }
    }

    // --- 5. Check .bonsaiignore ---
    if (exists(IGNORE_FILE)) {
        try {
            readText(IGNORE_FILE);
        } catch (Exception e) {
            critIgnore ~= "Reading the file is not possible. (" ~ e.msg ~ ")";
        }
    }

    // --- Discrepancies: JSON Metadata ---
    if (jsonParsed && jsonSnapshots.length > 0) {
        int[] validIds;
        string[][string] nameToIds;
        bool orderOk = true;
        int lastId = -2;

        foreach (s; jsonSnapshots) {
            nameToIds[s.name] ~= s.id;
            try {
                int val = to!int(s.id);
                validIds ~= val;
                if (lastId != -2 && val <= lastId) {
                    orderOk = false;
                }
                lastId = val;
            } catch (Exception) {}
        }

        if (!orderOk) {
            discJson ~= "The order of snapshots has been disrupted.";
        }

        if (validIds.length > 0) {
            int[] sortedIds = validIds.dup;
            sort(sortedIds);
            string[] gapDetails;
            for (size_t i = 0; i < sortedIds.length - 1; i++) {
                int curr = sortedIds[i];
                int next = sortedIds[i+1];
                if (next - curr > 1) {
                    gapDetails ~= ".. " ~ to!string(curr) ~ " ? " ~ to!string(next) ~ " ..";
                }
            }
            if (gapDetails.length > 0) {
                discJson ~= formatError("There are missing numbers in the ID indexing.", gapDetails);
            }
        }

        string[] dupNameDetails;
        foreach (name, ids; nameToIds) {
            if (ids.length > 1) {
                string[] pairs;
                foreach(id; ids) pairs ~= id ~ ": " ~ name;
                dupNameDetails ~= pairs.join(", ");
            }
        }
        if (dupNameDetails.length > 0) {
            discJson ~= formatError("There are snapshots with the same names.", dupNameDetails);
        }

        if (badTimestampDetails.length > 0) {
            discJson ~= formatError("The integrity of the timestamp has been compromised.", badTimestampDetails);
        }
    }

    // --- Discrepancies: Repository ---
    if (jsonParsed && jsonSnapshots.length == 0 && diskSnapshotDirs.length == 0) {
        discRepo ~= "No snapshots were created.";
    }
    if (badIdDetails.length > 0) {
        discRepo ~= formatError("A snapshot with a potentially incorrect ID has been found.", badIdDetails);
    }

    // --- Construct Report Output ---
    critCount = cast(int)(critBonsai.length + critSnapshots.length + critJson.length + critCurrent.length + critIgnore.length);
    discCount = cast(int)(discJson.length + discRepo.length);

    if (critCount == 0 && discCount == 0) {
        writeln("OK Everything all right (json:", jsonSnapshots.length, " = .bonsai/snapshots:", diskSnapshotDirs.length, ")");
        return;
    }

    if (discCount > 0) {
        writeln("! ", discCount, " discrepancies were found:");
        if (discJson.length > 0) {
            writeln("  * The integrity of bonsai.json has been compromised:");
            foreach (d; discJson) writeln("\t    ", d);
        }
        if (discRepo.length > 0) {
            writeln("  * Potential repository issues:");
            foreach (d; discRepo) writeln("\t    ", d);
        }
    }

    if (critCount > 0) {
        writeln("X ", critCount, " critical errors have been found:");
        if (critJson.length > 0) {
            writeln("  * There is a logical inconsistency in bonsai.json:");
            foreach (c; critJson) writeln("\t    ", c);
        }
        if (critCurrent.length > 0) {
            writeln("  * There is a logical inconsistency in the CURRENT file:");
            foreach (c; critCurrent) writeln("\t    ", c);
        }
        if (critSnapshots.length > 0) {
            writeln("  * .bonsai/snapshots is missing or corrupted:");
            foreach (c; critSnapshots) writeln("\t    ", c);
        }
        if (critBonsai.length > 0) {
            writeln("  * .bonsai is missing or corrupted:");
            foreach (c; critBonsai) writeln("\t    ", c);
        }
        if (critIgnore.length > 0) {
            writeln("  * There is a logical inconsistency in the .bonsaiignore file:");
            foreach (c; critIgnore) writeln("\t    ", c);
        }
    }
}
