%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2000-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%

-module(httpd_poll).
-behaviour(gen_server).


%% External API
-export([start/0, start_appup/2, start/3,stop/0,verbosity/1,poll_time/1]).

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


-define(default_verbosity,error).
-define(default_poll_time,60000). %% 60 seconds


-record(state,{host = "", port = -1, ptime = -1, tref = none, uris = []}).


%% start/0
%%
%% Description: Start polling HTTPD with default values
%%
start() -> 
    Options = default_options(otp), 
    start("gandalf", 8000, Options).

start_appup(Host, Port) ->
    Options = default_options(top), 
    start(Host, Port, Options).

%% start/3
%%
%% Description: Start polling HTTPD
%%
%% Parameters:
%%              Host        = string()
%%                            Host name of HTTPD
%%              Port        = integer()
%%                            Port number of HTTPD
%%              Options     = [Option]
%%              Option      = {poll_time,integer()} | {verbosity,verbosity()} |
%%                            {log_file,string()}   | {uris,[uri()]}
%%              verbosity() = silence | error | log | debug | trace
%%              uri()       = {string(),string}
%%                            First part is a descriptive string and the second
%%                            part is the actual URI.
%%
start(Host,Port,Options) ->
    gen_server:start({local,httpd_tester},?MODULE,[Host,Port,Options],[]).
    
stop() ->
    gen_server:call(httpd_tester,stop).


default_options(UriDesc) ->
    Verbosity = {verbosity,?default_verbosity},
    Uris      = {uris,uris(UriDesc)},
    PollTime  = {poll_time,?default_poll_time},
    Logging   = {log_file,"httpd_poll.log"},
    [Verbosity, Uris, PollTime, Logging].


options(Options) ->
    options(Options, default_options(otp), []).

options([], Defaults, Options) ->
    Options ++ Defaults;
options([{Key, _Val} = Opt|Opts], Defaults, Options) ->
    options(Opts, lists:keydelete(Key, 1, Defaults), [Opt | Options]).


verbosity(silence) ->
    set_verbosity(silence);
verbosity(error) ->
    set_verbosity(error);
verbosity(log) ->
    set_verbosity(log);
verbosity(debug) ->
    set_verbosity(debug);
verbosity(trace) ->
    set_verbosity(trace).

set_verbosity(Verbosity) ->
    gen_server:cast(httpd_tester,{verbosity,Verbosity}).

poll_time(NewTime) ->
    gen_server:call(httpd_tester,{poll_time,NewTime}).


%% ----------------------------------------------------------------------


init([Host, Port, Options0]) ->
    process_flag(trap_exit,true),
    Options = options(Options0),
    put(verbosity,get_verbosity(Options)),
    log_open(get_log_file(Options)),
    tstart(),
    PollTime = get_poll_time(Options),
    Ref = tcreate(PollTime),
    log("created"),
    {ok,#state{host  = Host,
	       port  = Port,
	       ptime = PollTime,
	       tref  = Ref,
	       uris  = get_uris(Options)}}.

uris(top) ->
    [uri_top_index()];

uris(otp) ->
    [
     uri_top_index(),
     uri_internal_product1(),
     uri_internal_product2(),
     uri_r13b03_test_results(),
     uri_bjorn1(),
     uri_bjorn2()
    ].

uri_top_index() -> 
    {"top page","/"}.

uri_internal_product1() -> 
    {"product internal page (1)","/product/internal/"}.

uri_internal_product2() -> 
    {"product internal page (2)","/product/internal"}.

uri_r13b03_test_results() ->
    {"daily build index page",
     "/product/internal/test/daily/logs.html"}.

uri_bjorn1() ->
    {"bjorns home page (1)","/~bjorn/"}.

uri_bjorn2() ->
    {"bjorns home page (2)","/~bjorn"}.


handle_call(stop, _From, State) ->
    vlog("stop request"),
    {stop, normal, ok, State};

handle_call({poll_time,NewTime}, _From, State) ->
    vlog("set new poll time: ~p",[NewTime]),
    OldTime = State#state.ptime,
    {stop, normal, OldTime, State#state{ptime = NewTime}};

handle_call(Request, _From, State) ->
    vlog("unexpected request(call): ~p",[Request]),
    {reply, ok, State}.


handle_cast({verbosity,Verbosity}, State) ->
    vlog("set (new) verbosity to: ~p",[Verbosity]),
    put(verbosity,Verbosity),
    {noreply, State};

handle_cast(Message, State) ->
    vlog("unexpected message(call): ~p",[Message]),
    {noreply, State}.


handle_info(poll_time,State) ->
    {{Description,Uri},Uris} = get_uri(State#state.uris),
    vlog("poll time for ~s",[Description]),
    do_poll(State#state.host,State#state.port,Uri),
    Ref = tcreate(State#state.ptime),
    {noreply, State#state{tref = Ref, uris = Uris}};

handle_info(Info, State) ->
    vlog("unexpected message(info): ~p",[Info]),
    {noreply, State}.


code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


terminate(_Reason, State) ->
    tcancel(State#state.tref),
    log_close(get(log_file)),
    ok.


get_uri([Uri|Uris]) ->
    {Uri,Uris++[Uri]}.

    
do_poll(Host,Port,Uri) ->
    (catch poll(create(Host,Port),Uri,"200")).

poll({ok,Socket},Uri,ExpStatus) ->
    vtrace("poll -> entry with Socket: ~p",[Socket]),
    put(latest_requested_uri,Uri),
    Req = "GET " ++ Uri ++ " HTTP/1.0\r\n\r\n",
    await_poll_response(send(Socket,Req),Socket,ExpStatus);
poll({error,Reason},_Req,_ExpStatus) ->
    verror("failed creating socket: ~p",[Reason]),
    log("failed creating socket: ~p",[Reason]),
    exit({error,Reason});
poll(O,_Req,_ExpStatus) ->
    verror("unexpected result from socket create: ~p",[O]),
    log("unexpected result from socket create: ~p",[O]),
    exit({unexpected_result,O}).

await_poll_response(ok,Socket,ExpStatusCode) ->
    vtrace("await_poll_response -> awaiting response with status ~s",
	   [ExpStatusCode]),
    receive 
	{tcp_closed,Socket} ->
	    verror("connection closed when awaiting poll response"),
	    log("connection closed when awaiting reply to GET of '~s'",
		[get(latest_requested_uri)]),
	    exit(connection_closed);
	{tcp,Socket,Response} ->
	    vdebug("received response"),
	    validate(ExpStatusCode,Socket,Response)
    after 10000 ->
	    verror("connection timeout waiting for poll response",[]),
	    log("connection timeout waiting for reply to GET of '~s'",
		[get(latest_requested_uri)]),
	    exit(connection_timed_out)
    end;
await_poll_response(Error,_Socket,_ExpStatusCode) ->
    verror("failed sending GET request for '~s' for reason: ~p",
	   [get(latest_requested_uri),Error]),
    log("failed sending GET request for '~s' for reason: ~p",
	[get(latest_requested_uri),Error]),
    exit(Error).
    

validate(ExpStatusCode,Socket,Response) ->
    Sz = sz(Response),
    vtrace("validate -> Entry with ~p bytes response",[Sz]),
    Size = trash_the_rest(Socket,Sz),
    close(Socket),
    case re:split(Response," ", [{return, list}]) of
	["HTTP/1.0",ExpStatusCode|_] ->
	    vlog("response (~p bytes) was ok",[Size]),
	    ok;
	["HTTP/1.0",StatusCode|_] -> 
	    verror("unexpected response status received: ~s => ~s",
		   [StatusCode,status_to_message(StatusCode)]),
	    log("unexpected result to GET of '~s': ~s => ~s",
		[get(latest_requested_uri),StatusCode,
		 status_to_message(StatusCode)]),
	    exit({unexpected_response_code,StatusCode,ExpStatusCode})
    end.


%% ------------------------------------------------------------------

trash_the_rest(Socket,N) ->
    receive
	{tcp, Socket, Trash} ->
	    vtrace("trash_the_rest -> trash ~p bytes",[sz(Trash)]),
	    trash_the_rest(Socket,add(N,sz(Trash)));
	{tcp_closed, Socket} ->
	    vdebug("socket closed after receiving ~p bytes",[N]),
	    N
    after 10000 ->
	    verror("connection timeout waiting for message"),
	    exit(connection_timed_out)
    end.


add(N1, N2) when is_integer(N1) andalso is_integer(N2) ->
    N1 + N2;
add(N1, _N2) when is_integer(N1) ->
    N1;
add(_N1, N2) when is_integer(N2) ->
    N2.

sz(L) when is_list(L) ->
    length(lists:flatten(L));
sz(B) when is_binary(B) ->
    byte_size(B);
sz(O) ->
    {unknown_size,O}.


%% --------------------------------------------------------------
%%
%% Status code to printable string
%%

status_to_message(L) when is_list(L) ->
    case (catch list_to_integer(L)) of
	I when is_integer(I) ->
	    status_to_message(I);
	_ ->
	    io_lib:format("UNKNOWN STATUS CODE: '~p'",[L])
    end;
status_to_message(100) -> "Section 10.1.1: Continue";
status_to_message(101) -> "Section 10.1.2: Switching Protocols";
status_to_message(200) -> "Section 10.2.1: OK";
status_to_message(201) -> "Section 10.2.2: Created";
status_to_message(202) -> "Section 10.2.3: Accepted";
status_to_message(203) -> "Section 10.2.4: Non-Authoritative Information";
status_to_message(204) -> "Section 10.2.5: No Content";
status_to_message(205) -> "Section 10.2.6: Reset Content";
status_to_message(206) -> "Section 10.2.7: Partial Content";
status_to_message(300) -> "Section 10.3.1: Multiple Choices";
status_to_message(301) -> "Section 10.3.2: Moved Permanently";
status_to_message(302) -> "Section 10.3.3: Found";
status_to_message(303) -> "Section 10.3.4: See Other";
status_to_message(304) -> "Section 10.3.5: Not Modified";
status_to_message(305) -> "Section 10.3.6: Use Proxy";
status_to_message(307) -> "Section 10.3.8: Temporary Redirect";
status_to_message(308) -> "RFC 9110, Section 15.4.9: Permanent Redirect";
status_to_message(400) -> "Section 10.4.1: Bad Request";
status_to_message(401) -> "Section 10.4.2: Unauthorized";
status_to_message(402) -> "Section 10.4.3: Peyment Required";
status_to_message(403) -> "Section 10.4.4: Forbidden";
status_to_message(404) -> "Section 10.4.5: Not Found";
status_to_message(405) -> "Section 10.4.6: Method Not Allowed";
status_to_message(406) -> "Section 10.4.7: Not Acceptable";
status_to_message(407) -> "Section 10.4.8: Proxy Authentication Required";
status_to_message(408) -> "Section 10.4.9: Request Time-Out";
status_to_message(409) -> "Section 10.4.10: Conflict";
status_to_message(410) -> "Section 10.4.11: Gone";
status_to_message(411) -> "Section 10.4.12: Length Required";
status_to_message(412) -> "Section 10.4.13: Precondition Failed";
status_to_message(413) -> "Section 10.4.14: Request Entity Too Large";
status_to_message(414) -> "Section 10.4.15: Request-URI Too Large";
status_to_message(415) -> "Section 10.4.16: Unsupported Media Type";
status_to_message(416) -> "Section 10.4.17: Requested range not satisfiable";
status_to_message(417) -> "Section 10.4.18: Expectation Failed";
status_to_message(500) -> "Section 10.5.1: Internal Server Error";
status_to_message(501) -> "Section 10.5.2: Not Implemented";
status_to_message(502) -> "Section 10.5.3: Bad Gatteway";
status_to_message(503) -> "Section 10.5.4: Service Unavailable";
status_to_message(504) -> "Section 10.5.5: Gateway Time-out";
status_to_message(505) -> "Section 10.5.6: HTTP Version not supported";
status_to_message(Code) -> io_lib:format("Unknown status code: ~p",[Code]).


%% ----------------------------------------------------------------

create(Host,Port) ->
    vtrace("create -> ~n\tHost: ~s~n\tPort: ~p",[Host,Port]),
    case gen_tcp:connect(Host,Port,[{packet,0},{reuseaddr,true}]) of
	{ok,Socket} ->
	    {ok,Socket};
	{error,{enfile,_}} ->
	    {error,enfile};
	Error ->
	    Error
    end.

close(Socket) ->
    gen_tcp:close(Socket).


send(Socket,Data) ->
    vtrace("send -> send ~p bytes of data",[length(Data)]),
    gen_tcp:send(Socket,Data).

    
%% ----------------------------------------------------------------

tstart() -> 
    timer:start().

tcreate(Time) -> 
    {ok,Ref} = timer:send_after(Time,poll_time),
    Ref.

tcancel(Ref) ->
    timer:cancel(Ref).

%% ----------------------------------------------------------------

log_open(undefined) ->
    ok;
log_open(FileName) ->
    put(log_file,fopen(FileName)).

log_close(undefined) ->
    ok;
log_close(Fd) ->
    fclose(Fd).

log(F) ->
    log(F,[]).

log(F,A) ->
    {{Year,Month,Day},{Hour,Min,Sec}} = local_time(),
    fwrite(get(log_file),
	   "~w.~w.~w ~w.~w.~w " ++ F ++ "~n",
	   [Year,Month,Day,Hour,Min,Sec] ++ A).

%% ----------------------------------------------------------------

fopen(Name) ->
    {ok,Fd} = file:open(Name,[write]),
    Fd.

fclose(Fd) ->
    file:close(Fd).

fwrite(undefined,_F,_A) ->
    ok;
fwrite(Fd,F,A) ->
    io:format(Fd,F,A).

    
%% ----------------------------------------------------------------

get_poll_time(Opts) ->
    get_option(poll_time,Opts,?default_poll_time).

get_log_file(Opts) ->
    get_option(log_file,Opts).

get_uris(Opts) ->
    get_option(uris,Opts,[]).

get_verbosity(Opts) ->
    get_option(verbosity,Opts,?default_verbosity).

get_option(Opt,Opts) ->
    get_option(Opt,Opts,undefined).

get_option(Opt,Opts,Default) ->
    case lists:keysearch(Opt,1,Opts) of
	{value,{Opt,Value}} ->
	    Value;
	false ->
	    Default
    end.

%% ----------------------------------------------------------------

%% sleep(T) -> receive after T -> ok end.

%% ----------------------------------------------------------------

%% vtrace(F)   -> vprint(get(verbosity),trace,F,[]).
vtrace(F,A) -> vprint(get(verbosity),trace,F,A).

vdebug(F)   -> vprint(get(verbosity),debug,F,[]).
vdebug(F,A) -> vprint(get(verbosity),debug,F,A).

vlog(F)     -> vprint(get(verbosity),log,F,[]).
vlog(F,A)   -> vprint(get(verbosity),log,F,A).

verror(F)   -> vprint(get(verbosity),error,F,[]).
verror(F,A) -> vprint(get(verbosity),error,F,A).

vprint(trace, Severity,  F,  A)    -> vprint(Severity,F,A);
vprint(debug, trace,    _F, _A)    -> ok;
vprint(debug, Severity,  F,  A)    -> vprint(Severity,F,A);
vprint(log,   log,       F,  A)    -> vprint(log,F,A);
vprint(log,   error,     F,  A)    -> vprint(log,F,A);
vprint(error, error,     F,  A)    -> vprint(error,F,A);
vprint(_Verbosity,_Severity,_F,_A) -> ok.

vprint(Severity,F,A) -> 
    {{Year,Month,Day},{Hour,Min,Sec}} = local_time(),
    io:format("~w.~w.~w ~w.~w.~w " ++ image_of(Severity) ++ F ++ "~n",
	      [Year,Month,Day,Hour,Min,Sec] ++ A).

image_of(error) -> "ERR: ";
image_of(log)   -> "LOG: "; 
image_of(debug) -> "DBG: ";
image_of(trace) -> "TRC: ".
   
local_time() -> calendar:local_time().


