/* BDF - Copyright (c) 2023-2025 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package fr.exemole.bdfserver.servlets;

import fr.exemole.bdfserver.api.BdfExtensionInitializer;
import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.BdfServerConstants;
import fr.exemole.bdfserver.api.EditSession;
import fr.exemole.bdfserver.api.externalsource.FichothequeSharing;
import fr.exemole.bdfserver.api.roles.SphereSupervisor;
import fr.exemole.bdfserver.api.users.BdfUser;
import fr.exemole.bdfserver.conf.BdfServerDirs;
import fr.exemole.bdfserver.conf.ConfConstants;
import fr.exemole.bdfserver.conf.ConfDirs;
import fr.exemole.bdfserver.conf.ConfUtils;
import fr.exemole.bdfserver.conf.WebappDirs;
import fr.exemole.bdfserver.impl.BdfServerImpl;
import fr.exemole.bdfserver.multi.MultiConf;
import fr.exemole.bdfserver.multi.api.Multi;
import fr.exemole.bdfserver.multi.api.MultiConstants;
import fr.exemole.bdfserver.multi.api.MultiEditor;
import fr.exemole.bdfserver.multi.api.MultiMetadata;
import fr.exemole.bdfserver.multi.api.MultiMetadataEditor;
import fr.exemole.bdfserver.multi.api.central.CentralSphere;
import fr.exemole.bdfserver.multi.api.central.CentralSphereEditor;
import fr.exemole.bdfserver.multi.api.namespaces.FichothequeSpace;
import fr.exemole.bdfserver.multi.commands.CreationParameters;
import fr.exemole.bdfserver.multi.impl.CentralSphereImpl;
import fr.exemole.bdfserver.multi.impl.MultiMetadataImpl;
import fr.exemole.bdfserver.storage.directory.StorageDirectoryUtils;
import fr.exemole.bdfserver.storage.directory.bdfdata.StartEngine;
import fr.exemole.bdfserver.storage.directory.tools.SphereCopyEngine;
import fr.exemole.bdfserver.tools.BdfMessageLocalisationFactory;
import fr.exemole.bdfserver.tools.roles.RoleUtils;
import fr.exemole.bdfserver.tools.storage.ResourceJsAnalyser;
import fr.exemole.bdfserver.tools.storage.Storages;
import fr.exemole.bdfserver.tools.zip.BdfServerZip;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;
import net.fichotheque.EditOrigin;
import net.fichotheque.Fichotheque;
import net.fichotheque.Metadata;
import net.fichotheque.SubsetKey;
import net.fichotheque.sphere.SphereEditor;
import net.mapeadores.util.attr.Attribute;
import net.mapeadores.util.buildinfo.BuildInfo;
import net.mapeadores.util.exceptions.NestedIOException;
import net.mapeadores.util.exceptions.ShouldNotOccurException;
import net.mapeadores.util.io.ResourceStorages;
import net.mapeadores.util.jslib.JsAnalyser;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.LangPreference;
import net.mapeadores.util.localisation.MessageLocalisation;
import net.mapeadores.util.localisation.MessageLocalisationProvider;
import net.mapeadores.util.logging.ErrorMessageException;
import net.mapeadores.util.mimetype.MimeTypeResolver;
import net.mapeadores.util.mimetype.MimeTypeUtils;
import net.mapeadores.util.text.Label;
import fr.exemole.bdfserver.storage.directory.StorageDirectory;


/**
 *
 * @author Vincent Calame
 */
class MultiImpl implements Multi {

    private final BDFWebapp bdfWebapp;
    private final MultiConf multiConf;
    private final String servletContextName;
    private final BdfMessageLocalisationFactory messageLocalisationFactory;
    private final InternalMessageLocalisationProvider messageLocalisationProvider = new InternalMessageLocalisationProvider();
    private final ResourceStorages webappResourceStorages;
    private final JsAnalyser webappsJsAnalyser;
    private final String authentificationSharing;
    private final Set<String> unmodifiableExistingNameSet;
    private final BuildInfo buildInfo;
    private final FichothequeSharing fichothequeSharing;
    private final SphereSupervisor sphereSupervisor;
    private final MimeTypeResolver mimeTypeResolver;
    private final List<BdfExtensionInitializer> extensionInitializerList;
    private final SortedMap<String, String> stateMap = new TreeMap<String, String>();
    private final Map<String, BdfServer> bdfServerMap = new HashMap<String, BdfServer>();
    private final Map<String, CentralSphereImpl> centralSphereMap = new LinkedHashMap<String, CentralSphereImpl>();
    private final Set<String> centralSphereNameSet = Collections.unmodifiableSet(centralSphereMap.keySet());
    private final ConfDirs multiConfDirs;
    private final MultiMetadataImpl multiMetadata;


    MultiImpl(BDFWebapp bdfWebapp, String servletContextName, BDFInitParameters initParameters) {
        WebappDirs webappDirs = initParameters.webappDirs();
        this.bdfWebapp = bdfWebapp;
        this.multiConf = MultiConf.build(ConfUtils.getMultiIniFile(webappDirs));
        this.servletContextName = servletContextName;
        this.buildInfo = BDFInit.buildInfo();
        this.mimeTypeResolver = MimeTypeUtils.DEFAULT_RESOLVER;
        this.extensionInitializerList = BDFInit.extensionInitializerList(webappDirs);
        this.webappResourceStorages = BDFInit.initWebappResourceStorages(webappDirs, extensionInitializerList);
        this.messageLocalisationFactory = BdfMessageLocalisationFactory.buildFactory(webappResourceStorages);
        for (String name : ConfUtils.getExistingNameSet(webappDirs)) {
            stateMap.put(name, Multi.NOT_INIT_STATE);
        }
        this.webappsJsAnalyser = new ResourceJsAnalyser(webappResourceStorages);
        this.authentificationSharing = initParameters.authentificationSharing();
        this.unmodifiableExistingNameSet = Collections.unmodifiableSet(stateMap.keySet());
        if (!authentificationSharing.equals(MultiConstants.NONE_SHARING)) {
            this.fichothequeSharing = new InternalFichothequeSharing();
        } else {
            this.fichothequeSharing = null;
        }
        if (authentificationSharing.equals(MultiConstants.STRICT_SHARING)) {
            this.sphereSupervisor = new InternalStrictSphereSupervisor();
        } else {
            this.sphereSupervisor = RoleUtils.ALL_SPHERESUPERVISOR;
        }
        this.multiConfDirs = webappDirs.getMultiDirs();
        StorageDirectory storageDirectory = StorageDirectoryUtils.toStorageDirectory(multiConfDirs, true);
        this.multiMetadata = MultiMetadataImpl.build(storageDirectory, multiConf);
        for (SubsetKey sphereKey : initParameters.centralSphereList()) {
            String name = sphereKey.getSubsetName();
            centralSphereMap.put(name, CentralSphereImpl.build(storageDirectory, name));
        }
        checkMetadata();
    }

    @Override
    public MultiConf getMultiConf() {
        return multiConf;
    }

    @Override
    public String getServletContextName() {
        return servletContextName;
    }

    @Override
    public ResourceStorages getWebappsResourceStorages() {
        return webappResourceStorages;
    }

    @Override
    public MimeTypeResolver getMimeTypeResolver() {
        return mimeTypeResolver;
    }

    @Override
    public JsAnalyser getWebappsJsAnalyser() {
        return webappsJsAnalyser;
    }

    @Override
    public String getAuthentificationSharing() {
        return authentificationSharing;
    }

    @Override
    public Set<String> getExistingNameSet() {
        return unmodifiableExistingNameSet;
    }

    @Override
    public BuildInfo getBuildInfo() {
        return buildInfo;
    }

    @Override
    public String getState(String fichothequeName) {
        String state = stateMap.get(fichothequeName);
        if (state != null) {
            return state;
        } else {
            return Multi.NOT_EXISTING_STATE;
        }
    }

    @Override
    public BdfServer getBdfServer(String name) throws ErrorMessageException {
        BdfServer bdfServer = bdfServerMap.get(name);
        if (bdfServer != null) {
            return bdfServer;
        }
        WebappDirs webappDirs = bdfWebapp.getWebappDirs();
        String state = stateMap.get(name);
        if (state == null) {
            boolean here = false;
            if ((multiConf != null) && (ConfUtils.isValidMultiName(name))) {
                if (ConfUtils.isExistingName(webappDirs, name)) {
                    here = true;
                }
            }
            if (here) {
                name = name.intern();
                stateMap.put(name, Multi.NOT_INIT_STATE);
            } else {
                throw new ErrorMessageException(MultiConstants.UNKNOWN_FICHOTHEQUE_ERROR, name);
            }
        } else if (state.equals(Multi.INACTIVE_STATE)) {
            throw new ErrorMessageException(MultiConstants.INACTIVE_FICHOTHEQUE_ERROR);
        }
        String intern = name.intern();
        synchronized (intern) {
            bdfServer = bdfServerMap.get(intern);
            if (bdfServer == null) {
                Map<String, Object> contextObjectMap = new HashMap<String, Object>();
                contextObjectMap.put(BdfServerConstants.SMTPMANAGER_CONTEXTOBJECT, bdfWebapp.getSmtpManager());
                contextObjectMap.put(BdfServerConstants.SESSIONOBSERVER_CONTEXTOBJECT, bdfWebapp.getSessionObserverInstance(name));
                contextObjectMap.put(BdfServerConstants.FICHOTHEQUESHARING_CONTEXTOBJECT, fichothequeSharing);
                contextObjectMap.put(BdfServerConstants.SPHERESUPERVISOR_CONTEXTOBJECT, sphereSupervisor);
                contextObjectMap.put(BdfServerConstants.MIMETYPERESOLVER_CONTEXTOBJECT, mimeTypeResolver);
                Storages storages = BDFInit.storages(webappDirs.getBdfServerDirs(name), webappResourceStorages);
                bdfServer = new BdfServerImpl(name, storages, buildInfo, extensionInitializerList, contextObjectMap);
                bdfServerMap.put(intern, bdfServer);
                stateMap.put(intern, Multi.ACTIVE_STATE);
            }
        }
        return bdfServer;
    }

    @Override
    public BdfServerDirs getBdfServerDirs(String name) {
        if (multiConf == null) {
            name = ConfConstants.UNIQUE_NAME;
        }
        if (!stateMap.containsKey(name)) {
            return null;
        }
        return bdfWebapp.getWebappDirs().getBdfServerDirs(name);
    }

    @Override
    public ConfDirs getMultiDirs() {
        return multiConfDirs;
    }

    @Override
    public MultiMetadata getMultiMetadata() {
        return multiMetadata;
    }

    @Override
    public MultiEditor getMultiEditor(EditOrigin editOrigin) {
        return new InternalMultiEditor(editOrigin);
    }


    @Override
    public MessageLocalisationProvider getMessageLocalisationProvider() {
        return messageLocalisationProvider;
    }

    @Override
    public Set<String> getCentralSphereNameSet() {
        return centralSphereNameSet;
    }

    @Override
    public CentralSphere getCentralSphere(String name) {
        return centralSphereMap.get(name);
    }

    @Override
    public synchronized void createFichotheque(CreationParameters creationParameters) {
        String newFichothequeName = creationParameters.getNewFichothequeName();
        if (!ConfUtils.isValidMultiName(newFichothequeName)) {
            throw new IllegalArgumentException("wrong name: " + newFichothequeName);
        }
        if (stateMap.containsKey(newFichothequeName)) {
            throw new IllegalArgumentException("existing name: " + newFichothequeName);
        }
        newFichothequeName = newFichothequeName.intern();
        BdfServerDirs dirs = bdfWebapp.getWebappDirs().getBdfServerDirs(newFichothequeName);
        switch (creationParameters.getType()) {
            case CreationParameters.NEW_TYPE: {
                StartEngine.start(dirs, creationParameters.startValues())
                        .addFirstSphere();
                break;
            }
            case CreationParameters.NEW_WITH_SPHERES_TYPE: {
                StartEngine.start(dirs, creationParameters.startValues());
                SphereCopyEngine.run(dirs, creationParameters.getSourceBdfServer());
                break;
            }
            case CreationParameters.NEW_WITH_CENTRAL_TYPE: {
                StartEngine.start(dirs, creationParameters.startValues())
                        .addCentralSpheres();
                break;
            }
            case CreationParameters.DUPLICATE_TYPE: {
                try {
                    File tempZipFile = File.createTempFile("bdf", "zip");
                    BdfServerZip bdfServerZip = new BdfServerZip(creationParameters.getSourceBdfServer(), creationParameters.getSubsetEligibility());
                    try (ZipOutputStream os = new ZipOutputStream(new FileOutputStream(tempZipFile))) {
                        os.setLevel(Deflater.NO_COMPRESSION);
                        bdfServerZip.zip(os);
                    }
                    ConfUtils.copyZippedFiles(dirs, tempZipFile);
                    tempZipFile.delete();
                } catch (IOException ioe) {
                    throw new NestedIOException(ioe);
                }
                break;
            }
        }
        BdfServer bdfServer;
        try {
            bdfServer = getBdfServer(newFichothequeName);
        } catch (ErrorMessageException eme) {
            throw new ShouldNotOccurException(eme);
        }
        switch (creationParameters.getType()) {
            case CreationParameters.NEW_WITH_CENTRAL_TYPE: {
                try (EditSession editSession = bdfServer.initEditSession("_admin", "CentralSphere")) {
                    for (String sphereName : creationParameters.startValues().centralSpheres()) {
                        CentralSphere centralSphere = getCentralSphere(sphereName);
                        Metadata metadata = centralSphere.getMetadata();
                        SphereEditor sphereEditor = editSession.getFichothequeEditor().getSphereEditor(SubsetKey.build(SubsetKey.CATEGORY_SPHERE, sphereName));
                        for (Label label : metadata.getTitleLabels()) {
                            sphereEditor.getSphereMetadataEditor().putTitle(label);
                        }
                    }
                }
                break;
            }
        }

    }

    private void checkMetadata() {
        Attribute inactiveAttribute = multiMetadata.getAttributes().getAttribute(FichothequeSpace.INACTIVE_KEY);
        Set<String> newInactiveSet = new HashSet<String>();
        if (inactiveAttribute != null) {
            for (String value : inactiveAttribute) {
                newInactiveSet.add(value);
            }
        }
        for (Map.Entry<String, String> entry : stateMap.entrySet()) {
            if (entry.getValue().equals(Multi.INACTIVE_STATE)) {
                if (!newInactiveSet.contains(entry.getKey())) {
                    entry.setValue(Multi.NOT_INIT_STATE);
                }
            }
        }
        for (String name : newInactiveSet) {
            if (stateMap.containsKey(name)) {
                stateMap.put(name, Multi.INACTIVE_STATE);
                bdfServerMap.remove(name);
            }
        }
    }


    private class InternalFichothequeSharing implements FichothequeSharing {


        private InternalFichothequeSharing() {

        }

        @Override
        public Fichotheque getFichotheque(String name) {
            try {
                return getBdfServer(name).getFichotheque();
            } catch (ErrorMessageException eme) {
                return null;
            }
        }

    }


    private class InternalMessageLocalisationProvider implements MessageLocalisationProvider {

        private InternalMessageLocalisationProvider() {

        }

        @Override
        public MessageLocalisation getMessageLocalisation(LangPreference langPreference, Locale formatLocale) {
            return messageLocalisationFactory.newInstance(langPreference, formatLocale);
        }

        @Override
        public MessageLocalisation getMessageLocalisation(Lang lang) {
            return messageLocalisationFactory.newInstance(lang);
        }

    }


    private class InternalMultiEditor implements MultiEditor {

        private final EditOrigin editOrigin;
        private final Map<String, CentralSphereEditor> sphereEditorMap = new HashMap<String, CentralSphereEditor>();
        private MultiMetadataEditor multiMetadataEditor = null;

        private InternalMultiEditor(EditOrigin editOrigin) {
            this.editOrigin = editOrigin;
        }

        @Override
        public Multi getMulti() {
            return MultiImpl.this;
        }

        @Override
        public EditOrigin getEditOrigin() {
            return editOrigin;
        }

        @Override
        public MultiMetadataEditor getMultiMetadataEditor() {
            if (multiMetadataEditor == null) {
                multiMetadataEditor = multiMetadata.getMultiMetadataEditor(editOrigin);
            }
            return multiMetadataEditor;
        }

        @Override
        public CentralSphereEditor getCentralSphereEditor(String sphereName) {
            CentralSphereEditor centralSphereEditor = sphereEditorMap.get(sphereName);
            if (centralSphereEditor != null) {
                return centralSphereEditor;
            }
            CentralSphereImpl centralSphere = centralSphereMap.get(sphereName);
            if (centralSphere == null) {
                throw new IllegalArgumentException("Unknown sphere: " + sphereName);
            }
            centralSphereEditor = centralSphere.getCentralSphereEditor(editOrigin);
            sphereEditorMap.put(sphereName, centralSphereEditor);
            return centralSphereEditor;
        }

        @Override
        public void saveChanges() {
            if (multiMetadataEditor != null) {
                multiMetadata.saveChanges(multiMetadataEditor);
                checkMetadata();
            }
            for (Map.Entry<String, CentralSphereEditor> entry : sphereEditorMap.entrySet()) {
                CentralSphereImpl centralSphere = centralSphereMap.get(entry.getKey());
                centralSphere.saveChanges(entry.getValue());
            }
        }

    }


    private class InternalStrictSphereSupervisor implements SphereSupervisor {

        private InternalStrictSphereSupervisor() {

        }

        @Override
        public boolean allowCoreChange(BdfUser bdfUser, String sphereName) {
            if (!centralSphereMap.containsKey(sphereName)) {
                return true;
            }
            BDFSession bdfSession = bdfWebapp.getMatchingSession(bdfUser);
            if (bdfSession == null) {
                return false;
            }
            return bdfSession.isMultiAdmin();
        }

    }

}
