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

-export([
    handle_all_docs_req/2,
    handle_local_docs_req/2,
    handle_design_docs_req/2,
    handle_reindex_req/3,
    handle_view_req/3,
    handle_temp_view_req/2,
    handle_info_req/3,
    handle_compact_req/3,
    handle_cleanup_req/2
]).

-export([
    parse_boolean/1,
    parse_int/1,
    parse_pos_int/1,
    prepend_val/1,
    parse_body_and_query/2,
    parse_body_and_query/3,
    parse_params/2,
    parse_params/3,
    parse_params/4,
    view_cb/2,
    row_to_json/1,
    row_to_json/2,
    check_view_etag/3
]).

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

handle_all_docs_req(#httpd{method = 'GET'} = Req, Db) ->
    all_docs_req(Req, Db, undefined);
handle_all_docs_req(#httpd{method = 'POST'} = Req, Db) ->
    chttpd:validate_ctype(Req, "application/json"),
    Keys = couch_mrview_util:get_view_keys(chttpd:json_body_obj(Req)),
    all_docs_req(Req, Db, Keys);
handle_all_docs_req(Req, _Db) ->
    chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").

handle_local_docs_req(#httpd{method = 'GET'} = Req, Db) ->
    all_docs_req(Req, Db, undefined, <<"_local">>);
handle_local_docs_req(#httpd{method = 'POST'} = Req, Db) ->
    chttpd:validate_ctype(Req, "application/json"),
    Keys = couch_mrview_util:get_view_keys(chttpd:json_body_obj(Req)),
    all_docs_req(Req, Db, Keys, <<"_local">>);
handle_local_docs_req(Req, _Db) ->
    chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").

handle_design_docs_req(#httpd{method = 'GET'} = Req, Db) ->
    all_docs_req(Req, Db, undefined, <<"_design">>);
handle_design_docs_req(#httpd{method = 'POST'} = Req, Db) ->
    chttpd:validate_ctype(Req, "application/json"),
    Keys = couch_mrview_util:get_view_keys(chttpd:json_body_obj(Req)),
    all_docs_req(Req, Db, Keys, <<"_design">>);
handle_design_docs_req(Req, _Db) ->
    chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").

handle_reindex_req(
    #httpd{
        method = 'POST',
        path_parts = [_, _, DName, <<"_reindex">>]
    } = Req,
    Db,
    _DDoc
) ->
    chttpd:validate_ctype(Req, "application/json"),
    ok = couch_db:check_is_admin(Db),
    couch_mrview:trigger_update(Db, <<"_design/", DName/binary>>),
    chttpd:send_json(Req, 201, {[{<<"ok">>, true}]});
handle_reindex_req(Req, _Db, _DDoc) ->
    chttpd:send_method_not_allowed(Req, "POST").

handle_view_req(
    #httpd{
        method = 'GET',
        path_parts = [_, _, DDocName, _, VName, <<"_info">>]
    } = Req,
    Db,
    _DDoc
) ->
    DbName = couch_db:name(Db),
    DDocId = <<"_design/", DDocName/binary>>,
    {ok, Info} = couch_mrview:get_view_info(DbName, DDocId, VName),

    FinalInfo =
        [
            {db_name, DbName},
            {ddoc, DDocId},
            {view, VName}
        ] ++ Info,
    chttpd:send_json(Req, 200, {FinalInfo});
handle_view_req(#httpd{method = 'GET'} = Req, Db, DDoc) ->
    [_, _, _, _, ViewName] = Req#httpd.path_parts,
    couch_stats:increment_counter([couchdb, httpd, view_reads]),
    design_doc_view(Req, Db, DDoc, ViewName, undefined);
handle_view_req(#httpd{method = 'POST'} = Req, Db, DDoc) ->
    chttpd:validate_ctype(Req, "application/json"),
    [_, _, _, _, ViewName] = Req#httpd.path_parts,
    Props = chttpd:json_body_obj(Req),
    Keys = couch_mrview_util:get_view_keys(Props),
    Queries = couch_mrview_util:get_view_queries(Props),
    case {Queries, Keys} of
        {Queries, undefined} when is_list(Queries) ->
            IncrBy = length(Queries),
            couch_stats:increment_counter([couchdb, httpd, view_reads], IncrBy),
            multi_query_view(Req, Db, DDoc, ViewName, Queries);
        {undefined, Keys} when is_list(Keys) ->
            couch_stats:increment_counter([couchdb, httpd, view_reads]),
            design_doc_view(Req, Db, DDoc, ViewName, Keys);
        {undefined, undefined} ->
            throw({
                bad_request,
                "POST body must contain `keys` or `queries` field"
            });
        {_, _} ->
            throw({bad_request, "`keys` and `queries` are mutually exclusive"})
    end;
handle_view_req(Req, _Db, _DDoc) ->
    chttpd:send_method_not_allowed(Req, "GET,POST,HEAD").

handle_temp_view_req(#httpd{method = 'POST'} = Req, Db) ->
    chttpd:validate_ctype(Req, "application/json"),
    ok = couch_db:check_is_admin(Db),
    {Body} = chttpd:json_body_obj(Req),
    DDoc = couch_mrview_util:temp_view_to_ddoc({Body}),
    Keys = couch_mrview_util:get_view_keys({Body}),
    couch_stats:increment_counter([couchdb, httpd, temporary_view_reads]),
    design_doc_view(Req, Db, DDoc, <<"temp">>, Keys);
handle_temp_view_req(Req, _Db) ->
    chttpd:send_method_not_allowed(Req, "POST").

handle_info_req(#httpd{method = 'GET'} = Req, Db, DDoc) ->
    [_, _, Name, _] = Req#httpd.path_parts,
    {ok, Info} = couch_mrview:get_info(Db, DDoc),
    chttpd:send_json(
        Req,
        200,
        {[
            {name, Name},
            {view_index, {Info}}
        ]}
    );
handle_info_req(Req, _Db, _DDoc) ->
    chttpd:send_method_not_allowed(Req, "GET").

handle_compact_req(#httpd{method = 'POST'} = Req, Db, DDoc) ->
    chttpd:validate_ctype(Req, "application/json"),
    ok = couch_db:check_is_admin(Db),
    ok = couch_mrview:compact(Db, DDoc),
    chttpd:send_json(Req, 202, {[{ok, true}]});
handle_compact_req(Req, _Db, _DDoc) ->
    chttpd:send_method_not_allowed(Req, "POST").

handle_cleanup_req(#httpd{method = 'POST'} = Req, Db) ->
    chttpd:validate_ctype(Req, "application/json"),
    ok = couch_db:check_is_admin(Db),
    ok = couch_mrview:cleanup(Db),
    chttpd:send_json(Req, 202, {[{ok, true}]});
handle_cleanup_req(Req, _Db) ->
    chttpd:send_method_not_allowed(Req, "POST").

all_docs_req(Req, Db, Keys) ->
    all_docs_req(Req, Db, Keys, undefined).

all_docs_req(Req, Db, Keys, NS) ->
    case is_restricted(Db, NS) of
        true ->
            case (catch couch_db:check_is_admin(Db)) of
                ok ->
                    do_all_docs_req(Req, Db, Keys, NS);
                _ when NS == <<"_local">> ->
                    throw({forbidden, <<"Only admins can access _local_docs">>});
                _ ->
                    case is_public_fields_configured(Db) of
                        true ->
                            do_all_docs_req(Req, Db, Keys, NS);
                        false ->
                            throw(
                                {forbidden,
                                    <<"Only admins can access _all_docs", " of system databases.">>}
                            )
                    end
            end;
        false ->
            do_all_docs_req(Req, Db, Keys, NS)
    end.

is_restricted(_Db, <<"_local">>) ->
    true;
is_restricted(Db, _) ->
    couch_db:is_system_db(Db).

is_public_fields_configured(Db) ->
    DbName = ?b2l(couch_db:name(Db)),
    case config:get("couch_httpd_auth", "authentication_db", "_users") of
        DbName ->
            UsersDbPublic = chttpd_util:get_chttpd_auth_config(
                "users_db_public", "false"
            ),
            PublicFields = chttpd_util:get_chttpd_auth_config("public_fields"),
            case {UsersDbPublic, PublicFields} of
                {"true", PublicFields} when PublicFields =/= undefined ->
                    true;
                {_, _} ->
                    false
            end;
        _ ->
            false
    end.

do_all_docs_req(Req, Db, Keys, NS) ->
    Args0 = couch_mrview_http:parse_body_and_query(Req, Keys),
    Args1 = set_namespace(NS, Args0),
    ETagFun = fun(Sig, Acc0) ->
        check_view_etag(Sig, Acc0, Req)
    end,
    Args = Args1#mrargs{preflight_fun = ETagFun},
    {ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
        Max = chttpd:chunked_response_buffer_size(),
        VAcc0 = #vacc{db = Db, req = Req, threshold = Max},
        DbName = ?b2l(couch_db:name(Db)),
        UsersDbName = config:get(
            "couch_httpd_auth",
            "authentication_db",
            "_users"
        ),
        IsAdmin = is_admin(Db),
        Callback = get_view_callback(DbName, UsersDbName, IsAdmin),
        ValidatedArgs = couch_mrview_util:validate_all_docs_args(Db, Args),
        couch_mrview:query_all_docs(Db, ValidatedArgs, Callback, VAcc0)
    end),
    case is_record(Resp, vacc) of
        true -> {ok, Resp#vacc.resp};
        _ -> {ok, Resp}
    end.

set_namespace(NS, #mrargs{extra = Extra} = Args) ->
    Args#mrargs{extra = [{namespace, NS} | Extra]}.

is_admin(Db) ->
    case catch couch_db:check_is_admin(Db) of
        {unauthorized, _} ->
            false;
        ok ->
            true
    end.

% admin users always get all fields
get_view_callback(_, _, true) ->
    fun view_cb/2;
% if we are operating on the users db and we aren't
% admin, filter the view
get_view_callback(_, _, false) ->
    fun filtered_view_cb/2;
% non _users databases get all fields
get_view_callback(_, _, _) ->
    fun view_cb/2.

design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
    Args0 = parse_params(Req, Keys),
    ETagFun = fun(Sig, Acc0) ->
        check_view_etag(Sig, Acc0, Req)
    end,
    Args = Args0#mrargs{preflight_fun = ETagFun},
    {ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
        Max = chttpd:chunked_response_buffer_size(),
        VAcc0 = #vacc{db = Db, req = Req, threshold = Max},
        couch_mrview:query_view(Db, DDoc, ViewName, Args, fun view_cb/2, VAcc0)
    end),
    case is_record(Resp, vacc) of
        true -> {ok, Resp#vacc.resp};
        _ -> {ok, Resp}
    end.

multi_query_view(Req, Db, DDoc, ViewName, Queries) ->
    Args0 = parse_params(Req, undefined),
    {ok, _, _, Args1} = couch_mrview_util:get_view(Db, DDoc, ViewName, Args0),
    ArgQueries = lists:map(
        fun({Query}) ->
            QueryArg = parse_params(Query, undefined, Args1),
            couch_mrview_util:validate_args(Db, DDoc, QueryArg)
        end,
        Queries
    ),
    {ok, Resp2} = couch_httpd:etag_maybe(Req, fun() ->
        Max = chttpd:chunked_response_buffer_size(),
        VAcc0 = #vacc{db = Db, req = Req, prepend = "\r\n", threshold = Max},
        %% TODO: proper calculation of etag
        Etag = [$", couch_uuids:new(), $"],
        Headers = [{"ETag", Etag}],
        FirstChunk = "{\"results\":[",
        {ok, Resp0} = chttpd:start_delayed_json_response(VAcc0#vacc.req, 200, Headers, FirstChunk),
        VAcc1 = VAcc0#vacc{resp = Resp0},
        VAcc2 = lists:foldl(
            fun(Args, Acc0) ->
                {ok, Acc1} = couch_mrview:query_view(Db, DDoc, ViewName, Args, fun view_cb/2, Acc0),
                Acc1
            end,
            VAcc1,
            ArgQueries
        ),
        {ok, Resp1} = chttpd:send_delayed_chunk(VAcc2#vacc.resp, "\r\n]}"),
        {ok, Resp2} = chttpd:end_delayed_json_response(Resp1),
        {ok, VAcc2#vacc{resp = Resp2}}
    end),
    case is_record(Resp2, vacc) of
        true -> {ok, Resp2#vacc.resp};
        _ -> {ok, Resp2}
    end.

filtered_view_cb({row, Row0}, Acc) ->
    Row1 = lists:map(
        fun
            ({doc, null}) ->
                {doc, null};
            ({doc, Body}) ->
                Doc = couch_users_db:strip_non_public_fields(#doc{body = Body}),
                {doc, Doc#doc.body};
            (KV) ->
                KV
        end,
        Row0
    ),
    view_cb({row, Row1}, Acc);
filtered_view_cb(Obj, Acc) ->
    view_cb(Obj, Acc).

%% these clauses start (and possibly end) the response
view_cb({error, Reason}, #vacc{resp = undefined} = Acc) ->
    {ok, Resp} = chttpd:send_error(Acc#vacc.req, Reason),
    {ok, Acc#vacc{resp = Resp}};
view_cb(complete, #vacc{resp = undefined} = Acc) ->
    % Nothing in view
    {ok, Resp} = chttpd:send_json(Acc#vacc.req, 200, {[{rows, []}]}),
    {ok, Acc#vacc{resp = Resp}};
view_cb(Msg, #vacc{resp = undefined} = Acc) ->
    %% Start response
    Headers = [],
    {ok, Resp} = chttpd:start_delayed_json_response(Acc#vacc.req, 200, Headers),
    view_cb(Msg, Acc#vacc{resp = Resp, should_close = true});
%% ---------------------------------------------------

%% From here on down, the response has been started.

view_cb({error, Reason}, #vacc{resp = Resp} = Acc) ->
    {ok, Resp1} = chttpd:send_delayed_error(Resp, Reason),
    {ok, Acc#vacc{resp = Resp1}};
view_cb(complete, #vacc{resp = Resp, buffer = Buf, threshold = Max} = Acc) ->
    % Finish view output and possibly end the response
    {ok, Resp1} = chttpd:close_delayed_json_object(Resp, Buf, "\r\n]}", Max),
    case Acc#vacc.should_close of
        true ->
            {ok, Resp2} = chttpd:end_delayed_json_response(Resp1),
            {ok, Acc#vacc{resp = Resp2}};
        _ ->
            {ok, Acc#vacc{
                resp = Resp1,
                meta_sent = false,
                row_sent = false,
                prepend = ",\r\n",
                buffer = [],
                bufsize = 0
            }}
    end;
view_cb({meta, Meta}, #vacc{meta_sent = false, row_sent = false} = Acc) ->
    % Sending metadata as we've not sent it or any row yet
    Parts =
        case couch_util:get_value(total, Meta) of
            undefined -> [];
            Total -> [io_lib:format("\"total_rows\":~p", [Total])]
        end ++
            case couch_util:get_value(offset, Meta) of
                undefined -> [];
                Offset -> [io_lib:format("\"offset\":~p", [Offset])]
            end ++
            case couch_util:get_value(update_seq, Meta) of
                undefined ->
                    [];
                null ->
                    ["\"update_seq\":null"];
                UpdateSeq when is_integer(UpdateSeq) ->
                    [io_lib:format("\"update_seq\":~B", [UpdateSeq])];
                UpdateSeq when is_binary(UpdateSeq) ->
                    [io_lib:format("\"update_seq\":\"~s\"", [UpdateSeq])]
            end ++ ["\"rows\":["],
    Chunk = [prepend_val(Acc), "{", string:join(Parts, ","), "\r\n"],
    {ok, AccOut} = maybe_flush_response(Acc, Chunk, iolist_size(Chunk)),
    {ok, AccOut#vacc{prepend = "", meta_sent = true}};
view_cb({meta, _Meta}, #vacc{} = Acc) ->
    %% ignore metadata
    {ok, Acc};
view_cb({row, Row}, #vacc{meta_sent = false} = Acc) ->
    %% sorted=false and row arrived before meta
    % Adding another row
    Chunk = [prepend_val(Acc), "{\"rows\":[\r\n", row_to_json(Row)],
    maybe_flush_response(Acc#vacc{meta_sent = true, row_sent = true}, Chunk, iolist_size(Chunk));
view_cb({row, Row}, #vacc{meta_sent = true} = Acc) ->
    % Adding another row
    Chunk = [prepend_val(Acc), row_to_json(Row)],
    maybe_flush_response(Acc#vacc{row_sent = true}, Chunk, iolist_size(Chunk)).

maybe_flush_response(#vacc{bufsize = Size, threshold = Max} = Acc, Data, Len) when
    Size > 0 andalso (Size + Len) > Max
->
    #vacc{buffer = Buffer, resp = Resp} = Acc,
    {ok, R1} = chttpd:send_delayed_chunk(Resp, Buffer),
    {ok, Acc#vacc{prepend = ",\r\n", buffer = Data, bufsize = Len, resp = R1}};
maybe_flush_response(Acc0, Data, Len) ->
    #vacc{buffer = Buf, bufsize = Size} = Acc0,
    Acc = Acc0#vacc{
        prepend = ",\r\n",
        buffer = [Buf | Data],
        bufsize = Size + Len
    },
    {ok, Acc}.

prepend_val(#vacc{prepend = Prepend}) ->
    case Prepend of
        undefined ->
            "";
        _ ->
            Prepend
    end.

row_to_json(Row) ->
    Id = couch_util:get_value(id, Row),
    row_to_json(Id, Row).

row_to_json(error, Row) ->
    % Special case for _all_docs request with KEYS to
    % match prior behavior.
    Key = couch_util:get_value(key, Row),
    Val = couch_util:get_value(value, Row),
    Reason = couch_util:get_value(reason, Row),
    ReasonProp =
        if
            Reason == undefined -> [];
            true -> [{reason, Reason}]
        end,
    Obj = {[{key, Key}, {error, Val}] ++ ReasonProp},
    ?JSON_ENCODE(Obj);
row_to_json(Id0, Row) ->
    Id =
        case Id0 of
            undefined -> [];
            Id0 -> [{id, Id0}]
        end,
    Key = couch_util:get_value(key, Row, null),
    Val = couch_util:get_value(value, Row),
    Doc =
        case couch_util:get_value(doc, Row) of
            undefined -> [];
            Doc0 -> [{doc, Doc0}]
        end,
    Obj = {Id ++ [{key, Key}, {value, Val}] ++ Doc},
    ?JSON_ENCODE(Obj).

parse_params(#httpd{} = Req, Keys) ->
    parse_params(chttpd:qs(Req), Keys);
parse_params(Props, Keys) ->
    Args = #mrargs{extra = [{view_row_map, true}]},
    parse_params(Props, Keys, Args).

parse_params(Props, Keys, Args) ->
    parse_params(Props, Keys, Args, []).

parse_params(Props, Keys, #mrargs{} = Args0, Options) ->
    IsDecoded = lists:member(decoded, Options),
    Args1 =
        case lists:member(keep_group_level, Options) of
            true ->
                Args0;
            _ ->
                % group_level set to undefined to detect if explicitly set by user
                case Keys of
                    [_] ->
                        Args0#mrargs{group = undefined, group_level = undefined};
                    _ ->
                        Args0#mrargs{keys = Keys, group = undefined, group_level = undefined}
                end
        end,
    lists:foldl(
        fun({K, V}, Acc) ->
            parse_param(K, V, Acc, IsDecoded)
        end,
        Args1,
        Props
    ).

parse_body_and_query(#httpd{method = 'POST'} = Req, Keys) ->
    Props = chttpd:json_body_obj(Req),
    parse_body_and_query(Req, Props, Keys);
parse_body_and_query(Req, Keys) ->
    parse_params(
        chttpd:qs(Req),
        Keys,
        #mrargs{
            keys = Keys,
            group = undefined,
            group_level = undefined,
            extra = [{view_row_map, true}]
        },
        [keep_group_level]
    ).

parse_body_and_query(Req, {Props}, Keys) ->
    Args = #mrargs{
        keys = Keys, group = undefined, group_level = undefined, extra = [{view_row_map, true}]
    },
    BodyArgs0 = parse_params(Props, Keys, Args, [decoded]),
    BodyArgs1 =
        case is_view(Req) of
            true -> treat_single_keys_as_key(BodyArgs0);
            false -> BodyArgs0
        end,
    parse_params(chttpd:qs(Req), Keys, BodyArgs1, [keep_group_level]).

is_view(#httpd{path_parts = [_, _, _, <<"_view">> | _]}) ->
    true;
is_view(#httpd{}) ->
    false.

treat_single_keys_as_key(#mrargs{keys = Keys} = BodyArgs) ->
    case Keys of
        [_] -> BodyArgs#mrargs{keys = undefined};
        _ -> BodyArgs
    end.

parse_param(Key, Val, Args, IsDecoded) when is_binary(Key) ->
    parse_param(binary_to_list(Key), Val, Args, IsDecoded);
parse_param(Key, Val, Args, IsDecoded) ->
    case Key of
        "" ->
            Args;
        "reduce" ->
            Args#mrargs{reduce = parse_boolean(Val)};
        "key" when IsDecoded ->
            Args#mrargs{start_key = Val, end_key = Val};
        "key" ->
            JsonKey = ?JSON_DECODE(Val),
            Args#mrargs{start_key = JsonKey, end_key = JsonKey};
        "keys" when IsDecoded ->
            case Val of
                [SingleKey] -> Args#mrargs{start_key = SingleKey, end_key = SingleKey};
                _ -> Args#mrargs{keys = Val}
            end;
        "keys" ->
            case ?JSON_DECODE(Val) of
                [SingleKey] -> Args#mrargs{start_key = SingleKey, end_key = SingleKey};
                Keys -> Args#mrargs{keys = Keys}
            end;
        "startkey" when IsDecoded ->
            Args#mrargs{start_key = Val};
        "start_key" when IsDecoded ->
            Args#mrargs{start_key = Val};
        "startkey" ->
            Args#mrargs{start_key = ?JSON_DECODE(Val)};
        "start_key" ->
            Args#mrargs{start_key = ?JSON_DECODE(Val)};
        "startkey_docid" ->
            Args#mrargs{start_key_docid = couch_util:to_binary(Val)};
        "start_key_doc_id" ->
            Args#mrargs{start_key_docid = couch_util:to_binary(Val)};
        "endkey" when IsDecoded ->
            Args#mrargs{end_key = Val};
        "end_key" when IsDecoded ->
            Args#mrargs{end_key = Val};
        "endkey" ->
            Args#mrargs{end_key = ?JSON_DECODE(Val)};
        "end_key" ->
            Args#mrargs{end_key = ?JSON_DECODE(Val)};
        "endkey_docid" ->
            Args#mrargs{end_key_docid = couch_util:to_binary(Val)};
        "end_key_doc_id" ->
            Args#mrargs{end_key_docid = couch_util:to_binary(Val)};
        "limit" ->
            Args#mrargs{limit = parse_pos_int(Val)};
        "stale" when Val == "ok" orelse Val == <<"ok">> ->
            Args#mrargs{stable = true, update = false};
        "stale" when Val == "update_after" orelse Val == <<"update_after">> ->
            Args#mrargs{stable = true, update = lazy};
        "stale" ->
            throw({query_parse_error, <<"Invalid value for `stale`.">>});
        "stable" when Val == "true" orelse Val == <<"true">> orelse Val == true ->
            Args#mrargs{stable = true};
        "stable" when Val == "false" orelse Val == <<"false">> orelse Val == false ->
            Args#mrargs{stable = false};
        "stable" ->
            throw({query_parse_error, <<"Invalid value for `stable`.">>});
        "update" when Val == "true" orelse Val == <<"true">> orelse Val == true ->
            Args#mrargs{update = true};
        "update" when Val == "false" orelse Val == <<"false">> orelse Val == false ->
            Args#mrargs{update = false};
        "update" when Val == "lazy" orelse Val == <<"lazy">> ->
            Args#mrargs{update = lazy};
        "update" ->
            throw({query_parse_error, <<"Invalid value for `update`.">>});
        "descending" ->
            case parse_boolean(Val) of
                true -> Args#mrargs{direction = rev};
                _ -> Args#mrargs{direction = fwd}
            end;
        "skip" ->
            Args#mrargs{skip = parse_pos_int(Val)};
        "group" ->
            Args#mrargs{group = parse_boolean(Val)};
        "group_level" ->
            Args#mrargs{group_level = parse_pos_int(Val)};
        "inclusive_end" ->
            Args#mrargs{inclusive_end = parse_boolean(Val)};
        "include_docs" ->
            Args#mrargs{include_docs = parse_boolean(Val)};
        "attachments" ->
            case parse_boolean(Val) of
                true ->
                    Opts = Args#mrargs.doc_options,
                    Args#mrargs{doc_options = [attachments | Opts]};
                false ->
                    Args
            end;
        "att_encoding_info" ->
            case parse_boolean(Val) of
                true ->
                    Opts = Args#mrargs.doc_options,
                    Args#mrargs{doc_options = [att_encoding_info | Opts]};
                false ->
                    Args
            end;
        "update_seq" ->
            Args#mrargs{update_seq = parse_boolean(Val)};
        "conflicts" ->
            Args#mrargs{conflicts = parse_boolean(Val)};
        "callback" ->
            Args#mrargs{callback = couch_util:to_binary(Val)};
        "sorted" ->
            Args#mrargs{sorted = parse_boolean(Val)};
        "partition" ->
            Partition = couch_util:to_binary(Val),
            couch_partition:validate_partition(Partition),
            couch_mrview_util:set_extra(Args, partition, Partition);
        _ ->
            BKey = couch_util:to_binary(Key),
            BVal = couch_util:to_binary(Val),
            Args#mrargs{extra = [{BKey, BVal} | Args#mrargs.extra]}
    end.

parse_boolean(true) ->
    true;
parse_boolean(false) ->
    false;
parse_boolean(Val) when is_binary(Val) ->
    parse_boolean(?b2l(Val));
parse_boolean(Val) ->
    case string:to_lower(Val) of
        "true" ->
            true;
        "false" ->
            false;
        _ ->
            Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]),
            throw({query_parse_error, ?l2b(Msg)})
    end.

parse_int(Val) when is_integer(Val) ->
    Val;
parse_int(Val) ->
    case (catch list_to_integer(Val)) of
        IntVal when is_integer(IntVal) ->
            IntVal;
        _ ->
            Msg = io_lib:format("Invalid value for integer: ~p", [Val]),
            throw({query_parse_error, ?l2b(Msg)})
    end.

parse_pos_int(Val) ->
    case parse_int(Val) of
        IntVal when IntVal >= 0 ->
            IntVal;
        _ ->
            Fmt = "Invalid value for positive integer: ~p",
            Msg = io_lib:format(Fmt, [Val]),
            throw({query_parse_error, ?l2b(Msg)})
    end.

check_view_etag(Sig, Acc0, Req) ->
    ETag = chttpd:make_etag(Sig),
    case chttpd:etag_match(Req, ETag) of
        true -> throw({etag_match, ETag});
        false -> {ok, Acc0#vacc{etag = ETag}}
    end.
