%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2017-2025 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries.
%%
%% @hidden
-module(ra_log).

-include_lib("stdlib/include/assert.hrl").
-compile([inline_list_funcs]).

-export([pre_init/2,
         init/1,
         close/1,
         begin_tx/1,
         commit_tx/1,
         append/2,
         write/2,
         write_sparse/3,
         append_sync/2,
         write_sync/2,
         fold/5,
         fold/6,
         sparse_read/2,
         partial_read/3,
         execute_read_plan/4,
         read_plan_info/1,
         previous_wal_index/1,
         last_index_term/1,
         set_last_index/2,
         handle_event/2,
         last_written/1,
         fetch/2,
         fetch_term/2,
         next_index/1,
         snapshot_state/1,
         snapshot_size/1,
         set_snapshot_state/2,
         install_snapshot/4,
         recover_snapshot/1,
         snapshot_index_term/1,
         update_release_cursor/5,
         checkpoint/5,
         promote_checkpoint/2,

         can_write/1,
         exists/2,
         has_pending/1,
         overview/1,
         %% config
         write_config/2,
         read_config/1,

         delete_everything/1,
         release_resources/3,

         tick/2,
         assert/1
        ]).

-include("ra.hrl").

-define(DEFAULT_RESEND_WINDOW_SEC, 20).
-define(MIN_SNAPSHOT_INTERVAL, 4096).
-define(MIN_CHECKPOINT_INTERVAL, 16384).
-define(LOG_APPEND_TIMEOUT, 5000).
-define(WAL_RESEND_TIMEOUT, 5000).
-define(ETSTBL, ra_log_snapshot_state).

-type ra_meta_key() :: atom().
-type segment_ref() :: {File :: binary(), ra_range:range()}.
-type event_body() :: {written, ra_term(), ra_seq:state()} |
                      {segments, [{ets:tid(), ra:range()}], [segment_ref()]} |
                      {resend_write, ra_index()} |
                      {snapshot_written, ra_idxterm(),
                       LiveIndexes :: ra_seq:state(),
                       ra_snapshot:kind(),
                       SnapshotSize :: non_neg_integer() | undefined,
                       Duration :: non_neg_integer()} |
                      {compaction_result, term()} |
                      major_compaction |
                      {down, pid(), term()}.

-type event() :: {ra_log_event, event_body()}.
-type transform_fun() :: fun ((ra_index(), ra_term(), ra_server:command()) -> term()).

-type effect() ::
    {delete_snapshot, Dir :: file:filename_all(), ra_idxterm()} |
    {monitor, process, log, pid()} |
    ra_snapshot:effect() |
    ra_server:effect().
%% logs can have effects too so that they can be coordinated with other state
%% such as avoiding to delete old snapshots whilst they are still being
%% replicated

-type effects() :: [effect()].

-record(cfg, {uid :: ra_uid(),
              log_id :: unicode:chardata(),
              directory :: file:filename_all(),
              min_snapshot_interval = ?MIN_SNAPSHOT_INTERVAL :: non_neg_integer(),
              min_checkpoint_interval = ?MIN_CHECKPOINT_INTERVAL :: non_neg_integer(),
              snapshot_module :: module(),
              resend_window_seconds = ?DEFAULT_RESEND_WINDOW_SEC :: integer(),
              wal :: atom(),
              segment_writer :: atom(),
              counter :: undefined | counters:counters_ref(),
              names :: ra_system:names()}).

-record(?MODULE,
        {cfg = #cfg{},
         %% mutable data below
         range :: ra:range(),
         last_term = 0 :: ra_term(),
         last_written_index_term = {0, 0} :: ra_idxterm(),
         snapshot_state :: ra_snapshot:state(),
         current_snapshot :: option(ra_idxterm()),
         last_resend_time :: option({integer(), WalPid :: pid() | undefined}),
         last_wal_write :: {pid(), Ms :: integer(), ra:index() | -1},
         reader :: ra_log_segments:state(),
         mem_table :: ra_mt:state(),
         tx = false :: false | {true, ra:range()},
         pending = [] :: ra_seq:state(),
         live_indexes = [] :: ra_seq:state()
        }).

-record(read_plan, {dir :: file:filename_all(),
                    read :: #{ra_index() := log_entry()},
                    plan :: ra_log_segments:read_plan()}).

-opaque read_plan() :: #read_plan{}.
-opaque state() :: #?MODULE{}.

-type ra_log_init_args() :: #{uid := ra_uid(),
                              system_config => ra_system:config(),
                              log_id => unicode:chardata(),
                              %% Deprecated in favor of `min_snapshot_interval'
                              %% but this value is used as a fallback if
                              %% `min_snapshot_interval' is not provided.
                              snapshot_interval => non_neg_integer(),
                              min_snapshot_interval => non_neg_integer(),
                              min_checkpoint_interval => non_neg_integer(),
                              resend_window => integer(),
                              max_open_segments => non_neg_integer(),
                              snapshot_module => module(),
                              machine => ra_machine:machine(),
                              counter => counters:counters_ref(),
                              initial_access_pattern => sequential | random,
                              max_checkpoints => non_neg_integer(),
                              major_compaction_strategy =>
                                  ra_log_segments:major_compaction_strategy()}.


-type overview() ::
    #{type := ra_log,
      range := ra:range(),
      last_index := ra:index(),
      last_term := ra_term(),
      last_written_index_term := ra_idxterm(),
      num_segments := non_neg_integer(),
      open_segments => non_neg_integer(),
      snapshot_index => undefined | ra_index(),
      snapshot_term => undefined | ra_index(),
      mem_table_size => non_neg_integer(),
      latest_checkpoint_index => undefined | ra_index(),
      atom() => term()}.

-export_type([state/0,
              read_plan/0,
              ra_log_init_args/0,
              ra_meta_key/0,
              segment_ref/0,
              event/0,
              event_body/0,
              effect/0,
              overview/0
             ]).

-define(SNAPSHOTS_DIR, <<"snapshots">>).
-define(CHECKPOINTS_DIR, <<"checkpoints">>).
-define(RECOVERY_CHECKPOINT_DIR, <<"recovery_checkpoint">>).

pre_init(Machine, #{uid := UId,
                    system_config := #{data_dir := DataDir}} = Conf) ->
    Dir = server_data_dir(DataDir, UId),
    SnapModule = maps:get(snapshot_module, Conf, ?DEFAULT_SNAPSHOT_MODULE),
    MaxCheckpoints = maps:get(max_checkpoints, Conf, ?DEFAULT_MAX_CHECKPOINTS),
    SnapshotsDir = filename:join(Dir, ?SNAPSHOTS_DIR),
    CheckpointsDir = filename:join(Dir, ?CHECKPOINTS_DIR),
    RecoveryCheckpointDir = filename:join(Dir, ?RECOVERY_CHECKPOINT_DIR),
    _ = ra_snapshot:init(UId, SnapModule, SnapshotsDir,
                         CheckpointsDir, RecoveryCheckpointDir,
                         Machine, undefined, MaxCheckpoints),
    ok.

-spec init(ra_log_init_args()) -> state().
init(#{uid := UId,
       system_config := #{data_dir := DataDir,
                          names := #{wal := Wal,
                                     segment_writer := SegWriter} = Names}
      } = Conf) ->
    Dir = server_data_dir(DataDir, UId),
    MaxOpen = maps:get(max_open_segments, Conf, 1),
    SnapModule = maps:get(snapshot_module, Conf, ?DEFAULT_SNAPSHOT_MODULE),
    %% this has to be patched by ra_server
    LogId = maps:get(log_id, Conf, UId),
    ResendWindow = maps:get(resend_window, Conf, ?DEFAULT_RESEND_WINDOW_SEC),
    SnapInterval = maps:get(min_snapshot_interval, Conf,
                            maps:get(snapshot_interval, Conf,
                                     ?MIN_SNAPSHOT_INTERVAL)),
    CPInterval = maps:get(min_checkpoint_interval, Conf,
                          ?MIN_CHECKPOINT_INTERVAL),
    MaxCheckpoints = maps:get(max_checkpoints, Conf, ?DEFAULT_MAX_CHECKPOINTS),
    SnapshotsDir = filename:join(Dir, ?SNAPSHOTS_DIR),
    CheckpointsDir = filename:join(Dir, ?CHECKPOINTS_DIR),
    RecoveryCheckpointDir = filename:join(Dir, ?RECOVERY_CHECKPOINT_DIR),
    Counter = maps:get(counter, Conf, undefined),

    %% ensure directories are there
    ok = ra_lib:make_dir(Dir),
    ok = ra_lib:make_dir(SnapshotsDir),
    ok = ra_lib:make_dir(CheckpointsDir),
    ok = ra_lib:make_dir(RecoveryCheckpointDir),
    % initialise metrics for this server
    LogSyncBaseName = maps:get(log_sync, Names),
    SyncServer = {pool, LogSyncBaseName, ra_log_sync:pool_size()},
    Machine = maps:get(machine, Conf, undefined),
    SnapshotState = ra_snapshot:init(UId, SnapModule, SnapshotsDir,
                                     CheckpointsDir, RecoveryCheckpointDir,
                                     SyncServer, Machine,
                                     Counter, MaxCheckpoints),
    {SnapIdx, SnapTerm} = case ra_snapshot:current(SnapshotState) of
                              undefined -> {-1, 0};
                              Curr -> Curr
                          end,
    %% Live indexes are already loaded into ETS by ra_snapshot:init
    LiveIndexes = ra_log_snapshot_state:live_indexes(?ETSTBL, UId),

    AccessPattern = maps:get(initial_access_pattern, Conf, sequential),
    {ok, Mt0} = ra_log_ets:mem_table_please(Names, UId),
    % recover current range and any references to segments
    % this queries the segment writer and thus blocks until any
    % segments it is currently processed have been finished
    MtRange = ra_mt:range(Mt0),
    ok = ra_log_segments:purge_dangling_symlinks(Dir),
    SegRefs = my_segrefs(UId, SegWriter),
    SegmentMaxCount = maps:get(segment_max_entries, Conf, ?SEGMENT_MAX_ENTRIES),
    SegmentMaxSize = maps:get(segment_max_size_bytes, Conf, ?SEGMENT_MAX_SIZE_B),
    MajorCompStrat = maps:get(major_compaction_strategy, Conf,
                              ?DEF_MAJOR_COMPACTION_STRAT),
    CompConf = #{max_size => SegmentMaxSize,
                 major_strategy => MajorCompStrat,
                 max_count => SegmentMaxCount},
    Reader = ra_log_segments:init(UId, Dir, MaxOpen, AccessPattern, SegRefs,
                                  Counter, CompConf, LogId),
    SegmentRange = ra_log_segments:range(Reader),
    %% The ranges can be sparse at this point so ra_range:add/2 does
    %% not do the right thing here as it requires a contiguous range
    Range = ra_range:combine(MtRange, SegmentRange),

    %% if the mt range contains indexes that overwrite part of the segment
    %% range we need to truncate the segment range to the index before
    %% the first mem table index
    TruncSegmentRange = case MtRange of
                            undefined ->
                                SegmentRange;
                            {FstMtIdx, _} ->
                                ra_range:limit(FstMtIdx, SegmentRange)
                        end,

    case ra_range:overlap(MtRange, SegmentRange) of
        {_, _} = Overlap ->
            ?INFO("~ts: ra_log:init/1 mem table and segment ranges overlap ~w"
                  "mem table range ~w, segment range ~w",
                  [LogId, Overlap, MtRange, SegmentRange]);
        _ -> ok
    end,
    %% TODO: if MtRange and SegmentRange overlaps check if the overlap exists in the
    %% mt and if it is the same in the segments, if so we can set first on the
    %% mt to match the end + 1 of the SegmentRange

    ReaderFiles = #{Fn => []
                    || {Fn, _} <- ra_log_segments:segment_refs(Reader)},
    [begin
         ?DEBUG("~ts: deleting overwritten segment ~w",
                [LogId, SR]),
         ?CATCH(prim_file:delete(filename:join(Dir, F))),
         ok
     end
     || {F, _} = SR <- SegRefs, not is_map_key(F, ReaderFiles)],

    %% assert there is no gap between the snapshot
    %% and the first index in the log
    Mt = case Range of
             undefined ->
                 Mt0;
             {FstIdx, LstIdx} ->
                 case FstIdx == SnapIdx + 1 orelse
                      ra_range:in(SnapIdx, Range) orelse
                      SnapIdx > LstIdx of
                     true ->
                         {DeleteSpecs, Mt1} = ra_mt:set_first(FstIdx, Mt0),
                         ok = exec_mem_table_delete(Names, UId, DeleteSpecs),
                         Mt1;
                     false ->
                         exit({corrupt_log,
                               gap_between_snapshot_and_log_range,
                               {SnapIdx, Range}})
                 end
         end,
    LastWalIdx = case ra_log_wal:last_writer_seq(Wal, UId) of
                     {ok, undefined} ->
                         -1;
                     {ok, Idx} ->
                         Idx;
                     {error, wal_down} ->
                         %% TODO: we could enter a condition loop here to
                         %% wait for the WAL to come back
                         ?ERROR("~ts: ra_log:init/1 cannot complete as wal"
                                " process is down.",
                                [LogId]),
                         exit(wal_down)
                     end,

    %% recover the pending seq
    MaxConfirmedWrittenIdx = case TruncSegmentRange of
                                 {_, LastSegIdx} ->
                                     max(LastWalIdx, LastSegIdx);
                                 _ ->
                                     max(LastWalIdx, 0)
                             end,
    Pending = ra_seq:floor(MaxConfirmedWrittenIdx + 1, ra_mt:indexes(Mt)),
    ?DEBUG_IF(Pending =/= [],
              "~ts: recovered pending indexes ~w",
              [LogId, Pending]),
    Cfg = #cfg{directory = Dir,
               uid = UId,
               log_id = LogId,
               min_snapshot_interval = SnapInterval,
               min_checkpoint_interval = CPInterval,
               wal = Wal,
               segment_writer = SegWriter,
               resend_window_seconds = ResendWindow,
               snapshot_module = SnapModule,
               counter = Counter,
               names = Names},
    State0 = #?MODULE{cfg = Cfg,
                      range = ra_range:truncate(SnapIdx, Range),
                      reader = Reader,
                      mem_table = Mt,
                      snapshot_state = SnapshotState,
                      current_snapshot = ra_snapshot:current(SnapshotState),
                      last_wal_write = {whereis(Wal), now_ms(), LastWalIdx},
                      live_indexes = LiveIndexes,
                      pending = Pending
                     },
    put_counter(Cfg, ?C_RA_SVR_METRIC_SNAPSHOT_INDEX, SnapIdx),
    LastIdx = case Range of
                  undefined ->
                      SnapIdx;
                  {_, Lst} ->
                      Lst
              end,
    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, LastIdx),
    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_WRITTEN_INDEX, LastIdx),
    put_counter(Cfg, ?C_RA_SVR_METRIC_NUM_SEGMENTS,
                ra_log_segments:segment_ref_count(Reader)),
    case ra_snapshot:latest_checkpoint(SnapshotState) of
        undefined ->
            ok;
        {ChIdx, _ChTerm} ->
            put_counter(Cfg, ?C_RA_SVR_METRIC_CHECKPOINT_INDEX, ChIdx)
    end,

    % recover the last term
    {LastTerm0, State2} = case Range of
                              undefined ->
                                  {SnapTerm, State0};
                              {_, LI} ->
                                  fetch_term(LI, State0)
                          end,
    LastSegRefIdx = case TruncSegmentRange of
                        undefined ->
                            -1;
                        {_, L} ->
                            L
                    end,
    LastWrittenIdx = lists:max([LastWalIdx, SnapIdx, LastSegRefIdx]),
    {LastWrittenTerm, State3} = case LastWrittenIdx of
                                    SnapIdx ->
                                        {SnapTerm, State2};
                                    _ ->
                                        fetch_term(LastWrittenIdx, State2)
                                end,

    LastTerm = ra_lib:default(LastTerm0, -1),
    State4 = State3#?MODULE{last_term = LastTerm,
                            last_written_index_term =
                                {LastWrittenIdx, LastWrittenTerm}},

    % initialized with a default 0 index 0 term dummy value
    % and an empty meta data map
    State = maybe_append_first_entry(State4),
    ?DEBUG("~ts: ra_log:init recovered last_index_term ~w"
           " snapshot_index_term ~w, last_written_index_term ~w",
           [LogId, last_index_term(State), {SnapIdx, SnapTerm},
            State#?MODULE.last_written_index_term
           ]),
    assert(resend_pending(LastWalIdx, State)).

-spec close(state()) -> ok.
close(#?MODULE{cfg = #cfg{uid = _UId},
               reader = Reader}) ->
    % deliberately ignoring return value
    % close all open segments
    _ = ra_log_segments:close(Reader),
    ok.

-spec begin_tx(state()) -> state().
begin_tx(State) ->
    State#?MODULE{tx = {true, undefined}}.

-spec commit_tx(state()) -> {ok, state()} | {error, wal_down, state()}.
commit_tx(#?MODULE{cfg = #cfg{uid = UId,
                              wal = Wal} = Cfg,
                   tx = {true, TxRange},
                   range = Range,
                   pending = Pend0,
                   mem_table = Mt1} = State) ->
    %% TODO: staged could contain entries from previous? I don't think that is
    %% ever the case as that would mean overwriting withing a single append batch
    Entries = ra_mt:staged(Mt1),
    Tid = ra_mt:tid(Mt1),
    WriterId = {UId, self()},
    PrevIdx = previous_wal_index(State),
    {WalCommands, Num, _} =
        lists:foldl(fun ({Idx, Term, Cmd0}, {WC, N, Prev}) ->
                            Cmd = {ttb, term_to_iovec(Cmd0)},
                            WalC = {append, WriterId, Tid, Prev, Idx, Term, Cmd},
                            {[WalC | WC], N+1, Idx}
                    end, {[], 0, PrevIdx}, Entries),
    {_, LastIdx} = TxRange,

    case ra_log_wal:write_batch(Wal, lists:reverse(WalCommands)) of
        {ok, Pid} ->
            %% commit after send to WAL, else abort
            {_, Mt} = ra_mt:commit(Mt1),
            ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, Num),
            {ok, State#?MODULE{tx = false,
                               range = ra_range:add(TxRange, Range),
                               last_wal_write = {Pid, now_ms(), LastIdx},
                               mem_table = Mt}};
        {error, wal_down} ->
            {Idx, _, _} = hd(Entries),
            Mt = ra_mt:abort(Mt1),
            %% TODO: review this -  still need to return the state here
            {error, wal_down,
             State#?MODULE{tx = false,
                           pending = ra_seq:limit(Idx - 1, Pend0),
                           mem_table = Mt}}
    end;
commit_tx(#?MODULE{tx = false} = State) ->
    State.

-define(IS_NEXT_IDX(Idx, Range),
        Range == undefined orelse
        element(2, Range) + 1 =:= Idx).

-define(IS_IN_RANGE(Idx, Range),
        Range =/= undefined andalso
        Idx >= element(1, Range) andalso
        Idx =< element(2, Range)).

-spec append(Entry :: log_entry(), State :: state()) ->
    state() | no_return().
append({Idx, Term, Cmd0} = Entry,
       #?MODULE{cfg = #cfg{uid = UId,
                           wal = Wal} = Cfg,
                range = Range,
                tx = false,
                pending = Pend0,
                mem_table = Mt0} = State)
      when ?IS_NEXT_IDX(Idx, Range) ->
    case ra_mt:insert(Entry, Mt0) of
        {ok, Mt} ->
            Cmd = {ttb, term_to_iovec(Cmd0)},
            case ra_log_wal:write(Wal, {UId, self()}, ra_mt:tid(Mt),
                                  previous_wal_index(State),
                                  Idx, Term, Cmd) of
                {ok, Pid} ->
                    Pend = ra_seq:limit(Idx - 1, Pend0),
                    ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, 1),
                    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
                    State#?MODULE{range = ra_range:extend(Idx, Range),
                                  last_term = Term,
                                  last_wal_write = {Pid, now_ms(), Idx},
                                  pending = ra_seq:append(Idx, Pend),
                                  mem_table = Mt};
                {error, wal_down} ->
                    error(wal_down)
            end;
        {error, Reason} ->
            ?DEBUG("~ts: mem table ~s detected appending index ~b, "
                   "opening new mem table",
                   [Cfg#cfg.log_id, Reason, Idx]),
            %% this function uses the infinity timeout
            {ok, M0} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                                       Cfg#cfg.uid, Mt0),
            append(Entry, State#?MODULE{mem_table = M0})
    end;
append({Idx, Term, _Cmd} = Entry,
       #?MODULE{cfg = Cfg,
                tx = {true, TxRange},
                pending = Pend0,
                mem_table = Mt0} = State)
      when ?IS_NEXT_IDX(Idx, TxRange) ->
    case ra_mt:stage(Entry, Mt0) of
        {ok, Mt} ->
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
            State#?MODULE{tx = {true, ra_range:extend(Idx, TxRange)},
                          last_term = Term,
                          pending = ra_seq:append(Idx, Pend0),
                          mem_table = Mt};
        {error, Reason} ->
            ?DEBUG("~ts: mem table ~s detected appending index ~b, tx=true "
                   "opening new mem table",
                   [Cfg#cfg.log_id, Reason, Idx]),
            %% this function uses the infinity timeout
            {ok, M0} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                                       Cfg#cfg.uid, Mt0),
            append(Entry, State#?MODULE{mem_table = M0})
    end;
append({Idx, _, _}, #?MODULE{range = Range,
                             tx = Tx}) ->
    Msg = lists:flatten(io_lib:format("tried writing ~b - current range ~w tx ~p",
                                      [Idx, Range, Tx])),
    exit({integrity_error, Msg}).

-spec write(Entries :: [log_entry()], State :: state()) ->
    {ok, state()} |
    {error, {integrity_error, term()} | wal_down}.
write([{FstIdx, _, _} | _Rest] = Entries,
      #?MODULE{cfg = Cfg,
               range = Range,
               pending = Pend0,
               mem_table = Mt0} = State0)
  when Range == undefined orelse
       (FstIdx =< element(2, Range) + 1 andalso
        FstIdx >= 0) ->
    case stage_entries(Cfg, Entries, Mt0) of
        {ok, Mt} ->
            Pend = ra_seq:limit(FstIdx - 1, Pend0),
            wal_write_batch(State0#?MODULE{mem_table = Mt,
                                           pending = Pend}, Entries);
        Error ->
            Error
    end;
write([], State) ->
    {ok, State};
write([{Idx, _, _} | _], #?MODULE{cfg = #cfg{uid = UId},
                                  range = Range}) ->
    Msg = lists:flatten(io_lib:format("~s: ra_log:write/2 "
                                      "tried writing ~b - current range ~w",
                                      [UId, Idx, Range])),
    {error, {integrity_error, Msg}}.

-spec write_sparse(log_entry(), option(ra:index()), state()) ->
    {ok, state()} | {error, wal_down | gap_detected}.
write_sparse({Idx, Term, Cmd} = Entry, PrevIdx0,
             #?MODULE{cfg = #cfg{uid = UId,
                                 wal = Wal} = Cfg,
                      range = Range,
                      pending = Pend0,
                      mem_table = Mt0} = State0)
  when PrevIdx0 == undefined orelse
       Range == undefined orelse
       (PrevIdx0 == element(2, Range)) ->
    {ok, Mt} = ra_mt:insert_sparse(Entry, PrevIdx0, Mt0),
    ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, 1),
    Tid = ra_mt:tid(Mt),
    PrevIdx = previous_wal_index(State0),
    case ra_log_wal:write(Wal, {UId, self()}, Tid, PrevIdx, Idx,
                          Term, Cmd) of
        {ok, Pid} ->
            ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, 1),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
            NewRange = case Range of
                           undefined ->
                               ra_range:new(Idx);
                           {S, _} ->
                               ra_range:new(S, Idx)
                       end,
            Pend = ra_seq:limit(Idx - 1, Pend0),
            {ok, State0#?MODULE{range = NewRange,
                                last_term = Term,
                                mem_table = Mt,
                                pending = ra_seq:append(Idx, Pend),
                                last_wal_write = {Pid, now_ms(), Idx}}};
        {error, wal_down} = Err->
            Err
    end.

-spec fold(FromIdx :: ra_index(), ToIdx :: ra_index(),
           fun((log_entry(), Acc) -> Acc), Acc, state()) ->
    {Acc, state()} when Acc :: term().
fold(From0, To0, Fun, Acc0, State) ->
    fold(From0, To0, Fun, Acc0, State, error).

-spec fold(FromIdx :: ra_index(), ToIdx :: ra_index(),
           fun((log_entry(), Acc) -> Acc), Acc, state(),
            MissingKeyStrategy :: error | return) ->
    {Acc, state()} when Acc :: term().
fold(From0, To0, Fun, Acc0,
     #?MODULE{cfg = Cfg,
              mem_table = Mt,
              range = {StartIdx, EndIdx},
              reader = Reader0} = State, MissingKeyStrat)
  when To0 >= From0 andalso
       To0 >= StartIdx ->

    %% TODO: move to ra_range function
    From = max(From0, StartIdx),
    To = min(To0, EndIdx),

    ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPS, 1),
    MtOverlap = ra_mt:range_overlap({From, To}, Mt),
    case MtOverlap of
        {undefined, {RemStart, RemEnd}} ->
            {Reader, Acc} = ra_log_segments:fold(RemStart, RemEnd, Fun,
                                                 Acc0, Reader0,
                                                 MissingKeyStrat),
            {Acc, State#?MODULE{reader = Reader}};
        {{MtStart, MtEnd}, {RemStart, RemEnd}} ->
            {Reader, Acc1} = ra_log_segments:fold(RemStart, RemEnd, Fun,
                                                  Acc0, Reader0,
                                                  MissingKeyStrat),
            Acc = ra_mt:fold(MtStart, MtEnd, Fun, Acc1, Mt, MissingKeyStrat),
            NumRead = MtEnd - MtStart + 1,
            ok = incr_counter(Cfg, ?C_RA_LOG_READ_MEM_TBL, NumRead),
            {Acc, State#?MODULE{reader = Reader}};
        {{MtStart, MtEnd}, undefined} ->
            Acc = ra_mt:fold(MtStart, MtEnd, Fun, Acc0, Mt, MissingKeyStrat),
            %% TODO: if fold is short circuited with MissingKeyStrat == return
            %% this count isn't correct, it doesn't massively matter so leaving
            %% for now
            NumRead = MtEnd - MtStart + 1,
            ok = incr_counter(Cfg, ?C_RA_LOG_READ_MEM_TBL, NumRead),
            {Acc, State}
    end;
fold(_From, _To, _Fun, Acc, State, _) ->
    {Acc, State}.

%% @doc Reads a list of indexes.
%% Found indexes are returned in the same order as the input list of indexes
%% @end
-spec sparse_read([ra_index()], state()) ->
    {[log_entry()], state()}.
sparse_read(Indexes0, #?MODULE{cfg = Cfg,
                               reader = Reader0,
                               range = Range,
                               live_indexes = LiveIndexes,
                               mem_table = Mt} = State) ->
    ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPS, 1),
    %% indexes need to be sorted high -> low for correct and efficient reading
    Sort = ra_lib:lists_detect_sort(Indexes0),
    Indexes1 = case Sort of
                   unsorted ->
                       lists:sort(fun erlang:'>'/2, Indexes0);
                   ascending ->
                       lists:reverse(Indexes0);
                   _ ->
                       % descending or undefined
                       Indexes0
               end,

    %% drop any indexes that are larger than the last index available
    %% or smaller than first index and not in live indexes
    Indexes2 = lists:filter(fun (I) ->
                                    ra_range:in(I, Range) orelse
                                    ra_seq:in(I, LiveIndexes)
                            end, Indexes1),
    {Entries0, MemTblNumRead, Indexes} = ra_mt:get_items(Indexes2, Mt),
    ok = incr_counter(Cfg, ?C_RA_LOG_READ_MEM_TBL, MemTblNumRead),
    {Entries1, Reader} = ra_log_segments:sparse_read(Reader0, Indexes, Entries0),
    %% here we recover the original order of indexes
    Entries = case Sort of
                  descending ->
                      lists:reverse(Entries1);
                  unsorted ->
                      Lookup = lists:foldl(
                                 fun ({I, _, _} = E, Acc) ->
                                         maps:put(I, E, Acc)
                                 end, #{}, Entries1),
                      maps_with_values(Indexes0, Lookup);
                  _ ->
                      %% nothing to do for ascending or undefined
                      Entries1
              end,
    {Entries, State#?MODULE{reader = Reader}}.


%% read a list of indexes,
%% found indexes be returned in the same order as the input list of indexes
-spec partial_read([ra_index()], state(),
                   fun ((ra_index(),
                         ra_term(),
                         ra_server:command()) -> term())) ->
    read_plan().
partial_read(Indexes0, #?MODULE{cfg = Cfg,
                                reader = Reader0,
                                range = Range,
                                snapshot_state = SnapState,
                                mem_table = Mt},
            TransformFun) ->
    ok = incr_counter(Cfg, ?C_RA_LOG_READ_OPS, 1),
    %% indexes need to be sorted high -> low for correct and efficient reading
    Sort = ra_lib:lists_detect_sort(Indexes0),
    Indexes1 = case Sort of
                   unsorted ->
                       lists:sort(fun erlang:'>'/2, Indexes0);
                   ascending ->
                       lists:reverse(Indexes0);
                   _ ->
                       % descending or undefined
                       Indexes0
               end,
    LastIdx = case Range of
                  undefined ->
                      case ra_snapshot:current(SnapState) of
                          undefined ->
                              -1;
                          {SnapIdx, _} ->
                              SnapIdx
                      end;
                  {_, End} ->
                      End
              end,

    %% drop any indexes that are larger than the last index available
    Indexes2 = lists:dropwhile(fun (I) -> I > LastIdx end, Indexes1),
    {Entries0, MemTblNumRead, Indexes} = ra_mt:get_items(Indexes2, Mt),
    ok = incr_counter(Cfg, ?C_RA_LOG_READ_MEM_TBL, MemTblNumRead),
    Read = lists:foldl(fun ({I, T, Cmd}, Acc) ->
                               maps:put(I, TransformFun(I, T, Cmd), Acc)
                       end, #{}, Entries0),

    Plan = ra_log_segments:read_plan(Reader0, Indexes),
    #read_plan{dir = Cfg#cfg.directory,
               read = Read,
               plan = Plan}.


-spec execute_read_plan(read_plan(), undefined | ra_flru:state(),
                        TransformFun :: transform_fun(),
                        ra_log_segments:read_plan_options()) ->
    {#{ra_index() => Command :: term()}, ra_flru:state()}.
execute_read_plan(#read_plan{dir = Dir,
                             read = Read,
                             plan = Plan}, Flru0, TransformFun,
                  Options) ->
    ra_log_segments:exec_read_plan(Dir, Plan, Flru0, TransformFun,
                                   Options, Read).

-spec read_plan_info(read_plan()) -> map().
read_plan_info(#read_plan{read = Read,
                          plan = Plan}) ->
    NumSegments = length(Plan),
    NumInSegments = lists:foldl(fun ({_, Idxs}, Acc) ->
                                        Acc + length(Idxs)
                                end, 0, Plan),
    #{num_read => map_size(Read),
      num_in_segments => NumInSegments,
      num_segments => NumSegments}.


-spec previous_wal_index(state()) -> ra_idxterm() | -1.
previous_wal_index(#?MODULE{range = Range}) ->
    case Range of
        undefined ->
            -1;
        {_, LastIdx} ->
            LastIdx
    end.

-spec last_index_term(state()) -> option(ra_idxterm()).
last_index_term(#?MODULE{range = {_, LastIdx},
                         last_term = LastTerm}) ->
    {LastIdx, LastTerm};
last_index_term(#?MODULE{current_snapshot = CurSnap}) ->
    CurSnap.

-spec last_written(state()) -> ra_idxterm().
last_written(#?MODULE{last_written_index_term = LWTI}) ->
    LWTI.

%% forces the last index and last written index back to a prior index
-spec set_last_index(ra_index(), state()) ->
    {ok, state()} | {not_found, state()}.
set_last_index(Idx, #?MODULE{cfg = Cfg,
                             range = Range,
                             snapshot_state = SnapState,
                             mem_table = Mt0,
                             last_written_index_term = {LWIdx0, _}} = State0) ->
    Cur = ra_snapshot:current(SnapState),
    %% After set_last_index, recovery depends on the snapshot state.
    %% If Idx matches the snapshot index, we can recover from there.
    case fetch_term(Idx, State0) of
        {undefined, State} when element(1, Cur) =/= Idx ->
            %% not found and Idx isn't equal to latest snapshot index
            {not_found, State};
        {_, State} when element(1, Cur) =:= Idx ->
            {_, SnapTerm} = Cur,
            %% Idx is equal to the current snapshot
            {ok, Mt} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                                       Cfg#cfg.uid, Mt0),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_WRITTEN_INDEX, Idx),
            {ok, State#?MODULE{range = ra_range:limit(Idx + 1, Range),
                               last_term = SnapTerm,
                               mem_table = Mt,
                               last_written_index_term = Cur}};
        {Term, State1} ->
            LWIdx = min(Idx, LWIdx0),
            {LWTerm, State2} = case Cur of
                                   {LWIdx, SnapTerm} ->
                                       {SnapTerm, State1};
                                   _ ->
                                       fetch_term(LWIdx, State1)
                               end,

            %% this should always be found but still assert just in case
            %% _if_ this ends up as a genuine reversal next time we try
            %% to write to the mem table it will detect this and open
            %% a new one
            true = LWTerm =/= undefined,
            {ok, Mt} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                                       Cfg#cfg.uid, Mt0),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_WRITTEN_INDEX, LWIdx),
            {ok, State2#?MODULE{range = ra_range:limit(Idx + 1, Range),
                                last_term = Term,
                                mem_table = Mt,
                                last_written_index_term = {LWIdx, LWTerm}}}
    end.

-spec handle_event(event_body(), state()) ->
    {state(), [effect()]}.
handle_event({written, Term, WrittenSeq},
             #?MODULE{cfg = Cfg,
                      last_written_index_term = {PrevIdx, _},
                      snapshot_state = SnapState,
                      pending = Pend0} = State0) ->
    CurSnap = ra_snapshot:current(SnapState),
    %% gap detection
    %% 1. pending has lower indexes than the ra_seq:first index in WrittenSeq
    %% 2.
    LastWrittenIdx = ra_seq:last(WrittenSeq),
    case fetch_term(LastWrittenIdx, State0) of
        {Term, State} when is_integer(Term) ->
            ok = put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_WRITTEN_INDEX,
                             LastWrittenIdx),

            case ra_seq:remove_prefix(WrittenSeq, Pend0) of
                {ok, Pend} ->
                    {State#?MODULE{last_written_index_term = {LastWrittenIdx, Term},
                                   pending = Pend}, []};
                {error, not_prefix} ->
                    ?DEBUG("~ts: ~w not prefix of ~w",
                           [Cfg#cfg.log_id, WrittenSeq, Pend0]),
                    {resend_pending(PrevIdx, State0), []}
            end;
        {undefined, State} when LastWrittenIdx =< element(1, CurSnap) ->
            % A snapshot happened before the written event came in
            % This can only happen on a leader when consensus is achieved by
            % followers returning appending the entry and the leader committing
            % and processing a snapshot before the written event comes in.
            %
            % At this point the items may still be in pending so need to
            % remove them
            {ok, Pend} = ra_seq:remove_prefix(WrittenSeq, Pend0),
            {State#?MODULE{pending = Pend}, []};
        {OtherTerm, State} when OtherTerm =/= Term ->
            %% term mismatch, let's reduce the seq and try again to see
            %% if any entries in the range are valid
            case ra_seq:limit(LastWrittenIdx - 1, WrittenSeq) of
                [] ->
                    ?DEBUG("~ts: written event did not find term ~b for index ~b "
                           "found ~w",
                           [Cfg#cfg.log_id, Term, LastWrittenIdx, OtherTerm]),
                    {State, []};
                NewWrittenSeq ->
                    %% retry with a reduced range
                    handle_event({written, Term, NewWrittenSeq}, State0)
            end
    end;
handle_event({segments, TidSeqs, NewSegs},
             #?MODULE{cfg = #cfg{uid = UId,
                                 log_id = LogId,
                                 directory = Dir,
                                 names = Names} = Cfg,
                      reader = Reader0,
                      pending = Pend0,
                      last_written_index_term = LWIT0,
                      mem_table = Mt0} = State0) ->
    {Reader1, OverwrittenSegRefs} =
        ra_log_segments:update_segments(NewSegs, Reader0),

    put_counter(Cfg, ?C_RA_SVR_METRIC_NUM_SEGMENTS,
                ra_log_segments:segment_ref_count(Reader1)),
    %% the tid ranges arrive in the reverse order they were written
    %% (new -> old) so we need to foldr here to process the oldest first
    Mt = lists:foldr(
           fun ({Tid, Seq}, Acc0) ->
                   {Spec, Acc} = ra_mt:record_flushed(Tid, Seq, Acc0),
                   ok = ra_log_ets:execute_delete(Names, UId, Spec),
                   Acc
           end, Mt0, TidSeqs),

    %% it is theoretically possible that the segment writer flush _could_
    %% over take WAL notifications
    FstPend = ra_seq:first(Pend0),
    MtRange = ra_mt:range(Mt),
    SegRange = ra_log_segments:range(Reader1),
    Pend = case MtRange of
               {Start, _End} when is_integer(FstPend) andalso
                                  Start > FstPend ->
                   %% set the first pending item to that of the first mem
                   %% table index
                   ra_seq:floor(Start, Pend0);
               {_, _} ->
                   Pend0;
               undefined ->
                   %% if there are no mem entries, all pending items must be
                   %% flushed
                   []
           end,

    %% if the last written index term is lower than the last segment index
    %% we need to update this.
    {LWIT, Reader} =
        case LWIT0 of
            {LWI, _} when is_tuple(SegRange) andalso
                          element(2, SegRange) > LWI ->
                {_, LastSegIdx} = SegRange,
                {LWTerm, Reader2} = ra_log_segments:fetch_term(LastSegIdx,
                                                               Reader1),
                {{LastSegIdx, LWTerm}, Reader2};
            _ ->
                {LWIT0, Reader1}
        end,
    ?DEBUG("~ts: ~b new segment(s) received - mem table range ~w"
           " segment range ~w",
           [LogId, length(NewSegs), MtRange, SegRange]),
    State = State0#?MODULE{reader = Reader,
                           pending = Pend,
                           last_written_index_term = LWIT,
                           mem_table = Mt},
    Fun = fun () ->
                  [begin
                    ?DEBUG("~ts: deleting overwritten segment ~w",
                           [LogId, SR]),
                    ?CATCH(prim_file:delete(filename:join(Dir, F))),
                    ok
                   end
                   || {F, _} = SR <- OverwrittenSegRefs],
                  ok
          end,
    {State, [{bg_work, Fun, fun (_Err) -> ok end}]};
handle_event({compaction_result, Result},
             #?MODULE{cfg = #cfg{log_id = _LogId},
                      current_snapshot = {CurSnapIdx, _},
                      live_indexes = LiveIndexes,
                      reader = Segments0} = State) ->
    % ?DEBUG("~ts: compaction result ~p", [LogId, Result]),
    Compaction = ra_log_segments:compaction(Segments0),
    {Segments1, Effs} = ra_log_segments:handle_compaction_result(Result,
                                                                 Segments0),
    case Compaction of
        {_Type, SnapIdx} when CurSnapIdx > SnapIdx ->
            %% snapshot has moved whilst compacting, need to perform another
            %% minor at least
            {Segments, Effs2} = ra_log_segments:schedule_compaction(minor,
                                                                    CurSnapIdx,
                                                                    LiveIndexes,
                                                                    Segments1),
            {State#?MODULE{reader = Segments}, Effs ++ Effs2};
        _ ->
            {State#?MODULE{reader = Segments1}, Effs}
    end;
handle_event(major_compaction, #?MODULE{cfg = #cfg{log_id = LogId},
                                        reader = Reader0,
                                        live_indexes = LiveIndexes,
                                        snapshot_state = SS} = State) ->
    case ra_snapshot:current(SS) of
        {SnapIdx, _} ->
            ?DEBUG("~ts: ra_log: major_compaction requested at snapshot index ~b, "
                   "~b live indexes",
                   [LogId, SnapIdx, ra_seq:length(LiveIndexes)]),
            {Reader, Effs} = ra_log_segments:schedule_compaction(major, SnapIdx,
                                                                 LiveIndexes, Reader0),
            {State#?MODULE{reader = Reader}, Effs};
        _ ->
            {State, []}
    end;
handle_event({snapshot_written, {SnapIdx, _} = Snap, LiveIndexes,
              SnapKind, SnapshotSize, Duration},
             #?MODULE{cfg = #cfg{uid = UId,
                                 log_id = LogId,
                                 names = Names} = Cfg,
                      range = {FstIdx, _} = Range,
                      mem_table = Mt0,
                      pending = Pend0,
                      last_written_index_term =
                          {LastWrittenIdx, _} = LWIdxTerm0,
                      snapshot_state = SnapState0} = State0)
%% only update snapshot if it is newer than the last snapshot
  when SnapIdx >= FstIdx ->
    % ?assert(ra_snapshot:pending(SnapState0) =/= undefined),
    SnapState1 = ra_snapshot:complete_snapshot(Snap, SnapKind, LiveIndexes,
                                               SnapshotSize, SnapState0),
    ?DEBUG("~ts: ra_log: ~s with ~b bytes written at index ~b with ~b live indexes in ~bms",
           [LogId, SnapKind, SnapshotSize, SnapIdx, ra_seq:length(LiveIndexes), Duration]),
    case SnapKind of
        snapshot ->
            put_counter(Cfg, ?C_RA_SVR_METRIC_SNAPSHOT_INDEX, SnapIdx),
            %% Delete old snapshot files. This is done as an effect
            %% so that if an old snapshot is still being replicated
            %% the cleanup can be delayed until it is safe.
            DeleteCurrentSnap = {delete_snapshot,
                                 ra_snapshot:directory(SnapState1, snapshot),
                                 ra_snapshot:current(SnapState0)},
            %% Also delete any checkpoints older than this snapshot.
            {SnapState, Checkpoints} =
                ra_snapshot:take_older_checkpoints(SnapIdx, SnapState1),
            CPEffects = [{delete_snapshot,
                          ra_snapshot:directory(SnapState, checkpoint),
                          Checkpoint} || Checkpoint <- Checkpoints],
            Effects0 = [DeleteCurrentSnap | CPEffects],

            LWIdxTerm =
                case LastWrittenIdx > SnapIdx of
                    true ->
                        LWIdxTerm0;
                    false ->
                        Snap
                end,

            %% remove all pending below smallest live index as the wal
            %% may not write them
            %% TODO: test that a written event can still be processed if it
            %% contains lower indexes than pending
            SmallestLiveIdx = case ra_seq:first(LiveIndexes) of
                                  undefined ->
                                      SnapIdx + 1;
                                  I ->
                                      I
                              end,
            %% TODO: optimise - ra_seq:floor/2 is O(n),
            Pend = ra_seq:floor(SmallestLiveIdx, Pend0),
            %% delete from mem table
            %% this will race with the segment writer but if the
            %% segwriter detects a missing index it will query the snaphost
            %% state and if that is higher it will resume flush
            {Spec, Mt1} = ra_mt:set_first(SmallestLiveIdx, Mt0),
            ok = exec_mem_table_delete(Names, UId, Spec),

            State = State0#?MODULE{range = ra_range:truncate(SnapIdx, Range),
                                   last_written_index_term = LWIdxTerm,
                                   mem_table = Mt1,
                                   pending = Pend,
                                   live_indexes = LiveIndexes,
                                   current_snapshot = Snap,
                                   snapshot_state = SnapState},
            {Reader, CompEffs} = ra_log_segments:schedule_compaction(minor, SnapIdx,
                                                                     LiveIndexes,
                                                                     State#?MODULE.reader),
            Effects = CompEffs ++ Effects0,
            {State#?MODULE{reader = Reader}, Effects};
        checkpoint ->
            put_counter(Cfg, ?C_RA_SVR_METRIC_CHECKPOINT_INDEX, SnapIdx),
            %% If we already have the maximum allowed number of checkpoints,
            %% remove some checkpoints to make space.
            {SnapState, CPs} = ra_snapshot:take_extra_checkpoints(SnapState1),
            Effects = [{delete_snapshot,
                        ra_snapshot:directory(SnapState, SnapKind),
                        CP} || CP <- CPs],
            {State0#?MODULE{snapshot_state = SnapState}, Effects}
    end;
handle_event({snapshot_written, {Idx, Term} = Snap, _Indexes,
              SnapKind, _SnapshotSize, _Duration},
             #?MODULE{cfg =#cfg{log_id = LogId},
                      snapshot_state = SnapState} = State0) ->
    %% if the snapshot/checkpoint is stale we just want to delete it
    Current = ra_snapshot:current(SnapState),
    ?INFO("~ts: old snapshot_written received for index ~b in term ~b
          current snapshot ~w, deleting old ~s",
           [LogId, Idx, Term, Current, SnapKind]),
    Effects = [{delete_snapshot,
                ra_snapshot:directory(SnapState, SnapKind),
                Snap}],
    {State0, Effects};
handle_event({snapshot_error, Snap, SnapKind, Error},
             #?MODULE{cfg =#cfg{log_id = LogId},
                      snapshot_state = SnapState0} = State0) ->
    ?INFO("~ts: snapshot error for ~w ~s ", [LogId, Snap, SnapKind]),
    SnapState = ra_snapshot:handle_error(Snap, Error, SnapState0),
    {State0#?MODULE{snapshot_state = SnapState}, []};
handle_event({resend_write, Idx},
             #?MODULE{cfg = #cfg{log_id = LogId}} = State) ->
    % resend missing entries from mem tables.
    ?INFO("~ts: ra_log: wal requested resend from ~b",
          [LogId, Idx]),
    {resend_from(Idx, State), []};
handle_event({down, _Pid, _Info}, #?MODULE{} = State) ->
    {State, []}.

-spec next_index(state()) -> ra_index().
next_index(#?MODULE{tx = {true, {_, Last}}}) ->
    Last + 1;
next_index(#?MODULE{range = {_, LastIdx}}) ->
    LastIdx + 1;
next_index(#?MODULE{current_snapshot = {SnapIdx, _}}) ->
    SnapIdx + 1;
next_index(#?MODULE{current_snapshot = undefined}) ->
    0.

-spec fetch(ra_index(), state()) ->
    {option(log_entry()), state()}.
fetch(Idx, State0) ->
    case sparse_read([Idx], State0) of
        {[Entry], State} ->
            {Entry, State};
        {[], State} ->
            {undefined, State}
    end.

-spec fetch_term(ra_index(), state()) ->
    {option(ra_term()), state()}.
fetch_term(Idx, #?MODULE{mem_table = Mt,
                         range = Range,
                         reader = Reader0} = State0)
  when ?IS_IN_RANGE(Idx, Range) ->
    case ra_mt:lookup_term(Idx, Mt) of
        undefined ->
            {Term, Reader} = ra_log_segments:fetch_term(Idx, Reader0),
            {Term, State0#?MODULE{reader = Reader}};
        Term when is_integer(Term) ->
            {Term, State0}
    end;
fetch_term(_Idx, #?MODULE{} = State0) ->
    {undefined, State0}.

-spec snapshot_state(State :: state()) -> ra_snapshot:state().
snapshot_state(State) ->
    State#?MODULE.?FUNCTION_NAME.

-spec snapshot_size(State :: state()) -> non_neg_integer() | undefined.
snapshot_size(#?MODULE{snapshot_state = SnapshotState}) ->
    ra_snapshot:snapshot_size(SnapshotState).

-spec set_snapshot_state(ra_snapshot:state(), state()) -> state().
set_snapshot_state(SnapState, State) ->
    State#?MODULE{snapshot_state = SnapState}.

-spec install_snapshot(ra_idxterm(), module(), ra_seq:state(), state()) ->
    {ok, state(), effects()}.
install_snapshot({SnapIdx, SnapTerm} = IdxTerm, MacMod, LiveIndexes,
                 #?MODULE{cfg = #cfg{uid = UId,
                                     names = Names} = Cfg,
                          snapshot_state = SnapState0,
                          pending = Pend0,
                          mem_table = Mt0} = State0)
  when is_atom(MacMod) ->
    ok = incr_counter(Cfg, ?C_RA_LOG_SNAPSHOTS_INSTALLED, 1),
    ok = put_counter(Cfg, ?C_RA_SVR_METRIC_SNAPSHOT_INDEX, SnapIdx),
    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, SnapIdx),
    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_WRITTEN_INDEX, SnapIdx),

    {SnapState, Checkpoints} =
        ra_snapshot:take_older_checkpoints(SnapIdx, SnapState0),
    CPEffects = [{delete_snapshot,
                  ra_snapshot:directory(SnapState, checkpoint),
                  Checkpoint} || Checkpoint <- Checkpoints],
    SmallestLiveIndex = case ra_seq:first(LiveIndexes) of
                            undefined ->
                                SnapIdx + 1;
                            I ->
                                I
                        end,
    %% TODO: more mt entries could potentially be cleared up in the
    %% mem table here if we walked the live indexes
    {Spec, Mt1} = ra_mt:set_first(SmallestLiveIndex, Mt0),
    ok = exec_mem_table_delete(Names, UId, Spec),
    Pend = ra_seq:floor(SmallestLiveIndex, Pend0),
    %% always create a new mem table here as we could have written
    %% sparese entries in the snapshot install
    %% TODO: check an empty mt doesn't leak
    {ok, Mt} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                               Cfg#cfg.uid, Mt1),
    State = State0#?MODULE{snapshot_state = SnapState,
                           current_snapshot = IdxTerm,
                           range = undefined,
                           last_term = SnapTerm,
                           live_indexes = LiveIndexes,
                           mem_table = Mt,
                           pending = Pend,
                           last_written_index_term = IdxTerm},
    {Reader, CompEffs} = ra_log_segments:schedule_compaction(minor, SnapIdx,
                                                             LiveIndexes,
                                                             State#?MODULE.reader),
    {ok, State#?MODULE{reader = Reader}, CompEffs ++ CPEffects}.


-spec recover_snapshot(State :: state()) ->
    option({ra_snapshot:meta(), term()}).
recover_snapshot(#?MODULE{snapshot_state = SnapState}) ->
    case ra_snapshot:recover(SnapState) of
        {ok, Meta, MacState} ->
            {Meta, MacState};
        {error, no_current_snapshot} ->
            undefined;
        {error, Reason} ->
            %% Other errors during recovery - log and return undefined
            %% This allows the system to start fresh if snapshots are corrupted
            ?WARN("ra_log: snapshot recovery failed: ~p", [Reason]),
            undefined
    end.

-spec snapshot_index_term(State :: state()) -> option(ra_idxterm()).
snapshot_index_term(#?MODULE{snapshot_state = SS}) ->
    ra_snapshot:current(SS).

-spec update_release_cursor(Idx :: ra_index(),
                            Cluster :: ra_cluster(),
                            MacCtx :: {MacVer :: ra_machine:version(), module()},
                            MacState :: term(), State :: state()) ->
    {state(), effects()}.
update_release_cursor(Idx, Cluster, {MacVersion, MacModule} = MacCtx,
                      MacState, State)
  when is_atom(MacModule) andalso
       is_integer(MacVersion) ->
    suggest_snapshot(snapshot, Idx, Cluster, MacCtx, MacState, State).

-spec checkpoint(Idx :: ra_index(), Cluster :: ra_cluster(),
                 MacCtx :: {MacVer :: ra_machine:version(), module()},
                 MacState :: term(), State :: state()) ->
    {state(), effects()}.
checkpoint(Idx, Cluster, {MacVersion, MacModule} = MacCtx, MacState, State)
  when is_atom(MacModule) andalso
       is_integer(MacVersion) ->
    suggest_snapshot(checkpoint, Idx, Cluster, MacCtx, MacState, State).

suggest_snapshot(SnapKind, Idx, Cluster, MacCtx, MacState,
                 #?MODULE{snapshot_state = SnapshotState} = State) ->
    case ra_snapshot:pending(SnapshotState) of
        undefined ->
            suggest_snapshot0(SnapKind, Idx, Cluster, MacCtx, MacState, State);
        _ ->
            %% Only one snapshot or checkpoint may be written at a time to
            %% prevent excessive I/O usage.
            {State, []}
    end.

promote_checkpoint(Idx, #?MODULE{cfg = Cfg,
                                 snapshot_state = SnapState0} = State) ->
    case ra_snapshot:pending(SnapState0) of
        {_IdxTerm, snapshot} ->
            %% If we're currently writing a snapshot, skip promoting a
            %% checkpoint.
            {State, []};
        _ ->
            {WasPromoted, SnapState, Effects} =
                ra_snapshot:promote_checkpoint(Idx, SnapState0),
            if WasPromoted ->
                   ok = incr_counter(Cfg, ?C_RA_LOG_CHECKPOINTS_PROMOTED, 1);
               true ->
                   ok
            end,

            {State#?MODULE{snapshot_state = SnapState}, Effects}
    end.

-spec tick(Now :: integer(), state()) -> state().
tick(Now, #?MODULE{cfg = #cfg{wal = Wal},
                   mem_table = Mt,
                   last_written_index_term = {LastWrittenIdx, _},
                   last_wal_write = {WalPid, Ms, _}} = State) ->
    CurWalPid = whereis(Wal),
    MtRange = ra_mt:range(Mt),
    case Now > Ms + ?WAL_RESEND_TIMEOUT andalso
         is_pid(CurWalPid) andalso
         CurWalPid =/= WalPid andalso
         ra_range:in(LastWrittenIdx + 1, MtRange)
    of
        true ->
            %% the wal has restarted, it has been at least 5s and there are
            %% cached items, resend them
            resend_from(LastWrittenIdx + 1, State);
        false ->
            State
    end.

assert(#?MODULE{cfg = #cfg{log_id = LogId},
                range = Range,
                snapshot_state = SnapState,
                current_snapshot = CurrSnap,
                live_indexes = LiveIndexes
               } = State) ->
    %% These assertions verify log state consistency during recovery.
    %% Consider removing once log recovery is stable and well-tested.
    ?DEBUG("~ts: ra_log: asserting Range ~w Snapshot ~w",
           [LogId, Range, CurrSnap]),
    %% perform assertions to ensure log state is correct
    ?assert(CurrSnap =:= ra_snapshot:current(SnapState)),
    ?assert(Range == undefined orelse
            CurrSnap == undefined orelse
            element(1, Range) - 1 == element(1, CurrSnap)),
    ?assert(CurrSnap == undefined orelse
            LiveIndexes == [] orelse
            ra_seq:last(LiveIndexes) =< element(1, CurrSnap)),
    State.

suggest_snapshot0(SnapKind, Idx, Cluster, {MachineVersion, MacModule},
                  MacState, State0) ->
    case should_snapshot(SnapKind, Idx, State0) of
        true ->
            % TODO: here we use the current cluster configuration in
            % the snapshot,
            % _not_ the configuration at the snapshot point.
            % Given cluster changes
            % are applied as they are received I cannot think of any scenarios
            % where this can cause a problem. That said there may
            % well be :dragons: here.
            % The MacState is a reference to the state at
            % the release_cursor point.
            % It can be some dehydrated form of the state itself
            % or a reference for external storage (e.g. ETS table)
            case fetch_term(Idx, State0) of
                {undefined, _} ->
                    {State0, []};
                {Term, State} ->
                    ClusterServerIds =
                        maps:map(fun (_, V) ->
                                         maps:with([voter_status], V)
                                 end, Cluster),
                    Meta = #{index => Idx,
                             term => Term,
                             cluster => ClusterServerIds,
                             machine_version => MachineVersion},
                    write_snapshot(Meta, MacModule, MacState,
                                   SnapKind, State)
            end;
        false ->
            {State0, []}
    end.

should_snapshot(snapshot, Idx,
                #?MODULE{cfg = #cfg{min_snapshot_interval = SnapInter},
                         reader = Reader,
                         snapshot_state = SnapState}) ->
    SnapLimit = case ra_snapshot:current(SnapState) of
                    undefined -> SnapInter;
                    {I, _} -> I + SnapInter
                end,
    % The release cursor index is the last entry _not_ contributing
    % to the current state. I.e. the last entry that can be discarded.
    % We should take a snapshot if the new snapshot index would allow us
    % to discard any segments or if the we've handled enough commands
    % since the last snapshot.
    CanFreeSegments = case ra_log_segments:range(Reader) of
                          undefined ->
                              false;
                          {Start, _End} ->
                              %% this isn't 100% guaranteed to free a segment
                              %% but there is a good chance
                              Idx > Start
                      end,
    CanFreeSegments orelse Idx > SnapLimit;
should_snapshot(checkpoint, Idx,
                #?MODULE{cfg = #cfg{min_checkpoint_interval = CheckpointInter},
                         snapshot_state = SnapState}) ->
    CheckpointLimit = case ra_snapshot:latest_checkpoint(SnapState) of
                          undefined -> CheckpointInter;
                          {I, _} -> I + CheckpointInter
                      end,
    Idx > CheckpointLimit.

-spec append_sync(Entry :: log_entry(), State :: state()) ->
    state() | no_return().
append_sync({Idx, Term, _} = Entry, Log0) ->
    Log = append(Entry, Log0),
    await_written_idx(Idx, Term, Log).

-spec write_sync(Entries :: [log_entry()], State :: state()) ->
    {ok, state()} |
    {error, {integrity_error, term()} | wal_down}.
write_sync(Entries, Log0) ->
    {Idx, Term, _} = lists:last(Entries),
    case ra_log:write(Entries, Log0) of
        {ok, Log} ->
            {ok, await_written_idx(Idx, Term, Log)};
        {error, _} = Err ->
            Err
    end.

-spec can_write(state()) -> boolean().
can_write(#?MODULE{cfg = #cfg{wal = Wal}}) ->
    undefined =/= whereis(Wal).

-spec exists(ra_idxterm(), state()) ->
    {boolean(), state()}.
exists({Idx, Term}, Log0) ->
    case fetch_term(Idx, Log0) of
        {Term, Log} when is_integer(Term) ->
            {true, Log};
        {_, Log} ->
            {false, Log}
    end.

-spec overview(state()) -> overview().
-spec has_pending(state()) -> boolean().
has_pending(#?MODULE{pending = []}) ->
    false;
has_pending(#?MODULE{}) ->
    true.

overview(#?MODULE{range = Range,
                  last_term = LastTerm,
                  last_written_index_term = LWIT,
                  snapshot_state = SnapshotState,
                  current_snapshot = CurrSnap,
                  reader = Reader,
                  last_wal_write = {_LastPid, LastMs, LastWalIdx},
                  mem_table = Mt,
                  pending = Pend
                 } = State) ->
    {LastIndex, _} = last_index_term(State),
    #{type => ?MODULE,
      range => Range,
      last_index => LastIndex,
      last_term => LastTerm,
      last_written_index_term => LWIT,
      num_segments => ra_log_segments:segment_ref_count(Reader),
      segments_range => ra_log_segments:range(Reader),
      open_segments => ra_log_segments:num_open_segments(Reader),
      snapshot_index => case CurrSnap of
                            undefined -> undefined;
                            {I, _} -> I
                        end,
      snapshot_term => case CurrSnap of
                           undefined -> undefined;
                           {_, T} -> T
                       end,
      latest_checkpoint_index =>
          case ra_snapshot:latest_checkpoint(SnapshotState) of
              undefined -> undefined;
              {I, _} -> I
          end,
      snapshot_size => ra_snapshot:snapshot_size(SnapshotState),
      mem_table_range => ra_mt:range(Mt),
      mem_table_info => ra_mt:info(Mt),
      last_wal_write => LastMs,
      last_wal_index => LastWalIdx,
      num_pending => ra_seq:length(Pend)
     }.

-spec write_config(ra_server:config(), state()) -> ok.
write_config(Config0, #?MODULE{cfg = #cfg{directory = Dir}}) ->
    ConfigPath = filename:join(Dir, <<"config">>),
    TmpConfigPath = filename:join(Dir, <<"config.tmp">>),
    % clean config of potentially unserialisable data
    Config = maps:without([parent,
                           counter,
                           has_changed,
                           %% don't write system config to disk as it will
                           %% be updated each time
                           system_config], Config0),
    ok = ra_lib:write_file(TmpConfigPath,
                           list_to_binary(io_lib:format("~p.", [Config]))),
    ok = prim_file:rename(TmpConfigPath, ConfigPath),
    %% ignore the result as not supported on windows
    _ = ra_lib:sync_dir(Dir),
    ok.

-spec read_config(state() | file:filename_all()) ->
    {ok, ra_server:config()} | {error, term()}.
read_config(#?MODULE{cfg = #cfg{directory = Dir}}) ->
    read_config(Dir);
read_config(Dir) ->
    ConfigPath = filename:join(Dir, <<"config">>),
    case ra_lib:consult(ConfigPath) of
        {ok, _} = Res ->
            Res;
        {error, {_, erl_parse, _}} ->
            {error, parsing};
        Err ->
            Err
    end.

-spec delete_everything(state()) -> ok.
delete_everything(#?MODULE{cfg = #cfg{uid = UId,
                                      names = Names,
                                      directory = Dir},
                           snapshot_state = _SnapState} = Log) ->
    _ = close(Log),
    %% if there is a snapshot process pending it could cause the directory
    %% deletion to fail, best kill the snapshot process first
    ok = ra_log_ets:delete_mem_tables(Names, UId),
    ?CATCH(ra_log_snapshot_state:delete(?ETSTBL, UId)),
    try ra_lib:recursive_delete(Dir) of
        ok -> ok
    catch
        _:_ = Err ->
            ?WARN("ra_log:delete_everything/1 failed to delete "
                  "directory ~ts. Error: ~p", [Dir, Err])
    end,
    ok.

-spec release_resources(non_neg_integer(),
                        sequential | random, state()) -> state().
release_resources(MaxOpenSegments, AccessPattern,
                  #?MODULE{cfg = #cfg{},
                           reader = Reader} = State) ->
    State#?MODULE{reader = ra_log_segments:update_conf(MaxOpenSegments,
                                                       AccessPattern, Reader)}.

%%% Local functions


%% only used by resend to wal functionality and doesn't update the mem table
wal_rewrite(#?MODULE{cfg = #cfg{uid = UId,
                                wal = Wal} = Cfg,
                     last_wal_write = {_, _, _}} = State,
            Tid, PrevIdx, {Idx, Term, Cmd}) ->
    case ra_log_wal:write(Wal, {UId, self()}, Tid,
                          PrevIdx, Idx, Term, Cmd) of
        {ok, Pid} ->
            ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, 1),
            put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, Idx),
            State#?MODULE{last_term = Term,
                          last_wal_write = {Pid, now_ms(), Idx}
                         };
        {error, wal_down} ->
            error(wal_down)
    end.

wal_write_batch(#?MODULE{cfg = #cfg{uid = UId,
                                    wal = Wal} = Cfg,
                         pending = Pend0,
                         range = Range,
                         mem_table = Mt0} = State,
                [{FstIdx, _, _} | _] = Entries) ->
    WriterId = {UId, self()},
    PrevIdx = previous_wal_index(State),
    %% all entries in a transaction are written to the same tid
    Tid = ra_mt:tid(Mt0),
    {WalCommands, Num, LastIdx, Pend} =
        lists:foldl(fun ({Idx, Term, Cmd0}, {WC, N, Prev, P}) ->
                            Cmd = {ttb, term_to_iovec(Cmd0)},
                            WalC = {append, WriterId, Tid, Prev, Idx, Term, Cmd},
                            {[WalC | WC], N+1, Idx, ra_seq:append(Idx, P)}
                    end, {[], 0, PrevIdx, Pend0}, Entries),

    [{_, _, _, _PrevIdx, LastIdx, LastTerm, _} | _] = WalCommands,
    {_, Mt} = ra_mt:commit(Mt0),
    put_counter(Cfg, ?C_RA_SVR_METRIC_LAST_INDEX, LastIdx),
    ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_OPS, Num),
    NewRange = case Range of
                   undefined ->
                       ra_range:new(FstIdx, LastIdx);
                   {Start, _} ->
                       ra_range:new(Start, LastIdx)
               end,
    case ra_log_wal:write_batch(Wal, lists:reverse(WalCommands)) of
        {ok, Pid} ->
            {ok, State#?MODULE{range = NewRange,
                               last_term = LastTerm,
                               last_wal_write = {Pid, now_ms(), LastIdx},
                               mem_table = Mt,
                               pending = Pend}};
        {error, wal_down} = Err ->
            %% if we get there the entry has already been inserted
            %% into the mem table but never reached the wal
            %% the resend logic will take care of that
            Err
    end.

maybe_append_first_entry(#?MODULE{range = undefined,
                                  current_snapshot = undefined} = State0) ->
    State1 = append({0, 0, undefined}, State0),
    receive
        {ra_log_event, {written, 0, [0]} = Evt} ->
            State2 = State1#?MODULE{range = ra_range:new(0)},
            {State, _Effs} = handle_event(Evt, State2),
            State
    after 60000 ->
              exit({?FUNCTION_NAME, timeout})
    end;
maybe_append_first_entry(State) ->
    State.

resend_from(Idx, #?MODULE{cfg = #cfg{uid = UId}} = State0) ->
    %% the wal will always request the next sequential
    %% index before the last one it got
    try resend_from0(Idx, Idx - 1, State0) of
        State -> State
    catch
        error:wal_down ->
            ?WARN("~ts: ra_log: resending from ~b failed with wal_down",
                  [UId, Idx]),
            State0
    end.

resend_pending(_, #?MODULE{pending = []} = State) ->
    State;
resend_pending(PrevIdx, #?MODULE{pending = Pend} = State) ->
    resend_from0(ra_seq:first(Pend), PrevIdx, State).

resend_from0(Idx, PrevIdx,
             #?MODULE{cfg = Cfg,
                      last_resend_time = undefined,
                      pending = Pend0,
                      mem_table = Mt} = State0) ->
    ?DEBUG("~ts: ra_log: resending pending range ~w from ~b prev idx ~b",
           [State0#?MODULE.cfg#cfg.log_id, ra_seq:range(Pend0), Idx, PrevIdx]),
    Pend = ra_seq:floor(Idx, Pend0),
    ok = incr_counter(Cfg, ?C_RA_LOG_WRITE_RESENDS, ra_seq:length(Pend)),
    State1 = State0#?MODULE{last_resend_time = {erlang:system_time(seconds),
                                                whereis(Cfg#cfg.wal)},
                            pending = Pend},
    {_, State} = ra_seq:fold(fun (I, {P, Acc}) ->
                                     {I, T, C} = ra_mt:lookup(I, Mt),
                                     Tid = ra_mt:tid_for(I, T, Mt),
                                     {I, wal_rewrite(Acc, Tid, P, {I, T, C})}
                             end,
                             {PrevIdx, State1},
                             Pend),
    State;
resend_from0(Idx, _PrevIdx,
             #?MODULE{last_resend_time = {LastResend, WalPid},
                      cfg = #cfg{resend_window_seconds = ResendWindow}} = State) ->
    case erlang:system_time(seconds) > LastResend + ResendWindow orelse
         (is_pid(WalPid) andalso not is_process_alive(WalPid)) of
        true ->
            % it has been more than the resend window since last resend
            % _or_ the wal has been restarted since then
            % ok to try again
            resend_from(Idx, State#?MODULE{last_resend_time = undefined});
        false ->
            State
    end.

stage_entries(Cfg, [Entry | Rem] = Entries, Mt0) ->
    case ra_mt:stage(Entry, Mt0) of
        {ok, Mt} ->
            stage_entries0(Cfg, Rem, Mt);
        {error, OverwritingOrLimitReached} ->
            ?DEBUG("~ts: mem table ~s detected whilst staging entries, opening new mem table",
                   [Cfg#cfg.log_id, OverwritingOrLimitReached]),
            %% TODO: do we need error handling here - this function uses the infinity
            %% timeout
            {ok, Mt} = ra_log_ets:new_mem_table_please(Cfg#cfg.names,
                                                       Cfg#cfg.uid, Mt0),
            stage_entries(Cfg, Entries, Mt)
    end.

stage_entries0(_Cfg, [], Mt) ->
    {ok, Mt};
stage_entries0(Cfg, [Entry | Rem], Mt0) ->
    case ra_mt:stage(Entry, Mt0) of
        {ok, Mt} ->
            stage_entries0(Cfg, Rem, Mt);
        {error, overwriting} ->
            Range = ra_mt:range(Mt0),
            Msg = io_lib:format("ra_log:stage_entries/3 "
                                "tried writing ~p - mem table range ~w",
                                [Rem, Range]),
            {error, {integrity_error, lists:flatten(Msg)}}
    end.



write_snapshot(Meta, MacModule, MacState, SnapKind,
               #?MODULE{cfg = Cfg,
                        snapshot_state = SnapState0} = State) ->
    Counter = case SnapKind of
                  snapshot -> ?C_RA_LOG_SNAPSHOTS_WRITTEN;
                  checkpoint -> ?C_RA_LOG_CHECKPOINTS_WRITTEN
              end,
    ok = incr_counter(Cfg, Counter, 1),
    {SnapState, Effects} = ra_snapshot:begin_snapshot(Meta, MacModule, MacState,
                                                      SnapKind, SnapState0),
    {State#?MODULE{snapshot_state = SnapState}, Effects}.

my_segrefs(UId, SegWriter) ->
    SegFiles = ra_log_segment_writer:my_segments(SegWriter, UId),
    lists:foldl(fun (File, Acc) ->
                        %% if a server recovered when a segment had been opened
                        %% but never had any entries written the segref would be
                        %% undefined
                        case ra_log_segment:info(File) of
                            #{ref := SegRef,
                              file_type := regular}
                              when is_tuple(SegRef) ->
                                [SegRef | Acc];
                            _ ->
                                Acc
                        end
                end, [], SegFiles).

%% TODO: implement synchronous writes using gen_batch_server:call/3
await_written_idx(Idx, Term, Log0) ->
    receive
        {ra_log_event, {written, Term, _Seq} = Evt} ->
            {Log, _} = handle_event(Evt, Log0),
            case last_written(Log) of
                {Idx, Term} ->
                    Log;
                _ ->
                    await_written_idx(Idx, Term, Log)
            end
    after ?LOG_APPEND_TIMEOUT ->
              throw(ra_log_append_timeout)
    end.

incr_counter(#cfg{counter = Cnt}, Ix, N) when Cnt =/= undefined ->
    counters:add(Cnt, Ix, N);
incr_counter(#cfg{counter = undefined}, _Ix, _N) ->
    ok.

put_counter(#cfg{counter = Cnt}, Ix, N) when Cnt =/= undefined ->
    counters:put(Cnt, Ix, N);
put_counter(#cfg{counter = undefined}, _Ix, _N) ->
    ok.

server_data_dir(Dir, UId) ->
    filename:join(Dir, UId).

maps_with_values(Keys, Map) ->
    lists:foldr(
      fun (K, Acc) ->
              case Map of
                  #{K := Value} ->
                      [Value | Acc];
                  _ ->
                      Acc
              end
      end, [], Keys).

now_ms() ->
    erlang:system_time(millisecond).

exec_mem_table_delete(#{} = Names, UId, Specs)
  when is_list(Specs) ->
    [ra_log_ets:execute_delete(Names, UId, Spec)
     || Spec <- Specs],
    ok.
