% 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.

-module(chttpd_external).

-compile(tuple_calls).

-export([send_external_response/2]).
-export([json_req_obj_fields/0, json_req_obj/2, json_req_obj/3, json_req_obj/4]).
-export([default_or_content_type/2, parse_external_response/1]).

-import(chttpd, [send_error/4]).

-include_lib("couch/include/couch_db.hrl").

json_req_obj(Req, Db) ->
    json_req_obj(Req, Db, null).
json_req_obj(Req, Db, DocId) ->
    json_req_obj(Req, Db, DocId, all).
json_req_obj(Req, Db, DocId, all) ->
    Fields = json_req_obj_fields(),
    json_req_obj(Req, Db, DocId, Fields);
json_req_obj(#httpd{} = Req, Db, DocId, Fields) when is_list(Fields) ->
    MochiReq =
        case scrub_json_request() of
            true -> chttpd_util:scrub_mochiweb_client_req(Req#httpd.mochi_req);
            false -> Req#httpd.mochi_req
        end,
    Req1 = Req#httpd{mochi_req = MochiReq},
    {[{Field, json_req_obj_field(Field, Req1, Db, DocId)} || Field <- Fields]}.

json_req_obj_fields() ->
    [
        <<"info">>,
        <<"uuid">>,
        <<"id">>,
        <<"method">>,
        <<"requested_path">>,
        <<"path">>,
        <<"raw_path">>,
        <<"query">>,
        <<"headers">>,
        <<"body">>,
        <<"peer">>,
        <<"form">>,
        <<"cookie">>,
        <<"userCtx">>,
        <<"secObj">>
    ].

json_req_obj_field(<<"info">>, #httpd{}, Db, _DocId) ->
    {ok, Info} = get_db_info(Db),
    {Info};
json_req_obj_field(<<"uuid">>, #httpd{}, _Db, _DocId) ->
    couch_uuids:new();
json_req_obj_field(<<"id">>, #httpd{}, _Db, DocId) ->
    DocId;
json_req_obj_field(<<"method">>, #httpd{method = Method}, _Db, _DocId) ->
    Method;
json_req_obj_field(<<"requested_path">>, #httpd{requested_path_parts = Path}, _Db, _DocId) ->
    Path;
json_req_obj_field(<<"path">>, #httpd{path_parts = Path}, _Db, _DocId) ->
    Path;
json_req_obj_field(<<"raw_path">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
    ?l2b(Req:get(raw_path));
json_req_obj_field(<<"query">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
    json_query_keys(to_json_terms(Req:parse_qs()));
json_req_obj_field(<<"headers">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
    Headers = Req:get(headers),
    Hlist = mochiweb_headers:to_list(Headers),
    to_json_terms(Hlist);
json_req_obj_field(<<"body">>, #httpd{req_body = undefined, mochi_req = Req}, _Db, _DocId) ->
    MaxSize = chttpd_util:get_chttpd_config_integer(
        "max_http_request_size", 4294967296
    ),
    try
        Req:recv_body(MaxSize)
    catch
        exit:{shutdown, _} ->
            exit({bad_request, <<"Invalid request body">>});
        exit:normal ->
            exit({bad_request, <<"Invalid request body">>})
    end;
json_req_obj_field(<<"body">>, #httpd{req_body = Body}, _Db, _DocId) ->
    Body;
json_req_obj_field(<<"peer">>, #httpd{peer = undefined, mochi_req = Req}, _, _) ->
    ?l2b(Req:get(peer));
json_req_obj_field(<<"peer">>, #httpd{peer = Peer}, _Db, _DocId) ->
    ?l2b(Peer);
json_req_obj_field(<<"form">>, #httpd{mochi_req = Req, method = Method} = HttpReq, Db, DocId) ->
    Body = json_req_obj_field(<<"body">>, HttpReq, Db, DocId),
    ParsedForm =
        case Req:get_primary_header_value("content-type") of
            "application/x-www-form-urlencoded" ++ _ when
                Method =:= 'POST' orelse Method =:= 'PUT'
            ->
                mochiweb_util:parse_qs(Body);
            _ ->
                []
        end,
    to_json_terms(ParsedForm);
json_req_obj_field(<<"cookie">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
    case scrub_json_request() of
        true -> {[]};
        false -> to_json_terms(Req:parse_cookie())
    end;
json_req_obj_field(<<"userCtx">>, #httpd{}, Db, _DocId) ->
    couch_util:json_user_ctx(Db);
json_req_obj_field(<<"secObj">>, #httpd{user_ctx = UserCtx}, Db, _DocId) ->
    get_db_security(Db, UserCtx).

get_db_info(Db) ->
    case couch_db:is_clustered(Db) of
        true ->
            fabric:get_db_info(Db);
        false ->
            couch_db:get_db_info(Db)
    end.

get_db_security(Db, #user_ctx{}) ->
    case couch_db:is_clustered(Db) of
        true ->
            fabric:get_security(Db);
        false ->
            couch_db:get_security(Db)
    end.

to_json_terms(Data) ->
    to_json_terms(Data, []).
to_json_terms([], Acc) ->
    {lists:reverse(Acc)};
to_json_terms([{Key, Value} | Rest], Acc) when is_atom(Key) ->
    to_json_terms(Rest, [{atom_to_binary(Key), list_to_binary(Value)} | Acc]);
to_json_terms([{Key, Value} | Rest], Acc) ->
    to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]).

json_query_keys({Json}) ->
    json_query_keys(Json, []).
json_query_keys([], Acc) ->
    {lists:reverse(Acc)};
json_query_keys([{<<"startkey">>, Value} | Rest], Acc) ->
    json_query_keys(Rest, [{<<"startkey">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"endkey">>, Value} | Rest], Acc) ->
    json_query_keys(Rest, [{<<"endkey">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"key">>, Value} | Rest], Acc) ->
    json_query_keys(Rest, [{<<"key">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"descending">>, Value} | Rest], Acc) ->
    json_query_keys(Rest, [{<<"descending">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([Term | Rest], Acc) ->
    json_query_keys(Rest, [Term | Acc]).

send_external_response(Req, Response) ->
    #extern_resp_args{
        code = Code,
        data = Data,
        ctype = CType,
        headers = Headers0,
        json = Json
    } = parse_external_response(Response),
    Headers1 = default_or_content_type(CType, Headers0),
    case Json of
        nil ->
            Headers2 = chttpd_util:maybe_add_csp_header("showlist", Headers1, "sandbox"),
            chttpd:send_response(Req, Code, Headers2, Data);
        Json ->
            chttpd:send_json(Req, Code, Headers1, Json)
    end.

parse_external_response({Response}) ->
    lists:foldl(
        fun({Key, Value}, Args) ->
            case {Key, Value} of
                {"", _} ->
                    Args;
                {<<"code">>, Value} ->
                    Args#extern_resp_args{code = Value};
                {<<"stop">>, true} ->
                    Args#extern_resp_args{stop = true};
                {<<"json">>, Value} ->
                    Args#extern_resp_args{
                        json = Value,
                        ctype = "application/json"
                    };
                {<<"body">>, Value} ->
                    Args#extern_resp_args{data = Value, ctype = "text/html; charset=utf-8"};
                {<<"base64">>, Value} ->
                    Args#extern_resp_args{
                        data = base64:decode(Value),
                        ctype = "application/binary"
                    };
                {<<"headers">>, {Headers}} ->
                    NewHeaders = lists:map(
                        fun({Header, HVal}) ->
                            {couch_util:to_list(Header), couch_util:to_list(HVal)}
                        end,
                        Headers
                    ),
                    Args#extern_resp_args{headers = NewHeaders};
                % unknown key
                _ ->
                    Msg = lists:flatten(
                        io_lib:format("Invalid data from external server: ~p", [{Key, Value}])
                    ),
                    throw({external_response_error, Msg})
            end
        end,
        #extern_resp_args{},
        Response
    ).

default_or_content_type(DefaultContentType, Headers) ->
    IsContentType = fun({X, _}) -> string:to_lower(X) == "content-type" end,
    case lists:any(IsContentType, Headers) of
        false ->
            [{"Content-Type", DefaultContentType} | Headers];
        true ->
            Headers
    end.

scrub_json_request() ->
    config:get_boolean("chttpd", "scrub_json_request", true).
