% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved.
%
% SPDX-License-Identifier: AGPL-3.0-only
%
% Author: Philipp Maier <pmaier@sysmocom.de> / sysmocom - s.f.m.c. GmbH

-module(mnesia_db_rest).
-include_lib("stdlib/include/qlc.hrl").
-include("mnesia_db_rest.hrl").
-include("mnesia_db_work.hrl").

% REST functions, to be called by the REST server (from outside via cowboy)
-export([list/1, lookup/2, create/3, delete/2]).

% debugging
-export([dump/0]).

% trigger recurring events (called automatically by timer from this module)
-export([timer_cleanup/0]).

% transaction functions (to be called from a transaction)
-export([trans_set_status/4]).

% helper function (to be called from a transaction) to set the status of an item in the rest table. This function
% is intended to be called from mnesia_db_work to report back the status of a specific work item.
trans_set_status(ResourceId, Status, Outcome, Debuginfo) ->
    Q = qlc:q([X || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]),
    Rows = qlc:e(Q),
    Timestamp = os:system_time(seconds),
    case Rows of
        [Row | []] ->
            mnesia:write(Row#rest{
                status = Status, timestamp = Timestamp, outcome = Outcome, debuginfo = Debuginfo
            }),
            ok;
        [] ->
            error;
        _ ->
            error
    end.

% Create REST resource (order)
create(Facility, EidValue, Order) ->
    % Ensure that an euicc entry is present in the database for the given EidValue. In case no entry exists yet, a new
    % entry is created with default parameters from sys.config, which is is sufficient in many usecases. From the
    % technical perspective, this is just a convenience feature, which frees the REST API user from having to create an
    % euicc entry manually before performming the first operation.
    ok = mnesia_db_euicc:create_if_not_exist(EidValue),

    % Continue with the normal creation of the REST resource.
    ResourceId = uuid:uuid_to_string(uuid:get_v4_urandom()),
    Timestamp = os:system_time(seconds),
    Row = #rest{
        resourceId = ResourceId,
        facility = Facility,
        eidValue = EidValue,
        order = Order,
        status = new,
        timestamp = Timestamp,
        outcome = [],
        debuginfo = none
    },
    Trans = fun() ->
        Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.resourceId == ResourceId]),
        Present = qlc:e(Q),
        case Present of
            [] ->
                mnesia:write(Row);
            _ ->
                error
        end
    end,
    {atomic, Result} = mnesia:transaction(Trans),
    {Result, ResourceId}.

% Lookup REST resource (order)
lookup(ResourceId, Facility) ->
    Trans = fun() ->
        Q = qlc:q([
            {
                X#rest.status,
                X#rest.timestamp,
                X#rest.eidValue,
                X#rest.order,
                X#rest.outcome,
                X#rest.debuginfo
            }
         || X <- mnesia:table(rest),
            X#rest.resourceId == ResourceId,
            X#rest.facility == Facility
        ]),
        qlc:e(Q)
    end,
    {atomic, Result} = mnesia:transaction(Trans),
    case Result of
        [{Status, Timestamp, EidValue, Order, Outcome, Debuginfo}] ->
            {Status, Timestamp, EidValue, Order, Outcome, Debuginfo};
        [] ->
            none;
        _ ->
            error
    end.

% Delete REST resource (order)
delete(ResourceId, Facility) ->
    Trans = fun() ->
        QRest = qlc:q([
            X#rest.resourceId
         || X <- mnesia:table(rest),
            X#rest.resourceId == ResourceId,
            X#rest.facility == Facility
        ]),
        RestPresent = qlc:e(QRest),
        case RestPresent of
            [] ->
                none;
            _ ->
                OidRest = {rest, ResourceId},
                ok = mnesia:delete(OidRest),

                % There may be an orphaned work item now, which we must also remove. This will also kill
                % the order in case it is currently in progress.
                QWork = qlc:q([
                    X#work.pid
                 || X <- mnesia:table(work), X#work.resourceId == ResourceId
                ]),
                WorkPresent = qlc:e(QWork),
                case WorkPresent of
                    [] ->
                        ok;
                    [Pid] ->
                        OidWork = {work, Pid},
                        mnesia:delete(OidWork);
                    _ ->
                        error
                end
        end
    end,
    {atomic, Result} = mnesia:transaction(Trans),
    Result.

% List the resource identifiers of currently present REST resources
list(Facility) ->
    Trans = fun() ->
        Q = qlc:q([X#rest.resourceId || X <- mnesia:table(rest), X#rest.facility == Facility]),
        qlc:e(Q)
    end,
    {atomic, Result} = mnesia:transaction(Trans),
    Result.

mark_stuck(Timeout) ->
    % There may be corner cases where the order gets stuck because the other entity suddenly stops responding. In
    % this case the order will stall. When it remains stalled for too long (minutes), we should remove all related
    % items from the work table and put an appropriate outcome (error code) into the rest table.

    TimestampNow = os:system_time(seconds),

    % Remove Resource from work table and set an appropriate status in the rest table
    HandleResource = fun(ResourceId) ->
        % find the pid of the work item that is stuck and then delete it
        Q = qlc:q([X#work.pid || X <- mnesia:table(work), X#work.resourceId == ResourceId]),
        WorkPresent = qlc:e(Q),
        case WorkPresent of
            [Pid] ->
                Oid = {work, Pid},
                ok = mnesia:delete(Oid);
            _ ->
                ok
        end,

        % set status in the rest table
        trans_set_status(ResourceId, done, [{[{procedureError, stuckOrder}]}], none)
    end,

    % Find all rest resources that stall in status "work" and older than the specified timeout value
    Trans = fun() ->
        Q = qlc:q([
            X#rest.resourceId
         || X <- mnesia:table(rest),
            X#rest.status == work,
            TimestampNow - X#rest.timestamp > Timeout
        ]),
        Rows = qlc:e(Q),
        case Rows of
            [] ->
                ok;
            Rows ->
                [HandleResource(Row) || Row <- Rows],
                ok
        end
    end,

    {atomic, Result} = mnesia:transaction(Trans),
    Result.

mark_noshow(Timeout) ->
    % There may be corner cases where the order is never processed because the related IPAd/eUICC never shows up to
    % fetch the related eUICC package. If we see an order staying in status "new" for too long (days, weeks), we should
    % mark it as "done" and put an appropriate outcome (error code) into the rest table.

    TimestampNow = os:system_time(seconds),

    % Remove Resource from work table and set an appropriate status in the rest table
    HandleResource = fun(ResourceId) ->
        trans_set_status(ResourceId, done, [{[{procedureError, noshowOrder}]}], none)
    end,

    % Find all rest resources that stall in status "work" and older than the specified timeout value
    Trans = fun() ->
        Q = qlc:q([
            X#rest.resourceId
         || X <- mnesia:table(rest),
            X#rest.status == new,
            TimestampNow - X#rest.timestamp > Timeout
        ]),
        Rows = qlc:e(Q),
        case Rows of
            [] ->
                ok;
            Rows ->
                [HandleResource(Row) || Row <- Rows],
                ok
        end
    end,

    {atomic, Result} = mnesia:transaction(Trans),
    Result.

delete_expired(Timeout) ->
    % There may be cases where orders stay unmaintained for too long. When an order stays in status "done" for too long
    % (hours, days), than this may mean that the REST API user lost interest. In this case the related items should be
    % removed from the rest table after a reasonable timeout.

    TimestampNow = os:system_time(seconds),

    % Remove Resource from the rest table. Since an abandonned order won't have a coresponding work item in the work
    % table we do not have to worry about creating an orphaned work item.
    HandleResource = fun(ResourceId) ->
        Oid = {rest, ResourceId},
        ok = mnesia:delete(Oid)
    end,

    % Find all rest resources that linger in the rest table for a long time and are not in the status "new"
    Trans = fun() ->
        Q = qlc:q([
            X#rest.resourceId
         || X <- mnesia:table(rest),
            X#rest.status == done,
            TimestampNow - X#rest.timestamp > Timeout
        ]),
        Rows = qlc:e(Q),
        case Rows of
            [] ->
                ok;
            Rows ->
                [HandleResource(Row) || Row <- Rows],
                ok
        end
    end,

    {atomic, Result} = mnesia:transaction(Trans),
    Result.

% Run a cleanup cycle the database
timer_cleanup() ->
    {ok, RestTimeoutStuck} = application:get_env(onomondo_eim, rest_timeout_stuck),
    {ok, RestTimeoutNoshow} = application:get_env(onomondo_eim, rest_timeout_noshow),
    {ok, RestTimeoutExpired} = application:get_env(onomondo_eim, rest_timeout_expired),
    RcStalled = mark_stuck(RestTimeoutStuck),
    RcNoshow = mark_noshow(RestTimeoutNoshow),
    RcExpired = delete_expired(RestTimeoutExpired),

    case {RcStalled, RcNoshow, RcExpired} of
        {ok, ok, ok} ->
            ok;
        _ ->
            logger:error("Cleanup: database error~n"),
            error
    end,

    % Next cleanup in 10 secs.
    {ok, _} = timer:apply_after(10000, mnesia_db_rest, timer_cleanup, []),
    ok.

% Dump all currently pending rest items (for debugging, to be called from console)
dump() ->
    Trans = fun() ->
        Q = qlc:q([X || X <- mnesia:table(rest)]),
        qlc:e(Q)
    end,
    {atomic, Result} = mnesia:transaction(Trans),
    Result.
