/*-
 * ~~~~~~licensing~~~~~~
 * route-producing
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.routes.design.library.storage;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;

import ru.entaxy.esb.resources.EntaxyResource;
import ru.entaxy.esb.resources.EntaxyResourceProvider;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.routes.design.library.ComponentLibrary;
import ru.entaxy.platform.routes.design.library.storage.ProvidedLibraryDescriptor.ProvidedLibraryResource;

@Component(service = ComponentLibraryStorage.class, immediate = true)
public class ComponentLibraryStorage {

    private static final Logger LOG = LoggerFactory.getLogger(ComponentLibraryStorage.class);

    protected static final String PROVIDER_FILTER =
            "(protocol=" + ComponentLibrary.COMPONENT_LIBRARY_RESOURCE_PROTOCOL + ")";

    public static final String LIBRARY_DESCRIPTOR_NAME = "library.json";

    protected static final String LIBRARIES_RESOURCE = "libraries.json";

    @Reference(cardinality = ReferenceCardinality.MANDATORY, target = PROVIDER_FILTER)
    EntaxyResourceProvider resourceProvider;

    protected BundleContext bundleContext;

    // protected ComponentLibraryTracker libraryTracker;

    protected EntaxyResource librariesResource;

    protected Libraries libraries;

    protected Object librariesLock = new Object();

    @Activate
    public void activate(ComponentContext componentContext) {
        this.bundleContext = componentContext.getBundleContext();
        reload();

        /*
        if (libraryTracker != null)
            libraryTracker.close();
        libraryTracker = ComponentLibraryTracker.create(this, this.bundleContext);
        libraryTracker.open();
        */
    }

    @Deactivate
    public void deactivate() {
        /*
        if (libraryTracker != null)
            libraryTracker.close();
        */
    }

    protected void load() {
        librariesResource = resourceProvider.getResource(LIBRARIES_RESOURCE);
        if (!librariesResource.exists()) {
            libraries = new Libraries();
            try {
                updateLibrariesResource();
            } catch (IOException e) {
                LOG.error("Error saving [" + LIBRARIES_RESOURCE + "]", e);
            }
        } else {
            libraries = new Libraries(librariesResource.getAsString());
        }
    }

    protected void updateLibrariesResource() throws IOException {
        try (InputStream is = new ByteArrayInputStream(libraries.toJson().toString().getBytes())) {
            librariesResource.save(is);
        }
    }

    public void reload() {
        load();
    }

    synchronized public void addLibrary(ProvidedLibraryDescriptor descriptor, Library library) {
        Library existing = libraries.get(library.getName());
        long bundleLastModified = descriptor.bundle.getLastModified();
        if (existing != null) {
            if (existing.lastModified != bundleLastModified) {
                LOG.info("Library [{}] must be updated", library.getName());
                synchronized (librariesLock) {
                    try {
                        uploadLibraryResources(descriptor);
                    } catch (Exception e) {
                        LOG.error(String.format("Error uploading resources for library [%s]", library.getName()), e);
                    }
                    libraries.update(library, bundleLastModified);
                    try {
                        updateLibrariesResource();
                        LOG.info("Updated library [{}]", library.getName());
                        notifyListeners();
                    } catch (IOException e) {
                        LOG.error("Error updating libraries", e);
                    }
                }
            } else {
                LOG.info("Library [{}] not changed", library.getName());
            }
        } else {
            LOG.info("Adding new library [{}]", library.getName());
            library.lastModified = descriptor.bundle.getLastModified();

            synchronized (librariesLock) {
                try {
                    uploadLibraryResources(descriptor);
                } catch (Exception e) {
                    LOG.error(String.format("Error uploading resources for library [%s]", library.getName()), e);
                }
                libraries.add(library);
                try {
                    updateLibrariesResource();
                    LOG.info("Added new library [{}]", library.getName());
                    notifyListeners();
                } catch (IOException e) {
                    LOG.error("Error updating libraries", e);
                }
            }

        }
    }

    protected void uploadLibraryResources(ProvidedLibraryDescriptor descriptor) throws Exception {

        for (ProvidedLibraryResource provided : descriptor.getLibraryResources()) {
            String targetPath = descriptor.libraryName + "/" + provided.path;
            EntaxyResource resource = resourceProvider.getResource(targetPath);
            try (InputStream is = provided.url.openStream()) {
                resource.save(is);
            } catch (IOException e) {
                LOG.error(String.format("Error uploading resource [%s]", targetPath), e);
                throw e;
            }

        }

    }

    public List<ComponentLibrary> getLibraries() {
        return libraries.librariesMap.values().stream().map(l -> (ComponentLibrary) l).collect(Collectors.toList());
    }

    protected class Libraries {

        Map<String, Library> librariesMap;

        Object librariesMapLock = new Object();

        public Libraries() {
            librariesMap = new HashMap<>();
        }

        public Libraries(String jsonOrigin) {
            JsonObject jo = JSONUtils.getJsonRootObject(jsonOrigin);
            Type type = new TypeToken<HashMap<String, Library>>() {}.getType();
            librariesMap = JSONUtils.GSON.fromJson(jo, type);
            for (Library lib : librariesMap.values())
                lib.libraryUrl = resourceProvider.getResource(lib.getName()).getURL();
        }

        public JsonElement toJson() {
            return JSONUtils.GSON.toJsonTree(librariesMap);
        }

        public Library get(String libraryName) {
            return librariesMap.get(libraryName);
        }

        public void add(Library library) {
            synchronized (librariesMapLock) {
                library.libraryUrl = resourceProvider.getResource(library.getName()).getURL();
                this.librariesMap.put(library.getName(), library);
            }
        }

        public void update(Library library, long lastModified) {
            this.librariesMap.get(library.getName()).update(library);
            this.librariesMap.get(library.getName()).lastModified = lastModified;
        }
    }

    public static abstract class ObjectFilterAware {

        public String objectFilter;

        transient Filter targetFilter = null;

        public Filter getObjectFilter() {
            if (!CommonUtils.isValid(objectFilter))
                return null;
            if (targetFilter == null) {
                try {
                    return FrameworkUtil.createFilter(objectFilter);
                } catch (InvalidSyntaxException e) {
                    LOG.error(String.format("Error creating filter for [%s]", objectFilter), e);
                }
            }
            return targetFilter;
        }

    }

    public static class Library extends ObjectFilterAware implements ComponentLibrary {

        public String library;

        public long lastModified = -1;

        public List<IncludedSchema> schemas;

        transient public String libraryUrl;

        @Override
        public String getName() {
            return library;
        }

        @Override
        public String getUrl() {
            return libraryUrl;
        }

        public void update(Library library) {
            this.lastModified = library.lastModified;
            if (this.schemas == null) {
                this.schemas = new ArrayList<>();
            } else {
                this.schemas.clear();
            }
            if (library.schemas != null)
                this.schemas.addAll(library.schemas);
        }

        @Override
        public List<ComponentLibrary.Schema> getSchemas() {
            return this.schemas.stream().map(s -> (ComponentLibrary.Schema) s).collect(Collectors.toList());
        }

        public static class IncludedSchema extends ObjectFilterAware implements ComponentLibrary.Schema {
            public String root;
            public String namespace;
            public String prefix;
            public boolean isPublic;
            public List<Map<String, Object>> transform;
            public String uiDescriptor;

            transient List<IncludedTransformation> transformations = null;

            @Override
            public String getRoot() {
                return root;
            }

            @Override
            public String getNamespace() {
                return namespace;
            }

            @Override
            public String getPrefix() {
                return prefix;
            }

            @Override
            public boolean isPublic() {
                return isPublic;
            }

            @Override
            public List<Transformation> getTransformations() {
                if (this.transformations == null) {
                    this.transformations = new ArrayList<>();
                    for (Map<String, Object> transformationData : transform) {
                        transformations.add(new IncludedTransformation(transformationData));
                    }
                }
                return this.transformations.stream().map(t -> (ComponentLibrary.Transformation) t)
                        .collect(Collectors.toList());
            }

            @Override
            public String getUiDescriptor() {
                return uiDescriptor;
            }
        }
        public static class IncludedTransformation extends ObjectFilterAware
                implements ComponentLibrary.Transformation {

            protected Map<String, Object> data;

            public IncludedTransformation(Map<String, Object> data) {
                this.data = new HashMap<>(data);
                this.objectFilter = data.getOrDefault("objectFilter", "").toString();
            }

            @Override
            public String getProcessor() {
                return data.getOrDefault("processor", "unknown").toString();
            }

            @Override
            public String getSource() {
                return data.getOrDefault("source", "unknown").toString();
            }

        }
    }

    protected List<StorageListener> listeners = new ArrayList<>();

    protected Object listenersLock = new Object();

    public void registerListener(StorageListener listener) {
        synchronized (listenersLock) {
            if (!listeners.contains(listener))
                listeners.add(listener);
        }
    }

    public void unregisterListener(StorageListener listener) {
        synchronized (listenersLock) {
            listeners.remove(listener);
        }
    }

    protected void notifyListeners() {
        synchronized (listenersLock) {
            for (StorageListener listener : listeners)
                listener.storageUpdated();
        }
    }

    public interface StorageListener {
        void storageUpdated();
    }
}
