/*-
 * ~~~~~~licensing~~~~~~
 * resources-service
 * ==========
 * 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.esb.resources.impl.service;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.CollectionType;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import ru.entaxy.esb.resources.EntaxyResource;
import ru.entaxy.esb.resources.EntaxyResourceMetadata;
import ru.entaxy.esb.resources.EntaxyResourceProcessor;
import ru.entaxy.esb.resources.EntaxyResourceProvider;
import ru.entaxy.esb.resources.EntaxyResourceProviderProxy;
import ru.entaxy.esb.resources.EntaxyResourceService;
import ru.entaxy.esb.resources.EntaxyResourceURLFactory;
import ru.entaxy.esb.resources.impl.ResourceWrapper;
import ru.entaxy.platform.base.support.CommonUtils;

@Component(service = EntaxyResourceService.class, immediate = true)
public class EntaxyResourceServiceImpl implements EntaxyResourceService {

    private static final Logger log = LoggerFactory.getLogger(EntaxyResourceServiceImpl.class);

    protected BundleContext bundleContext;

    protected ProvidersController controller;

    @Activate
    public EntaxyResourceServiceImpl(BundleContext bundleContext) {
        super();
        this.bundleContext = bundleContext;
        this.controller = new ProvidersController(bundleContext, this);
    }

    @Reference(cardinality = ReferenceCardinality.MULTIPLE, collectionType = CollectionType.SERVICE,
            policyOption = ReferencePolicyOption.GREEDY, unbind = "removeProvider", policy = ReferencePolicy.DYNAMIC)
    public void addProvider(EntaxyResourceProvider provider) {
        controller.addProvider(provider);
    }

    public void removeProvider(EntaxyResourceProvider provider) {
        controller.removeProvider(provider);
    }

    @Reference(cardinality = ReferenceCardinality.MULTIPLE, collectionType = CollectionType.SERVICE,
            policyOption = ReferencePolicyOption.GREEDY, unbind = "removeProxy", policy = ReferencePolicy.DYNAMIC)
    public void addProxy(EntaxyResourceProviderProxy proxy) {
        controller.addProxy(proxy);
    }

    public void removeProxy(EntaxyResourceProviderProxy proxy) {
        controller.removeProxy(proxy);
    }

    @Override
    public boolean isProtocolAvailable(String protocol) {
        return controller.hasProvider(protocol);
    }

    @Override
    public void registerProtocolCallback(String protocol, EntaxyResourceProtocolAvailableCallback callback) {
        controller.registerProtocolCallback(protocol, callback);
    }

    @Override
    public EntaxyResource getResource(String resourceLocation) {
        if (!CommonUtils.isValid(resourceLocation))
            return null;
        String location = resourceLocation;
        try {
            location = EntaxyResourceURLFactory
                    .getLocalUri(resourceLocation.replaceAll(" ", "%20")).toString()
                    .replaceAll("%20", " ");
        } catch (URISyntaxException e) {
            log.warn(String.format("Failed getting local URI for [%s]", resourceLocation), e);
        }
        int index = location.indexOf(":");
        if (index > 0) {
            String protocol = location.substring(0, index);
            String path = location.substring(index + 1);
            if (controller.hasProvider(protocol)) {
                return wrapResource(controller.getProvider(protocol).getResource(path), protocol, path);
            } else {
                log.warn("Provider not found for: [{}]", location);
                return null;
            }
        }
        return null;
    }

    @Override
    public List<EntaxyResource> getResources(String resourceLocation) {
        if (!CommonUtils.isValid(resourceLocation))
            return Collections.emptyList();

        String location = resourceLocation;
        try {
            location = EntaxyResourceURLFactory
                    .getLocalUri(resourceLocation.replaceAll(" ", "%20")).toString()
                    .replaceAll("%20", " ");
        } catch (URISyntaxException e) {
            log.warn(String.format("Failed getting local URI for [%s]", resourceLocation), e);
        }

        int index = location.indexOf(":");
        String protocol, path = "";
        if (index > 0) {
            protocol = location.substring(0, index);
            path = location.substring(index + 1);
        } else {
            protocol = location;
        }
        if (controller.hasProvider(protocol)) {
            List<EntaxyResource> resources = controller.getProvider(protocol).getResources(path);
            if (resources == null)
                return Collections.emptyList();

            final String rootPath = path;

            return resources.stream()
                    .map(r -> wrapResource(r, protocol, rootPath + "/" + r.getName()))
                    .collect(Collectors.toList());
        } else {
            log.warn("Provider not found for: [{}]", location);
            return Collections.emptyList();
        }
    }

    @Override
    public Map<String, EntaxyResourceProvider> getProviders() {
        return controller.getProviders();
    }

    protected void updateResourceTimestamp(EntaxyResource resource, String timestamp) {
        if (resource == null)
            return;
        if (!resource.isFolder() && !resource.exists())
            return;
        EntaxyResourceMetadata metadata = resource.getMetadata(true);
        EntaxyResourceMetadata.MetadataSection section =
                metadata.getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME);
        section.getContent().remove(EntaxyResourceMetadata.SECTION_RESOURCE.ATTRIBUTES.TIMESTAMP);
        section.getContent().addProperty(EntaxyResourceMetadata.SECTION_RESOURCE.ATTRIBUTES.TIMESTAMP, timestamp);
        metadata.update();
    }

    protected void updateParentTimestamps(EntaxyResource resource, String timestamp) {
        String url = resource.getURL();
        while (url.lastIndexOf('/') > 0) {
            url = url.substring(0, url.lastIndexOf('/'));
            updateResourceTimestamp(getResource(url), timestamp);
        }
    }

    protected EntaxyResource wrapResource(EntaxyResource origin, String originProtocol, String originLocation) {
        // check if it's sequence resource
        if (origin.getName().startsWith("$")) {
            EntaxyResourceMetadata.MetadataSection section =
                    origin.getMetadata(true).getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME);
            if (section.getContent().has("@SEQUENCE")) {
                JsonElement je = section.getContent().get("@SEQUENCE");
                if (je.isJsonObject()) {
                    return new SequenceResourceWrapper(origin, originProtocol, originLocation, this);
                }
            }
        }

        return new StandardResourceWrapper(origin, originProtocol, originLocation, this);
    }

    protected static class SequenceResourceWrapper extends ResourceWrapper implements EntaxyResourceProcessor {

        EntaxyResourceServiceImpl service;

        String delimiter = null;

        List<SequenceItem> sequence;

        protected abstract class SequenceItem {
            abstract boolean isRequired();

            abstract InputStream getInputStream();

            abstract long getSize();
        }

        protected class ResourceSequenceItem extends SequenceItem {

            EntaxyResource entaxyResource;

            boolean required = false;

            public ResourceSequenceItem(EntaxyResource entaxyResource, boolean required) {
                this.entaxyResource = entaxyResource;
                this.required = required;
            }

            @Override
            boolean isRequired() {
                return required;
            }

            @Override
            InputStream getInputStream() {
                return entaxyResource.getInputStream();
            }

            @Override
            long getSize() {
                return entaxyResource.getSize();
            }
        }

        protected class StringSequenceItem extends SequenceItem {

            String value;

            public StringSequenceItem(String value) {
                this.value = value;
            }

            @Override
            boolean isRequired() {
                return true;
            }

            @Override
            InputStream getInputStream() {
                return new ByteArrayInputStream(value.getBytes());
            }

            @Override
            long getSize() {
                return value.length();
            }
        }

        public SequenceResourceWrapper(EntaxyResource origin, String originProtocol, String originLocation,
                EntaxyResourceServiceImpl serviceImpl) {
            super(origin, originProtocol, originLocation, new ArrayList<>());
            this.processors.add(this);
            this.service = serviceImpl;

            buildSequence();


        }

        protected void buildSequence() {

            sequence = new ArrayList<>();

            JsonObject sequenceContent =
                    origin.getMetadata(true).getSection(EntaxyResourceMetadata.SECTION_RESOURCE.NAME).getContent()
                            .get("@SEQUENCE").getAsJsonObject();
            try {
                delimiter = sequenceContent.get("delimiter").getAsString().replace("__LF__", "\n");
            } catch (Exception ignore) {
                // NOOP
            }

            try {
                JsonArray arr = sequenceContent.get("items").getAsJsonArray();
                for (int i = 0; i < arr.size() - 1; i++) {
                    String url = arr.get(i).getAsJsonObject().get("url").getAsString();
                    boolean required = false;
                    try {
                        required = arr.get(i).getAsJsonObject().get("required").getAsBoolean();
                    } catch (Exception ignore) {
                        // NOOP
                    }
                    EntaxyResource resource = service.getResource(url);
                    ResourceSequenceItem si = new ResourceSequenceItem(resource, required);
                    sequence.add(si);
                    if (delimiter != null)
                        sequence.add(new StringSequenceItem(delimiter));
                }

                String url = arr.get(arr.size() - 1).getAsJsonObject().get("url").getAsString();
                boolean required = false;
                try {
                    required = arr.get(arr.size() - 1).getAsJsonObject().get("required").getAsBoolean();
                } catch (Exception ignore) {
                    // NOOP
                }
                EntaxyResource resource = service.getResource(url);
                ResourceSequenceItem si = new ResourceSequenceItem(resource, required);
                sequence.add(si);

            } catch (Exception ignore) {
                // NOOP
            }

        }

        @Override
        public long getSize() {
            long size = 0;
            for (SequenceItem si : sequence)
                size += si.getSize();
            return size;
        }

        @Override
        public InputStream getInputStream() {
            SequenceInputStream sequenceInputStream = new SequenceInputStream(Collections
                    .enumeration(sequence.stream().map(item -> item.getInputStream()).collect(Collectors.toList())));
            return sequenceInputStream;
        }

        @Override
        public InputStream preProcess(InputStream origin, EntaxyResource resource) {
            return null;
        }

        @Override
        public void postProcess(EntaxyResource resource) {

        }

    }

    protected static class StandardResourceWrapper extends ResourceWrapper implements EntaxyResourceProcessor {

        EntaxyResourceServiceImpl service;

        public StandardResourceWrapper(EntaxyResource origin, String originProtocol, String originLocation,
                EntaxyResourceServiceImpl serviceImpl) {
            super(origin, originProtocol, originLocation, new ArrayList<>());
            this.processors.add(this);
            this.service = serviceImpl;
        }

        @Override
        public InputStream preProcess(InputStream origin, EntaxyResource resource) {
            return null;
        }

        @Override
        public void postProcess(EntaxyResource resource) {
            String timestamp = "" + Calendar.getInstance().getTimeInMillis();
            service.updateResourceTimestamp(resource, timestamp);
            service.updateParentTimestamps(resource, timestamp);
        }

        @Override
        public void save(InputStream inputStream) {
            // origin.save(inputStream);
            super.save(inputStream);
        }
    }

}
