/* Copyright 2016 Software Freedom Conservancy Inc.
 * Copyright 2017 Jens Georg <mail@jensge.org>
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

namespace Publishing.RESTSupport.OAuth1 {
    internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\";

    public class Session : Publishing.RESTSupport.Session {
        private string? request_phase_token = null;
        private string? request_phase_token_secret = null;
        private string? access_phase_token = null;
        private string? access_phase_token_secret = null;
        private string? username = null;
        private string? consumer_key = null;
        private string? consumer_secret = null;

        public Session(string? endpoint_uri = null) {
            base(endpoint_uri);
        }

        public override bool is_authenticated() {
            return (access_phase_token != null && access_phase_token_secret != null &&
                    username != null);
        }

        public void authenticate_from_persistent_credentials(string token, string secret,
                string username) {
            this.access_phase_token = token;
            this.access_phase_token_secret = secret;
            this.username = username;

            this.authenticated();
        }

        public void deauthenticate() {
            access_phase_token = null;
            access_phase_token_secret = null;
            username = null;
        }

        public void set_api_credentials(string consumer_key, string consumer_secret) {
            this.consumer_key = consumer_key;
            this.consumer_secret = consumer_secret;
        }

        public string sign_transaction(Publishing.RESTSupport.Transaction txn,
                                     Publishing.RESTSupport.Argument[]? extra_arguments = null) {
            string http_method = txn.get_method().to_string();

            debug("signing transaction with parameters:");
            debug("HTTP method = " + http_method);

            Publishing.RESTSupport.Argument[] base_string_arguments = txn.get_arguments();

            foreach (var arg in extra_arguments) {
                base_string_arguments += arg;
            }

            Publishing.RESTSupport.Argument[] sorted_args =
                Publishing.RESTSupport.Argument.sort(base_string_arguments);

            var arguments_string = Argument.serialize_list(sorted_args);

            string? signing_key = null;
            if (access_phase_token_secret != null) {
                debug("access phase token secret available; using it as signing key");

                signing_key = consumer_secret + "&" + access_phase_token_secret;
            } else if (request_phase_token_secret != null) {
                debug("request phase token secret available; using it as signing key");

                signing_key = consumer_secret + "&" + request_phase_token_secret;
            } else {
                debug("neither access phase nor request phase token secrets available; using API " +
                        "key as signing key");

                signing_key = consumer_secret + "&";
            }

            string signature_base_string = http_method + "&" + Soup.URI.encode(
                    txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" +
                Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA);

            debug("signature base string = '%s'", signature_base_string);

            debug("signing key = '%s'", signing_key);

            // compute the signature
            string signature = RESTSupport.hmac_sha1(signing_key, signature_base_string);
            signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA);

            debug("signature = '%s'", signature);

            return signature;
        }

        public void set_request_phase_credentials(string token, string secret) {
            this.request_phase_token = token;
            this.request_phase_token_secret = secret;
        }

        public void set_access_phase_credentials(string token, string secret, string username) {
            this.access_phase_token = token;
            this.access_phase_token_secret = secret;
            this.username = username;

            authenticated();
        }

        public string get_oauth_nonce() {
            TimeVal currtime = TimeVal();
            currtime.get_current_time();

            return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() +
                    currtime.tv_usec.to_string());
        }

        public string get_oauth_timestamp() {
            return GLib.get_real_time().to_string().substring(0, 10);
        }

        public string get_consumer_key() {
            assert(consumer_key != null);
            return consumer_key;
        }

        public string get_request_phase_token() {
            assert(request_phase_token != null);
            return request_phase_token;
        }

        public string get_access_phase_token() {
            assert(access_phase_token != null);
            return access_phase_token;
        }

        public bool has_access_phase_token() {
            return access_phase_token != null;
        }

        public string get_access_phase_token_secret() {
            assert(access_phase_token_secret != null);
            return access_phase_token_secret;
        }

        public string get_username() {
            assert(is_authenticated());
            return username;
        }
    }

    public class Transaction : Publishing.RESTSupport.Transaction {
        public Transaction(Session session, Publishing.RESTSupport.HttpMethod method =
                Publishing.RESTSupport.HttpMethod.POST) {
            base(session, method);
            setup_arguments();
        }

        public Transaction.with_uri(Session session, string uri,
                Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) {
            base.with_endpoint_url(session, uri, method);
            setup_arguments();
        }

        private void setup_arguments() {
            var session = (Session) get_parent_session();

            add_argument("oauth_nonce", session.get_oauth_nonce());
            add_argument("oauth_signature_method", "HMAC-SHA1");
            add_argument("oauth_version", "1.0");
            add_argument("oauth_timestamp", session.get_oauth_timestamp());
            add_argument("oauth_consumer_key", session.get_consumer_key());
            if (session.has_access_phase_token()) {
                add_argument("oauth_token", session.get_access_phase_token());
            }
        }


        public override void execute() throws Spit.Publishing.PublishingError {
            var signature = ((Session) get_parent_session()).sign_transaction(this);
            add_argument("oauth_signature", signature);

            base.execute();
        }
    }

    public class UploadTransaction : Publishing.RESTSupport.UploadTransaction {
        protected unowned Publishing.RESTSupport.OAuth1.Session session;
        private Publishing.RESTSupport.Argument[] auth_header_fields;

        public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session,
                                 Spit.Publishing.Publishable publishable,
                                 string endpoint_uri) {
            base.with_endpoint_url(session, publishable, endpoint_uri);

            this.auth_header_fields = new Publishing.RESTSupport.Argument[0];
            this.session = session;

            add_authorization_header_field("oauth_nonce", session.get_oauth_nonce());
            add_authorization_header_field("oauth_signature_method", "HMAC-SHA1");
            add_authorization_header_field("oauth_version", "1.0");
            add_authorization_header_field("oauth_timestamp", session.get_oauth_timestamp());
            add_authorization_header_field("oauth_consumer_key", session.get_consumer_key());
            add_authorization_header_field("oauth_token", session.get_access_phase_token());
        }

        public void add_authorization_header_field(string key, string value) {
            auth_header_fields += new Publishing.RESTSupport.Argument(key, value);
        }

        public string get_authorization_header_string() {
            return "OAuth " + Argument.serialize_list(auth_header_fields, true, ", ");
        }

        public void authorize() {
            var signature = session.sign_transaction(this, auth_header_fields);
            add_authorization_header_field("oauth_signature", signature);


            string authorization_header = get_authorization_header_string();

            debug("executing upload transaction: authorization header string = '%s'",
                    authorization_header);
            add_header("Authorization", authorization_header);

        }
    }
}