package org.gcube.application.geoportal.service.engine.providers.ucd;

import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.gcube.application.cms.caches.AbstractScopedMap;
import org.gcube.application.cms.caches.ObjectManager;
import org.gcube.application.cms.implementations.ISInterface;
import org.gcube.application.cms.implementations.ImplementationProvider;
import org.gcube.application.cms.implementations.WorkspaceManager;
import org.gcube.application.cms.serialization.Serialization;
import org.gcube.application.geoportal.common.model.document.filesets.RegisteredFile;
import org.gcube.application.geoportal.common.model.rest.ConfigurationException;
import org.gcube.application.geoportal.common.model.useCaseDescriptor.UseCaseDescriptor;
import org.gcube.common.resources.gcore.GenericResource;
import org.gcube.common.storagehub.client.dsl.FolderContainer;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.*;
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * Retrieves available UCDs from a single Generic Resource
 *  - secondary Type : "CMS"
 *  - name : "UCDs"
 *
 * containing e.g.
 *   <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 *       <UCDs>
 *           <record label="Human readable name" ucdUrl="some url" defaultForRegex="[opt] regex"/>
 *           <record label="Another readable name" ucdUrl="some other url" defaultForRegex="[opt] regex"/>
 *       </UCDs>
 *
 */
@Slf4j
public class SingleISResourceUCDProvider extends AbstractScopedMap<ProfileMap> implements ObjectManager<UseCaseDescriptor> {

    private static final String GR_SECONDARY_TYPE="CMS";
    private static final String GR_NAME="UCDs";


    public SingleISResourceUCDProvider() {
        super("Single IS Resource UCD Provider");
        setTTL(Duration.of(1, ChronoUnit.MICROS)); // no cache
    }

    @Override
    public UseCaseDescriptor insert(UseCaseDescriptor object) throws ConfigurationException{
        try{
            log.info("Registering UCID {}",object.getId());
            log.debug("Full UCD {} ",object);
            // Register entity in WS
            WorkspaceManager manager =new WorkspaceManager();
            WorkspaceManager.FolderOptions fo=new WorkspaceManager.FolderOptions("UCDs");

            fo.setParent(manager.getAppBase());
            fo.setFolderDescription("UCDs registered from server");
            FolderContainer folder = manager.createFolder(fo);

            String jsonString = Serialization.write(object);
            WorkspaceManager.FileOptions fileOptions=new WorkspaceManager.FileOptions(object.getId(),new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)));

            fileOptions.setFileDescription(object.getName()+", ID : "+object.getId());
            fileOptions.setParent(folder);
            RegisteredFile f = manager.registerFile(fileOptions);
            log.info("Registered File {}",f);

            // Create / Update GR


            ISInterface is = ImplementationProvider.get().getProvidedObjectByClass(ISInterface.class);
            List<GenericResource> l = is.getGenericResource(GR_SECONDARY_TYPE,GR_NAME);
            log.debug("Found {} resources ",l.size());
            GenericResource toupdate = null;
            ISBean bean = new ISBean();
            // If GR present, then use it
            try{
                toupdate = l.get(0);
                log.debug("GR IS {}",toupdate);
                bean = read(toupdate.profile().bodyAsString());
            }catch (Throwable t){
                log.warn("Unable to read from selected GR, creating it..",t);
                toupdate = new GenericResource();
                toupdate.newProfile().name(GR_NAME).type(GR_SECONDARY_TYPE).
                    description("Generated by service at "+ LocalDateTime.now());
            }

            if(bean.getRecords()==null) bean.setRecords(new ArrayList<>());
            ISBean.Record record = new ISBean.Record();
            record.setUcdUrl(f.getLink());
            record.setLabel(object.getName());
            bean.getRecords().add(record);
            log.debug("Inserting record {} ",record);
            String xml = write(bean).replaceFirst("<\\?.*\\?>","");
            //remove processing instructions
            toupdate.profile().newBody(xml);
            log.info("Creating / Updating GR {} ",toupdate);
            is.createUpdateGR(toupdate);

        }catch(ConfigurationException e){
            throw e;
        } catch (Throwable e) {
            throw new ConfigurationException(e);
        }
        return object;
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "UCDs")
    @Data
    @EqualsAndHashCode
    public static class ISBean {

        @NoArgsConstructor
        @ToString
        @Getter
        @Setter
        @AllArgsConstructor
        @XmlAccessorType(XmlAccessType.FIELD)
        @EqualsAndHashCode
        public static class Record {
            @XmlAttribute
            private String label;
            @XmlAttribute
            private String ucdUrl;
            @XmlAttribute
            private String defaultForRegex;
        }

        @XmlElement(name = "record")
        private List<Record> records;
    }


    @Override
    public void init() {
        super.init();
        try{
            directInit();
        }catch(Throwable t){
            throw new RuntimeException("Unable to initialize. Can't read Use case descriptors ",t);
        }
    }

    static void directInit() throws JAXBException {
        jaxbContext = JAXBContext.newInstance(ISBean.class);
        marshaller = jaxbContext.createMarshaller();
        unmarshaller = jaxbContext.createUnmarshaller();
    }
    private static JAXBContext jaxbContext=null;
    private static Marshaller marshaller=null;
    private static Unmarshaller unmarshaller = null;


    static ISBean read(String xml) throws JAXBException {
        log.trace("Loading from xml {}",xml);
        if(xml == null || xml.isEmpty()) return new ISBean();
        return (ISBean) unmarshaller.unmarshal(new StringReader(xml));
    }




    static String write(ISBean obj) throws JAXBException {
        log.trace("Writing {} to xml",obj);
        StringWriter writer = new StringWriter();
        marshaller.marshal(obj,writer);
        writer.flush();
        return writer.toString();
    }

    @Override
    protected ProfileMap retrieveObject(String key) throws ConfigurationException {
        try {
            log.info("Loading UCDs for context {} ",key);
            ISInterface is = ImplementationProvider.get().getProvidedObjectByClass(ISInterface.class);
            List<GenericResource> l = is.getGenericResource(GR_SECONDARY_TYPE,GR_NAME);
            log.debug("Found {} resources ",l.size());

            ProfileMap toReturn = new ProfileMap();
            l.forEach(g->{
                try {
                    log.debug("Reading from ID {} ", g.id());
                    ISBean bean = read(g.profile().bodyAsString());
                    log.debug("Found {} records ", bean.getRecords().size());
                    int before = toReturn.size();
                    bean.getRecords().forEach(record -> {
                        try {
                            log.debug("Loading UCD From {} ", record);
                            String json = new Scanner(
                                    new URL(record.getUcdUrl()).openStream(),
                                    "UTF-8").useDelimiter("\\A").next();
                            log.trace("JSON IS {} ",json);
                            UseCaseDescriptor ucd = Serialization.read(json, UseCaseDescriptor.class);
                            if(toReturn.containsKey(ucd.getId()))
                                log.warn("DUPLICATE UCID found {} in resource {} ",ucd.getId(),g.id());
                            toReturn.put(ucd.getId(), ucd);
                        }catch (Throwable t){
                            log.warn("Unable to read record {} from GR ID {} ",record,g.id(),t);
                        }
                    });
                    log.debug("Loaded {} from {} ",toReturn.size()-before,g.id());
                }catch (Throwable t){
                    log.warn("Unable to read from GR {} ",g.id(),t);
                }
            });
            return toReturn;
        }catch(Throwable t){
            throw new ConfigurationException("Unable to load UCDs for "+key,t);
        }
    }
}
