-module(ts_api).
-compile(export_all).

-include("ts_db_records.hrl").
-include("yaws_api.hrl").

out(YArg) ->
    % retreive the session data
    Session = ts_api_session:get_session(YArg),

    %get the app mod data
    PathString = YArg#arg.appmoddata,

    % split the path
    PathElements = case PathString of
        undefined -> []; %handle no end slash: /ts_api
        _Any -> string:tokens(PathString, "/")
    end,

    % process the request
    case catch dispatch_request(YArg, Session, PathElements) of
        {'EXIT', Err} ->
            % TODO: log error internally
            error_logger:error_report("TimeStamper: ~p", [Err]),
            io:format("Error: ~n~p", [Err]),
            make_json_500(YArg, Err);
        Other -> Other
    end.

% ================================== %
% ======== DISPATCH METHODS ======== %
% ================================== %

%% Entry point to the TimeStamper API dispatch system
dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{"see_docs", "/ts_api_doc"}]);

dispatch_request(YArg, Session, [H|T]) ->

    case {Session, H} of
        {_, "login"}  -> do_login(YArg);
        {_, "logout"} -> do_logout(YArg);

        {not_logged_in, _} -> make_json_401(YArg);
        {session_expired, _} -> make_json_401(YArg, [{"error", "session expired"}]);

        {_S, "app"}       -> dispatch_app(YArg, Session, T);
        {_S, "users"}     -> dispatch_user(YArg, Session, T);
        {_S, "timelines"} -> dispatch_timeline(YArg, Session, T);
        {_S, "entries"}   -> dispatch_entry(YArg, Session, T);
        {_S, _Other}    -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/"}])
    end.

% -------- Dispatch for /app -------- %

dispatch_app(YArg, Session, Params) ->
    HTTPMethod = (YArg#arg.req)#http_request.method,

    case {HTTPMethod, Params} of

        {'OPTIONS', ["user_summary", _]} -> make_CORS_options(YArg, "GET");

        {'GET', ["user_summary", UsernameStr]} ->
            case {Session#ts_api_session.username,
                  UsernameStr} of

                {Username, Username} -> get_user_summary(YArg, Username);
                _ -> make_json_401(YArg)
            end;

        {_BadMethod, ["user_summary", _UsernameStr]} ->
            make_json_405(YArg, [{"see_docs", "/ts_api_docs/app.html"}]);

        _Other -> make_json_404(YArg, [{"see_docs", "/ts_api_docs/app.html"}])

    end.

% -------- Dispatch for /user -------- %

dispatch_user(YArg, Session, []) ->
    dispatch_user(YArg, Session, [Session#ts_api_session.username]);

dispatch_user(YArg, Session, [Username]) ->
    HTTPMethod = (YArg#arg.req)#http_request.method,

    % compare to the logged-in user
    case {HTTPMethod, Session#ts_api_session.username} of

        {'OPTIONS', Username} -> make_CORS_options(YArg, "GET, PUT");
        {'GET', Username}     -> get_user(YArg, Username);
        {'PUT', Username}     -> put_user(YArg, Username);

        {_BadMethod, Username} ->
            make_json_405(YArg, [{"see_docs", "/ts_api_doc/users.html"}]);

        _Other -> make_json_401(YArg, [{"see_docs", "/ts_api_doc/users.html"}])
    end.

% -------- Dispatch for /timeline -------- %

dispatch_timeline(YArg, _Session, []) ->
    make_json_404(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}]);

dispatch_timeline(YArg, Session, [Username|_T] = PathElements) ->

    case Session#ts_api_session.username of
        Username -> dispatch_timeline(YArg, PathElements);
        _Other   -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/users.html"}])
    end.

% just username, list timelines
dispatch_timeline(YArg, [Username]) ->
    HTTPMethod = (YArg#arg.req)#http_request.method,

    case HTTPMethod of
        'OPTIONS' -> make_CORS_options(YArg, "GET");
        'GET'     -> list_timelines(YArg, Username);
        _Other    -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}])
    end;
    
dispatch_timeline(YArg, [Username, TimelineId]) ->
    HTTPMethod = (YArg#arg.req)#http_request.method,

    case HTTPMethod of
        'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE");
        'GET'    -> get_timeline(YArg, Username, TimelineId);
        'PUT'    -> put_timeline(YArg, Username, TimelineId);
        'DELETE' -> delete_timeline(YArg, Username, TimelineId);
        _Other   -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}])
    end;

dispatch_timeline(YArg, _Other) ->
    make_json_404(YArg, [{"see_docs", "/ts_api_doc/timelines.html"}]).

% -------- Dispatch for /entry -------- %

dispatch_entry(YArg, _Session, []) ->
    make_json_404(YArg, [{"see_docs", "/ts_aip_doc/entries.html"}]);

dispatch_entry(YArg, Session, [Username|_T] = PathElements) ->

    case Session#ts_api_session.username of
        Username -> dispatch_entry(YArg, PathElements);
        _Other   -> make_json_404(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
    end.
    
dispatch_entry(YArg, [Username, TimelineId]) ->
    HTTPMethod = (YArg#arg.req)#http_request.method,

    case HTTPMethod of
        'OPTIONS' -> make_CORS_options(YArg, "GET, POST");
        'GET'     -> list_entries(YArg, Username, TimelineId);
        'POST'    -> post_entry(YArg, Username, TimelineId);
        _Other    -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
    end;

dispatch_entry(YArg, [Username, TimelineId, UrlEntryId]) ->
    EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers
    HTTPMethod = (YArg#arg.req)#http_request.method,

    case HTTPMethod of
        'OPTIONS'-> make_CORS_options(YArg, "GET, PUT, DELETE");
        'GET'    -> get_entry(YArg, Username, TimelineId, EntryId);
        'PUT'    -> put_entry(YArg, Username, TimelineId, EntryId);
        'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId);
        _Other   -> make_json_405(YArg, [{"see_docs", "/ts_api_doc/entries.html"}])
    end;

dispatch_entry(YArg, _Other) ->
    make_json_404(YArg, [{"see_docs", "/ts_api_doc/entries.html"}]).

% ============================== %
% ======== IMPLEMENTATION ====== %
% ============================== %

do_login(YArg) ->
    EJSON = parse_json_body(YArg),

    {struct, Fields} = EJSON,

    case {lists:keyfind(username, 1, Fields),
          lists:keyfind(password, 1, Fields)} of

        % username and password found
        {{username, Username}, {password, Password}} ->

            % check the uname, password
            case ts_user:check_credentials(Username, Password) of
                % they are good
                true ->
                    {CookieVal, _Session} = ts_api_session:new(Username),

                    [{header, {set_cookie, io_lib:format(
                        "ts_api_session=~s; Path=/",
                            [CookieVal])}},
                     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
                     {header, ["Access-Control-Allow-Credentials: ", "true"]},
                     {content, "application/json",
                        json:encode({struct, [{"status", "ok"}]})}];

                % they are not good
                false -> make_json_401(YArg, [{"error",
                    "bad username/password combination"}])
            end;

        _Other -> make_json_400(YArg, [{"see_docs", "/ts_api_doc/login.html"}])
    end.

do_logout(YArg) ->
    Cookie = (YArg#arg.headers)#headers.cookie,
    CookieVal = yaws_api:find_cookie_val("ts_api_session", Cookie),
    ts_api_session:logout(CookieVal),
    [{status, 200},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

get_user_summary(YArg, Username) ->
    % find user record
    case ts_user:lookup(Username) of

        % no user record, barf
        no_record -> make_json_404(YArg);

        % found user record, let us build the return
        User ->

            % get user extended data properties
            UserExtData = ts_ext_data:get_properties(User),
            % convert to intermediate JSON form
            EJSONUser = ts_json:record_to_ejson(User, UserExtData),

            % get the user's timelins
            Timelines = ts_timeline:list(Username, 0, 100),

            % get each timeline's extended data and convert to EJSON
            EJSONTimelines = {array,
                lists:map(
                    fun(Timeline) ->
                        ts_json:record_to_ejson(Timeline,
                            ts_ext_data:get_properties(Timeline))
                    end,
                    Timelines)},

            % write response out
            make_json_200(YArg, {struct,
               [{user, EJSONUser},
                {timelines, EJSONTimelines}
            ]})
    end.

get_user(YArg, Username) ->
    % find the user record
    case ts_user:lookup(Username) of
        % no such user, barf
        no_record -> make_json_404(YArg);
        % found, return a 200 with the record
        User -> make_json_200_record(YArg, User)
    end.

put_user(YArg, Username) ->

    % parse the POST data
    EJSON = parse_json_body(YArg),

    {UR, ExtData} =
        try ts_json:ejson_to_record_strict(#ts_user{username=Username}, EJSON)
        catch throw:{InputError, StackTrace} ->
            error_logger:error_report("Bad input in put_user/2: ~p",
                [InputError]),
            throw(make_json_400(YArg, [{request_error, InputError}]))
        end,

    % update the record (we do not support creating users via the API right now)
    {ok, UpdatedRec} = ts_user:update(UR, ExtData),

    % return a 200
    make_json_200_record(YArg, UpdatedRec).

list_timelines(YArg, Username) -> 
    % pull out the POST data
    QueryData = yaws_api:parse_query(YArg),

    % read or default the Start
    Start = case lists:keyfind("start", 1, QueryData) of
        {"start", StartVal} -> list_to_integer(StartVal);
        false -> 0
    end,

    % read or default the Length
    Length = case lists:keyfind(length, 1, QueryData) of
        {"length", LengthVal} ->
            erlang:min(list_to_integer(LengthVal), 50);
        false -> 50
    end,

    % list the timelines from the database
    Timelines = ts_timeline:list(Username, Start, Length),

    % convert them all to their EJSON form, adding in extended data for each
    EJSONTimelines = {array, lists:map(
        fun (Timeline) ->
            ts_json:record_to_ejson(Timeline, 
                ts_ext_data:get_properties(Timeline))
        end,
        Timelines)},

    % return response
    make_json_200(YArg, EJSONTimelines).

get_timeline(YArg, Username, TimelineId) ->
    % look for timeline
    case ts_timeline:lookup(Username, TimelineId) of
        % no such timeline, return 404
        no_record -> make_json_404(YArg, [{"error", "no such timeline"}]);
        % return the timeline data
        Timeline -> make_json_200_record(YArg, Timeline)
    end.

put_timeline(YArg, Username, TimelineId) ->
    % parse the POST data
    EJSON = parse_json_body(YArg),
    %{struct, Fields} = EJSON,

    % parse into a timeline record
    {TR, ExtData} =
        try ts_json:ejson_to_record_strict(
            #ts_timeline{ref={Username, TimelineId}}, EJSON)
        % we can not parse it, tell the user
        catch throw:{InputError, _StackTrace} ->
            error_logger:error_report("Bad input: ~p", [InputError]),
            throw(make_json_400(YArg, [{request_error, InputError}]))
        end,

    % write the changes.
    ts_timeline:write(TR, ExtData),

    % return a 200
    make_json_200_record(YArg, TR).

delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}.

list_entries(YArg, Username, TimelineId) ->
    % pull out the POST data
    QueryData = yaws_api:parse_query(YArg),

    % first determine if we are listing by date
    case {ts_timeline:lookup(Username, TimelineId),
          lists:keyfind("byDate", 1, QueryData)} of

        {no_record, _ByDateField} -> make_json_404(
            [{"error", "no such timeline"},
             {"see_docs", "/ts_api_doc/entries.html#LIST"}]);

        % listing by date range
        {Timeline, {"byDate", "true"}} -> 

            % look for the start date; default to the beginning of the timeline
            StartDate = case lists:keyfind("startDate", 1, QueryData) of
                % TODO: error handling if the date is badly formatted
                {"startDate", StartDateVal} -> ts_json:decode_datetime(StartDateVal);
                false -> Timeline#ts_timeline.created
            end,

            % look for end date; default to right now
            EndDate = case lists:keyfind("endDate", 1, QueryData) of
                % TODO: error handling if the date is badly formatted
                {"endDate", EndDateVal} -> ts_json:decode_datetime(EndDateVal);
                false -> calendar:now_to_universal_time(erlang:now())
            end,

            % read sort order and list entries
            Entries = case lists:keyfind("order", 1, QueryData) of
                % descending sort order
                {"order", "desc"} -> ts_entry:list_desc(
                        {Username, TimelineId}, StartDate, EndDate);

                % ascending order--{other, asc}--and default
                _Other -> ts_entry:list_asc(
                        {Username, TimelineId}, StartDate, EndDate)
            end,

            EJSONEntries = {array, lists:map(
                fun (Entry) ->
                    ts_json:record_to_ejson(Entry,
                        ts_ext_data:get_properties(Entry))
                end,
                Entries)},

            make_json_200(YArg, EJSONEntries);

        % listing by table position
        _Other ->

            % read or default the Start
            Start = case lists:keyfind("start", 1, QueryData) of
                {"start", StartVal} -> list_to_integer(StartVal);
                false -> 0
            end,

            % read or default the Length
            Length = case lists:keyfind("length", 1, QueryData) of
                {"length", LengthVal} ->
                    erlang:min(list_to_integer(LengthVal), 500);
                false -> 50
            end,

            % read sort order and list entries
            Entries = case lists:keyfind("order", 1, QueryData) of
                {"order", "desc"} -> ts_entry:list_desc(
                        {Username, TimelineId}, Start, Length);

                _UnknownOrder -> ts_entry:list_asc(
                        {Username, TimelineId}, Start, Length)
            end,

            EJSONEntries = {array, lists:map(
                fun (Entry) ->
                    ts_json:record_to_ejson(Entry,
                        ts_ext_data:get_properties(Entry))
                end,
                Entries)},

            make_json_200(YArg, EJSONEntries)
    end.

get_entry(YArg, Username, TimelineId, EntryId) ->
    case ts_entry:lookup(Username, TimelineId, EntryId) of
        % no such entry
        no_record -> make_json_404(YArg, [{"error", "no such entry"}]);
        % return the entry data
        Entry -> make_json_200_record(YArg, Entry)
    end.

post_entry(YArg, Username, TimelineId) ->

    % parse the request body
    EJSON = parse_json_body(YArg),

    % parse into ts_entry record
    {ER, ExtData} = try ts_json:ejson_to_record_strict(
            #ts_entry{ref = {Username, TimelineId, undefined}}, EJSON)
         catch _:InputError ->
            error_logger:error_report("Bad input: ~p", [InputError]),
            throw(make_json_400(YArg, [{request_error, InputError}]))
         end,

    case ts_entry:new(ER, ExtData) of
        % record created
        {ok, CreatedRecord} ->
            
            [{status, 201}, make_json_200_record(YArg, CreatedRecord)];

        OtherError -> 
            error_logger:error_report("Could not create entry: ~p", [OtherError]),
            make_json_500(YArg, OtherError)
    end.

put_entry(YArg, Username, TimelineId, EntryId) -> 
    
    % parse the POST data
    EJSON = parse_json_body(YArg),

    % parse into ts_entry record
    {ER, ExtData} = try ts_json:ejson_to_record_strict(
            #ts_entry{ref={Username, TimelineId, EntryId}}, EJSON)
         catch _:InputError ->
            error_logger:error_report("Bad input: ~p", [InputError]),
            throw(make_json_400(YArg))
         end,

    ts_entry:write(ER, ExtData),
    make_json_200_record(YArg, ER).

delete_entry(YArg, Username, TimelineId, EntryId) -> 

    % find the record to delete
    case ts_entry:lookup(Username, TimelineId, EntryId) of

        no_record -> make_json_404(YArg);

        Record ->
            % try to delete
            case ts_entry:delete(Record) of
                ok -> {status, 200};
                Error ->
                    error_logger:error_report("Error occurred deleting entry record: ~p", [Error]),
                    make_json_500(YArg, Error)
            end
    end.

% ============================== %
% ======== UTIL METHODS ======== %
% ============================== %

parse_json_body(YArg) ->
    case catch json:decode([], binary_to_list(YArg#arg.clidata)) of
        {done, {ok, EJSON}, _} -> EJSON;
        Error ->
            % TODO: log error internally
            error_logger:error_report("Error parsing JSON request body: ~p", [Error]),
            throw(make_json_400(YArg))
    end.

get_origin_header(YArg) ->
    Headers = (YArg#arg.headers)#headers.other,
    case lists:keyfind("Origin", 3, Headers) of
        false -> "*";
        {http_header, 0, "Origin", _, Origin}  -> Origin;
        _ -> make_json_500(YArg, "Unrecognized Origin header.")
    end.

make_CORS_options(YArg, AllowedMethods) ->
    [{status, 200}, 
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Methods: ", AllowedMethods]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_CORS_options(_YArg, AllowedOrigins, AllowedMethods) ->
    [{status, 200}, 
     {header, ["Access-Control-Allow-Origin: ", AllowedOrigins]},
     {header, ["Access-Control-Allow-Methods: ", AllowedMethods]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

%% Create a JSON 200 response.
make_json_200(YArg, EJSONResponse) ->
    JSONResponse = json:encode(EJSONResponse),
    [{content, "application/json", JSONResponse},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].


make_json_200_record(YArg, Record) ->
    RecordExtData = ts_ext_data:get_properties(Record),
    EJSON = ts_json:record_to_ejson(Record, RecordExtData),
    JSONResponse = json:encode(EJSON),
    [{content, "application/json", JSONResponse},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_json_400(YArg) -> make_json_400(YArg, []).
make_json_400(YArg, Fields) ->
    F1 = case lists:keyfind(status, 1, Fields) of
        false -> Fields ++ [{"status", "bad request"}];
        _Else -> Fields
    end,

    [{status, 400}, {content, "application/json", json:encode({struct, F1})},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_json_401(YArg) -> make_json_401(YArg, []).
make_json_401(YArg, Fields) ->
    % add default status if not provided
    F1 = case lists:keyfind(status, 1, Fields) of
        false -> Fields ++ [{"status", "unauthorized"}];
        _Else -> Fields
    end,

    [{status, 401},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]},
     {content, "application/json", json:encode({struct, F1})}].

%% Create a JSON 404 response.
make_json_404(YArg) -> make_json_404(YArg, []).
make_json_404(YArg, Fields) ->
    % add default status if not provided
    F1 = case lists:keyfind(status, 1, Fields) of
        false -> Fields ++ [{"status", "not found"}];
        _Else -> Fields
    end,

    % add the path they requested
    F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}],

    [{status, 404}, {content, "application/json", json:encode({struct, F2})},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_json_405(YArg) -> make_json_405(YArg, []).
make_json_405(YArg, Fields) ->
    % add default status if not provided
    F1 = case lists:keyfind(status, 1, Fields) of
        false -> Fields ++ [{"status", "method not allowed"}];
        _Else -> Fields
    end,

    % add the path they requested
    % F2 = F1 ++ [{path, io_lib:format("~p", [(YArg#arg.req)#http_request.path])}],

    [{status, 405}, {content, "application/json", json:encode({struct, F1})},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_json_500(YArg, Error) ->
    io:format("Error: ~n~p", [Error]),
    EJSON = {struct, [
        {"status", "internal server error"},
        {"error", lists:flatten(io_lib:format("~p", [Error]))}]},
    [{status, 500}, {content, "application/json", json:encode(EJSON)},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].

make_json_500(YArg) ->
    EJSON = {struct, [
        {"status", "internal server error"}]},
    [{status, 500}, {content, "application/json", json:encode(EJSON)},
     {header, ["Access-Control-Allow-Origin: ", get_origin_header(YArg)]},
     {header, ["Access-Control-Allow-Credentials: ", "true"]}].
