-module(projection).
-behaviour(gen_server).
-export([new/2, new/3, fold_activity/2, replay/2,
         name/1, state/1, fold_fn/1]).
-export([start_link/3, async_fold/2, query/1, stop/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).

%% Pure-functional projection driver per design §10.
%%
%% A projection is a property list:
%%   [{name, atom}, {state, term}, {fold, fun}]
%%
%% The fold function is `fun (Activity, State) -> NewState`. v1
%% uses Erlang funs as the fold body — the genesis bundle's SX
%% `:fold` bodies are stored as binaries; an SX-source eval
%% bridge will plug them into the same projection record once
%% it lands (Step 7d). For now, callers supply Erlang funs
%% directly when constructing a projection.
%%
%% `replay/2` is the cold-start primitive: fold an activity
%% list (e.g. `log:entries/1`) through the projection from its
%% initial state.

new(Name, InitialState) ->
    new(Name, InitialState, fun (_Activity, S) -> S end).

new(Name, InitialState, FoldFn) ->
    [{name, Name}, {state, InitialState}, {fold, FoldFn}].

fold_activity(Proj, Activity) ->
    Fn  = fold_fn(Proj),
    S0  = state(Proj),
    S1  = Fn(Activity, S0),
    set_field(state, S1, Proj).

replay(Proj, Activities) ->
    fold_each(Proj, Activities).

fold_each(Proj, []) -> Proj;
fold_each(Proj, [A | Rest]) ->
    fold_each(fold_activity(Proj, A), Rest).

%% Accessors

name(Proj)    -> field(name, Proj).
state(Proj)   -> field(state, Proj).
fold_fn(Proj) -> field(fold, Proj).

%% Internal

field(K, [{K, V} | _]) -> V;
field(K, [_ | Rest]) -> field(K, Rest);
field(_, []) -> erlang:error(badkey).

set_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
set_field(K, V, [P | Rest]) -> [P | set_field(K, V, Rest)];
set_field(K, V, []) -> [{K, V}].

%% ── Step 7b: gen_server wrapper ─────────────────────────────────
%%
%% Each projection runs in its own gen_server, registered under the
%% projection's Name atom. `async_fold/2` casts an activity into the
%% process; `query/1` synchronously fetches the current state.
%%
%% Port notes (mirroring Step 5b on the registry): `gen_server:start_link`
%% returns the raw Pid; `?MODULE` macro is unsupported; spawned
%% processes don't survive across separate `erlang-eval-ast` calls
%% so tests must inline start_link with their operations.

start_link(Name, InitialState, FoldFn) ->
    Pid = gen_server:start_link(projection, [Name, InitialState, FoldFn]),
    erlang:register(Name, Pid),
    Pid.

async_fold(Name, Activity) ->
    gen_server:cast(Name, {fold, Activity}).

query(Name) ->
    gen_server:call(Name, get_state).

stop(Name) ->
    R = gen_server:call(Name, '$gen_stop'),
    erlang:unregister(Name),
    R.

%% gen_server callbacks

init([Name, InitialState, FoldFn]) ->
    {ok, new(Name, InitialState, FoldFn)}.

handle_call(get_state, _From, Proj) ->
    {reply, state(Proj), Proj}.

handle_cast({fold, Activity}, Proj) ->
    {noreply, fold_activity(Proj, Activity)}.

handle_info(_, Proj) -> {noreply, Proj}.
