//! Audit Context
//!
//! Manages contextual information (user, app, host, pid) for audit entries.
//! Supports both auto-detected and application-provided values.

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;

/// Audit context that tracks who/what is performing operations
pub const AuditContext = struct {
    allocator: Allocator,

    /// User identifier (provided by application)
    user: ?[]const u8 = null,
    /// Application name (provided by application)
    app: ?[]const u8 = null,
    /// Hostname (auto-detected or provided)
    host: ?[]const u8 = null,
    /// Process ID (auto-detected)
    pid: ?u32 = null,

    const Self = @This();

    /// Initialize with auto-detected values
    pub fn init(allocator: Allocator) Self {
        return Self{
            .allocator = allocator,
            .pid = getPid(),
            .host = null, // Detected lazily on first toEntryContext call
        };
    }

    /// Initialize with application-provided values
    pub fn initWithApp(allocator: Allocator, app_name: []const u8) !Self {
        return Self{
            .allocator = allocator,
            .app = try allocator.dupe(u8, app_name),
            .pid = getPid(),
            .host = null,
        };
    }

    pub fn deinit(self: *Self) void {
        if (self.user) |u| self.allocator.free(u);
        if (self.app) |a| self.allocator.free(a);
        if (self.host) |h| self.allocator.free(h);
        self.* = undefined;
    }

    /// Set the current user (e.g., after login)
    pub fn setUser(self: *Self, user: ?[]const u8) !void {
        if (self.user) |u| self.allocator.free(u);
        self.user = if (user) |u| try self.allocator.dupe(u8, u) else null;
    }

    /// Set the application name
    pub fn setApp(self: *Self, app: ?[]const u8) !void {
        if (self.app) |a| self.allocator.free(a);
        self.app = if (app) |a| try self.allocator.dupe(u8, a) else null;
    }

    /// Set the hostname (overrides auto-detection)
    pub fn setHost(self: *Self, host: ?[]const u8) !void {
        if (self.host) |h| self.allocator.free(h);
        self.host = if (host) |h| try self.allocator.dupe(u8, h) else null;
    }

    /// Get context for entry (detects host if needed)
    pub fn toEntryContext(self: *Self) !EntryContext {
        // Detect hostname if not set
        if (self.host == null) {
            self.host = try detectHostname(self.allocator);
        }

        return EntryContext{
            .user = self.user,
            .app = self.app,
            .host = self.host,
            .pid = self.pid,
        };
    }

    /// Clone context values for an entry (caller owns memory)
    pub fn cloneForEntry(self: *Self, allocator: Allocator) !OwnedEntryContext {
        // Detect hostname if not set
        if (self.host == null) {
            self.host = try detectHostname(self.allocator);
        }

        return OwnedEntryContext{
            .user = if (self.user) |u| try allocator.dupe(u8, u) else null,
            .app = if (self.app) |a| try allocator.dupe(u8, a) else null,
            .host = if (self.host) |h| try allocator.dupe(u8, h) else null,
            .pid = self.pid,
        };
    }
};

/// Entry context (borrowed references)
pub const EntryContext = struct {
    user: ?[]const u8,
    app: ?[]const u8,
    host: ?[]const u8,
    pid: ?u32,
};

/// Entry context with owned memory
pub const OwnedEntryContext = struct {
    user: ?[]const u8,
    app: ?[]const u8,
    host: ?[]const u8,
    pid: ?u32,

    pub fn deinit(self: *OwnedEntryContext, allocator: Allocator) void {
        if (self.user) |u| allocator.free(u);
        if (self.app) |a| allocator.free(a);
        if (self.host) |h| allocator.free(h);
        self.* = undefined;
    }
};

/// Get current process ID
fn getPid() ?u32 {
    if (builtin.os.tag == .linux or builtin.os.tag == .macos) {
        const pid = std.os.linux.getpid();
        return @intCast(pid);
    }
    return null;
}

/// Detect hostname from system
fn detectHostname(allocator: Allocator) !?[]const u8 {
    if (builtin.os.tag == .linux or builtin.os.tag == .macos) {
        var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
        const hostname = std.posix.gethostname(&buf) catch return null;
        return try allocator.dupe(u8, hostname);
    }
    return null;
}

test "AuditContext basic" {
    const allocator = std.testing.allocator;

    var ctx = AuditContext.init(allocator);
    defer ctx.deinit();

    // PID should be auto-detected
    try std.testing.expect(ctx.pid != null);

    // Set user and app
    try ctx.setUser("alice");
    try ctx.setApp("test_app");

    try std.testing.expectEqualStrings("alice", ctx.user.?);
    try std.testing.expectEqualStrings("test_app", ctx.app.?);
}

test "AuditContext initWithApp" {
    const allocator = std.testing.allocator;

    var ctx = try AuditContext.initWithApp(allocator, "my_app");
    defer ctx.deinit();

    try std.testing.expectEqualStrings("my_app", ctx.app.?);
    try std.testing.expect(ctx.user == null);
}

test "AuditContext toEntryContext" {
    const allocator = std.testing.allocator;

    var ctx = AuditContext.init(allocator);
    defer ctx.deinit();

    try ctx.setUser("bob");
    try ctx.setApp("app");

    const entry_ctx = try ctx.toEntryContext();

    try std.testing.expectEqualStrings("bob", entry_ctx.user.?);
    try std.testing.expectEqualStrings("app", entry_ctx.app.?);
    // Host should be detected
    try std.testing.expect(entry_ctx.host != null);
}

test "AuditContext cloneForEntry" {
    const allocator = std.testing.allocator;

    var ctx = AuditContext.init(allocator);
    defer ctx.deinit();

    try ctx.setUser("charlie");
    try ctx.setApp("clone_test");

    var owned = try ctx.cloneForEntry(allocator);
    defer owned.deinit(allocator);

    try std.testing.expectEqualStrings("charlie", owned.user.?);
    try std.testing.expectEqualStrings("clone_test", owned.app.?);

    // Modifying original shouldn't affect clone
    try ctx.setUser("david");
    try std.testing.expectEqualStrings("charlie", owned.user.?);
}
