// Copyright © 2013-2026 Jonathan Vasquez <jon@xyinn.org>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

//! The Cactus Backend
const std = @import("std");
const Settings = @import("settings.zig");
const Shard = @import("shard.zig");
const PathBuilder = @import("path_builder.zig");
const ProcessManager = @import("process_manager.zig");
const Filesystem = @import("filesystem.zig");
const assert = std.debug.assert;
const Enums = @import("enums.zig");
const Utilities = @import("utilities.zig");

const Cactus = @This();

const log = std.log.scoped(.backend);

main_init: std.process.Init,
gpa: std.mem.Allocator,
io: std.Io,
environ: std.process.Environ,

root_path: []const u8,
settings: Settings,
path_builder: PathBuilder,
process_manager: ProcessManager,

pub fn init(main_init: std.process.Init, version: []const u8) !Cactus {
    const gpa = main_init.gpa;
    const io = main_init.io;

    var aa: std.heap.ArenaAllocator = .init(gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    Utilities.printHeader(version);

    // Get root_path from args if any.
    const arguments = try main_init.minimal.args.toSlice(arena);
    for (arguments, 0..) |arg, index| {
        log.debug("[{d}] {s}", .{ index, arg });
    }

    // Get the directory path where Cactus is currently running from. This by
    // default is considered the Cactus Root Directory.
    var current_cactus_dir_path_buffer: [1024]u8 = undefined;
    const length = std.process.executableDirPath(io, &current_cactus_dir_path_buffer) catch |e| {
        log.err("Failed to get the directory Cactus is currently running in due to: {}\n", .{e});
        std.process.exit(1);
    };
    const cactus_dir_path_slice = current_cactus_dir_path_buffer[0..length];

    const default_root_path = try gpa.dupe(u8, cactus_dir_path_slice);
    defer gpa.free(default_root_path);

    log.debug("The default root path would be: {s}", .{default_root_path});

    // Determine the root path and take ownership of it.
    const root_path = blk: {
        if (arguments.len > 1) {
            log.info("Using Cactus Root Directory: {s}", .{std.mem.trimEnd(u8, arguments[1], "/")});
            break :blk try gpa.dupe(u8, std.mem.trimEnd(u8, arguments[1], "/"));
        }

        log.info("Using Cactus Root Directory (Default): {s}", .{default_root_path});
        break :blk try gpa.dupe(u8, default_root_path);
    };
    errdefer gpa.free(root_path);

    log.info("Root Path: {s}\n", .{root_path});

    const path_builder = try PathBuilder.init(gpa, root_path);

    // Create the root directory if needed. The base layout and settings will
    // be created if necessary regardless of detected install type.
    if (try Filesystem.createDirectoryIfNeeded(io, root_path)) {
        log.info("[ New Cactus Installation ]\n", .{});
    } else {
        log.info("[ Existing Cactus Installation ]\n", .{});
    }

    try Filesystem.createBaseLayout(io, &path_builder);
    try Filesystem.migrateOldSettingsNameToNewNameIfNeeded(io, path_builder);
    try Filesystem.createDefaultSettingsFileIfNeeded(arena, io, main_init.environ_map, path_builder);

    // Load the settings.
    const settings = try Settings.getSettings(gpa, io, path_builder);
    errdefer settings.deinit(gpa);

    const process_manager = ProcessManager.init();

    return .{
        .main_init = main_init,
        .gpa = gpa,
        .io = io,
        .environ = main_init.minimal.environ,
        .root_path = root_path,
        .settings = settings,
        .path_builder = path_builder,
        .process_manager = process_manager,
    };
}

pub fn deinit(self: *Cactus) void {
    self.gpa.free(self.root_path);
    self.settings.deinit(self.gpa);
    self.path_builder.deinit(self.gpa);
    self.process_manager.deinit(self.gpa);
}

// Create the requested shard. All fields are required. If the user doesn't need
// any flags or notes, just pass in an empty string.
//
// When a shard is created, the shard will also be automatically installed as well.
// So all files needed for the shard should already be in place in Common/.
//
// On failure, the shard creation will be rolled back.
pub fn create(
    self: *Cactus,
    worldstone: []const u8,
    name: []const u8,
    launcher: []const u8,
    flags: []const u8,
    notes: []const u8,
    insert_at_index: ?usize,
) !Enums.CreateStatusCodes {
    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = Shard.init(arena, self.io, worldstone, name, launcher, flags, notes) catch |e| switch (e) {
        error.RequiredFieldMissing => return .required_field_missing,
        error.InvalidCharacterDetected => return .invalid_character_detected,
        else => {
            log.warn("There was an error creating your shard: {}", .{e});
            return .failed;
        },
    };

    Filesystem.installIfNeeded(
        arena,
        self.io,
        self.environ,
        &self.settings,
        &self.path_builder,
        &shard,
        false,
        false,
    ) catch |e| switch (e) {
        error.RequiredFileMissing => {
            log.warn("Rolling back shard creation triggered during create phase ...", .{});
            try Filesystem.deleteShardAndPrefixIfNeeded(arena, self.io, &self.settings, &self.path_builder, &shard);
            return .required_file_missing;
        },
        else => {
            log.warn("There was an error creating your shard: {}", .{e});
            return .failed;
        },
    };

    try self.settings.addShard(self.gpa, self.io, shard, insert_at_index);
    try self.settings.save(arena, self.io, self.path_builder);
    return .success;
}

/// Modify the shard.
///
/// Everything except for the Worldstone and Name can be modified.
///
/// It is assumed that we are passing a valid index.
pub fn modify(
    self: *Cactus,
    index: usize,
    launcher: []const u8,
    flags: []const u8,
    notes: []const u8,
) !Enums.ModifyStatusCodes {
    assert(index < self.settings.shards.items.len);

    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = self.settings.getShard(index);
    log.info("Modifying Shard: {s} ...", .{shard.alias});
    shard.modify(self.gpa, launcher, flags, notes) catch |e| switch (e) {
        error.RequiredFieldMissing => return .required_field_missing,
        error.InvalidCharacterDetected => return .invalid_character_detected,
        else => return e,
    };
    try self.settings.save(arena, self.io, self.path_builder);
    return .success;
}

/// Clone the shard. The shard will be inserted after the cloned shard.
///
/// It is assumed that we are passing a valid index.
pub fn clone(self: *Cactus, index: usize) !Enums.CreateStatusCodes {
    assert(index < self.settings.shards.items.len);

    const shard = self.settings.getShard(index);
    log.info("Cloning Shard: {s} ...", .{shard.alias});

    return self.create(shard.worldstone, shard.name, shard.launcher, shard.flags, shard.notes, index + 1);
}

/// Destroy the shard.
///
/// This will also delete the shard and prefix directories if needed when there
/// are no more shards referencing the shard_key, and will also delete the respective
/// parent directories if no other shards are using the same platform.
///
/// It is assumed that we are passing a valid index.
pub fn destroy(self: *Cactus, index: usize) !Enums.DestroyStatusCodes {
    assert(index < self.settings.shards.items.len);

    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = self.settings.getShard(index);
    shard.print();

    if (shard.active) {
        return .active_shard;
    }

    // We need to take ownership of any values that we need before we remove
    // the shard since we are about to invalidate it.
    const cloned_shard = try Shard.copy(arena, self.io, shard.*);

    log.info("Removing shard ...", .{});
    self.settings.removeShard(self.gpa, index);

    // A shard+prefix should only be removed if there are no more shard aliases
    // referencing it. If there are no more shards referencing the same worldstone,
    // the shard parent directory is also deleted from shards and prefixes folders.
    if (!self.settings.isShardAliased(cloned_shard.alias)) {
        log.info("Removing shard and prefix directories if needed ...", .{});
        try Filesystem.deleteShardAndPrefixIfNeeded(arena, self.io, &self.settings, &self.path_builder, &cloned_shard);
    }

    try self.settings.save(arena, self.io, self.path_builder);
    return .success;
}

/// Shift the shard. The shard will be moved up or down a single index.
///
/// It is assumed that we are passing a valid index.
pub fn shift(self: *Cactus, index: usize, direction: enum { up, down }) !?usize {
    assert(index < self.settings.shards.items.len);

    var new_index: usize = undefined;
    if (direction == .up) {
        if (index == 0) {
            log.debug("No need to shift. Already in final position.", .{});
            return null;
        } else {
            new_index = index - 1;
        }
    } else {
        if (index == self.settings.getShardsLength() - 1) {
            log.debug("No need to shift. Already in final position.", .{});
            return null;
        } else {
            new_index = index + 1;
        }
    }

    log.debug("Shifting Shard ...", .{});
    try self.settings.shiftShard(index, new_index);
    return new_index;
}

/// Summon the shard.
///
/// It is assumed that we are passing a valid index.
///
/// If a shard is detected not to exist, it will be attempted to be created
/// automatically. If creation fails, an alert will be displayed but no files
/// will be deleted.
pub fn summon(self: *Cactus, index: usize) !void {
    assert(index < self.settings.shards.items.len);

    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = self.settings.getShard(index);
    log.info("Summoning Shard: {s} ...", .{shard.alias});
    log.info("Shard Prefix: {s}", .{try self.path_builder.getPrefixPath(arena, shard)});

    // The save path and any other needed values will be auto-healed on every
    // summon as long as the shard or any other aliased shards are not active.
    const should_heal_save_path = if (shard.active or try self.settings.areAnyAliasedShardsActive(shard)) blk: {
        log.debug("Auto healing not required since either this shard or another aliased shard is active.", .{});
        break :blk false;
    } else blk: {
        log.debug("Auto healing enabled. The save path and any other necessary values will be healed.", .{});
        break :blk true;
    };

    // Automatically install any shard files if needed. This is for ergonomics.
    Filesystem.installIfNeeded(
        arena,
        self.io,
        self.environ,
        &self.settings,
        &self.path_builder,
        shard,
        false,
        should_heal_save_path,
    ) catch |e| switch (e) {
        error.RequiredFileMissing => {
            log.warn("Unable to summon your shard. Please reset your shard and try again.", .{});
            return;
        },
        else => return e,
    };

    // Summon Shard
    try self.process_manager.run(
        self.gpa,
        self.io,
        self.environ,
        &self.settings,
        self.path_builder,
        shard,
    );
}

/// Reset the shard.
///
/// It is assumed that we are passing a valid index.
pub fn reset(self: Cactus, index: usize) !Enums.ResetStatusCodes {
    assert(index < self.settings.shards.items.len);

    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = self.settings.getShard(index);
    log.info("Resetting Shard: {s} ...", .{shard.alias});

    if (shard.active) {
        return .active_shard;
    }

    if (try self.settings.areAnyAliasedShardsActive(shard)) {
        return .active_aliased_shard;
    }

    Filesystem.installIfNeeded(
        arena,
        self.io,
        self.environ,
        &self.settings,
        &self.path_builder,
        shard,
        true,
        false,
    ) catch |e| switch (e) {
        error.RequiredFileMissing => {},
        else => return e,
    };

    return .success;
}

// Caller owns memory.
pub fn backup(self: Cactus, index: usize) !?[]const u8 {
    var aa: std.heap.ArenaAllocator = .init(self.gpa);
    const arena = aa.allocator();
    defer aa.deinit();

    const shard = self.settings.getShard(index);
    log.info("Backing Up Shard: {s} ...", .{shard.alias});

    if (shard.active) {
        log.warn("Can't backup a shard that is currently active.", .{});
        return null;
    }

    if (ProcessManager.is_aliased_shard_active(
        shard.id,
        &self.settings,
        &self.process_manager.tracked_process_shard_ids,
    )) {
        log.warn("Can't backup a shard which has another shard with the same alias active.", .{});
        return null;
    }

    const full_path_to_backup = try Filesystem.createBackup(arena, self.io, &self.settings, shard, &self.path_builder);
    return try self.gpa.dupe(u8, full_path_to_backup);
}

/// Update settings.
pub fn updateSettings(self: *Cactus, options: Settings.UpdateSettingsOptions) !Enums.UpdateSettingsStatusCodes {
    log.debug("Updating and saving settings ...", .{});
    return try self.settings.update(self.gpa, self.io, self.path_builder, options);
}
