/*
* Copyright (c) 2010 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*           http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.ovirt.engine.api.common.resource;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.text.MessageFormat;
import java.util.concurrent.Executor;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Response.Status;

import org.ovirt.engine.api.common.util.LinkHelper;
import org.ovirt.engine.api.common.util.ReapedMap;
import org.ovirt.engine.api.common.util.StatusUtils;
import org.ovirt.engine.api.model.Action;
import org.ovirt.engine.api.model.BaseResource;
import org.ovirt.engine.api.model.CreationStatus;
import org.ovirt.engine.api.model.Fault;
import org.ovirt.engine.api.resource.ActionResource;

public abstract class AbstractActionableResource<R extends BaseResource> extends AbstractUpdatableResource<R> {

    private static final long REAP_AFTER = 2 * 60 * 60 * 1000L; // 2 hours

    protected Executor executor;
    protected ReapedMap<String, ActionResource> actions;
    protected UriInfoProvider uriProvider;

    public AbstractActionableResource(String id) {
        this(id, new SimpleExecutor());
    }

    public AbstractActionableResource(String id, Executor executor) {
        super(id);
        this.executor = executor;
        actions = new ReapedMap<String, ActionResource>(REAP_AFTER);
    }

    public AbstractActionableResource(String id, Executor executor, UriInfoProvider uriProvider) {
        super(id);
        this.executor = executor;
        this.uriProvider = uriProvider;
        actions = new ReapedMap<String, ActionResource>(REAP_AFTER);
    }

    protected UriInfo getUriInfo() {
        return uriProvider.getUriInfo();
    }

    public UriInfoProvider getUriProvider() {
        return uriProvider;
    }

    protected R getModel() {
        R parent = newModel();
        parent.setId(getId());
        return parent;
    }

    /**
     * Perform an action, managing asynchrony and returning an appropriate
     * response.
     *
     * @param uriInfo  wraps the URI for the current request
     * @param action   represents the pending action
     * @param task     fulfils the action
     * @return
     */
    protected Response doAction(UriInfo uriInfo, final AbstractActionTask task) {
        Action action = task.action;
        Response.Status status = null;
        final ActionResource actionResource = new BaseActionResource<R>(uriInfo, task.action, getModel());
        if (action.isSetAsync() && action.isAsync()) {
            action.setStatus(StatusUtils.create(CreationStatus.PENDING));
            actions.put(action.getId(), actionResource);
            executor.execute(new Runnable() {
                public void run() {
                    perform(task);
                    actions.reapable(actionResource.getAction().getId());
                }
            });
            status = Status.ACCEPTED;
        } else {
            // no need for self link in action if synchronous (as no querying
            // will ever be needed)
            //
            perform(task);
            if (!action.getStatus().getState().equals(CreationStatus.FAILED.value())) {
                status = Status.OK;
            } else {
                status = Status.BAD_REQUEST;
            }
        }

        return Response.status(status).entity(action).build();
    }

    public ActionResource getActionSubresource(String action, String oid) {
        // redirect back to the target VM if action no longer cached
        // REVISIT: ultimately we should look at redirecting
        // to the event/audit log
        //
        ActionResource exists = actions.get(oid);
        return exists != null
               ? exists
               : new ActionResource() {
                    @Override
                    public Response get() {
                        R tmp = newModel();
                        tmp.setId(getId());
                        tmp = LinkHelper.addLinks(getUriInfo(), tmp);
                        Response.Status status = Response.Status.MOVED_PERMANENTLY;
                        return Response.status(status).location(URI.create(tmp.getHref())).build();
                    }
                    @Override
                    public Action getAction() {
                        return null;
                    }
                };
    }

    public Executor getExecutor() {
        return executor;
    }

    public void setExecutor(Executor executor) {
        this.executor = executor;
    }

    private void perform(AbstractActionTask task) {
        task.action.setStatus(StatusUtils.create(CreationStatus.IN_PROGRESS));
        if (task.action.getGracePeriod() != null) {
            try {
                Thread.sleep(task.action.getGracePeriod().getExpiry());
            } catch (Exception e) {
                // ignore
            }
        }
        task.run();
    }

    public static abstract class AbstractActionTask implements Runnable {
        protected Action action;
        protected String reason;

        public AbstractActionTask(Action action) {
            this(action, "");
        }

        public AbstractActionTask(Action action, String reason) {
            this.action = action;
            this.reason = reason;
        }

        public void run() {
            try {
                execute();
                if (!action.getStatus().getState().equals(org.ovirt.engine.api.model.CreationStatus.FAILED.value())) {
                    action.setStatus(StatusUtils.create(org.ovirt.engine.api.model.CreationStatus.COMPLETE));
                }
            } catch (Throwable t) {
                String message = t.getMessage() != null ? t.getMessage() : t.getClass().getName();
                setFault(MessageFormat.format(t.getCause() != null ? t.getCause().getMessage()
                                                                     :
                                                                     reason,
                                             message),
                         t);
            }
        }

        protected abstract void execute();

        protected void setFault(String reason, Throwable t) {
            Fault fault = new Fault();
            fault.setReason(reason);
            fault.setDetail(t.getMessage());
            action.setFault(fault);
            action.setStatus(StatusUtils.create(org.ovirt.engine.api.model.CreationStatus.FAILED));
        }

        protected static String trace(Throwable t) {
            StringWriter sw = new StringWriter();
            t.printStackTrace(new PrintWriter(sw, true));
            return sw.toString();
        }
    }

    protected static class DoNothingTask extends AbstractActionTask {
        public DoNothingTask(Action action) {
            super(action);
        }
        public void execute(){
        }
    }

    /**
     * Fallback executor, starts a new thread for each task.
     */
    protected static class SimpleExecutor implements Executor {
        public void execute(Runnable task) {
            new Thread(task).start();
        }
    }
}
