/*
 * Copyright 2015-2019 the original author or authors.
 *
 * 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.lastaflute.web.servlet.session;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.dbflute.helper.message.ExceptionMessageBuilder;
import org.dbflute.optional.OptionalThing;
import org.lastaflute.core.direction.FwAssistantDirector;
import org.lastaflute.core.message.UserMessages;
import org.lastaflute.web.LastaWebKey;
import org.lastaflute.web.direction.FwWebDirection;
import org.lastaflute.web.exception.SessionAttributeCannotCastException;
import org.lastaflute.web.exception.SessionAttributeNotFoundException;
import org.lastaflute.web.servlet.filter.hotdeploy.HotdeployHttpSession;
import org.lastaflute.web.servlet.request.scoped.ScopedMessageHandler;
import org.lastaflute.web.util.LaRequestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The simple implementation of session manager. <br>
 * This class is basically defined at DI setting file.
 * @author jflute
 */
public class SimpleSessionManager implements SessionManager {

    // ===================================================================================
    //                                                                          Definition
    //                                                                          ==========
    private static final Logger logger = LoggerFactory.getLogger(SimpleSessionManager.class);

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    /** The assistant director (AD) for framework. (NotNull: after initialization) */
    @Resource
    private FwAssistantDirector assistantDirector;

    /** The shared storage of session for session sharing. (NotNull, EmptyAllowed: if no storage) */
    protected OptionalThing<SessionSharedStorage> sessionSharedStorage = OptionalThing.empty(); // not null

    /** The arranger of HTTP session for session sharing. (NotNull, EmptyAllowed: if no arranger) */
    protected OptionalThing<HttpSessionArranger> httpSessionArranger = OptionalThing.empty(); // not null

    protected ScopedMessageHandler errorsHandler; // lazy loaded
    protected ScopedMessageHandler infoHandler; // lazy loaded

    // ===================================================================================
    //                                                                          Initialize
    //                                                                          ==========
    /**
     * Initialize this component. <br>
     * This is basically called by DI setting file.
     */
    @PostConstruct
    public synchronized void initialize() {
        final FwWebDirection direction = assistWebDirection();
        final SessionResourceProvider provider = direction.assistSessionResourceProvider();
        sessionSharedStorage = prepareSessionSharedStorage(provider);
        httpSessionArranger = prepareHttpSessionArranger(provider);
        showBootLogging();
    }

    protected FwWebDirection assistWebDirection() {
        return assistantDirector.assistWebDirection();
    }

    protected OptionalThing<SessionSharedStorage> prepareSessionSharedStorage(SessionResourceProvider provider) {
        final SessionSharedStorage specifiedStorage = provider != null ? provider.provideSharedStorage() : null;
        return OptionalThing.ofNullable(specifiedStorage, () -> {
            throw new IllegalStateException("Not found the session shared storage: " + provider);
        });
    }

    protected OptionalThing<HttpSessionArranger> prepareHttpSessionArranger(SessionResourceProvider provider) {
        final HttpSessionArranger specifiedStorage = provider != null ? provider.provideHttpSessionArranger() : null;
        return OptionalThing.ofNullable(specifiedStorage, () -> {
            throw new IllegalStateException("Not found the HTTP session arranger: " + provider);
        });
    }

    protected void showBootLogging() {
        if (logger.isInfoEnabled()) {
            logger.info("[Session Manager]");
            logger.info(" sessionSharedStorage: " + sessionSharedStorage);
            logger.info(" httpSessionArranger: " + httpSessionArranger);
        }
    }

    // ===================================================================================
    //                                                                  Attribute Handling
    //                                                                  ==================
    @Override
    public <ATTRIBUTE> OptionalThing<ATTRIBUTE> getAttribute(String key, Class<ATTRIBUTE> attributeType) {
        assertArgumentNotNull("key", key);
        final OptionalThing<ATTRIBUTE> foundShared = findAttributeInShareStorage(key, attributeType);
        if (foundShared.isPresent()) {
            return foundShared;
        }
        if (isSuppressHttpSession()) { // needs to check because it cannot use attribute name list in message
            return OptionalThing.ofNullable(null, () -> {
                final String msg = "Not found the session attribute in shared storage by the string key: " + key;
                throw new SessionAttributeNotFoundException(msg);
            });
        }
        final boolean withShared = true; // automatically synchronize with shared storage
        return findHttpAttribute(key, attributeType, withShared);
    }

    protected <ATTRIBUTE> OptionalThing<ATTRIBUTE> findAttributeInShareStorage(String key, Class<ATTRIBUTE> attributeType) {
        final OptionalThing<ATTRIBUTE> found = sessionSharedStorage.flatMap(storage -> storage.getAttribute(key, attributeType));
        if (logger.isDebugEnabled() && found.isPresent()) {
            logger.debug("Found the session attribute in shared storage: {}={}", key, found.get());
        }
        return found;
    }

    protected <ATTRIBUTE> OptionalThing<ATTRIBUTE> findHttpAttribute(String key, Class<ATTRIBUTE> attributeType, boolean withShared) {
        final HttpSession session = getSessionExisting();
        final Object original = session != null ? session.getAttribute(key) : null;
        if (original instanceof HotdeployHttpSession.SerializedObjectHolder) { // e.g. hot to cool in Tomcat
            logger.debug("...Removing relic session of hot deploy: {}", original);
            deleteHttpAttribute(key); // treated as no-existing
            if (withShared) {
                deleteAttributeFromSharedStorage(key); // also from shared by option
            }
            return OptionalThing.empty();
        }
        final ATTRIBUTE attribute;
        if (original != null) {
            try {
                attribute = attributeType.cast(original);
            } catch (ClassCastException e) {
                final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
                br.addNotice("Cannot cast the session attribute");
                br.addItem("Attribute Key");
                br.addElement(key);
                br.addItem("Specified Type");
                br.addElement(attributeType + "@" + Integer.toHexString(attributeType.hashCode()));
                br.addElement("loader: " + attributeType.getClassLoader());
                br.addItem("Existing Attribute");
                final Class<? extends Object> originType = original.getClass();
                br.addElement(originType + "@" + Integer.toHexString(originType.hashCode()));
                br.addElement("loader: " + originType.getClassLoader());
                br.addElement("toString(): " + original.toString());
                br.addItem("Attribute List");
                br.addElement(extractHttpAttributeNameList());
                final String msg = br.buildExceptionMessage();
                throw new SessionAttributeCannotCastException(msg, e);
            }
            if (withShared) {
                reflectAttributeToSharedStorage(key, attribute);
            }
        } else {
            attribute = null;
        }
        return OptionalThing.ofNullable(attribute, () -> {
            final List<String> nameList = extractHttpAttributeNameList();
            final String msg = "Not found the session attribute by the string key: " + key + " existing=" + nameList;
            throw new SessionAttributeNotFoundException(msg);
        });
    }

    protected void reflectAttributeToSharedStorage(String key, Object value) {
        sessionSharedStorage.ifPresent(storage -> {
            logger.debug("...Reflecting the session attribute to shared storage: {}={}", key, value);
            storage.setAttribute(key, value);
        });
    }

    @Override
    public void setAttribute(String key, Object value) {
        assertArgumentNotNull("key", key);
        assertArgumentNotNull("value", value);
        saveAttributeToSharedStorage(key, value);
        saveHttpAttribute(key, value);
    }

    protected void saveAttributeToSharedStorage(String key, Object value) {
        sessionSharedStorage.ifPresent(storage -> {
            logger.debug("...Saving the session attribute to shared storage: {}={}", key, value);
            storage.setAttribute(key, value);
        });
    }

    protected void saveHttpAttribute(String key, Object value) {
        if (isSuppressHttpSession()) { // needs to check because of or-created
            return;
        }
        getSessionOrCreated().setAttribute(key, value);
    }

    @Override
    public void removeAttribute(String key) {
        assertArgumentNotNull("key", key);
        deleteAttributeFromSharedStorage(key);
        deleteHttpAttribute(key);
    }

    protected void deleteAttributeFromSharedStorage(String key) {
        sessionSharedStorage.ifPresent(storage -> {
            logger.debug("...Removing the session attribute to shared storage: {}", key);
            storage.removeAttribute(key);
        });
    }

    protected void deleteHttpAttribute(String key) {
        final HttpSession session = getSessionExisting();
        if (session != null) {
            session.removeAttribute(key);
        }
    }

    // see interface ScopedAttributeHolder for the detail
    //@Override
    //@SuppressWarnings("unchecked")
    //public <ATTRIBUTE> OptionalThing<ATTRIBUTE> getAttribute(Class<ATTRIBUTE> typeKey) {
    //    assertArgumentNotNull("type", typeKey);
    //    final HttpSession session = getSessionExisting();
    //    final String key = typeKey.getName();
    //    final ATTRIBUTE attribute = session != null ? (ATTRIBUTE) session.getAttribute(key) : null;
    //    return OptionalThing.ofNullable(attribute, () -> {
    //        final List<String> nameList = getAttributeNameList();
    //        final String msg = "Not found the session attribute by the typed key: " + key + " existing=" + nameList;
    //        throw new SessionAttributeNotFoundException(msg);
    //    });
    //}
    //@Override
    //public void setAttribute(Object value) {
    //    assertArgumentNotNull("value", value);
    //    checkTypedAttributeSettingMistake(value);
    //    getSessionOrCreated().setAttribute(value.getClass().getName(), value);
    //}
    //protected void checkTypedAttributeSettingMistake(Object value) {
    //    if (value instanceof String) {
    //        final ExceptionMessageBuilder br = new ExceptionMessageBuilder();
    //        br.addNotice("The value for typed attribute was simple string type.");
    //        br.addItem("Advice");
    //        br.addElement("The value should not be string.");
    //        br.addElement("Do you forget value setting for the string key?");
    //        br.addElement("The typed attribute setting cannot accept string");
    //        br.addElement("to suppress setting mistake like this:");
    //        br.addElement("  (x):");
    //        br.addElement("    sessionManager.setAttribute(\"foo.bar\")");
    //        br.addElement("  (o):");
    //        br.addElement("    sessionManager.setAttribute(\"foo.bar\", value)");
    //        br.addElement("  (o):");
    //        br.addElement("    sessionManager.setAttribute(bean)");
    //        br.addItem("Specified Value");
    //        br.addElement(value != null ? value.getClass().getName() : null);
    //        br.addElement(value);
    //        final String msg = br.buildExceptionMessage();
    //        throw new IllegalArgumentException(msg);
    //    }
    //}
    //@Override
    //public void removeAttribute(Class<?> type) {
    //    assertArgumentNotNull("type", type);
    //    final HttpSession session = getSessionExisting();
    //    if (session != null) {
    //        session.removeAttribute(type.getName());
    //    }
    //}

    // ===================================================================================
    //                                                                    Session Handling
    //                                                                    ================
    @Override
    public String getSessionId() {
        return sessionSharedStorage.flatMap(storage -> storage.getSessionId()).orElseGet(() -> {
            if (isSuppressHttpSession()) { // needs to check because of or-created
                String msg = "Not found the session ID of shared storage. (required if no http session)";
                throw new IllegalStateException(msg);
            }
            return getSessionOrCreated().getId(); // normally here
        });
    }

    @Override
    public void invalidate() {
        destroySharedStorage();
        destroyHttpSession();
    }

    protected void destroySharedStorage() {
        sessionSharedStorage.ifPresent(storage -> storage.invalidate());
    }

    protected void destroyHttpSession() {
        final HttpSession session = getSessionExisting();
        if (session != null) {
            session.invalidate();
        }
    }

    @Override
    public void regenerateSessionId() {
        switchSessionIdOfSharedStorage();
        switchHttpSessionId();
    }

    protected void switchSessionIdOfSharedStorage() {
        sessionSharedStorage.ifPresent(storage -> storage.regenerateSessionId());
    }

    protected void switchHttpSessionId() {
        if (getSessionExisting() == null) { // this check is not required but be simple
            return; // unnecessary to regenerate
        }
        final Map<String, Object> httpSessionMap = extractHttpSessionMap();
        destroyHttpSession(); // regenerate ID
        for (Entry<String, Object> entry : httpSessionMap.entrySet()) {
            saveHttpAttribute(entry.getKey(), entry.getValue()); // inherit existing attributes
        }
    }

    protected Map<String, Object> extractHttpSessionMap() { // native only
        final List<String> keyList = extractHttpAttributeNameList();
        if (keyList.isEmpty()) {
            return Collections.emptyMap();
        }
        final Map<String, Object> httpSessionMap = new LinkedHashMap<String, Object>(keyList.size());
        final boolean withShared = false; // because of native only
        for (String key : keyList) { // already checked so at least one loop
            findHttpAttribute(key, Object.class, withShared).ifPresent(value -> { // almost be present, but just in case
                httpSessionMap.put(key, value);
            });
        }
        return httpSessionMap;
    }

    // ===================================================================================
    //                                                                    Message Handling
    //                                                                    ================
    @Override
    public ScopedMessageHandler errors() {
        if (errorsHandler == null) {
            synchronized (this) {
                if (errorsHandler == null) {
                    errorsHandler = createScopedMessageHandler(getErrorMessagesKey());
                }
            }
        }
        return errorsHandler;
    }

    protected String getErrorMessagesKey() {
        return LastaWebKey.ACTION_ERRORS_KEY;
    }

    @Override
    public ScopedMessageHandler info() {
        if (infoHandler == null) {
            synchronized (this) {
                if (infoHandler == null) {
                    infoHandler = createScopedMessageHandler(getInfoMessagesKey());
                }
            }
        }
        return infoHandler;
    }

    protected ScopedMessageHandler createScopedMessageHandler(String messagesKey) {
        return new ScopedMessageHandler(this, UserMessages.GLOBAL, messagesKey);
    }

    protected String getInfoMessagesKey() {
        return LastaWebKey.ACTION_INFO_KEY;
    }

    // ===================================================================================
    //                                                                        Assist Logic
    //                                                                        ============
    protected HttpServletRequest getRequest() { // basically not null (but null allowed when asynchronous process)
        return LaRequestUtil.getRequest();
    }

    protected HttpSession getSessionOrCreated() { // not null
        //if (isSuppressHttpSession()) {
        //    return ...; // #hope use empty session, but caller check for now by jflute
        //}
        return readyHttpSession(getRequest(), true); // #thinking needs to check for asynchronous process? by jflute
    }

    protected HttpSession getSessionExisting() { // null allowed
        if (isSuppressHttpSession()) {
            return null;
        }
        final HttpServletRequest request = getRequest(); // null allowed when e.g. asynchronous process
        return request != null ? readyHttpSession(request, false) : null;
    }

    protected boolean isSuppressHttpSession() {
        return sessionSharedStorage.map(storage -> storage.suppressesHttpSession()).orElse(false);
    }

    protected HttpSession readyHttpSession(HttpServletRequest request, boolean create) {
        if (httpSessionArranger.isPresent()) {
            return httpSessionArranger.get().create(request, create); // null allowed if create is false
        } else {
            return request.getSession(create); // as default
        }
        // it should return null without default process if arranger.create() returns null  
        //return httpSessionArranger.map(ger -> ger.create(request, create)).orElseGet(() -> {
        //    return request.getSession(create); // as default
        //});
    }

    protected List<String> extractHttpAttributeNameList() { // native only
        final HttpSession session = getSessionExisting();
        if (session == null) {
            return Collections.emptyList();
        }
        final Enumeration<String> attributeNames = session.getAttributeNames();
        final List<String> nameList = new ArrayList<String>();
        while (attributeNames.hasMoreElements()) {
            nameList.add((String) attributeNames.nextElement());
        }
        return Collections.unmodifiableList(nameList);
    }

    // ===================================================================================
    //                                                                        Small Helper
    //                                                                        ============
    protected void assertArgumentNotNull(String variableName, Object value) {
        if (variableName == null) {
            throw new IllegalArgumentException("The variableName should not be null.");
        }
        if (value == null) {
            throw new IllegalArgumentException("The argument '" + variableName + "' should not be null.");
        }
    }
}