/* BdfServer_Get - Copyright (c) 2009-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.get.instructions;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.instruction.BdfInstruction;
import fr.exemole.bdfserver.api.instruction.BdfInstructionConstants;
import fr.exemole.bdfserver.api.instruction.BdfParameters;
import fr.exemole.bdfserver.api.interaction.GetConstants;
import fr.exemole.bdfserver.api.users.BdfUser;
import fr.exemole.bdfserver.get.streamproducers.MissingFicheHtmlProducer;
import fr.exemole.bdfserver.get.streamproducers.transformation.TransformationFactory;
import fr.exemole.bdfserver.tools.BdfServerUtils;
import fr.exemole.bdfserver.tools.instruction.BdfErrors;
import fr.exemole.bdfserver.tools.instruction.DefaultBdfParameters;
import fr.exemole.bdfserver.tools.subsettree.TreeUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.ParseException;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import net.fichotheque.Fichotheque;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.corpus.Fiches;
import net.fichotheque.corpus.SortConstants;
import net.fichotheque.exportation.transformation.TransformationKey;
import net.fichotheque.extraction.ExtractionContext;
import net.fichotheque.permission.PermissionSummary;
import net.fichotheque.selection.FicheQuery;
import net.fichotheque.selection.FicheSelector;
import net.fichotheque.selection.SelectionContext;
import net.fichotheque.tools.corpus.FichesBuilder;
import net.fichotheque.tools.permission.PermissionUtils;
import net.fichotheque.tools.selection.SelectionDOMUtils;
import net.fichotheque.utils.CorpusUtils;
import net.fichotheque.utils.FichothequeUtils;
import net.fichotheque.utils.selection.FicheSelectorBuilder;
import net.mapeadores.util.io.IOUtils;
import net.mapeadores.util.io.StreamProducer;
import net.mapeadores.util.logging.ErrorMessageException;
import net.mapeadores.util.request.RequestMap;
import net.mapeadores.util.servlets.ResponseHandler;
import net.mapeadores.util.servlets.exceptions.BadRequestException;
import net.mapeadores.util.servlets.exceptions.ForbiddenException;
import net.mapeadores.util.servlets.handlers.HtmlResponseHandler;
import net.mapeadores.util.servlets.handlers.ResponseHandlerFactory;
import net.mapeadores.util.servlets.handlers.StreamResponseHandler;
import net.mapeadores.util.servlets.handlers.XmlResponseHandler;
import net.mapeadores.util.text.FileName;
import net.mapeadores.util.text.ValidExtension;
import net.mapeadores.util.xml.DOMUtils;
import net.mapeadores.util.xml.XMLUtils;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;


/**
 *
 * @author Vincent Calame
 */
public class FichesInstruction implements BdfInstruction {

    private final BdfServer bdfServer;
    private final RequestMap requestMap;
    private final String filePath;

    public FichesInstruction(BdfServer bdfServer, RequestMap requestMap, String filePath) {
        this.bdfServer = bdfServer;
        this.filePath = filePath;
        this.requestMap = requestMap;
    }

    @Override
    public short getBdfUserNeed() {
        return BdfInstructionConstants.MANDATORY_BDFUSER;
    }

    @Override
    public ResponseHandler runInstruction(BdfUser bdfUser) {
        BdfParameters bdfParameters = new DefaultBdfParameters(bdfServer, bdfUser);
        PathInfo pathInfo = parsePath();
        if (pathInfo == null) {
            return null;
        }
        if (pathInfo.isCompilation()) {
            return getCompilationResponseHandler(bdfParameters, pathInfo);
        }
        String ficheIdPart = pathInfo.getFicheIdPart();
        Corpus corpus = pathInfo.getCorpus();
        FicheMeta ficheMeta;
        if (ficheIdPart.equals("test")) {
            if (corpus.size() == 0) {
                throw new BadRequestException("_ error.unsupported.emptycorpus_test");
            }
            ficheMeta = getTestFicheMeta(corpus, bdfUser);
        } else {
            try {
                int id = Integer.parseInt(ficheIdPart);
                if (id < 1) {
                    throw new BadRequestException("_ error.wrong.id", ficheIdPart);
                }
                ficheMeta = corpus.getFicheMetaById(id);
                if (ficheMeta == null) {
                    return getMissingFicheMetaResponseHandler(bdfParameters, corpus, id, pathInfo.getExtension());
                }
            } catch (NumberFormatException nfe) {
                throw new BadRequestException("_ error.wrong.id", ficheIdPart);
            }
        }
        return getFicheResponseHandler(bdfParameters, ficheMeta, pathInfo);
    }

    private FicheMeta getTestFicheMeta(Corpus corpus, BdfUser bdfUser) {
        List<FicheMeta> ficheMetaList = CorpusUtils.getFicheMetaListByCorpus(bdfUser.getSelectedFiches(), corpus);
        if (!ficheMetaList.isEmpty()) {
            return ficheMetaList.get(0);
        }
        return corpus.getFicheMetaList().get(0);
    }

    private ResponseHandler getFicheResponseHandler(BdfParameters bdfParameters, FicheMeta ficheMeta, PathInfo pathInfo) {
        boolean canRead = bdfParameters.getPermissionSummary().canRead(ficheMeta);
        if (!canRead) {
            if (pathInfo.getExtensionString().equals("html")) {
                return ResponseHandlerFactory.getHtmlErrorInstance(bdfParameters.getMessageLocalisation(), "_ error.permission.readfiche", ficheMeta.getGlobalId());
            } else {
                throw new ForbiddenException();
            }
        }
        String extraXml = getExtraXml(requestMap);
        TransformationKey transformationKey = new TransformationKey(ficheMeta.getSubsetKey());
        ExtractionContext extractionContext = bdfParameters.getDefaultExtractionContext();
        TransformationFactory transformationFactory = TransformationFactory.init(bdfParameters)
                .extractionContext(extractionContext)
                .transformationKey(transformationKey)
                .templateName(pathInfo.getTemplateName())
                .dynamicObject(ficheMeta)
                .extraXml(extraXml);
        switch (pathInfo.getExtensionString()) {
            case "xml":
                return XmlResponseHandler.init(transformationFactory.getSourceXmlProducer());
            case "html":
                return StreamResponseHandler.init(transformationFactory.getStreamProducer());
            case "odcontent":
                return extractOdFile(transformationFactory.streamExtension(ValidExtension.ODT).outputProperty("indent", "yes").getStreamProducer(), "content.xml");
            case "odstyles":
                return extractOdFile(transformationFactory.streamExtension(ValidExtension.ODT).getStreamProducer(), "styles.xml");
            default:
                return StreamResponseHandler.init(transformationFactory.streamExtension(pathInfo.getExtension()).getStreamProducer());
        }
    }

    private ResponseHandler getCompilationResponseHandler(BdfParameters bdfParameters, PathInfo pathInfo) {
        BdfUser bdfUser = bdfParameters.getBdfUser();
        TransformationKey transformationKey = TransformationKey.COMPILATION_INSTANCE;
        String extraXml = getExtraXml(requestMap);
        int limit = getLimit(requestMap);
        Fiches fiches;
        try {
            FicheQuery ficheQuery = getFicheQuery(requestMap, bdfParameters.getFichotheque());
            if (ficheQuery != null) {
                String sortType = getSortType(requestMap);
                PermissionSummary permissionSummary = bdfParameters.getPermissionSummary();
                SelectionContext selectionContext = BdfServerUtils.initSelectionContextBuilder(bdfParameters)
                        .setSubsetAccessPredicate(permissionSummary.getSubsetAccessPredicate())
                        .setFichePredicate(PermissionUtils.getFichePredicate(permissionSummary))
                        .toSelectionContext();
                FicheSelector ficheSelector = FicheSelectorBuilder.init(selectionContext)
                        .add(ficheQuery)
                        .toFicheSelector();
                fiches = FichesBuilder.build(sortType, bdfUser.getWorkingLang())
                        .initSubsetKeyOrder(TreeUtils.getCorpusKeyList(bdfServer))
                        .populate(ficheSelector)
                        .toFiches();
            } else {
                fiches = bdfUser.getSelectedFiches();
            }
        } catch (ErrorMessageException eme) {
            return ResponseHandlerFactory.getHtmlInstance(bdfParameters.getMessageLocalisation(), eme.getErrorMessage());
        }
        if (limit > 0) {
            fiches = CorpusUtils.reduce(fiches, limit);
        }
        ExtractionContext extractionContext = bdfParameters.getDefaultExtractionContext();
        TransformationFactory transformationFactory = TransformationFactory.init(bdfParameters)
                .extractionContext(extractionContext)
                .transformationKey(transformationKey)
                .templateName(pathInfo.getTemplateName())
                .extraXml(extraXml)
                .staticFiches(fiches);
        switch (pathInfo.getExtension().toString()) {
            case "xml":
                return XmlResponseHandler.init(transformationFactory.getSourceXmlProducer());
            case "html":
                return StreamResponseHandler.init(transformationFactory.getStreamProducer());
            case "odcontent":
                return extractOdFile(transformationFactory.streamExtension(ValidExtension.ODT).outputProperty("indent", "yes").getStreamProducer(), "content.xml");
            case "odstyles":
                return extractOdFile(transformationFactory.streamExtension(ValidExtension.ODT).getStreamProducer(), "styles.xml");
            default:
                return StreamResponseHandler.init(transformationFactory.streamExtension(pathInfo.getExtension()).getStreamProducer());
        }
    }

    private ResponseHandler getMissingFicheMetaResponseHandler(BdfParameters bdfParameters, Corpus corpus, int ficheId, ValidExtension validExtension) {
        if (!validExtension.toString().equals("html")) {
            throw new BadRequestException("_ error.unknown.id", String.valueOf(ficheId));
        }
        SubsetKey corpusKey = corpus.getSubsetKey();
        int level = bdfParameters.getPermissionSummary().getReadLevel(corpusKey);
        if (level == PermissionSummary.SUMMARYLEVEL_0_NONE) {
            return ResponseHandlerFactory.getHtmlErrorInstance(bdfParameters.getMessageLocalisation(), "_ error.permission.readfiche", FichothequeUtils.toGlobalId(corpusKey, ficheId));
        }
        return HtmlResponseHandler.init(new MissingFicheHtmlProducer(bdfParameters, corpus, ficheId));
    }

    private ResponseHandler extractOdFile(StreamProducer streamProducer, String file) {
        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        try {
            streamProducer.writeStream(byteOutputStream);
        } catch (IOException ioe) {

        }
        String contentString = "";
        try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(byteOutputStream.toByteArray()))) {
            ZipEntry zipEntry;
            while ((zipEntry = zis.getNextEntry()) != null) {
                String name = zipEntry.getName();
                if (name.equals(file)) {
                    contentString = IOUtils.toString(zis, "UTF-8");
                }
            }
        } catch (IOException ioe) {

        }
        return XmlResponseHandler.init(XMLUtils.toXmlProducer(contentString));
    }

    private PathInfo parsePath() {
        FileName fileName;
        try {
            fileName = FileName.parse(filePath);
        } catch (ParseException pe) {
            return null;
        }
        String baseName = fileName.getBasename();
        ValidExtension validExtension;
        try {
            validExtension = ValidExtension.parse(fileName.getExtension());
        } catch (ParseException pe) {
            throw new BadRequestException("_ error.wrong.extension", fileName.getExtension());
        }
        String[] tokens = baseName.split("-");
        int length = tokens.length;
        if (length < 1) {
            return null;
        }
        String firstToken = tokens[0];
        if (isCompilation(firstToken, length)) {
            String templateName = "";
            if (length > 1) {
                templateName = tokens[1];
            }
            templateName = checkTemplateName(templateName);
            return new PathInfo(validExtension, templateName);
        } else {
            if (length < 2) {
                return null;
            }
            Corpus corpus;
            try {
                SubsetKey corpusKey = SubsetKey.parse(SubsetKey.CATEGORY_CORPUS, firstToken);
                corpus = (Corpus) bdfServer.getFichotheque().getSubset(corpusKey);
            } catch (java.text.ParseException pe) {
                throw new BadRequestException("_ error.wrong.corpusname", firstToken);
            }
            if (corpus == null) {
                throw new BadRequestException("_ error.unknown.corpus", firstToken);
            }
            String templateName = "";
            if (length > 2) {
                templateName = tokens[2];
            }
            templateName = checkTemplateName(templateName);
            return new PathInfo(corpus, tokens[1], validExtension, templateName);
        }
    }

    private String checkTemplateName(String templateName) {
        String templateParam = requestMap.getParameter(GetConstants.TEMPLATE_PARAMNAME);
        if (templateParam != null) {
            templateName = templateParam;
        }
        return templateName;
    }

    private static String getExtraXml(RequestMap requestMap) {
        String extraXml = requestMap.getParameter(GetConstants.EXTRA_XML_PARAMNAME);
        if (extraXml != null) {
            String testError = XMLUtils.testXml("<root>" + extraXml + "</root>");
            if (testError != null) {
                extraXml = "<error>" + XMLUtils.escape(testError) + "<error>";
            }
        }
        return extraXml;
    }

    private static int getLimit(RequestMap requestMap) {
        int limit = 0;
        String limitString = requestMap.getParameter(GetConstants.LIMIT_PARAMNAME);
        if (limitString != null) {
            try {
                limit = Integer.parseInt(limitString);
            } catch (NumberFormatException nfe) {

            }
        }
        return limit;
    }

    private static FicheQuery getFicheQuery(RequestMap requestMap, Fichotheque fichotheque) throws ErrorMessageException {
        String queryXml = requestMap.getParameter(GetConstants.QUERY_XML_PARAMNAME);
        if (queryXml != null) {
            org.w3c.dom.Document document;
            try {
                document = DOMUtils.parseDocument(queryXml);
            } catch (SAXException sae) {
                throw new ErrorMessageException("_ error.exception.xml.sax", sae.getMessage());
            }
            Element root = document.getDocumentElement();
            switch (root.getTagName()) {
                case "fiche-query":
                case "fiche-select":
                    return SelectionDOMUtils.getFicheConditionEntry(fichotheque, root).getFicheQuery();
                default:
                    throw new ErrorMessageException("_ error.wrong.xml.root", root.getTagName(), "fiche-query");
            }
        } else {
            return null;
        }

    }

    private static String getSortType(RequestMap requestMap) throws ErrorMessageException {
        String sorttypeParamValue = requestMap.getParameter(GetConstants.SORTTYPE_PARAMNAME);
        if (sorttypeParamValue != null) {
            try {
                return SortConstants.checkSortType(sorttypeParamValue);
            } catch (IllegalArgumentException iae) {
                throw BdfErrors.unknownParameterValue(GetConstants.SORTTYPE_PARAMNAME, sorttypeParamValue);
            }
        } else {
            return SortConstants.ID_ASC;
        }
    }

    private static boolean isCompilation(String token, int tokenLength) {
        switch (token) {
            case "compilation":
                if (tokenLength == 1) {
                    return true;
                } else {
                    return false;// si tokenLength > 1, risque de confusion avec un corpus de nom technique « compilation »
                }
            case "_compilation":
                return true;
            default:
                return false;
        }
    }


    private static class PathInfo {

        private final Corpus corpus;
        private final String ficheIdPart;
        private final ValidExtension extension;
        private final String templateName;

        private PathInfo(ValidExtension extension, String templateName) {
            this(null, null, extension, templateName);
        }

        private PathInfo(Corpus corpus, String ficheIdPart, ValidExtension extension, String templateName) {
            this.corpus = corpus;
            this.ficheIdPart = ficheIdPart;
            this.extension = extension;
            this.templateName = templateName;
        }

        public boolean isCompilation() {
            return (corpus == null);
        }

        public Corpus getCorpus() {
            return corpus;
        }

        public String getFicheIdPart() {
            return ficheIdPart;
        }

        public String getExtensionString() {
            return extension.toString();
        }

        public ValidExtension getExtension() {
            return extension;
        }

        public String getTemplateName() {
            return templateName;
        }

    }

}
