%% 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(fluxer_gateway_config).

-export([load/0, load_from/1]).

-type config() :: map().
-type log_level() :: debug | info | notice | warning | error | critical | alert | emergency.

-spec load() -> config().
load() ->
    case os:getenv("FLUXER_CONFIG") of
        false -> erlang:error({missing_env, "FLUXER_CONFIG"});
        "" -> erlang:error({missing_env, "FLUXER_CONFIG"});
        Path -> load_from(Path)
    end.

-spec load_from(string()) -> config().
load_from(Path) when is_list(Path) ->
    case file:read_file(Path) of
        {ok, Content} ->
            Json = json:decode(Content),
            build_config(Json);
        {error, Reason} ->
            erlang:error({json_read_failed, Path, Reason})
    end.

-spec build_config(map()) -> config().
build_config(Json) ->
    Service = get_map(Json, [<<"services">>, <<"gateway">>]),
    Gateway = get_map(Json, [<<"gateway">>]),
    Telemetry = get_map(Json, [<<"telemetry">>]),
    Sentry = get_map(Json, [<<"sentry">>]),
    Vapid = get_map(Json, [<<"auth">>, <<"vapid">>]),
    #{
        port => get_int(Service, <<"port">>, 8080),
        rpc_tcp_port => get_int(Service, <<"rpc_tcp_port">>, 8772),
        api_host => get_env_or_string("FLUXER_GATEWAY_API_HOST", Service, <<"api_host">>, "api"),
        api_canary_host => get_optional_string(Service, <<"api_canary_host">>),
        admin_reload_secret => get_optional_binary(Service, <<"admin_reload_secret">>),
        rpc_secret_key => get_binary(Gateway, <<"rpc_secret">>, undefined),
        identify_rate_limit_enabled => get_bool(Service, <<"identify_rate_limit_enabled">>, false),
        push_enabled => get_bool(Service, <<"push_enabled">>, true),
        push_user_guild_settings_cache_mb => get_int(
            Service,
            <<"push_user_guild_settings_cache_mb">>,
            1024
        ),
        push_subscriptions_cache_mb => get_int(Service, <<"push_subscriptions_cache_mb">>, 1024),
        push_blocked_ids_cache_mb => get_int(Service, <<"push_blocked_ids_cache_mb">>, 1024),
        presence_cache_shards => get_optional_int(Service, <<"presence_cache_shards">>),
        presence_bus_shards => get_optional_int(Service, <<"presence_bus_shards">>),
        presence_shards => get_optional_int(Service, <<"presence_shards">>),
        guild_shards => get_optional_int(Service, <<"guild_shards">>),
        session_shards => get_optional_int(Service, <<"session_shards">>),
        push_badge_counts_cache_mb => get_int(Service, <<"push_badge_counts_cache_mb">>, 256),
        push_badge_counts_cache_ttl_seconds =>
            get_int(Service, <<"push_badge_counts_cache_ttl_seconds">>, 60),
        push_dispatcher_max_inflight => get_int(Service, <<"push_dispatcher_max_inflight">>, 16),
        push_dispatcher_max_queue => get_int(Service, <<"push_dispatcher_max_queue">>, 2048),
        gateway_http_rpc_connect_timeout_ms =>
            get_int(Service, <<"gateway_http_rpc_connect_timeout_ms">>, 5000),
        gateway_http_rpc_recv_timeout_ms =>
            get_int(Service, <<"gateway_http_rpc_recv_timeout_ms">>, 30000),
        gateway_http_push_connect_timeout_ms =>
            get_int(Service, <<"gateway_http_push_connect_timeout_ms">>, 3000),
        gateway_http_push_recv_timeout_ms =>
            get_int(Service, <<"gateway_http_push_recv_timeout_ms">>, 5000),
        gateway_http_rpc_max_concurrency =>
            get_int(Service, <<"gateway_http_rpc_max_concurrency">>, 512),
        gateway_rpc_tcp_max_input_buffer_bytes =>
            get_int(Service, <<"gateway_rpc_tcp_max_input_buffer_bytes">>, 2097152),
        gateway_http_push_max_concurrency =>
            get_int(Service, <<"gateway_http_push_max_concurrency">>, 256),
        gateway_http_failure_threshold =>
            get_int(Service, <<"gateway_http_failure_threshold">>, 6),
        gateway_http_recovery_timeout_ms =>
            get_int(Service, <<"gateway_http_recovery_timeout_ms">>, 15000),
        gateway_http_cleanup_interval_ms =>
            get_int(Service, <<"gateway_http_cleanup_interval_ms">>, 30000),
        gateway_http_cleanup_max_age_ms =>
            get_int(Service, <<"gateway_http_cleanup_max_age_ms">>, 300000),
        media_proxy_endpoint => get_optional_binary(Service, <<"media_proxy_endpoint">>),
        vapid_email => get_binary(Vapid, <<"email">>, <<>>),
        vapid_public_key => get_optional_binary(Vapid, <<"public_key">>),
        vapid_private_key => get_optional_binary(Vapid, <<"private_key">>),
        gateway_metrics_enabled => get_optional_bool(Service, <<"gateway_metrics_enabled">>),
        gateway_metrics_report_interval_ms =>
            get_optional_int(Service, <<"gateway_metrics_report_interval_ms">>),
        release_node => get_string(Service, <<"release_node">>, "fluxer_gateway@127.0.0.1"),
        logger_level => get_log_level(Service, <<"logger_level">>, info),
        telemetry => #{
            enabled => get_bool(Telemetry, <<"enabled">>, true),
            otlp_endpoint => get_string(Telemetry, <<"otlp_endpoint">>, ""),
            api_key => get_string(Telemetry, <<"api_key">>, ""),
            service_name => get_string(Telemetry, <<"service_name">>, "fluxer-gateway"),
            environment => get_string(Telemetry, <<"environment">>, "development"),
            trace_sampling_ratio => get_float(Telemetry, <<"trace_sampling_ratio">>, 1.0)
        },
        sentry => #{
            build_sha => get_string(Sentry, <<"build_sha">>, ""),
            release_channel => get_string(Sentry, <<"release_channel">>, "")
        }
    }.

-spec get_map(map(), [binary()]) -> map().
get_map(Map, Keys) ->
    case get_in(Map, Keys) of
        Value when is_map(Value) -> Value;
        _ -> #{}
    end.

-spec get_int(map(), binary(), integer()) -> integer().
get_int(Map, Key, Default) when is_integer(Default) ->
    to_int(get_value(Map, Key), Default).

-spec get_optional_int(map(), binary()) -> integer() | undefined.
get_optional_int(Map, Key) ->
    to_optional_int(get_value(Map, Key)).

-spec get_bool(map(), binary(), boolean()) -> boolean().
get_bool(Map, Key, Default) when is_boolean(Default) ->
    to_bool(get_value(Map, Key), Default).

-spec get_optional_bool(map(), binary()) -> boolean() | undefined.
get_optional_bool(Map, Key) ->
    case get_value(Map, Key) of
        undefined -> undefined;
        Value -> to_bool(Value, undefined)
    end.

-spec get_string(map(), binary(), string()) -> string().
get_string(Map, Key, Default) when is_list(Default) ->
    to_string(get_value(Map, Key), Default).

-spec get_env_or_string(string(), map(), binary(), string()) -> string().
get_env_or_string(EnvVar, Map, Key, Default) when is_list(EnvVar), is_list(Default) ->
    case os:getenv(EnvVar) of
        false -> get_string(Map, Key, Default);
        "" -> get_string(Map, Key, Default);
        Value -> Value
    end.

-spec get_optional_string(map(), binary()) -> string() | undefined.
get_optional_string(Map, Key) ->
    case get_value(Map, Key) of
        undefined ->
            undefined;
        Value ->
            Clean = string:trim(to_string(Value, "")),
            case Clean of
                "" -> undefined;
                _ -> Clean
            end
    end.

-spec get_binary(map(), binary(), binary() | undefined) -> binary() | undefined.
get_binary(Map, Key, Default) ->
    to_binary(get_value(Map, Key), Default).

-spec get_optional_binary(map(), binary()) -> binary() | undefined.
get_optional_binary(Map, Key) ->
    case get_value(Map, Key) of
        undefined -> undefined;
        Value -> to_binary(Value, undefined)
    end.

-spec get_log_level(map(), binary(), log_level()) -> log_level().
get_log_level(Map, Key, Default) when is_atom(Default) ->
    Value = get_value(Map, Key),
    case normalize_log_level(Value) of
        undefined -> Default;
        Level -> Level
    end.

-spec get_float(map(), binary(), number()) -> float().
get_float(Map, Key, Default) when is_number(Default) ->
    to_float(get_value(Map, Key), Default).

-spec get_in(term(), [binary()]) -> term().
get_in(Map, [Key | Rest]) when is_map(Map) ->
    case get_value(Map, Key) of
        undefined -> undefined;
        Value when Rest =:= [] -> Value;
        Value -> get_in(Value, Rest)
    end;
get_in(_, _) ->
    undefined.

-spec get_value(term(), binary()) -> term().
get_value(Map, Key) when is_map(Map) ->
    case maps:get(Key, Map, undefined) of
        undefined when is_binary(Key) ->
            maps:get(binary_to_list(Key), Map, undefined);
        Value ->
            Value
    end.

-spec to_int(term(), integer() | undefined) -> integer() | undefined.
to_int(Value, _Default) when is_integer(Value) ->
    Value;
to_int(Value, _Default) when is_float(Value) ->
    trunc(Value);
to_int(Value, Default) ->
    case to_string(Value, "") of
        "" ->
            Default;
        Str ->
            case string:to_integer(Str) of
                {Int, _} when is_integer(Int) -> Int;
                {error, _} -> Default
            end
    end.

-spec to_optional_int(term()) -> integer() | undefined.
to_optional_int(Value) ->
    case to_int(Value, undefined) of
        undefined -> undefined;
        Int -> Int
    end.

-spec to_bool(term(), boolean() | undefined) -> boolean() | undefined.
to_bool(Value, _Default) when is_boolean(Value) ->
    Value;
to_bool(Value, Default) when is_atom(Value) ->
    case Value of
        true -> true;
        false -> false;
        _ -> Default
    end;
to_bool(Value, Default) ->
    case string:lowercase(to_string(Value, "")) of
        "true" -> true;
        "1" -> true;
        "false" -> false;
        "0" -> false;
        _ -> Default
    end.

-spec to_string(term(), string()) -> string().
to_string(Value, Default) when is_list(Default) ->
    case Value of
        undefined -> Default;
        Bin when is_binary(Bin) -> binary_to_list(Bin);
        Str when is_list(Str) -> Str;
        Atom when is_atom(Atom) -> atom_to_list(Atom);
        _ -> Default
    end.

-spec to_binary(term(), binary() | undefined) -> binary() | undefined.
to_binary(Value, Default) ->
    case Value of
        undefined -> Default;
        Bin when is_binary(Bin) -> Bin;
        Str when is_list(Str) -> list_to_binary(Str);
        Atom when is_atom(Atom) -> list_to_binary(atom_to_list(Atom));
        _ -> Default
    end.

-spec to_float(term(), float()) -> float().
to_float(Value, _Default) when is_float(Value) ->
    Value;
to_float(Value, _Default) when is_integer(Value) ->
    float(Value);
to_float(Value, Default) ->
    case to_string(Value, "") of
        "" ->
            Default;
        Str ->
            case string:to_float(Str) of
                {Float, _} when is_float(Float) -> Float;
                {error, _} -> Default
            end
    end.

-spec normalize_log_level(term()) -> log_level() | undefined.
normalize_log_level(undefined) ->
    undefined;
normalize_log_level(Level) when is_atom(Level) ->
    normalize_log_level(atom_to_list(Level));
normalize_log_level(Level) when is_binary(Level) ->
    normalize_log_level(binary_to_list(Level));
normalize_log_level(Level) when is_list(Level) ->
    case string:lowercase(string:trim(Level)) of
        "debug" -> debug;
        "info" -> info;
        "notice" -> notice;
        "warning" -> warning;
        "error" -> error;
        "critical" -> critical;
        "alert" -> alert;
        "emergency" -> emergency;
        _ -> undefined
    end;
normalize_log_level(_) ->
    undefined.
