%% Copyright (C) 2026 Fluxer Contributors
%%
%% This file is part of Fluxer.
%%
%% Fluxer is free software: you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% Fluxer is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%% GNU Affero General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with Fluxer. If not, see <https://www.gnu.org/licenses/>.

-module(presence_manager_shard).
-behaviour(gen_server).

-include_lib("fluxer_gateway/include/timeout_config.hrl").

-define(PID_CACHE_TABLE, presence_pid_cache).
-define(CACHE_TTL_MS, 300000).

-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-type user_id() :: integer().
-type presence_ref() :: {pid(), reference()}.
-type status() :: online | offline | idle | dnd.
-type event_type() :: atom() | binary().

-type start_or_lookup_request() :: #{
    user_id := user_id(),
    user_data := map(),
    guild_ids := [integer()],
    status := status(),
    friend_ids := [user_id()],
    group_dm_recipients := map()
}.

-type state() :: #{presences := #{user_id() => presence_ref()}}.

-spec start_link(non_neg_integer()) -> {ok, pid()} | {error, term()}.
start_link(ShardIndex) ->
    gen_server:start_link(?MODULE, #{shard_index => ShardIndex}, []).

-spec init(map()) -> {ok, state()}.
init(_Args) ->
    process_flag(trap_exit, true),
    {ok, #{presences => #{}}}.

-spec handle_call(Request, From, State) -> Result when
    Request ::
        {lookup, user_id()}
        | {start_or_lookup, start_or_lookup_request()}
        | {dispatch, user_id(), event_type(), term()}
        | get_local_count
        | get_global_count
        | term(),
    From :: gen_server:from(),
    State :: state(),
    Result :: {reply, Reply, state()},
    Reply ::
        {ok, pid()}
        | {error, not_found}
        | {error, registration_failed}
        | {error, process_disappeared}
        | {error, term()}
        | {ok, non_neg_integer()}
        | ok.
handle_call({lookup, UserId}, _From, State) ->
    do_lookup(UserId, State);
handle_call({dispatch, UserId, Event, Data}, _From, State) ->
    case lookup_presence(UserId, State) of
        {ok, PresencePid, NewState} ->
            gen_server:cast(PresencePid, {dispatch, Event, Data}),
            {reply, ok, NewState};
        {error, not_found, NewState} ->
            {reply, {error, not_found}, NewState}
    end;
handle_call({start_or_lookup, Request}, _From, State) ->
    do_start_or_lookup(Request, State);
handle_call({terminate_all_sessions, UserId}, _From, State) ->
    case terminate_sessions_for_user(UserId, State) of
        {Result, NewState} ->
            {reply, Result, NewState}
    end;
handle_call(get_local_count, _From, State) ->
    Presences = maps:get(presences, State),
    Count = process_registry:get_count(Presences),
    {reply, {ok, Count}, State};
handle_call(get_global_count, _From, State) ->
    Presences = maps:get(presences, State),
    Count = process_registry:get_count(Presences),
    {reply, {ok, Count}, State};
handle_call(_Unknown, _From, State) ->
    {reply, ok, State}.

-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast(_Unknown, State) ->
    {noreply, State}.

-spec handle_info(Info, State) -> {noreply, state()} when
    Info :: {'DOWN', reference(), process, pid(), term()} | term(),
    State :: state().
handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) ->
    Presences = maps:get(presences, State),
    NewPresences = process_registry:cleanup_on_down(Pid, Presences),
    {noreply, State#{presences := NewPresences}};
handle_info(_Unknown, State) ->
    {noreply, State}.

-spec terminate(Reason, State) -> ok when
    Reason :: term(),
    State :: state().
terminate(_Reason, _State) ->
    ok.

-spec code_change(term(), term(), term()) -> {ok, state()}.
code_change(_OldVsn, State, _Extra) when is_map(State) ->
    {ok, State};
code_change(_OldVsn, {state, Presences}, _Extra) ->
    {ok, #{presences => Presences}}.

-spec do_lookup(user_id(), state()) -> {reply, {ok, pid()} | {error, not_found}, state()}.
do_lookup(UserId, State) ->
    case lookup_presence(UserId, State) of
        {ok, Pid, NewState} ->
            {reply, {ok, Pid}, NewState};
        {error, not_found, NewState} ->
            {reply, {error, not_found}, NewState}
    end.

-spec do_start_or_lookup(start_or_lookup_request(), state()) ->
    {reply, {ok, pid()} | {error, registration_failed | process_disappeared | term()}, state()}.
do_start_or_lookup(Request, State) ->
    Presences = maps:get(presences, State),
    #{
        user_id := UserId,
        user_data := UserData,
        guild_ids := GuildIds,
        status := Status
    } = Request,
    case maps:get(UserId, Presences, undefined) of
        {Pid, _Ref} ->
            {reply, {ok, Pid}, State};
        undefined ->
            PresenceName = process_registry:build_process_name(presence, UserId),
            case whereis(PresenceName) of
                undefined ->
                    FriendIds = maps:get(friend_ids, Request, []),
                    GroupDmRecipients = maps:get(group_dm_recipients, Request, #{}),
                    PresenceData = #{
                        user_id => UserId,
                        user_data => UserData,
                        guild_ids => GuildIds,
                        status => Status,
                        friend_ids => FriendIds,
                        group_dm_recipients => GroupDmRecipients,
                        custom_status => maps:get(custom_status, Request, null)
                    },
                    case presence:start_link(PresenceData) of
                        {ok, Pid} ->
                            case
                                process_registry:register_and_monitor(PresenceName, Pid, Presences)
                            of
                                {ok, RegisteredPid, Ref, NewPresences0} ->
                                    CleanPresences = maps:remove(PresenceName, NewPresences0),
                                    NewPresences = maps:put(
                                        UserId, {RegisteredPid, Ref}, CleanPresences
                                    ),
                                    update_cache(UserId, RegisteredPid),
                                    {reply, {ok, RegisteredPid}, State#{
                                        presences := NewPresences
                                    }};
                                {error, registration_race_condition} ->
                                    {reply, {error, registration_failed}, State};
                                {error, _Reason} = Error ->
                                    {reply, Error, State}
                            end;
                        Error ->
                            {reply, Error, State}
                    end;
                _ExistingPid ->
                    case process_registry:lookup_or_monitor(PresenceName, UserId, Presences) of
                        {ok, Pid, _Ref, NewPresences} ->
                            update_cache(UserId, Pid),
                            {reply, {ok, Pid}, State#{presences := NewPresences}};
                        {error, not_found} ->
                            {reply, {error, process_disappeared}, State}
                    end
            end
    end.

-spec lookup_presence(user_id(), state()) -> {ok, pid(), state()} | {error, not_found, state()}.
lookup_presence(UserId, State) ->
    Presences = maps:get(presences, State),
    case maps:get(UserId, Presences, undefined) of
        {Pid, _Ref} ->
            {ok, Pid, State};
        undefined ->
            PresenceName = process_registry:build_process_name(presence, UserId),
            case process_registry:lookup_or_monitor(PresenceName, UserId, Presences) of
                {ok, Pid, Ref, NewPresences0} ->
                    CleanPresences = maps:remove(PresenceName, NewPresences0),
                    FinalPresences = maps:put(UserId, {Pid, Ref}, CleanPresences),
                    update_cache(UserId, Pid),
                    {ok, Pid, State#{presences := FinalPresences}};
                {error, not_found} ->
                    {error, not_found, State}
            end
    end.

-spec terminate_sessions_for_user(user_id(), state()) -> {ok, state()}.
terminate_sessions_for_user(UserId, State) ->
    Presences = maps:get(presences, State),
    case maps:get(UserId, Presences, undefined) of
        {Pid, _Ref} ->
            gen_server:cast(Pid, {terminate_all_sessions}),
            {ok, State};
        undefined ->
            PresenceName = process_registry:build_process_name(presence, UserId),
            case process_registry:lookup_or_monitor(PresenceName, UserId, Presences) of
                {ok, Pid, Ref, NewPresences0} ->
                    CleanPresences = maps:remove(PresenceName, NewPresences0),
                    FinalPresences = maps:put(UserId, {Pid, Ref}, CleanPresences),
                    update_cache(UserId, Pid),
                    gen_server:cast(Pid, {terminate_all_sessions}),
                    {ok, State#{presences := FinalPresences}};
                {error, not_found} ->
                    {ok, State}
            end
    end.

-spec update_cache(user_id(), pid()) -> ok.
update_cache(UserId, Pid) ->
    Timestamp = erlang:monotonic_time(millisecond),
    try
        ets:insert(?PID_CACHE_TABLE, {UserId, Pid, Timestamp}),
        ok
    catch
        _:_ -> ok
    end.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

init_returns_empty_presences_test() ->
    {ok, State} = init(#{shard_index => 0}),
    ?assertEqual(#{}, maps:get(presences, State)).

update_cache_handles_missing_table_test() ->
    ?assertEqual(ok, update_cache(999, self())).
-endif.
