%%% Copyright (c) 2014-2015, NORDUnet A/S.
%%% See LICENSE for licensing information.

%%% @doc Certificate Transparency (RFC 6962)

-module(v1).
%% API (URL)
-export([request/3]).

%% Public functions, i.e. part of URL.
request(post, "ct/v1/add-chain", Input) ->
    add_chain(Input, normal);

request(post, "ct/v1/add-pre-chain", Input) ->
    add_chain(Input, precert);

request(get, "ct/v1/get-sth", _Query) ->
    case plop:sth() of
        noentry ->
            lager:error("No valid STH found"),
            internalerror("No valid STH found");
        R ->
            success(R)
    end;

request(get, "ct/v1/get-sth-consistency", Query) ->
    case lists:sort(Query) of
        [{"first", FirstInput}, {"second", SecondInput}] ->
            {First, _} = string:to_integer(FirstInput),
            {Second, _} = string:to_integer(SecondInput),
            case lists:member(error, [First, Second]) of
                true ->
                    err400("get-sth-consistency: bad input:",
                           [FirstInput, SecondInput]);
                false ->
                    success(
                      {[{consistency,
                         [base64:encode(X) ||
                             X <- plop:consistency(First, Second)]}]})
            end;
        _ -> err400("get-sth-consistency: bad input:", Query)
    end;

request(get, "ct/v1/get-proof-by-hash", Query) ->
    case lists:sort(Query) of
        [{"hash", HashInput}, {"tree_size", TreeSizeInput}] ->
            Hash = case (catch base64:decode(HashInput)) of
                       {'EXIT', _} -> error;
                       H -> H
                   end,
            {TreeSize, _} = string:to_integer(TreeSizeInput),
            case lists:member(error, [Hash, TreeSize]) of
                true ->
                    err400("get-proof-by-hash: bad input:",
                           [HashInput, TreeSizeInput]);
                false ->
                      case plop:inclusion(Hash, TreeSize) of
                          {ok, Index, Path} ->
                              success({[{leaf_index, Index},
                                        {audit_path,
                                         [base64:encode(X) || X <- Path]}]});
                          {notfound, Msg} ->
                              err400("get-proof-by-hash: hash not found", Msg)
                      end
            end;
        _ -> err400("get-proof-by-hash: bad input:", Query)
    end;

request(get, "ct/v1/get-entries", Query) ->
    case lists:sort(Query) of
        [{"end", EndInput}, {"start", StartInput}] ->
            {Start, _} = string:to_integer(StartInput),
            {End, _} = string:to_integer(EndInput),
            case lists:member(error, [Start, End]) of
                true -> err400("get-entries: bad input:", [Start, End]);
                false -> success(
                           catlfish:entries(Start, min(End, Start + 999)))
            end;
        _ -> err400("get-entries: bad input:", Query)
    end;

request(get, "ct/v1/get-entry-and-proof", Query) ->
    case lists:sort(Query) of
        [{"leaf_index", IndexInput}, {"tree_size", TreeSizeInput}] ->
            {Index, _} = string:to_integer(IndexInput),
            {TreeSize, _} = string:to_integer(TreeSizeInput),
            case lists:member(error, [Index, TreeSize]) of
                true ->
                    err400("get-entry-and-proof: not integers: ",
                           [IndexInput, TreeSizeInput]);
                false -> success(catlfish:entry_and_proof(Index, TreeSize))
            end;
        _ -> err400("get-entry-and-proof: bad input:", Query)
    end;

request(get, "ct/v1/get-roots", _Query) ->
    R = [{certificates,
          [base64:encode(Der) ||
              Der <- catlfish:update_known_roots()]}],
    success({R});

request(_Method, _Path, _) ->
    none.

%% Private functions.
err400(Text, Input) ->
    {400, [{"Content-Type", "text/html"}],
     io_lib:format(
       "<html><body><p>~n" ++
           "~s~n" ++
           "~p~n" ++
           "</body></html>~n", [Text, Input])}.

success(Data) ->
    {200, [{"Content-Type", "text/json"}], mochijson2:encode(Data)}.

internalerror(Text) ->
    {500, [{"Content-Type", "text/html"}],
     io_lib:format(
       "<html><body><p>~n" ++
           "~s~n" ++
           "</body></html>~n", [Text])}.

-spec add_chain(any(), normal|precert) -> any().
add_chain(Input, Type) ->
    case (catch mochijson2:decode(Input)) of
        {error, E} ->
            err400("add-chain: bad input:", E);
        {struct, [{<<"chain">>, ChainB64List}]} ->
            case decode_chain(ChainB64List) of
                [LeafCert | CertChain] ->
                    case x509:normalise_chain(catlfish:known_roots(),
                                              [LeafCert|CertChain]) of
                        {ok, [Leaf | Chain]} ->
                            lager:info("adding ~p cert ~p",
                                       [Type, x509:cert_string(LeafCert)]),
                            success(catlfish:add_chain(Leaf, Chain, Type));
                        {error, Reason} ->
                            lager:info("rejecting ~p: ~p",
                                       [x509:cert_string(LeafCert), Reason]),
                            err400("add-chain: invalid chain", Reason)
                    end;
                {invalid, ErrText} ->
                    err400(io:format("add-chain: ~p", [ErrText]), [ChainB64List])
            end;
        _ -> err400("add-chain: missing input: chain", Input)
    end.

-spec decode_chain(string()) -> {invalid, string()} | [binary()].
decode_chain(B64List) ->
    case (catch [base64:decode(X) || X <- B64List]) of
        {'EXIT', _} -> {invalid, "invalid base64-encoded chain"};
        L -> L
    end.