// 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.UUID;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

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;
import org.apache.logging.log4j.ThreadContext;

/**
 * LogContext records information about the environment the API call is made.  This
 * class must be always be available in all CloudStack code.
 */
public class LogContext {
    protected static Logger LOGGER = LogManager.getLogger(LogContext.class);
    private static ManagedThreadLocal<LogContext> s_currentContext = new ManagedThreadLocal<LogContext>();

    private String logContextId;
    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<String, String> context = new HashMap<String, String>();

    static EntityManager s_entityMgr;

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

    protected LogContext() {
    }

    protected LogContext(long userId, long accountId, String logContextId) {
        this.userId = userId;
        this.accountId = accountId;
        this.logContextId = logContextId;
    }

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

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

    public String getContextParameter(String key) {
        return context.get(key);
    }

    public long getCallingUserId() {
        return userId;
    }

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

    public String getLogContextId() {
        return logContextId;
    }

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

    public static LogContext current() {
        LogContext context = s_currentContext.get();
        if (context == null) {
            context = registerSystemLogContextOnceOnly();
        }
        return context;
    }

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

    protected static LogContext register(User callingUser, Account callingAccount, Long userId, Long accountId, String contextId) {
        LogContext callingContext = null;
        if (userId == null || accountId == null) {
            callingContext = new LogContext(callingUser, callingAccount, contextId);
        } else {
            callingContext = new LogContext(userId, accountId, contextId);
        }
        s_currentContext.set(callingContext);
        ThreadContext.put("logcontextid", UuidUtils.first(contextId));
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Registered for log: " + callingContext);
        }
        return callingContext;
    }

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

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

    public static LogContext registerSystemLogContextOnceOnly() {
        try {
            LogContext 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) {
            LOGGER.error("Failed to register the system log context.", e);
            throw new CloudRuntimeException("Failed to register system log context", e);
        }
    }

    public static LogContext 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 LogContext 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 LogContext 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 unregister() {
        LogContext context = s_currentContext.get();
        if (context != null) {
            s_currentContext.remove();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Unregistered: " + context);
            }
        }
        ThreadContext.clearMap();
    }

    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;
    }

    /**
     * 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<String, String> getContextParameters() {
        return context;
    }

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

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

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