//! `/api/pause` — the global pause of filtering (ruling 15).
//!
//! Pausing suspends filtering only: local records, forward zones, the cache,
//! the upstream and the query log all keep working (milestone-7 ruling 18).
//!
//! `until` is null both while filtering is on and while an indefinite pause is
//! in force, so `paused` is the field that disambiguates the two. The pause is
//! deliberately not persisted, so a restart resumes filtering.

const std = @import("std");

const http_util = @import("../http_util.zig");
const mutations = @import("mutations.zig");
const pause_mod = @import("../../server/pause.zig");
const server = @import("../server.zig");

const Failure = mutations.Failure;
const Request = http_util.Request;
const HandlerError = http_util.HandlerError;

/// Longest pause a single request may set: one week. An operator who wants
/// longer wants the indefinite pause, which is one word shorter to ask for.
pub const max_duration_seconds: u32 = 7 * 24 * 3600;

const Body = struct {
    paused: bool,
    duration_seconds: ?u32 = null,
};

pub const View = struct {
    paused: bool,
    /// Unix seconds when filtering resumes; null while unpaused and null while
    /// the pause is indefinite.
    until: ?i64,
};

/// The state a `Pause` is in at `now_s`, in the shape the API answers with.
pub fn view(pause: *const pause_mod.Pause, now_s: i64) View {
    const until = pause.until.load(.monotonic);
    if (until == 0) return .{ .paused = false, .until = null };
    if (until < 0) return .{ .paused = true, .until = null };
    if (now_s >= until) return .{ .paused = false, .until = null };
    return .{ .paused = true, .until = until };
}

fn apply(
    state: *server.WebState,
    io: std.Io,
    body: Body,
) union(enum) { view: View, fail: Failure } {
    const pause = state.pause orelse return .{ .fail = .{ .unavailable = "filtering is not running" } };
    if (body.duration_seconds) |seconds| {
        if (!body.paused) return .{ .fail = .{
            .invalid = "duration_seconds is only meaningful with paused = true",
        } };
        if (seconds == 0 or seconds > max_duration_seconds) return .{ .fail = .{
            .invalid = "duration_seconds must be 1 to 604800",
        } };
    }

    const now_s = mutations.nowSeconds(io);
    if (body.paused) {
        pause.pauseFor(now_s, body.duration_seconds);
    } else {
        pause.unpause();
    }
    return .{ .view = view(pause, now_s) };
}

pub fn get(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
    const pause = state.pause orelse
        return mutations.respondFailure(request, .{ .unavailable = "filtering is not running" }, "");

    return http_util.respondJson(request, .ok, view(pause, mutations.nowSeconds(io)), &.{});
}

pub fn post(state: *server.WebState, io: std.Io, request: *Request) HandlerError!void {
    const parsed = http_util.parseBody(Body, request) catch |err|
        return mutations.respondBadBody(request, err);

    return switch (apply(state, io, parsed.value)) {
        .fail => |failure| mutations.respondFailure(request, failure, "pausing"),
        .view => |current| http_util.respondJson(request, .ok, current, &.{}),
    };
}

// ---------------------------------------------------------------------------
// tests
// ---------------------------------------------------------------------------

const testing = std.testing;

test "an unpaused server reports neither a pause nor an expiry" {
    const pause: pause_mod.Pause = .{};
    const current = view(&pause, 1_700_000_000);
    try testing.expect(!current.paused);
    try testing.expectEqual(@as(?i64, null), current.until);
}

test "an indefinite pause reports paused with no expiry" {
    var pause: pause_mod.Pause = .{};
    pause.pauseFor(1_000, null);
    const current = view(&pause, 1_000);
    try testing.expect(current.paused);
    try testing.expectEqual(@as(?i64, null), current.until);
}

test "a timed pause reports the second filtering comes back" {
    var pause: pause_mod.Pause = .{};
    pause.pauseFor(1_000, 60);
    try testing.expectEqual(@as(?i64, 1_060), view(&pause, 1_000).until);
    // Past its expiry it reads as unpaused, exactly as the query path sees it.
    try testing.expect(!view(&pause, 1_060).paused);
    try testing.expectEqual(@as(?i64, null), view(&pause, 1_060).until);
}

test "a pause round trip goes through the running pause flag" {
    var bench: mutations.Bench = undefined;
    try bench.init(testing.allocator);
    defer bench.deinit(testing.allocator);

    var pause: pause_mod.Pause = .{};
    bench.state.pause = &pause;

    const paused = apply(&bench.state, bench.io(), .{ .paused = true, .duration_seconds = 60 });
    try testing.expect(paused.view.paused);
    try testing.expect(pause.isPaused(mutations.nowSeconds(bench.io())));
    try testing.expect(paused.view.until.? > mutations.nowSeconds(bench.io()));

    const resumed = apply(&bench.state, bench.io(), .{ .paused = false });
    try testing.expect(!resumed.view.paused);
    try testing.expect(!pause.isPaused(mutations.nowSeconds(bench.io())));
}

test "an indefinite pause set through the API never expires" {
    var bench: mutations.Bench = undefined;
    try bench.init(testing.allocator);
    defer bench.deinit(testing.allocator);

    var pause: pause_mod.Pause = .{};
    bench.state.pause = &pause;

    const paused = apply(&bench.state, bench.io(), .{ .paused = true });
    try testing.expect(paused.view.paused);
    try testing.expectEqual(@as(?i64, null), paused.view.until);
    try testing.expect(pause.isPaused(std.math.maxInt(i64) - 1));
}

test "a duration outside the range, or one sent with paused false, is refused" {
    var bench: mutations.Bench = undefined;
    try bench.init(testing.allocator);
    defer bench.deinit(testing.allocator);

    var pause: pause_mod.Pause = .{};
    bench.state.pause = &pause;

    try testing.expect(apply(&bench.state, bench.io(), .{
        .paused = true,
        .duration_seconds = 0,
    }).fail == .invalid);
    try testing.expect(apply(&bench.state, bench.io(), .{
        .paused = true,
        .duration_seconds = max_duration_seconds + 1,
    }).fail == .invalid);
    try testing.expect(apply(&bench.state, bench.io(), .{
        .paused = false,
        .duration_seconds = 60,
    }).fail == .invalid);

    try testing.expect(!pause.isPaused(mutations.nowSeconds(bench.io())));
}

test "pausing a server that has no pause flag is unavailable" {
    var state: server.WebState = .{ .gpa = testing.allocator };
    try testing.expect(apply(&state, undefined, .{ .paused = true }).fail == .unavailable);
}
