// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.apache.cloudstack.context;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.UUID;

import com.cloud.projects.Project;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;

import org.apache.cloudstack.managed.threadlocal.ManagedThreadLocal;

import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;

/**
 * CallContext records information about the environment the call is made.  This
 * class must be always be available in all CloudStack code.  Every thread
 * entry point must set the context and remove it when the thread finishes.
 */
public class CallContext {
    private static final Logger s_logger = Logger.getLogger(CallContext.class);
    private static ManagedThreadLocal<CallContext> s_currentContext = new ManagedThreadLocal<CallContext>();
    private static ManagedThreadLocal<Stack<CallContext>> s_currentContextStack = new ManagedThreadLocal<Stack<CallContext>>() {
        @Override
        protected Stack<CallContext> initialValue() {
            return new Stack<CallContext>();
        }
    };

    private String contextId;
    private Account account;
    private long accountId;
    private long startEventId = 0;
    private String eventDescription;
    private String eventDetails;
    private String eventType;
    private boolean isEventDisplayEnabled = true; // default to true unless specifically set
    private User user;
    private long userId;
    private final Map<Object, Object> context = new HashMap<Object, Object>();
    private Project project;

    static EntityManager s_entityMgr;

    public static void init(EntityManager entityMgr) {
        s_entityMgr = entityMgr;
    }

    protected CallContext() {
    }

    protected CallContext(long userId, long accountId, String contextId) {
        this.userId = userId;
        this.accountId = accountId;
        this.contextId = contextId;
    }

    protected CallContext(User user, Account account, String contextId) {
        this.user = user;
        userId = user.getId();
        this.account = account;
        accountId = account.getId();
        this.contextId = contextId;
    }

    public void putContextParameter(Object key, Object value) {
        context.put(key, value);
    }

    /**
     * @param key any not null key object
     * @return the value of the key from context map
     * @throws NullPointerException if the specified key is nul
     */
    public Object getContextParameter(Object key) {
        Object value = context.get(key);
        //check if the value is present in the toString value of the key
        //due to a bug in the way we update the key by serializing and deserializing, it sometimes gets toString value of the key. @see com.cloud.api.ApiAsyncJobDispatcher#runJob
        if(value == null ) {
            value = context.get(key.toString());
        }
        return value;
    }

    public long getCallingUserId() {
        return userId;
    }

    public User getCallingUser() {
        if (user == null) {
            user = s_entityMgr.findById(User.class, userId);
        }
        return user;
    }

    public String getContextId() {
        return contextId;
    }

    public Account getCallingAccount() {
        if (account == null) {
            account = s_entityMgr.findById(Account.class, accountId);
        }
        return account;
    }

    public static CallContext current() {
        CallContext context = s_currentContext.get();

        // TODO other than async job and api dispatches, there are many system background running threads
        // that do not setup CallContext at all, however, many places in code that are touched by these background tasks
        // assume not-null CallContext. Following is a fix to address therefore caused NPE problems
        //
        // There are security implications with this. It assumes that all system background running threads are
        // indeed have no problem in running under system context.
        //
        if (context == null) {
            context = registerSystemCallContextOnceOnly();
        }

        return context;
    }

    /**
     * This method should only be called if you can propagate the context id
     * from another CallContext.
     *
     * @param callingUser calling user
     * @param callingAccount calling account
     * @param contextId context id propagated from another call context
     * @return CallContext
     */
    public static CallContext register(User callingUser, Account callingAccount, String contextId) {
        return register(callingUser, callingAccount, null, null, contextId);
    }

    protected static CallContext register(User callingUser, Account callingAccount, Long userId, Long accountId, String contextId) {
        /*
                Unit tests will have multiple times of setup/tear-down call to this, remove assertions to all unit test to run
                assert s_currentContext.get() == null : "There's a context already so what does this new register context mean? " + s_currentContext.get().toString();
                if (s_currentContext.get() != null) { // FIXME: This should be removed soon.  I added this check only to surface all the places that have this problem.
                    throw new CloudRuntimeException("There's a context already so what does this new register context mean? " + s_currentContext.get().toString());
                }
        */
        CallContext callingContext = null;
        if (userId == null || accountId == null) {
            callingContext = new CallContext(callingUser, callingAccount, contextId);
        } else {
            callingContext = new CallContext(userId, accountId, contextId);
        }
        s_currentContext.set(callingContext);
        NDC.push("ctx-" + UuidUtils.first(contextId));
        if (s_logger.isTraceEnabled()) {
            s_logger.trace("Registered: " + callingContext);
        }

        s_currentContextStack.get().push(callingContext);

        return callingContext;
    }

    public static CallContext registerPlaceHolderContext() {
        CallContext context = new CallContext(0, 0, UUID.randomUUID().toString());
        s_currentContext.set(context);

        s_currentContextStack.get().push(context);
        return context;
    }

    public static CallContext register(User callingUser, Account callingAccount) {
        return register(callingUser, callingAccount, UUID.randomUUID().toString());
    }

    public static CallContext registerSystemCallContextOnceOnly() {
        try {
            CallContext context = s_currentContext.get();
            if (context == null) {
                return register(null, null, User.UID_SYSTEM, Account.ACCOUNT_ID_SYSTEM, UUID.randomUUID().toString());
            }
            assert context.getCallingUserId() == User.UID_SYSTEM : "You are calling a very specific method that registers a one time system context.  This method is meant for background threads that does processing.";
            return context;
        } catch (Exception e) {
            s_logger.error("Failed to register the system call context.", e);
            throw new CloudRuntimeException("Failed to register system call context", e);
        }
    }

    public static CallContext register(String callingUserUuid, String callingAccountUuid) {
        Account account = s_entityMgr.findByUuid(Account.class, callingAccountUuid);
        if (account == null) {
            throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, callingAccountUuid);
        }

        User user = s_entityMgr.findByUuid(User.class, callingUserUuid);
        if (user == null) {
            throw new CloudAuthenticationException("The user is no longer current.").add(User.class, callingUserUuid);
        }
        return register(user, account);
    }

    public static CallContext register(long callingUserId, long callingAccountId) throws CloudAuthenticationException {
        Account account = s_entityMgr.findById(Account.class, callingAccountId);
        if (account == null) {
            throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, Long.toString(callingAccountId));
        }
        User user = s_entityMgr.findById(User.class, callingUserId);
        if (user == null) {
            throw new CloudAuthenticationException("The user is no longer current.").add(User.class, Long.toString(callingUserId));
        }
        return register(user, account);
    }

    public static CallContext register(long callingUserId, long callingAccountId, String contextId) throws CloudAuthenticationException {
        Account account = s_entityMgr.findById(Account.class, callingAccountId);
        if (account == null) {
            throw new CloudAuthenticationException("The account is no longer current.").add(Account.class, Long.toString(callingAccountId));
        }
        User user = s_entityMgr.findById(User.class, callingUserId);
        if (user == null) {
            throw new CloudAuthenticationException("The user is no longer current.").add(User.class, Long.toString(callingUserId));
        }
        return register(user, account, contextId);
    }

    public static void unregisterAll() {
        while (unregister() != null) {
            // NOOP
        }
    }

    public static CallContext unregister() {
        CallContext context = s_currentContext.get();
        if (context == null) {
            return null;
        }
        s_currentContext.remove();
        if (s_logger.isTraceEnabled()) {
            s_logger.trace("Unregistered: " + context);
        }
        String contextId = context.getContextId();
        String sessionIdOnStack = null;
        String sessionIdPushedToNDC = "ctx-" + UuidUtils.first(contextId);
        while ((sessionIdOnStack = NDC.pop()) != null) {
            if (sessionIdOnStack.isEmpty() || sessionIdPushedToNDC.equals(sessionIdOnStack)) {
                break;
            }
            if (s_logger.isTraceEnabled()) {
                s_logger.trace("Popping from NDC: " + contextId);
            }
        }

        Stack<CallContext> stack = s_currentContextStack.get();
        stack.pop();

        if (!stack.isEmpty()) {
            s_currentContext.set(stack.peek());
        } else {
            s_currentContext.set(null);
        }

        return context;
    }

    public void setStartEventId(long startEventId) {
        this.startEventId = startEventId;
    }

    public long getStartEventId() {
        return startEventId;
    }

    public long getCallingAccountId() {
        return accountId;
    }

    public String getCallingAccountUuid() {
        return getCallingAccount().getUuid();
    }

    public String getCallingUserUuid() {
        return getCallingUser().getUuid();
    }

    public void setEventDetails(String eventDetails) {
        this.eventDetails = eventDetails;
    }

    public String getEventDetails() {
        return eventDetails;
    }

    public String getEventType() {
        return eventType;
    }

    public void setEventType(String eventType) {
        this.eventType = eventType;
    }

    public String getEventDescription() {
        return eventDescription;
    }

    public void setEventDescription(String eventDescription) {
        this.eventDescription = eventDescription;
    }

    public Project getProject() {
        return this.project;
    }

    public void setProject(Project project) {
        this.project = project;
    }

    /**
     * Whether to display the event to the end user.
     * @return true - if the event is to be displayed to the end user, false otherwise.
     */
    public boolean isEventDisplayEnabled() {
        return isEventDisplayEnabled;
    }

    public void setEventDisplayEnabled(boolean eventDisplayEnabled) {
        isEventDisplayEnabled = eventDisplayEnabled;
    }

    public Map<Object, Object> getContextParameters() {
        return context;
    }

    public void putContextParameters(Map<Object, Object> details){
        if (details == null) return;
        for(Map.Entry<Object,Object>entry : details.entrySet()){
            putContextParameter(entry.getKey(), entry.getValue());
        }
    }

    public static void setActionEventInfo(String eventType, String description) {
        CallContext context = CallContext.current();
        if (context != null) {
            context.setEventType(eventType);
            context.setEventDescription(description);
        }
    }

    @Override
    public String toString() {
        return new StringBuilder("CCtxt[acct=").append(getCallingAccountId())
            .append("; user=")
            .append(getCallingUserId())
            .append("; id=")
            .append(contextId)
            .append("]")
            .toString();
    }
}
