package org.keycloak.services.resources.admin;

import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.audit.AuditProvider;
import org.keycloak.audit.Event;
import org.keycloak.audit.EventQuery;
import org.keycloak.audit.EventType;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmAuditRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.resources.flows.Flows;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Base resource class for the admin REST api of one realm
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class RealmAdminResource {
    protected static final Logger logger = Logger.getLogger(RealmAdminResource.class);
    protected RealmAuth auth;
    protected RealmModel realm;
    private TokenManager tokenManager;

    @Context
    protected KeycloakSession session;

    @Context
    protected UriInfo uriInfo;

    public RealmAdminResource(RealmAuth auth, RealmModel realm, TokenManager tokenManager) {
        this.auth = auth;
        this.realm = realm;
        this.tokenManager = tokenManager;

        auth.init(RealmAuth.Resource.REALM);
    }

    /**
     * Base path for managing applications under this realm.
     *
     * @return
     */
    @Path("applications")
    public ApplicationsResource getApplications() {
        ApplicationsResource applicationsResource = new ApplicationsResource(realm, auth);
        ResteasyProviderFactory.getInstance().injectProperties(applicationsResource);
        //resourceContext.initResource(applicationsResource);
        return applicationsResource;
    }

    /**
     * base path for managing oauth clients in this realm
     *
     * @return
     */
    @Path("oauth-clients")
    public OAuthClientsResource getOAuthClients() {
        OAuthClientsResource oauth = new OAuthClientsResource(realm, auth, session);
        ResteasyProviderFactory.getInstance().injectProperties(oauth);
        //resourceContext.initResource(oauth);
        return oauth;
    }

    /**
     * base path for managing realm-level roles of this realm
     *
     * @return
     */
    @Path("roles")
    public RoleContainerResource getRoleContainerResource() {
        return new RoleContainerResource(realm, auth, realm);
    }

    /**
     * Get the top-level representation of the realm.  It will not include nested information like User, Application, or OAuth
     * Client representations.
     *
     * @return
     */
    @GET
    @NoCache
    @Produces("application/json")
    public RealmRepresentation getRealm() {
        if (auth.hasView()) {
            return ModelToRepresentation.toRepresentation(realm);
        } else {
            auth.requireAny();

            RealmRepresentation rep = new RealmRepresentation();
            rep.setRealm(realm.getName());

            return rep;
        }
    }

    /**
     * Update the top-level information of this realm.  Any user, roles, application, or oauth client information in the representation
     * will be ignored.  This will only update top-level attributes of the realm.
     *
     * @param rep
     * @return
     */
    @PUT
    @Consumes("application/json")
    public Response updateRealm(final RealmRepresentation rep) {
        auth.requireManage();

        logger.debug("updating realm: " + realm.getName());
        try {
            new RealmManager(session).updateRealm(rep, realm);
            return Response.noContent().build();
        } catch (ModelDuplicateException e) {
            return Flows.errors().exists("Realm " + rep.getRealm() + " already exists");
        }
    }

    /**
     * Delete this realm.
     *
     */
    @DELETE
    public void deleteRealm() {
        auth.requireManage();

        if (!new RealmManager(session).removeRealm(realm)) {
            throw new NotFoundException("Realm doesn't exist");
        }
    }

    /**
     * Base path for managing users in this realm.
     *
     * @return
     */
    @Path("users")
    public UsersResource users() {
        UsersResource users = new UsersResource(realm, auth, tokenManager);
        ResteasyProviderFactory.getInstance().injectProperties(users);
        //resourceContext.initResource(users);
        return users;
    }

    /**
     * Path for managing all realm-level or application-level roles defined in this realm by it's id.
     *
     * @return
     */
    @Path("roles-by-id")
    public RoleByIdResource rolesById() {
        RoleByIdResource resource = new RoleByIdResource(realm, auth);
        ResteasyProviderFactory.getInstance().injectProperties(resource);
        //resourceContext.initResource(resource);
        return resource;
    }

    /**
     * Push the realm's revocation policy to any application that has an admin url associated with it.
     *
     */
    @Path("push-revocation")
    @POST
    public void pushRevocation() {
        auth.requireManage();
        new ResourceAdminManager().pushRealmRevocationPolicy(uriInfo.getRequestUri(), realm);
    }

    /**
     * Removes all user sessions.  Any application that has an admin url will also be told to invalidate any sessions
     * they have.
     *
     */
    @Path("logout-all")
    @POST
    public void logoutAll() {
        auth.requireManage();
        realm.removeUserSessions();
        new ResourceAdminManager().logoutAll(uriInfo.getRequestUri(), realm);
    }

    /**
     * Remove a specific user session. Any application that has an admin url will also be told to invalidate this
     * particular session.
     *
     * @param sessionId
     */
    @Path("sessions/{session}")
    @DELETE
    public void deleteSession(@PathParam("session") String sessionId) {
        UserSessionModel session = realm.getUserSession(sessionId);
        if (session == null) throw new NotFoundException("Sesssion not found");
        realm.removeUserSession(session);
        new ResourceAdminManager().logoutSession(uriInfo.getRequestUri(), realm, session.getId());
    }

    /**
     * Returns a JSON map.  The key is the application name, the value is the number of sessions that currently are active
     * with that application.  Only application's that actually have a session associated with them will be in this map.
     *
     * @return
     */
    @Path("application-session-stats")
    @GET
    @NoCache
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Integer> getApplicationSessionStats() {
        auth.requireView();
        Map<String, Integer> stats = new HashMap<String, Integer>();
        for (ApplicationModel applicationModel : realm.getApplications()) {
            int size = applicationModel.getActiveUserSessions();
            if (size == 0) continue;
            stats.put(applicationModel.getName(), size);
        }
        return stats;
    }

    /**
     * Any application that has an admin URL will be asked directly how many sessions they have active and what users
     * are involved with those sessions.
     *
     * @return
     */
    @Path("session-stats")
    @GET
    @NoCache
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, SessionStats> getSessionStats() {
        logger.info("session-stats");
        auth.requireView();
        Map<String, SessionStats> stats = new HashMap<String, SessionStats>();
        for (ApplicationModel applicationModel : realm.getApplications()) {
            if (applicationModel.getManagementUrl() == null) continue;
            SessionStats appStats = new ResourceAdminManager().getSessionStats(uriInfo.getRequestUri(), realm, applicationModel, false);
            stats.put(applicationModel.getName(), appStats);
        }
        return stats;
    }

    /**
     * View the audit provider and how it is configured.
     *
     * @return
     */
    @GET
    @Path("audit")
    @Produces("application/json")
    public RealmAuditRepresentation getRealmAudit() {
        auth.init(RealmAuth.Resource.AUDIT).requireView();

        return ModelToRepresentation.toAuditReprensetation(realm);
    }

    /**
     * Change the audit provider and/or it's configuration
     *
     * @param rep
     */
    @PUT
    @Path("audit")
    @Consumes("application/json")
    public void updateRealmAudit(final RealmAuditRepresentation rep) {
        auth.init(RealmAuth.Resource.AUDIT).requireManage();

        logger.debug("updating realm audit: " + realm.getName());
        new RealmManager(session).updateRealmAudit(rep, realm);
    }

    /**
     * Query audit events.  Returns all events, or will query based on URL query parameters listed here
     *
     * @param client app or oauth client name
     * @param event event type
     * @param user user id
     * @param ipAddress
     * @param firstResult
     * @param maxResults
     * @return
     */
    @Path("audit/events")
    @GET
    @NoCache
    @Produces(MediaType.APPLICATION_JSON)
    public List<Event> getAudit(@QueryParam("client") String client, @QueryParam("event") String event, @QueryParam("user") String user,
                                @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) {
        auth.init(RealmAuth.Resource.AUDIT).requireView();

        AuditProvider audit = session.getProvider(AuditProvider.class);

        EventQuery query = audit.createQuery().realm(realm.getId());
        if (client != null) {
            query.client(client);
        }
        if (event != null) {
            query.event(EventType.valueOf(event));
        }
        if (user != null) {
            query.user(user);
        }
        if (ipAddress != null) {
            query.ipAddress(ipAddress);
        }
        if (firstResult != null) {
            query.firstResult(firstResult);
        }
        if (maxResults != null) {
            query.maxResults(maxResults);
        }

        return query.getResultList();
    }

    /**
     * Delete all audit events.
     *
     */
    @Path("audit/events")
    @DELETE
    public void clearAudit() {
        auth.init(RealmAuth.Resource.AUDIT).requireManage();

        AuditProvider audit = session.getProvider(AuditProvider.class);
        audit.clear(realm.getId());
    }

    @Path("testLDAPConnection")
    @GET
    public Response testLDAPConnection(@QueryParam("action") String action, @QueryParam("connectionUrl") String connectionUrl,
                                       @QueryParam("bindDn") String bindDn, @QueryParam("bindCredential") String bindCredential) {
        auth.init(RealmAuth.Resource.REALM).requireManage();

        boolean result = new LDAPConnectionTestManager().testLDAP(action, connectionUrl, bindDn, bindCredential);
        return result ? Response.noContent().build() : Flows.errors().error("LDAP test error", Response.Status.BAD_REQUEST);
    }
}
