package de.ids_mannheim.korap.handlers;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import de.ids_mannheim.korap.config.Attributes;
import de.ids_mannheim.korap.config.ClientInfo;
import de.ids_mannheim.korap.constant.TokenType;
import de.ids_mannheim.korap.exceptions.DatabaseException;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.interfaces.db.PersistenceClient;
import de.ids_mannheim.korap.security.context.TokenContext;
import de.ids_mannheim.korap.user.User;
import de.ids_mannheim.korap.utils.BooleanUtils;
import de.ids_mannheim.korap.utils.TimeUtils;
import edu.emory.mathcs.backport.java.util.Collections;

/**
 * Created by hanl on 7/14/14.
 */
public class OAuthDb {

    private static final Logger jlog = LogManager.getLogger(OAuthDb.class);
    private NamedParameterJdbcTemplate jdbcTemplate;


    public OAuthDb (PersistenceClient client) {
        this.jdbcTemplate = (NamedParameterJdbcTemplate) client.getSource();
    }


    public ClientInfo getClient (String clientid) {
        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("cl", clientid);
        String sql = "select * from oauth2_client where client_id=:cl;";

        try {
            return this.jdbcTemplate.queryForObject(sql, s,
                    new RowMapper<ClientInfo>() {
                        @Override
                        public ClientInfo mapRow (ResultSet rs, int rowNum)
                                throws SQLException {
                            ClientInfo info = new ClientInfo(rs
                                    .getString("client_id"), rs
                                    .getString("client_secret"));
                            info.setId(rs.getInt("id"));
                            info.setClient_type(rs.getString("client_type"));
                            info.setRedirect_uri(rs.getString("redirect_uri"));
                            info.setUrl(rs.getString("url"));
                            info.setConfidential(rs
                                    .getBoolean("is_confidential"));
                            return info;
                        }
                    });
        }
        catch (EmptyResultDataAccessException ex) {
            jlog.error(clientid+" client found "+ ex.fillInStackTrace());
            return null;
        }
    }


    // fixme: what to delete? difference client/application table?
    public boolean revokeToken (String token) throws KustvaktException {
        String sql = "delete from oauth2_access_token WHERE access_token=:token;";
        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("token", token);
        try {
            return this.jdbcTemplate.update(sql, s) == 1;
        }
        catch (DataAccessException e) {
            jlog.error("token could not be revoked", e.fillInStackTrace());
            return false;
        }
    }


    public boolean revokeAuthorization (ClientInfo info, User user) {
        MapSqlParameterSource source = new MapSqlParameterSource();
        source.addValue("us", user.getId());
        source.addValue("cls", info.getClient_secret());
        source.addValue("clid", info.getClient_id());

        String tokens = "delete from oauth2_access_token where user_id=:us and client_id in "
                + "(select client_id from oauth2_client where client_id=:clid and client_secret=:cls);";

        try {
            this.jdbcTemplate.update(tokens, source);
        }
        catch (DataAccessException e) {
            jlog.error("authorization could not be revoked for user "+
                    user.getUsername());
            return false;
        }
        //fixme: if int row not updated, false!!
        return true;
    }


    public boolean addToken (String token, String refresh, Integer userid,
            String client_id, String scopes, int expiration)
            throws KustvaktException {
        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("token", token);
        s.addValue("rt", refresh);
        s.addValue("ex", new Timestamp(TimeUtils.plusSeconds(expiration)
                .getMillis()));
        s.addValue("us", userid);
        s.addValue("sc", scopes);
        s.addValue("st", BooleanUtils.getBoolean(true));
        s.addValue("cli", client_id);
        String sql = "insert into oauth2_access_token (access_token, refresh_token, scopes, client_id, user_id, expiration, status) "
                + "values (:token, :rt, :sc, :cli, :us, :ex, :st);";
        try {
            return this.jdbcTemplate.update(sql, s) == 1;
        }
        catch (DataAccessException e) {
            e.printStackTrace();
            jlog.error("token "+token+" could not be added for user "+ 
                    userid);
            return false;
        }
    }


    // returns the first token to find
    public String getToken (String client_id, Integer userid) {
        String sql = "select access_token from oauth2_access_token where user_id=:uid"
                + " and status=1 and client_id=:cli limit 1;";
        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("uid", userid);
        s.addValue("cli", client_id);
        try {
            return this.jdbcTemplate.queryForObject(sql, s, String.class);
        }
        catch (EmptyResultDataAccessException ex) {
            jlog.error("no token found for user "+ userid);
            return null;
        }
        catch (DataAccessException ex) {
            jlog.error("token retrieval failed for user "+ userid);
            return null;
        }
    }


    public List<ClientInfo> getAuthorizedClients (Integer userid) {
        String sql = "select cl.* from oauth2_client as cl where cl.client_id in (select cd.client_id from oauth2_access_token as cd "
                + "where cd.user_id=:user) or cl.is_confidential=:conf;";

        //todo: test query
        //        "select cl.* from oauth2_client as cl inner join oauth2_access_token as cd "
        //                + "on cd.client_id=cl.client_id where cd.user_id=:user or cl.is_confidential=:conf;"

        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("user", userid);
        s.addValue("conf", BooleanUtils.getBoolean(true));
        try {
            // secret is not returned for this function
            return this.jdbcTemplate.query(sql, s, new RowMapper<ClientInfo>() {

                @Override
                public ClientInfo mapRow (ResultSet rs, int rowNum)
                        throws SQLException {
                    ClientInfo info = new ClientInfo(rs.getString("client_id"), "*****");
                    info.setConfidential(rs.getBoolean("is_confidential"));
                    info.setUrl(rs.getString("url"));
                    info.setId(rs.getInt("id"));
                    info.setRedirect_uri(rs.getString("redirect_uri"));
                    return info;
                }
            });
        }
        catch (DataAccessException e) {
            jlog.error("Data access error", e);
            return Collections.emptyList();
        }

    }


    // todo: expired token must trigger an invalid token exception to trigger a refresh token
    public TokenContext getContext (final String token)
            throws KustvaktException {
        String sql = "select ko.username, oa.expiration, oa.scopes from oauth2_access_token as oa inner join korap_users as ko "
                + "on ko.id=oa.user_id where oa.access_token=:token and oa.expiration > :now;";
        MapSqlParameterSource s = new MapSqlParameterSource();
        s.addValue("token", token);
        s.addValue("now", new Timestamp(TimeUtils.getNow().getMillis()));

        try {
            TokenContext context = this.jdbcTemplate.queryForObject(sql, s,
                    new RowMapper<TokenContext>() {
                        @Override
                        public TokenContext mapRow (ResultSet rs, int rowNum)
                                throws SQLException {
                            long exp = rs.getTimestamp("expiration").getTime();
                            TokenContext c = new TokenContext();
                            c.setUsername(rs.getString(Attributes.USERNAME));
                            c.setExpirationTime(exp);
                            c.setToken(token);
                            c.setTokenType(TokenType.BEARER);
//                            c.setTokenType(Attributes.OAUTH2_AUTHORIZATION);
                            c.addContextParameter(Attributes.SCOPE,
                                    rs.getString(Attributes.SCOPE));
                            return c;
                        }
                    });
            return context;
        }
        catch (EmptyResultDataAccessException ee) {
            jlog.error("no context found for token "+ token);
            revokeToken(token);
            throw new KustvaktException(StatusCodes.EXPIRED, "token", token);
        }
        catch (DataAccessException e) {
            jlog.error("token context retrieval failed for "+ token);
            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
                    "invalid token", token);
        }

    }


    // subsequently delete all access and auth code tokens associated!
    public void removeClient (ClientInfo info, User user)
            throws KustvaktException {
        MapSqlParameterSource p = new MapSqlParameterSource();
        p.addValue("url", info.getUrl());
        p.addValue("cls", info.getClient_secret());
        p.addValue("clid", info.getClient_id());
        String sql = "delete from oauth2_client where client_id=:clid and client_secret=:cls and"
                + " url=:url;";
        try {
            this.jdbcTemplate.update(sql, p);
        }
        catch (DataAccessException e) {
            e.printStackTrace();
            jlog.error("removing client "+info.getClient_id()+" failed");
            throw new DatabaseException(new KustvaktException(user.getId(),
                    StatusCodes.ILLEGAL_ARGUMENT, "arguments given not valid",
                    info.toJSON()), StatusCodes.CLIENT_DEREGISTRATION_FAILED,
                    info.toJSON());

        }
    }


    public void registerClient (ClientInfo info, User user)
            throws KustvaktException {
        MapSqlParameterSource p = new MapSqlParameterSource();
        p.addValue("clid", info.getClient_id());
        p.addValue("con", info.isConfidential());
        p.addValue("cls", info.getClient_secret());
        p.addValue("clt", info.getClient_type());
        p.addValue("url", info.getUrl());
        p.addValue("r_url", info.getRedirect_uri());
        String sql = "insert into oauth2_client (client_id, client_secret, client_type, url, is_confidential, redirect_uri) "
                + "VALUES (:clid, :cls, :clt, :url, :con, :r_url);";
        try {
            this.jdbcTemplate.update(sql, p);
        }
        catch (DataAccessException e) {
            e.printStackTrace();
            jlog.error("registering client "+info.getClient_id()+" failed");
            throw new DatabaseException(new KustvaktException(user.getId(),
                    StatusCodes.ILLEGAL_ARGUMENT, "arguments given not valid",
                    info.toJSON()), StatusCodes.CLIENT_REGISTRATION_FAILED,
                    info.toJSON());
        }
    }
}
