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

-export([
    handle_doc_show_req/3,
    handle_doc_update_req/3,
    handle_view_list_req/3,
    list_cb/2
]).

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

% /db/_design/foo/_show/bar/docid
% show converts a json doc to a response of any content-type.
% it looks up the doc an then passes it to the query server.
% then it sends the response from the query server to the http client.

maybe_open_doc(Db, DocId) ->
    case catch couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of
        #doc{} = Doc -> Doc;
        {not_found, _} -> nil
    end.

handle_doc_show_req(
    #httpd{
        path_parts = [_, _, _, _, ShowName, DocId]
    } = Req,
    Db,
    DDoc
) ->
    % open the doc
    Doc = maybe_open_doc(Db, DocId),

    % we don't handle revs here b/c they are an internal api
    % returns 404 if there is no doc with DocId
    handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId);
handle_doc_show_req(
    #httpd{
        path_parts = [_, _, _, _, ShowName, DocId | Rest]
    } = Req,
    Db,
    DDoc
) ->
    DocParts = [DocId | Rest],
    DocId1 = ?l2b(string:join([?b2l(P) || P <- DocParts], "/")),

    % open the doc
    Doc = maybe_open_doc(Db, DocId1),

    % we don't handle revs here b/c they are an internal api
    % pass 404 docs to the show function
    handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId1);
handle_doc_show_req(
    #httpd{
        path_parts = [_, _, _, _, ShowName]
    } = Req,
    Db,
    DDoc
) ->
    % with no docid the doc is nil
    handle_doc_show(Req, Db, DDoc, ShowName, nil);
handle_doc_show_req(Req, _Db, _DDoc) ->
    chttpd:send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>).

handle_doc_show(Req, Db, DDoc, ShowName, Doc) ->
    handle_doc_show(Req, Db, DDoc, ShowName, Doc, null).

handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId) ->
    % get responder for ddoc/showname
    CurrentEtag = show_etag(Req, Doc, DDoc, []),
    chttpd:etag_respond(Req, CurrentEtag, fun() ->
        JsonReq = chttpd_external:json_req_obj(Req, Db, DocId),
        JsonDoc = couch_query_servers:json_doc(Doc),
        [<<"resp">>, ExternalResp] =
            couch_query_servers:ddoc_prompt(
                Db,
                DDoc,
                [<<"shows">>, ShowName],
                [JsonDoc, JsonReq]
            ),
        JsonResp = apply_etag(ExternalResp, CurrentEtag),
        chttpd_external:send_external_response(Req, JsonResp)
    end).

show_etag(#httpd{user_ctx = UserCtx} = Req, Doc, DDoc, More) ->
    Accept = chttpd:header_value(Req, "Accept"),
    DocPart =
        case Doc of
            nil -> nil;
            Doc -> chttpd:doc_etag(Doc)
        end,
    chttpd:make_etag({
        chttpd:doc_etag(DDoc),
        DocPart,
        Accept,
        {UserCtx#user_ctx.name, UserCtx#user_ctx.roles},
        More
    }).

% updates a doc based on a request
% handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db, _DDoc) ->
%     % anything but GET
%     send_method_not_allowed(Req, "POST,PUT,DELETE,ETC");

% This call is creating a new doc using an _update function to
% modify the provided request body.
% /db/_design/foo/_update/bar
handle_doc_update_req(
    #httpd{
        path_parts = [_, _, _, _, UpdateName]
    } = Req,
    Db,
    DDoc
) ->
    send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null);
% /db/_design/foo/_update/bar/docid
handle_doc_update_req(
    #httpd{
        path_parts = [_, _, _, _, UpdateName | DocIdParts]
    } = Req,
    Db,
    DDoc
) ->
    DocId = ?l2b(string:join([?b2l(P) || P <- DocIdParts], "/")),
    Doc = maybe_open_doc(Db, DocId),
    send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId);
handle_doc_update_req(Req, _Db, _DDoc) ->
    chttpd:send_error(Req, 404, <<"update_error">>, <<"Invalid path.">>).

send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) ->
    JsonReq = chttpd_external:json_req_obj(Req, Db, DocId),
    JsonDoc = couch_query_servers:json_doc(Doc),
    Cmd = [<<"updates">>, UpdateName],
    UpdateResp = couch_query_servers:ddoc_prompt(Db, DDoc, Cmd, [JsonDoc, JsonReq]),
    JsonResp =
        case UpdateResp of
            [<<"up">>, {NewJsonDoc}, {JsonResp0}] ->
                case
                    chttpd:header_value(
                        Req, "X-Couch-Full-Commit", "false"
                    )
                of
                    "true" ->
                        Options = [full_commit, {user_ctx, Req#httpd.user_ctx}];
                    _ ->
                        Options = [{user_ctx, Req#httpd.user_ctx}]
                end,
                NewDoc = couch_db:doc_from_json_obj_validate(Db, {NewJsonDoc}),
                {ok, NewRev} = couch_db:update_doc(Db, NewDoc, Options),
                NewRevStr = couch_doc:rev_to_str(NewRev),
                {JsonResp1} = apply_headers(JsonResp0, [
                    {<<"X-Couch-Update-NewRev">>, NewRevStr},
                    {<<"X-Couch-Id">>, couch_util:url_encode(NewDoc#doc.id)}
                ]),
                {[{<<"code">>, 201} | JsonResp1]};
            [<<"up">>, _Other, {JsonResp0}] ->
                {[{<<"code">>, 200} | JsonResp0]}
        end,
    % todo set location field
    chttpd_external:send_external_response(Req, JsonResp).

handle_view_list_req(#httpd{method = Method} = Req, Db, DDoc) when
    Method =:= 'GET' orelse Method =:= 'OPTIONS'
->
    case Req#httpd.path_parts of
        [_, _, _DName, _, LName, VName] ->
            % Same design doc for view and list
            handle_view_list(Req, Db, DDoc, LName, DDoc, VName, undefined);
        [_, _, _, _, LName, DName, VName] ->
            % Different design docs for view and list
            VDocId = <<"_design/", DName/binary>>,
            {ok, VDDoc} = couch_db:open_doc(Db, VDocId, [ejson_body]),
            handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, undefined);
        _ ->
            chttpd:send_error(Req, 404, <<"list_error">>, <<"Bad path.">>)
    end;
handle_view_list_req(#httpd{method = 'POST'} = Req, Db, DDoc) ->
    chttpd:validate_ctype(Req, "application/json"),
    {Props} = chttpd:json_body_obj(Req),
    Keys = proplists:get_value(<<"keys">>, Props),
    case Req#httpd.path_parts of
        [_, _, _DName, _, LName, VName] ->
            handle_view_list(Req, Db, DDoc, LName, DDoc, VName, Keys);
        [_, _, _, _, LName, DName, VName] ->
            % Different design docs for view and list
            VDocId = <<"_design/", DName/binary>>,
            {ok, VDDoc} = couch_db:open_doc(Db, VDocId, [ejson_body]),
            handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, Keys);
        _ ->
            chttpd:send_error(Req, 404, <<"list_error">>, <<"Bad path.">>)
    end;
handle_view_list_req(Req, _Db, _DDoc) ->
    chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").

handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, Keys) ->
    Args0 = couch_mrview_http:parse_body_and_query(Req, Keys),
    ETagFun = fun(BaseSig, Acc0) ->
        UserCtx = Req#httpd.user_ctx,
        Name = UserCtx#user_ctx.name,
        Roles = UserCtx#user_ctx.roles,
        Accept = chttpd:header_value(Req, "Accept"),
        Parts = {chttpd:doc_etag(DDoc), Accept, {Name, Roles}},
        ETag = chttpd:make_etag({BaseSig, Parts}),
        case chttpd:etag_match(Req, ETag) of
            true -> throw({etag_match, ETag});
            false -> {ok, Acc0#lacc{etag = ETag}}
        end
    end,
    Args = Args0#mrargs{preflight_fun = ETagFun},
    couch_httpd:etag_maybe(Req, fun() ->
        couch_query_servers:with_ddoc_proc(Db, DDoc, fun(QServer) ->
            Acc = #lacc{db = Db, req = Req, qserver = QServer, lname = LName},
            case VName of
                <<"_all_docs">> ->
                    ValidatedArgs = couch_mrview_util:validate_all_docs_args(Db, Args),
                    couch_mrview:query_all_docs(Db, ValidatedArgs, fun list_cb/2, Acc);
                _ ->
                    couch_mrview:query_view(Db, VDDoc, VName, Args, fun list_cb/2, Acc)
            end
        end)
    end).

list_cb({meta, Meta}, #lacc{code = undefined} = Acc) ->
    MetaProps =
        case couch_util:get_value(total, Meta) of
            undefined -> [];
            Total -> [{total_rows, Total}]
        end ++
            case couch_util:get_value(offset, Meta) of
                undefined -> [];
                Offset -> [{offset, Offset}]
            end ++
            case couch_util:get_value(update_seq, Meta) of
                undefined -> [];
                UpdateSeq -> [{update_seq, UpdateSeq}]
            end,
    start_list_resp({MetaProps}, Acc);
list_cb({row, Row}, #lacc{code = undefined} = Acc) ->
    {ok, NewAcc} = start_list_resp({[]}, Acc),
    send_list_row(Row, NewAcc);
list_cb({row, Row}, Acc) ->
    send_list_row(Row, Acc);
list_cb(complete, Acc) ->
    #lacc{qserver = {Proc, _}, req = Req, resp = Resp0} = Acc,
    if
        Resp0 =:= nil ->
            {ok, #lacc{resp = Resp}} = start_list_resp({[]}, Acc);
        true ->
            Resp = Resp0
    end,
    case couch_query_servers:proc_prompt(Proc, [<<"list_end">>]) of
        [<<"end">>, Data, Headers] ->
            Acc2 = fixup_headers(Headers, Acc#lacc{resp = Resp}),
            #lacc{resp = Resp2} = send_non_empty_chunk(Acc2, Data);
        [<<"end">>, Data] ->
            #lacc{resp = Resp2} = send_non_empty_chunk(Acc#lacc{resp = Resp}, Data)
    end,
    last_chunk(Req, Resp2),
    {ok, Resp2}.

start_list_resp(Head, Acc) ->
    #lacc{db = Db, req = Req, qserver = QServer, lname = LName} = Acc,
    JsonReq = json_req_obj(Req, Db),

    [<<"start">>, Chunk, JsonResp] = couch_query_servers:ddoc_proc_prompt(
        QServer,
        [<<"lists">>, LName],
        [Head, JsonReq]
    ),
    Acc2 = send_non_empty_chunk(fixup_headers(JsonResp, Acc), Chunk),
    {ok, Acc2}.

fixup_headers(Headers, #lacc{etag = ETag} = Acc) ->
    Headers2 = apply_etag(Headers, ETag),
    #extern_resp_args{
        code = Code,
        ctype = CType,
        headers = ExtHeaders
    } = chttpd_external:parse_external_response(Headers2),
    Headers3 = chttpd_external:default_or_content_type(CType, ExtHeaders),
    Headers4 = chttpd_util:maybe_add_csp_header("showlist", Headers3, "sandbox"),
    Acc#lacc{code = Code, headers = Headers4}.

send_list_row(Row, #lacc{qserver = {Proc, _}, req = Req, resp = Resp} = Acc) ->
    RowObj =
        case couch_util:get_value(id, Row) of
            undefined -> [];
            Id -> [{id, Id}]
        end ++
            case couch_util:get_value(key, Row) of
                undefined -> [];
                Key -> [{key, Key}]
            end ++
            case couch_util:get_value(value, Row) of
                undefined -> [];
                Val -> [{value, Val}]
            end ++
            case couch_util:get_value(doc, Row) of
                undefined -> [];
                Doc -> [{doc, Doc}]
            end,
    try couch_query_servers:proc_prompt(Proc, [<<"list_row">>, {RowObj}]) of
        [<<"chunks">>, Chunk, Headers] ->
            Acc2 = send_non_empty_chunk(fixup_headers(Headers, Acc), Chunk),
            {ok, Acc2};
        [<<"chunks">>, Chunk] ->
            Acc2 = send_non_empty_chunk(Acc, Chunk),
            {ok, Acc2};
        [<<"end">>, Chunk, Headers] ->
            #lacc{resp = Resp2} = send_non_empty_chunk(fixup_headers(Headers, Acc), Chunk),
            {ok, Resp3} = last_chunk(Req, Resp2),
            {stop, Resp3};
        [<<"end">>, Chunk] ->
            #lacc{resp = Resp2} = send_non_empty_chunk(Acc, Chunk),
            {ok, Resp3} = last_chunk(Req, Resp2),
            {stop, Resp3}
    catch
        Error ->
            {ok, Resp2} =
                case Resp of
                    undefined ->
                        {Code, _, _} = chttpd:error_info(Error),
                        #lacc{req = Req, headers = Headers} = Acc,
                        chttpd:start_chunked_response(Req, Code, Headers);
                    _ ->
                        {ok, Resp}
                end,
            {ok, Resp3} = chttpd:send_chunked_error(Resp2, Error),
            {stop, Resp3}
    end.

send_non_empty_chunk(Acc, []) ->
    Acc;
send_non_empty_chunk(#lacc{resp = undefined} = Acc, Chunk) ->
    #lacc{req = Req, code = Code, headers = Headers} = Acc,
    {ok, Resp} = chttpd:start_chunked_response(Req, Code, Headers),
    send_non_empty_chunk(Acc#lacc{resp = Resp}, Chunk);
send_non_empty_chunk(#lacc{resp = Resp} = Acc, Chunk) ->
    chttpd:send_chunk(Resp, Chunk),
    Acc.

apply_etag(JsonResp, undefined) ->
    JsonResp;
apply_etag({ExternalResponse}, CurrentEtag) ->
    % Here we embark on the delicate task of replacing or creating the
    % headers on the JsonResponse object. We need to control the Etag and
    % Vary headers. If the external function controls the Etag, we'd have to
    % run it to check for a match, which sort of defeats the purpose.
    apply_headers(ExternalResponse, [
        {<<"ETag">>, CurrentEtag},
        {<<"Vary">>, <<"Accept">>}
    ]).

apply_headers(JsonResp, []) ->
    JsonResp;
apply_headers(JsonResp, NewHeaders) ->
    case couch_util:get_value(<<"headers">>, JsonResp) of
        undefined ->
            {[{<<"headers">>, {NewHeaders}} | JsonResp]};
        JsonHeaders ->
            Headers = apply_headers1(JsonHeaders, NewHeaders),
            NewKV = {<<"headers">>, Headers},
            {lists:keyreplace(<<"headers">>, 1, JsonResp, NewKV)}
    end.
apply_headers1(JsonHeaders, [{Key, Value} | Rest]) ->
    NewJsonHeaders = json_apply_field({Key, Value}, JsonHeaders),
    apply_headers1(NewJsonHeaders, Rest);
apply_headers1(JsonHeaders, []) ->
    JsonHeaders.

% Maybe this is in the proplists API
% todo move to couch_util
json_apply_field(H, {L}) ->
    json_apply_field(H, L, []).

json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) ->
    % drop matching keys
    json_apply_field({Key, NewValue}, Headers, Acc);
json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
    % something else is next, leave it alone.
    json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
json_apply_field({Key, NewValue}, [], Acc) ->
    % end of list, add ours
    {[{Key, NewValue} | Acc]}.

% This loads the db info if we have a fully loaded db record, but we might not
% have the db locally on this node, so then load the info through fabric.
json_req_obj(Req, Db) ->
    case couch_db:is_clustered(Db) of
        true ->
            % use a separate process because we're already in a receive loop,
            % and json_req_obj calls fabric:get_db_info()
            JRO = fun() -> exit(chttpd_external:json_req_obj(Req, Db)) end,
            {Pid, Ref} = spawn_monitor(JRO),
            receive
                {'DOWN', Ref, process, Pid, JsonReq} -> JsonReq
            end;
        false ->
            chttpd_external:json_req_obj(Req, Db)
    end.

last_chunk(Req, undefined) ->
    chttpd:send_response(Req, 200, [], <<"">>);
last_chunk(_Req, Resp) ->
    chttpd:send_chunk(Resp, []).

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

apply_headers_test_() ->
    [
        should_apply_headers(),
        should_apply_headers_with_merge(),
        should_apply_headers_with_merge_overwrite()
    ].

should_apply_headers() ->
    ?_test(begin
        JsonResp = [{<<"code">>, 201}],
        Headers = [{<<"foo">>, <<"bar">>}],
        {Props} = apply_headers(JsonResp, Headers),
        JsonHeaders = couch_util:get_value(<<"headers">>, Props),
        ?assertEqual({Headers}, JsonHeaders)
    end).

should_apply_headers_with_merge() ->
    ?_test(begin
        BaseHeaders = [{<<"bar">>, <<"baz">>}],
        NewHeaders = [{<<"foo">>, <<"bar">>}],
        JsonResp = [
            {<<"code">>, 201},
            {<<"headers">>, {BaseHeaders}}
        ],
        {Props} = apply_headers(JsonResp, NewHeaders),
        JsonHeaders = couch_util:get_value(<<"headers">>, Props),
        ExpectedHeaders = {NewHeaders ++ BaseHeaders},
        ?assertEqual(ExpectedHeaders, JsonHeaders)
    end).

should_apply_headers_with_merge_overwrite() ->
    ?_test(begin
        BaseHeaders = [{<<"foo">>, <<"bar">>}],
        NewHeaders = [{<<"foo">>, <<"baz">>}],
        JsonResp = [
            {<<"code">>, 201},
            {<<"headers">>, {BaseHeaders}}
        ],
        {Props} = apply_headers(JsonResp, NewHeaders),
        JsonHeaders = couch_util:get_value(<<"headers">>, Props),
        ?assertEqual({NewHeaders}, JsonHeaders)
    end).

send_list_row_test_() ->
    Cases = couch_tests_combinatorics:product([
        [
            {"[<<\"end\">>, [], []]", fun(_, _) -> [<<"end">>, [], []] end},
            {"[<<\"end\">>, []]", fun(_, _) -> [<<"end">>, []] end},
            {"throw(timeout)", fun(_, _) -> throw(timeout) end}
        ],
        [
            req,
            undefined
        ]
    ]),
    {"Ensure send_list_row returns a valid response on end or error",
        {setup, fun setup/0, fun(_) -> meck:unload() end, [
            {
                lists:flatten(io_lib:format("~s -- ~p", [N, R])),
                should_return_valid_response(F, R)
            }
         || [{N, F}, R] <- Cases
        ]}}.

setup() ->
    ok = application:start(config, permanent),
    ok = meck:expect(
        chttpd,
        send_chunk,
        fun(Resp, _) -> {ok, Resp} end
    ),
    ok = meck:expect(
        chttpd,
        send_chunked_error,
        fun(Resp, _) -> {ok, Resp} end
    ),
    ok = meck:expect(
        chttpd,
        start_chunked_response,
        fun(_, _, _) -> {ok, resp} end
    ),
    ok = meck:expect(
        chttpd_external,
        parse_external_response,
        1,
        #extern_resp_args{headers = []}
    ).

should_return_valid_response(Spec, Req) ->
    ?_test(begin
        ok = meck:expect(couch_query_servers, proc_prompt, Spec),
        Acc = #lacc{qserver = {proc, undefined}, req = Req, resp = resp},
        ?assertEqual({stop, resp}, send_list_row([], Acc))
    end).

-endif.
