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

-export([
    replication_id/1,
    replication_id/2,
    convert/1
]).

-include_lib("ibrowse/include/ibrowse.hrl").

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

% replication_id/1 and replication_id/2 will attempt to fetch
% filter code for filtered replications. If fetching or parsing
% of the remotely fetched filter code fails they throw:
%  {filter_fetch_error, Error} exception.
%

replication_id(#rep{options = Options} = Rep) ->
    BaseId = replication_id(Rep, ?REP_ID_VERSION),
    {BaseId, maybe_append_options([continuous, create_target], Options)}.

% Versioned clauses for generating replication IDs.
% If a change is made to how replications are identified,
% please add a new clause and increase ?REP_ID_VERSION.

replication_id(#rep{} = Rep, 4) ->
    UUID = couch_server:get_uuid(),
    SrcInfo = get_v4_endpoint(Rep#rep.source),
    TgtInfo = get_v4_endpoint(Rep#rep.target),
    maybe_append_filters([UUID, SrcInfo, TgtInfo], Rep);
replication_id(#rep{} = Rep, 3) ->
    UUID = couch_server:get_uuid(),
    Src = get_rep_endpoint(Rep#rep.source),
    Tgt = get_rep_endpoint(Rep#rep.target),
    maybe_append_filters([UUID, Src, Tgt], Rep);
replication_id(#rep{} = Rep, 2) ->
    {ok, HostName} = inet:gethostname(),
    Port =
        case (catch mochiweb_socket_server:get(couch_httpd, port)) of
            P when is_number(P) ->
                P;
            _ ->
                % On restart we might be called before the couch_httpd process is
                % started.
                % TODO: we might be under an SSL socket server only, or both under
                % SSL and a non-SSL socket.
                % ... mochiweb_socket_server:get(https, port)
                config:get_integer("httpd", "port", 5984)
        end,
    Src = get_rep_endpoint(Rep#rep.source),
    Tgt = get_rep_endpoint(Rep#rep.target),
    maybe_append_filters([HostName, Port, Src, Tgt], Rep);
replication_id(#rep{} = Rep, 1) ->
    {ok, HostName} = inet:gethostname(),
    Src = get_rep_endpoint(Rep#rep.source),
    Tgt = get_rep_endpoint(Rep#rep.target),
    maybe_append_filters([HostName, Src, Tgt], Rep).

-spec convert([_] | binary() | {string(), string()}) -> {string(), string()}.
convert(Id) when is_list(Id) ->
    convert(?l2b(Id));
convert(Id0) when is_binary(Id0) ->
    % Spaces can result from mochiweb incorrectly unquoting + characters from
    % the URL path. So undo the incorrect parsing here to avoid forcing
    % users to url encode + characters.
    Id = binary:replace(Id0, <<" ">>, <<"+">>, [global]),
    lists:splitwith(fun(Char) -> Char =/= $+ end, ?b2l(Id));
convert({BaseId, Ext} = Id) when is_list(BaseId), is_list(Ext) ->
    Id.

% Private functions

maybe_append_filters(
    Base,
    #rep{source = Source, options = Options}
) ->
    Base2 =
        Base ++
            case couch_replicator_filters:parse(Options) of
                {ok, nil} ->
                    [];
                {ok, {view, Filter, QueryParams}} ->
                    [Filter, QueryParams];
                {ok, {user, {Doc, Filter}, QueryParams}} ->
                    case couch_replicator_filters:fetch(Doc, Filter, Source) of
                        {ok, Code} ->
                            [Code, QueryParams];
                        {error, Error} ->
                            throw({filter_fetch_error, Error})
                    end;
                {ok, {docids, DocIds}} ->
                    [DocIds];
                {ok, {mango, Selector}} ->
                    [Selector];
                {error, FilterParseError} ->
                    throw({error, FilterParseError})
            end,
    Base3 =
        Base2 ++
            case couch_util:get_value(winning_revs_only, Options) of
                undefined ->
                    [];
                false ->
                    [];
                true ->
                    [<<"winning_revs_only">>]
            end,
    Base4 =
        Base3 ++
            case couch_util:get_value(since_seq, Options) of
                undefined ->
                    [];
                SinceSeq ->
                    [SinceSeq]
            end,
    couch_util:to_hex(couch_hash:md5_hash(?term_to_bin(Base4))).

maybe_append_options(Options, RepOptions) ->
    lists:foldl(
        fun(Option, Acc) ->
            Acc ++
                case couch_util:get_value(Option, RepOptions, false) of
                    true ->
                        "+" ++ atom_to_list(Option);
                    false ->
                        ""
                end
        end,
        [],
        Options
    ).

get_rep_endpoint(#httpdb{url = Url, headers = Headers}) ->
    DefaultHeaders = (#httpdb{})#httpdb.headers,
    {remote, Url, Headers -- DefaultHeaders}.

get_v4_endpoint(#httpdb{} = HttpDb) ->
    {remote, Url, Headers} = get_rep_endpoint(HttpDb),
    {User, _} = couch_replicator_utils:get_basic_auth_creds(HttpDb),
    {Host, NonDefaultPort, Path} = get_v4_url_info(Url),
    % Keep this to ensure checkpoints don't change
    OAuth = undefined,
    {remote, User, Host, NonDefaultPort, Path, Headers, OAuth}.

get_v4_url_info(Url) when is_binary(Url) ->
    get_v4_url_info(binary_to_list(Url));
get_v4_url_info(Url) ->
    case ibrowse_lib:parse_url(Url) of
        {error, invalid_uri} ->
            % Tolerate errors here to avoid a bad user document
            % crashing the replicator
            {Url, undefined, undefined};
        #url{
            protocol = Schema,
            host = Host,
            port = Port,
            path = Path
        } ->
            NonDefaultPort = get_non_default_port(Schema, Port),
            {Host, NonDefaultPort, Path}
    end.

get_non_default_port(https, 443) ->
    default;
get_non_default_port(http, 80) ->
    default;
get_non_default_port(http, 5984) ->
    default;
get_non_default_port(_Schema, Port) ->
    Port.

-ifdef(TEST).

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

winning_revs_id_test_() ->
    {
        foreach,
        fun test_util:start_couch/0,
        fun test_util:stop_couch/1,
        [
            ?TDEF_FE(winning_revs_generates_new_id),
            ?TDEF_FE(winning_revs_false_same_as_undefined)
        ]
    }.

winning_revs_generates_new_id(_) ->
    RepDoc1 = [
        {<<"source">>, <<"http://foo.example.bar">>},
        {<<"target">>, <<"http://bar.example.foo">>}
    ],
    Rep1 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc1}),
    Id1 = replication_id(Rep1),
    RepDoc2 = RepDoc1 ++ [{<<"winning_revs_only">>, true}],
    Rep2 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc2}),
    Id2 = replication_id(Rep2),
    ?assertNotEqual(Id1, Id2).

winning_revs_false_same_as_undefined(_) ->
    RepDoc1 = [
        {<<"source">>, <<"http://foo.example.bar">>},
        {<<"target">>, <<"http://bar.example.foo">>}
    ],
    Rep1 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc1}),
    Id1 = replication_id(Rep1),
    RepDoc2 = RepDoc1 ++ [{<<"winning_revs_only">>, false}],
    Rep2 = couch_replicator_parse:parse_rep_doc_without_id({RepDoc2}),
    Id2 = replication_id(Rep2),
    ?assertEqual(Id1, Id2).

replication_id_convert_test_() ->
    [
        ?_assertEqual(Expected, convert(Id))
     || {Expected, Id} <- [
            {{"abc", ""}, "abc"},
            {{"abc", ""}, <<"abc">>},
            {{"abc", "+x+y"}, <<"abc+x+y">>},
            {{"abc", "+x+y"}, {"abc", "+x+y"}},
            {{"abc", "+x+y"}, <<"abc x y">>}
        ]
    ].

http_v4_endpoint_test_() ->
    [
        ?_assertMatch(
            {remote, User, Host, Port, Path, HeadersNoAuth, undefined},
            begin
                HttpDb = #httpdb{url = Url, headers = Headers, auth_props = Auth},
                HttpDb1 = couch_replicator_utils:normalize_basic_auth(HttpDb),
                get_v4_endpoint(HttpDb1)
            end
        )
     || {{User, Host, Port, Path, HeadersNoAuth}, {Url, Headers, Auth}} <- [
            {
                {undefined, "host", default, "/", []},
                {"http://host", [], []}
            },
            {
                {undefined, "host", default, "/", []},
                {"https://host", [], []}
            },
            {
                {undefined, "host", default, "/", []},
                {"http://host:5984", [], []}
            },
            {
                {undefined, "host", 1, "/", []},
                {"http://host:1", [], []}
            },
            {
                {undefined, "host", 2, "/", []},
                {"https://host:2", [], []}
            },
            {
                {undefined, "host", default, "/", [{"h", "v"}]},
                {"http://host", [{"h", "v"}], []}
            },
            {
                {undefined, "host", default, "/a/b", []},
                {"http://host/a/b", [], []}
            },
            {
                {"user", "host", default, "/", []},
                {"http://user:pass@host", [], []}
            },
            {
                {"user", "host", 3, "/", []},
                {"http://user:pass@host:3", [], []}
            },
            {
                {"user", "host", default, "/", []},
                {"http://user:newpass@host", [], []}
            },
            {
                {"user", "host", default, "/", []},
                {"http://host", [basic_auth("user", "pass")], []}
            },
            {
                {"user", "host", default, "/", []},
                {"http://host", [basic_auth("user", "newpass")], []}
            },
            {
                {"user3", "host", default, "/", []},
                {"http://user1:pass1@host", [basic_auth("user2", "pass2")],
                    auth_props("user3", "pass3")}
            },
            {
                {"user2", "host", default, "/", [{"h", "v"}]},
                {"http://host", [{"h", "v"}, basic_auth("user", "pass")],
                    auth_props("user2", "pass2")}
            },
            {
                {"user", "host", default, "/", [{"h", "v"}]},
                {"http://host", [{"h", "v"}], auth_props("user", "pass")}
            },
            {
                {undefined, "random_junk", undefined, undefined},
                {"random_junk", [], []}
            },
            {
                {undefined, "host", default, "/", []},
                {"http://host", [{"Authorization", "Basic bad"}], []}
            }
        ]
    ].

basic_auth(User, Pass) ->
    B64Auth = base64:encode_to_string(User ++ ":" ++ Pass),
    {"Authorization", "Basic " ++ B64Auth}.

auth_props(User, Pass) when is_list(User), is_list(Pass) ->
    [
        {<<"basic">>,
            {[
                {<<"username">>, list_to_binary(User)},
                {<<"password">>, list_to_binary(Pass)}
            ]}}
    ].

-endif.
