/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2012, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.gatein.api.management;

import org.exoplatform.container.PortalContainer;
import org.exoplatform.portal.mop.SiteKey;
import org.exoplatform.web.WebAppController;
import org.exoplatform.web.url.navigation.NavigationResource;
import org.exoplatform.web.url.simple.SimpleURL;
import org.exoplatform.web.url.simple.SimpleURLContext;
import org.gatein.api.BasicPortalRequest;
import org.gatein.api.EntityAlreadyExistsException;
import org.gatein.api.EntityNotFoundException;
import org.gatein.api.Portal;
import org.gatein.api.PortalRequest;
import org.gatein.api.Util;
import org.gatein.api.common.Attributes;
import org.gatein.api.common.Pagination;
import org.gatein.api.common.URIResolver;
import org.gatein.api.navigation.Navigation;
import org.gatein.api.navigation.NodePath;
import org.gatein.api.security.Group;
import org.gatein.api.security.Permission;
import org.gatein.api.security.User;
import org.gatein.api.site.Site;
import org.gatein.api.site.SiteId;
import org.gatein.api.site.SiteQuery;
import org.gatein.api.site.SiteType;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.management.api.ManagedUser;
import org.gatein.management.api.PathAddress;
import org.gatein.management.api.annotations.Managed;
import org.gatein.management.api.annotations.ManagedAfter;
import org.gatein.management.api.annotations.ManagedBefore;
import org.gatein.management.api.annotations.ManagedContext;
import org.gatein.management.api.annotations.ManagedOperation;
import org.gatein.management.api.annotations.ManagedRole;
import org.gatein.management.api.annotations.MappedAttribute;
import org.gatein.management.api.annotations.MappedPath;
import org.gatein.management.api.exceptions.ResourceNotFoundException;
import org.gatein.management.api.model.ModelList;
import org.gatein.management.api.model.ModelObject;
import org.gatein.management.api.model.ModelProvider;
import org.gatein.management.api.model.ModelReference;
import org.gatein.management.api.model.ModelString;
import org.gatein.management.api.model.ModelValue;
import org.gatein.management.api.operation.OperationContext;
import org.gatein.management.api.operation.OperationNames;

import java.util.List;
import java.util.Locale;

import static org.gatein.api.management.Utils.*;

/**
 * @author <a href="mailto:nscavell@redhat.com">Nick Scavelli</a>
 */
@SuppressWarnings("unused")
@Managed(value = "api", description = "GateIn API Management Resource")
public class GateInApiManagementResource {
    private static final Logger log = LoggerFactory.getLogger("org.gatein.api.management");

    private static final SiteQuery SITE_QUERY = new SiteQuery.Builder().withSiteTypes(SiteType.SITE).build();
    private static final SiteQuery SPACE_QUERY = new SiteQuery.Builder().withSiteTypes(SiteType.SPACE).build();
    private static final SiteQuery DASHBOARD_QUERY = new SiteQuery.Builder().withSiteTypes(SiteType.DASHBOARD).build();

    private final Portal portal;

    @ManagedContext
    private final ModelProvider modelProvider; // gatein-management will set this field via reflection

    public GateInApiManagementResource(Portal portal) {
        this(portal, null);
    }

    // Constructor for testing to specify ModelProvider instead of gatein-management
    GateInApiManagementResource(Portal portal, ModelProvider modelProvider) {
        this.portal = portal;
        this.modelProvider = modelProvider;
    }

    @ManagedBefore
    public void before(@ManagedContext OperationContext context) {
        PortalRequest portalRequest = PortalRequest.getInstance();
        if (portalRequest == null) {
            setCurrentPortalRequest(context);
        }
    }

    @ManagedAfter
    public void after() {
        if (PortalRequest.getInstance() instanceof BasicPortalRequest) {
            BasicPortalRequest.setInstance(null);
        }
    }

    // ------------------------------------------------- Portal Sites --------------------------------------------------//
    @Managed("/sites")
    public ModelList getSites(@ManagedContext PathAddress address, @MappedAttribute("emptySites") String emptySites,
                              @MappedAttribute("offset") String offset, @MappedAttribute("limit") String limit) {
        return _getSites(SITE_QUERY, address, emptySites, offset, limit);
    }

    @Managed("/sites/{site-name}")
    public ModelObject getSite(@MappedPath("site-name") String siteName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(siteName);
        return _getSite(id, context);
    }

    @Managed("/sites/{site-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.ADD_RESOURCE, description = "Adds a given site")
    public ModelObject addSite(@MappedPath("site-name") String siteName, @MappedAttribute("template") String template, @ManagedContext PathAddress address) {
        SiteId siteId = new SiteId(siteName);
        return _addSite(address, siteId, template);
    }

    @Managed("/sites/{site-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.REMOVE_RESOURCE, description = "Removes the given site")
    public void removeSite(@MappedPath("site-name") String siteName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(siteName);
        _removeSite(id, context);
    }

    @Managed("/sites/{site-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.UPDATE_RESOURCE, description = "Updates a given site")
    public ModelObject updateSite(@MappedPath("site-name") String siteName, @ManagedContext ModelObject siteModel, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(siteName);
        return _updateSite(id, siteModel, context);
    }

    @Managed("/sites/{site-name}/pages")
    public PageManagementResource getPages(@MappedPath("site-name") String siteName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(siteName);
        return pagesResource(id, context);
    }

    @Managed("/sites/{site-name}/navigation")
    public NavigationManagementResource getNavigation(@MappedPath("site-name") String siteName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(siteName);
        return navigationResource(id, context);
    }

    // --------------------------------------------- Group Sites (Spaces) ----------------------------------------------//
    @Managed("/spaces")
    public ModelList getSpaces(@ManagedContext PathAddress address, @MappedAttribute("emptySites") String emptySites,
                               @MappedAttribute("offset") String offset, @MappedAttribute("limit") String limit) {
        return _getSites(SPACE_QUERY, address, emptySites, offset, limit);
    }

    @Managed("/spaces/{group-name: .*}")
    public ModelObject getSpace(@MappedPath("group-name") String groupName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new Group(groupName));
        return _getSite(id, context);
    }

    @Managed("/spaces/{group-name: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.ADD_RESOURCE, description = "Adds a given site")
    public ModelObject addSpace(@MappedPath("group-name") String groupName, @MappedAttribute("template") String template, @ManagedContext PathAddress address) {
        SiteId siteId = new SiteId(new Group(groupName));
        return _addSite(address, siteId, template);
    }

    @Managed("/spaces/{group-name: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.REMOVE_RESOURCE, description = "Removes the given space")
    public void removeSpace(@MappedPath("group-name") String groupName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new Group(groupName));
        _removeSite(id, context);
    }

    @Managed("/spaces/{group-name: .*}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.UPDATE_RESOURCE, description = "Updates a given space")
    public ModelObject updateSpace(@MappedPath("group-name") String groupName, @ManagedContext ModelObject siteModel, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new Group(groupName));
        return _updateSite(id, siteModel, context);
    }

    @Managed("/spaces/{group-name: .*}/pages")
    public PageManagementResource getSpacePages(@MappedPath("group-name") String groupName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new Group(groupName));
        return pagesResource(id, context);
    }

    @Managed("/spaces/{group-name: .*}/navigation")
    public NavigationManagementResource getSpaceNavigation(@MappedPath("group-name") String groupName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new Group(groupName));
        return navigationResource(id, context);
    }

    // -------------------------------------------- User Sites (Dashboard) ---------------------------------------------//
    @Managed("/dashboards")
    public ModelList getDashboards(@ManagedContext PathAddress address, @MappedAttribute("emptySites") String emptySites,
                                   @MappedAttribute("offset") String offset, @MappedAttribute("limit") String limit) {
        return _getSites(DASHBOARD_QUERY, address, emptySites, offset, limit);
    }

    @Managed("/dashboards/{user-name}")
    public ModelObject getDashboard(@MappedPath("user-name") String userName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new User(userName));
        return _getSite(id, context);
    }

    @Managed("/dashboards/{user-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.ADD_RESOURCE, description = "Adds a given site")
    public ModelObject addDashboard(@MappedPath("user-name") String userName, @MappedAttribute("template") String template, @ManagedContext PathAddress address) {
        SiteId siteId = new SiteId(new User(userName));
        return _addSite(address, siteId, template);
    }

    @Managed("/dashboards/{user-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.REMOVE_RESOURCE, description = "Removes the given dashboard")
    public void removeDashboard(@MappedPath("user-name") String userName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new User(userName));
        _removeSite(id, context);
    }

    @Managed("/dashboards/{user-name}")
    @ManagedRole("administrators")
    @ManagedOperation(name = OperationNames.UPDATE_RESOURCE, description = "Updates a given space")
    public ModelObject updateDashboard(@MappedPath("user-name") String userName, @ManagedContext ModelObject siteModel, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new User(userName));
        return _updateSite(id, siteModel, context);
    }

    @Managed("/dashboards/{user-name}/pages")
    public PageManagementResource getDashboardPages(@MappedPath("user-name") String userName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new User(userName));
        return pagesResource(id, context);
    }

    @Managed("/dashboards/{user-name}/navigation")
    public NavigationManagementResource getDashboardNavigation(@MappedPath("user-name") String userName, @ManagedContext OperationContext context) {
        SiteId id = new SiteId(new User(userName));
        return navigationResource(id, context);
    }

    private NavigationManagementResource navigationResource(SiteId siteId, OperationContext context) {
        requireSite(siteId, context);
        Navigation navigation = portal.getNavigation(siteId);
        if (navigation == null) {
            throw new ResourceNotFoundException("Navigation does not exist for site " + siteId);
        }

        return new NavigationManagementResource(navigation, modelProvider);
    }

    private PageManagementResource pagesResource(SiteId siteId, OperationContext context) {
        requireSite(siteId, context);
        return new PageManagementResource(portal, modelProvider, siteId);
    }

    private ModelList _getSites(SiteQuery query, PathAddress address, String emptySitesParam, String offsetParam, String limitParam) {
        boolean emptySites = Boolean.valueOf(emptySitesParam);
        Pagination pagination = getPagination(offsetParam, limitParam, query.getPagination());
        query = new SiteQuery.Builder().from(query).includeEmptySites(emptySites).withPagination(pagination).build();

        // Query sites
        List<Site> sites = portal.findSites(query);
        return populateModel(sites, modelProvider.newModel(ModelList.class), address);
    }

    private ModelObject _getSite(SiteId id, OperationContext context) {
        Site site = requireSite(id, context);

        // Populate site model
        return populateModel(site, modelProvider.newModel(ModelObject.class), context.getAddress());
    }

    private ModelObject _addSite(PathAddress address, SiteId siteId, String template) {
        Site site;
        try {
            if (template == null) {
                site = portal.createSite(siteId);
            } else {
                site = portal.createSite(siteId, template);
            }
        } catch (EntityAlreadyExistsException e) {
            throw alreadyExists("Could not add site", siteId);
        }
        portal.saveSite(site);

        // Populate model
        return populateModel(site, modelProvider.newModel(ModelObject.class), address);
    }

    private void _removeSite(SiteId id, OperationContext context) {
        requireSite(id, context);
        try {
            boolean removed = portal.removeSite(id);
            if (!removed) throw new RuntimeException("Could not remove site + " + id + " for unknown reasons.");
        } catch (EntityNotFoundException e) {
            throw notFound("Cannot remove site", id);
        }
    }

    private ModelObject _updateSite(SiteId id, ModelObject siteModel, OperationContext context) {
        Site site = requireSite(id, context);

        if (siteModel.has("displayName")) {
            String displayName = get(siteModel, ModelString.class, "displayName").getValue();
            site.setDisplayName(displayName);
        }
        if (siteModel.has("description")) {
            String description = get(siteModel, ModelString.class, "description").getValue();
            site.setDescription(description);
        }
        if (siteModel.has("skin")) {
            String skin = get(siteModel, ModelString.class, "skin").getValue();
            site.setSkin(skin);
        }
        if (siteModel.has("locale")) {
            Locale locale = getLocale(siteModel, "locale");
            site.setLocale(locale);
        }
        if (siteModel.has("access-permissions")) {
            Permission permission = getPermission(siteModel, false, "access-permissions");
            site.setAccessPermission(permission);
        }
        if (siteModel.has("edit-permissions")) {
            Permission permission = getPermission(siteModel, true, "edit-permissions");
            site.setEditPermission(permission);
        }
        if (siteModel.hasDefined("attributes")) {
            ModelList list = get(siteModel, ModelList.class, "attributes");
            for (int i = 0; i < list.size(); i++) {
                ModelValue mv = list.get(i);
                String field = "attributes["+i+"]"; // Used for error reporting
                if (mv.getValueType() != ModelValue.ModelValueType.OBJECT) {
                    throw invalidType(mv, ModelValue.ModelValueType.OBJECT, field);
                }
                ModelObject attrModel = mv.asValue(ModelObject.class);
                if (!attrModel.hasDefined("key")) {
                    throw requiredField(field, "key");
                }
                String key = get(attrModel, ModelString.class, "key").getValue();
                if (!attrModel.has("value")) {
                    throw requiredField(field, "value");
                }
                String value = get(attrModel, ModelString.class, "value").getValue();
                site.getAttributes().put(key, value);
            }
        }

        portal.saveSite(site);

        return populateModel(site, modelProvider.newModel(ModelObject.class), context.getAddress());
    }

    private Site requireSite(SiteId id, OperationContext context) {
        Site site = portal.getSite(id);
        if (site == null) throw new ResourceNotFoundException("Site not found for " + id);

        // Verify current user has access to site
        verifyAccess(site, context);

        return site;
    }

    private ModelObject populateModel(Site site, ModelObject siteModel, PathAddress address) {
        // Site fields
        siteModel.set("name", site.getId().getName());
        siteModel.set("type", site.getId().getType().name().toLowerCase());
        siteModel.set("displayName", site.getDisplayName());
        siteModel.set("description", site.getDescription());
        siteModel.set("skin", site.getSkin());
        populate("locale", site.getLocale(), siteModel);
        populate("access-permissions", site.getAccessPermission(), siteModel);
        populate("edit-permissions", site.getEditPermission(), siteModel);
        ModelList attrList = siteModel.get("attributes", ModelList.class);
        Attributes attributes = site.getAttributes();
        for (String key : attributes.keySet()) {
            ModelObject attr = attrList.add().setEmptyObject();
            attr.set("key", key);
            attr.set("value", attributes.get(key));
        }

        // Pages
        ModelReference pagesRef = siteModel.get("pages", ModelReference.class);
        pagesRef.set(address.append("pages"));

        // Navigation
        ModelReference navigationRef = siteModel.get("navigation", ModelReference.class);
        navigationRef.set(address.append("navigation"));

        return siteModel;
    }

    private ModelList populateModel(List<Site> sites, ModelList list, PathAddress address) {
        for (Site site : sites) {
            if (hasPermission(site.getAccessPermission())) {
                ModelReference siteRef = list.add().asValue(ModelReference.class);
                siteRef.set("name", site.getName());
                siteRef.set("type", site.getType().getName());
                siteRef.set(address.append(site.getName()));
            }
        }

        return list;
    }

    private User getUser(ManagedUser managedUser) {
        if (managedUser == null) return User.anonymous();

        return new User(managedUser.getUserName());
    }

    private void setCurrentPortalRequest(OperationContext context) {
        final ManagedUser managedUser = context.getUser();
        final PathAddress address = context.getAddress();

        // Retrieve siteId from address (can be null)
        SiteId siteId = getSiteId(address);

        // Retrieve nodePath from address (can be null)
        NodePath nodePath = getNodePath(address);

        Locale locale = context.getLocale();
        // For some HTTP requests the locale is set to *, I guess to indicate a header 'Accept-Language: *' ?
        if (locale != null && locale.getLanguage().equals("*")) {
            locale = null;
        }

        User user = (managedUser == null || managedUser.getUserName() == null) ? User.anonymous() : new User(managedUser.getUserName());

        final PortalContainer container = PortalContainer.getInstance();
        final WebAppController controller = (WebAppController) container.getComponentInstanceOfType(WebAppController.class);
        URIResolver uriResolver = new URIResolver() {
            @Override
            public String resolveURI(SiteId siteId) {
                SiteKey siteKey = Util.from(siteId);
                NavigationResource navResource = new NavigationResource(siteKey, "");
                SimpleURL url = new SimpleURL(new SimpleURLContext(container, controller));
                url.setSchemeUse(false);
                url.setAuthorityUse(false);
                String urlString = url.setResource(navResource).toString();
                return urlString.substring(0, urlString.length() - 1);
            }
        };
        BasicPortalRequest.setInstance(new BasicPortalRequest(user, siteId, nodePath, locale, portal, uriResolver));
    }

    private static SiteId getSiteId(PathAddress address) {
        String siteName = address.resolvePathTemplate("site-name");
        if (siteName != null) {
            return new SiteId(siteName);
        }

        String groupName = address.resolvePathTemplate("group-name");
        if (groupName != null) {
            return new SiteId(new Group(groupName));
        }

        String userName = address.resolvePathTemplate("user-name");
        if (userName != null) {
            return new SiteId(new User(userName));
        }

        return null;
    }

    private static NodePath getNodePath(PathAddress address) {
        String path = address.resolvePathTemplate("path");
        if (path != null) {
            return NodePath.fromString(path);
        }

        return null;
    }

    static PathAddress getSiteAddress(SiteId siteId) {
        PathAddress address = PathAddress.pathAddress("api");
        switch (siteId.getType()) {
            case SITE:
                address = address.append("sites");
                break;
            case SPACE:
                address = address.append("spaces");
                break;
            case DASHBOARD:
                address = address.append("dashboards");
                break;
            default:
                throw new AssertionError();
        }

        return address.append(siteId.getName());
    }

    static PathAddress getPagesAddress(SiteId siteId) {
        return getSiteAddress(siteId).append("pages");
    }

    static PathAddress getNavigationAddress(SiteId siteId) {
        return getSiteAddress(siteId).append("navigation");
    }
}
