/*
 * Copyright (C) 2000 - 2026 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.kmelia.workflowextensions;

import com.itextpdf.text.*;
import com.itextpdf.text.Font.FontFamily;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import net.htmlparser.jericho.Source;
import org.silverpeas.components.kmelia.service.KmeliaService;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.annotation.Bean;
import org.silverpeas.core.contribution.attachment.AttachmentException;
import org.silverpeas.core.contribution.attachment.AttachmentService;
import org.silverpeas.core.contribution.attachment.model.DocumentType;
import org.silverpeas.core.contribution.attachment.model.SimpleDocument;
import org.silverpeas.core.contribution.attachment.model.SimpleDocumentPK;
import org.silverpeas.core.contribution.content.form.*;
import org.silverpeas.core.contribution.content.form.displayers.WysiwygFCKFieldDisplayer;
import org.silverpeas.core.contribution.content.form.field.ExplorerField;
import org.silverpeas.core.contribution.content.form.field.FileField;
import org.silverpeas.core.contribution.content.form.record.GenericFieldTemplate;
import org.silverpeas.core.contribution.publication.model.PublicationDetail;
import org.silverpeas.core.contribution.publication.model.PublicationPK;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateException;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateImpl;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateManager;
import org.silverpeas.core.i18n.I18n;
import org.silverpeas.core.node.model.NodeDetail;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.node.service.NodeService;
import org.silverpeas.core.persistence.Transaction;
import org.silverpeas.core.persistence.datasource.OperationContext;
import org.silverpeas.core.util.DateUtil;
import org.silverpeas.core.workflow.api.WorkflowException;
import org.silverpeas.core.workflow.api.instance.HistoryStep;
import org.silverpeas.core.workflow.api.instance.ProcessInstance;
import org.silverpeas.core.workflow.api.instance.UpdatableProcessInstance;
import org.silverpeas.core.workflow.api.model.Action;
import org.silverpeas.core.workflow.api.model.Parameter;
import org.silverpeas.core.workflow.api.model.State;
import org.silverpeas.core.workflow.external.impl.ExternalActionImpl;
import org.silverpeas.core.workflow.util.DataRecordUtil;
import org.silverpeas.kernel.logging.SilverLogger;
import org.silverpeas.kernel.util.StringUtil;

import java.io.*;
import java.util.Date;
import java.util.List;

@Bean
@Named("SendInKmeliaHandler")
public class SendInKmelia extends ExternalActionImpl {

  private static final String UNKNOWN = "unknown";

  @Inject
  private I18n i18n;
  @Inject
  private NodeService nodeService;
  @Inject
  private KmeliaService kmeliaService;
  @Inject
  private AttachmentService attachmentService;

  @Override
  public void execute() {
    final String role = getEvent().getUserRoleName();
    final String userId = OperationContext.getFromCache().getUser().getId();
    final Parameter parameter = getTriggerParameter("explorerFieldName");
    final Target target = new Target(role, userId, parameter).invoke();
    final String targetId = target.getTargetId();
    final String topicId = target.getTopicId();
    final String pubTitle = getPublicationTitle();
    final String pubDesc = getPublicationDescription();
    final String xmlFormName = getXmlFormName();
    final boolean formIsUsed = StringUtil.isDefined(xmlFormName);
    final PdfHistory pdfHistory = new PdfHistory().invoke();
    final boolean isHistoryEnable = pdfHistory.isEnable();
    final boolean isHistoryFirstAdding = pdfHistory.isFirstAdding();
    final String pdfHistoryName = pdfHistory.getFileName();


    // 1 - Create publication
    PublicationPK pubPK = new PublicationPK("0", targetId);
    Date now = new Date();
    String pubName = getProcessInstance().getTitle(role, getLanguage());
    pubName = applySubstitution(role, pubTitle, pubName);
    String desc = "";
    desc = applySubstitution(role, pubDesc, desc);
    PublicationDetail pubDetail = PublicationDetail.builder(getLanguage())
        .setPk(pubPK)
        .setNameAndDescription(pubName, desc)
        .created(now, userId)
        .setBeginDateTime(now, null)
        .setImportance(1)
        .build();

    if (formIsUsed) {
      pubDetail.setInfoId(xmlFormName);
    }

    KmeliaService kmelia = getKmeliaService();
    NodePK nodePK = new NodePK(topicId, targetId);
    String pubId = kmelia.createPublicationIntoTopic(pubDetail, nodePK);
    pubPK.setId(pubId);

    // 2 - Attach history as pdf file
    if (isHistoryEnable && isHistoryFirstAdding) {
      addPdfHistory(pdfHistoryName, role, pubPK, userId);
    }

    // 3 - Copy all instance regular files to publication
    ResourceReference fromPK = new ResourceReference(getProcessInstance().getInstanceId(),
        getProcessInstance().getModelId());
    ResourceReference toPK = new ResourceReference(pubPK);
    copyFiles(fromPK, toPK, DocumentType.attachment);

    if (isHistoryEnable && !isHistoryFirstAdding) {
      addPdfHistory(pdfHistoryName, role, pubPK, userId);
    }

    // process form content
    if (formIsUsed) {
      // target app use form : populate form fields
      populateFields(pubId, fromPK, toPK, xmlFormName);
    } else {
      // target app do not use form : copy files of workflow folder
      copyFiles(fromPK, toPK, DocumentType.form);
    }

    Parameter draftOutParameter = getTriggerParameter("forceDraftOut");
    if (draftOutParameter != null && StringUtil.getBooleanValue(draftOutParameter.getValue())) {
      getKmeliaService().draftOutPublication(pubPK, nodePK, SilverpeasRole.ADMIN.toString());
    }
  }

  private String getXmlFormName() {
    String xmlFormName = null;
    if (getTriggerParameter("xmlFormName") != null) {
      xmlFormName = getTriggerParameter("xmlFormName").getValue();
      if (StringUtil.isDefined(xmlFormName) && xmlFormName.lastIndexOf(".xml") != -1) {
        xmlFormName = xmlFormName.substring(0, xmlFormName.lastIndexOf(".xml"));
      }
    }
    return xmlFormName;
  }

  private String getPublicationTitle() {
    return getTriggerParameter("pubTitle").getValue();
  }

  private String getPublicationDescription() {
    final String pubDesc;
    final Parameter paramDescription = getTriggerParameter("pubDescription");
    if (paramDescription != null && StringUtil.isDefined(paramDescription.getValue())) {
      pubDesc = paramDescription.getValue();
    } else {
      pubDesc = null;
    }
    return pubDesc;
  }

  private String applySubstitution(final String role, final String text, final String defaultText) {
    String result = defaultText;
    if (StringUtil.isDefined(text)) {
      try {
        result = DataRecordUtil.applySubstitution(text,
            getProcessInstance().getAllDataRecord(role, "fr"),
            getProcessInstance().getProcessModel().getDataFolder().getItems(),
            "fr");
      } catch (WorkflowException e) {
        SilverLogger.getLogger(this).error(e.getMessage(), e);
      }
    }
    return result;
  }

  public void populateFields(String pubId, ResourceReference fromPK, ResourceReference toPK,
      String xmlFormName) {
    // Get the current instance
    UpdatableProcessInstance currentProcessInstance =
        (UpdatableProcessInstance) getProcessInstance();
    try {
      // register xmlForm of publication
      PublicationTemplateManager.getInstance()
          .addDynamicPublicationTemplate(toPK.getComponentInstanceId() + ":" + xmlFormName,
              xmlFormName + ".xml");

      PublicationTemplateImpl pubTemplate =
          (PublicationTemplateImpl) PublicationTemplateManager.getInstance()
              .getPublicationTemplate(toPK.getComponentInstanceId() + ":" + xmlFormName);
      DataRecord record = pubTemplate.getRecordSet().getEmptyRecord();
      record.setId(pubId);
      for (String fieldName : record.getFieldNames()) {
        Object fieldValue =
            getFieldValue(fromPK, toPK, currentProcessInstance, pubTemplate, fieldName);
        record.getField(fieldName).setObjectValue(fieldValue);
      }
      // Update
      pubTemplate.getRecordSet().save(record);

    } catch (PublicationTemplateException | FormException e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
  }

  private Object getFieldValue(final ResourceReference fromPK, final ResourceReference toPK,
      final UpdatableProcessInstance currentProcessInstance,
      final PublicationTemplateImpl pubTemplate, final String fieldName)
      throws FormException, PublicationTemplateException {
    Object fieldValue = null;
    try {
      Field fieldOfFolder = currentProcessInstance.getField(fieldName);
      FieldTemplate fieldTemplate = pubTemplate.getRecordTemplate().getFieldTemplate(fieldName);
      fieldValue = fieldOfFolder.getObjectValue();
      // Check file attachment in order to put them inside form
      if (fieldOfFolder instanceof FileField) {

        fieldValue = copyFormFile(fromPK, toPK, ((FileField) fieldOfFolder).getAttachmentId());
      } else if ("wysiwyg".equals(fieldTemplate.getDisplayerName())) {
        WysiwygFCKFieldDisplayer displayer = new WysiwygFCKFieldDisplayer();
        fieldValue =
            displayer.duplicateContent(fieldTemplate, fromPK, toPK, i18n.getDefaultLanguage());
        displayer.cloneContents(fromPK, toPK, null);
      }
    } catch (WorkflowException | IOException e) {
      SilverLogger.getLogger(this).warn(e);
    }
    return fieldValue;
  }

  private String copyFormFile(ResourceReference fromPK, ResourceReference toPK,
      String attachmentId) {
    SimpleDocument attachment;
    if (StringUtil.isDefined(attachmentId)) {
      // Retrieve attachment detail to copy
      attachment = attachmentService
          .searchDocumentById(new SimpleDocumentPK(attachmentId, fromPK.getInstanceId()), null);
      if (attachment != null) {
        SimpleDocumentPK copyPK = copyFileWithoutDocumentTypeChange(attachment, toPK);
        return copyPK.getId();
      }
    }
    return null;
  }

  private void copyFiles(ResourceReference fromPK, ResourceReference toPK,
      DocumentType fromType) {
    try {
      List<SimpleDocument> origins = attachmentService
          .listDocumentsByForeignKeyAndType(fromPK, fromType, getLanguage());
      for (SimpleDocument origin : origins) {
        copyFile(origin, toPK, DocumentType.attachment);
      }

    } catch (AttachmentException e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
  }

  private SimpleDocumentPK copyFileWithoutDocumentTypeChange(SimpleDocument file,
      ResourceReference toPK) {
    return copyFile(file, toPK, null);
  }

  private SimpleDocumentPK copyFile(SimpleDocument file, ResourceReference toPK,
      DocumentType type) {
    if (type != null) {
      file.setDocumentType(type);
    }
    return attachmentService.copyDocument(file, toPK);
  }

  private byte[] generatePDF(final String role, ProcessInstance instance) {
    Document document = new Document();

    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      PdfWriter.getInstance(document, baos);
      document.open();

      HistoryStep[] steps = instance.getHistorySteps();
      for (HistoryStep historyStep : steps) {
        generatePDFStep(role, historyStep, document);
      }

      document.close();

      return baos.toByteArray();
    } catch (Exception e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
    return new byte[0];
  }

  private void generatePDFStep(final String role, HistoryStep step, Document document) {
    if (step != null) {
      generatePDFStepHeader(role, step, document);
      generatePDFStepContent(role, step, document);
    }
  }

  private void generatePDFStepHeader(final String role, HistoryStep step,
      Document document) {
    try {
      String activity = "";
      if (step.getResolvedState() != null) {
        State resolvedState =
            step.getProcessInstance().getProcessModel().getState(step.getResolvedState());
        activity = resolvedState.getLabel(role, getLanguage());
      }

      String sAction = getAction(role, step);

      String actor = step.getUser().getFullName();
      String substituteId = step.getSubstituteId();
      if (StringUtil.isDefined(substituteId)) {
        actor = User.getById(substituteId).getDisplayedName() + " >> " + actor;
      }

      String date = DateUtil.getOutputDateAndHour(step.getActionDate(), getLanguage());

      String header = "";
      if (StringUtil.isDefined(activity)) {
        header += activity + " - ";
      }
      header += sAction + " (" + actor + " - " + date + ")";

      Font fontHeader = new Font(FontFamily.HELVETICA, 12, Font.NORMAL);

      PdfPCell pCell = new PdfPCell(new Phrase(header, fontHeader));
      pCell.setFixedHeight(28);
      pCell.setBackgroundColor(new BaseColor(239, 239, 239));
      pCell.setVerticalAlignment(Element.ALIGN_MIDDLE);

      PdfPTable pTable = new PdfPTable(1);
      pTable.setWidthPercentage(100);
      pTable.addCell(pCell);

      document.add(pTable);
    } catch (Exception e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
  }

  private String getAction(final String role, final HistoryStep step) {
    try {
      final String sAction;
      if ("#question#".equals(step.getAction())) {
        sAction = getString("processManager.question");
      } else if ("#response#".equals(step.getAction())) {
        sAction = getString("processManager.response");
      } else if ("#reAssign#".equals(step.getAction())) {
        sAction = getString("processManager.reAffectation");
      } else {
        Action action = step.getProcessInstance().getProcessModel().getAction(step.getAction());
        sAction = action.getLabel(role, getLanguage());
      }
      return sAction;
    } catch (WorkflowException we) {
      return "##";
    }
  }

  private void generatePDFStepContent(final String role, HistoryStep step,
      Document document) {
    try {
      Form form;
      if ("#question#".equals(step.getAction()) || "#response#".equals(step.getAction())) {
        form = null;
      } else {
        form = getProcessInstance().getProcessModel()
            .getPresentationForm(step.getAction(), role, getLanguage());
      }

      if (form != null && step.getActionRecord() != null) {
        DataRecord data = step.getActionRecord();
        PagesContext pageContext = new PagesContext();
        pageContext.setLanguage(getLanguage());

        // Force simpletext displayers because itext cannot display HTML Form fields (select,
        // radio...)
        float[] colsWidth = {25, 75};
        PdfPTable tableContent = new PdfPTable(colsWidth);
        tableContent.setWidthPercentage(100);
        List<FieldTemplate> fieldTemplates = form.getFieldTemplates();
        for (FieldTemplate fieldTemplate : fieldTemplates) {
          generatePdfFieldContent(step, data, pageContext, tableContent,
              (GenericFieldTemplate) fieldTemplate);
        }
        document.add(tableContent);
      }
    } catch (Exception e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
  }

  private void generatePdfFieldContent(final HistoryStep step, final DataRecord data,
      final PagesContext pageContext, final PdfPTable tableContent,
      final GenericFieldTemplate fieldTemplate) {
    try {
      Font fontLabel = new Font(FontFamily.HELVETICA, 10, Font.BOLD);
      Font fontValue = new Font(FontFamily.HELVETICA, 10, Font.NORMAL);
      String fieldLabel = fieldTemplate.getLabel(getLanguage());
      String fieldValue = null;
      Field field = data.getField(fieldTemplate.getFieldName());
      String componentId = step.getProcessInstance().getProcessModel().getModelId();

      // wysiwyg field
      if ("wysiwyg".equals(fieldTemplate.getDisplayerName())) {
        String file = new WysiwygFCKFieldDisplayer().getFile(componentId,
            getProcessInstance().getInstanceId(), fieldTemplate.getFieldName(), getLanguage());

        // Extract the text content of the html code
        Source source = new Source(new FileInputStream(file));
        fieldValue = source.getTextExtractor().toString();
      } else if (FileField.TYPE.equals(fieldTemplate.getTypeName())) {
        // Field file type
        if (StringUtil.isDefined(field.getValue())) {
          SimpleDocument doc = attachmentService
              .searchDocumentById(new SimpleDocumentPK(field.getValue(), componentId), null);
          if (doc != null) {
            fieldValue = doc.getFilename();
          }
        }
      } else {
        // Other field types
        FieldDisplayer<Field> fieldDisplayer =
            TypeManager.getInstance().getDisplayer(fieldTemplate.getTypeName(), "simpletext");
        StringWriter sw = new StringWriter();
        PrintWriter out = new PrintWriter(sw);
        fieldDisplayer.display(out, field, fieldTemplate, pageContext);
        fieldValue = sw.toString();
      }

      boolean displayField = Util.isEmptyFieldsDisplayed() || StringUtil.isDefined(fieldValue);

      if (displayField) {
        PdfPCell cell = new PdfPCell(new Phrase(fieldLabel, fontLabel));
        cell.setBorderWidth(0);
        cell.setPaddingBottom(5);
        tableContent.addCell(cell);

        cell = new PdfPCell(new Phrase(fieldValue, fontValue));
        cell.setBorderWidth(0);
        cell.setPaddingBottom(5);
        tableContent.addCell(cell);
      }
    } catch (Exception e) {
      SilverLogger.getLogger(this).error(e.getMessage(), e);
    }
  }

  private String getString(String key) {
    return key;
  }

  private String getLanguage() {
    return i18n.getDefaultLanguage();
  }

  private KmeliaService getKmeliaService() {
    return kmeliaService;
  }

  private void addPdfHistory(final String pdfHistoryName, final String role, PublicationPK pubPK,
      String userId) {
    final String fileName;
    if (pdfHistoryName != null && !pdfHistoryName.trim().isEmpty()) {
      if (!pdfHistoryName.endsWith(".pdf")) {
        fileName = pdfHistoryName + ".pdf";
      } else {
        fileName = pdfHistoryName;
      }
    } else {
      fileName = "processHistory_" + getProcessInstance().getInstanceId() + ".pdf";
    }
    byte[] pdf = generatePDF(role, getProcessInstance());
    getKmeliaService().addAttachmentToPublication(pubPK, userId, fileName, "", pdf);
  }

  private class Target {
    private final String role;
    private final String userId;
    private final Parameter parameter;
    private String targetId;
    private String topicId;

    public Target(final String role, final String userId, final Parameter parameter) {
      this.role = role;
      this.userId = userId;
      this.parameter = parameter;
    }

    public String getTargetId() {
      return targetId;
    }

    public String getTopicId() {
      return topicId;
    }

    public Target invoke() {
      if (parameter != null && StringUtil.isDefined(parameter.getValue())) {
        String explorerFieldName = parameter.getValue();
        // getting place to create publication from explorer field
        try {
          ExplorerField explorer = (ExplorerField) getProcessInstance().getField(explorerFieldName);
          ResourceReference pk = (ResourceReference) explorer.getObjectValue();
          targetId = pk.getInstanceId();
          topicId = pk.getId();
        } catch (WorkflowException e) {
          SilverLogger.getLogger(SendInKmelia.this).error(e.getMessage(), e);
          targetId = UNKNOWN;
          topicId = UNKNOWN;
        }
      } else {
        targetId = getTriggerParameter("targetComponentId").getValue();
        Parameter paramTopicPath = getTriggerParameter("targetFolderPath");
        if (paramTopicPath != null && StringUtil.isDefined(paramTopicPath.getValue())) {
          try {
            String path = DataRecordUtil.applySubstitution(paramTopicPath.getValue(),
                getProcessInstance().getAllDataRecord(role, "fr"),
                getProcessInstance().getProcessModel().getDataFolder().getItems(),
                "fr");
            topicId = getNodeId(path, targetId, userId);
          } catch (WorkflowException e) {
            SilverLogger.getLogger(SendInKmelia.this).error(e.getMessage(), e);
            topicId = "0";
          }
        } else {
          topicId = getTriggerParameter("targetTopicId").getValue();
        }
      }
      return this;
    }

    /**
     * Creates the identifier of the last node in the specified path. If some of the nodes in the
     * path doesn't exist, then creates them. In case of a node creation, the following requirements
     * are satisfied:
     * <ul>
     *   <li>if the creation fails, the root node is returned and the current transaction isn't
     *   rolled back so that the publication can be put into the returned node,</li>
     *   <li>the node creation is effectively applied and not just put in the current transaction's
     *   cache so that the node can be got later in the treatment.</li>
     * </ul>
     * @param explicitPath the path of the topic in which the publication will be put.
     * @return the unique identifier of the topic referred by the given path.
     */
    private String getNodeId(String explicitPath, final String targetId, final String userId) {
      return Transaction.performInNew(() -> {
        String[] path = explicitPath.substring(1).split("/");
        NodePK nodePK = new NodePK(UNKNOWN, targetId);
        String parentId = NodePK.ROOT_NODE_ID;
        for (String name : path) {
          NodeDetail existingNode =
              getNodeService().getDetailByNameAndFatherId(nodePK, name, Integer.parseInt(parentId));
          if (existingNode != null) {
            // topic exists
            parentId = existingNode.getNodePK().getId();
          } else {
            // topic does not exist, creating it
            NodeDetail newNode = new NodeDetail();
            newNode.setName(name);
            newNode.setNodePK(new NodePK(UNKNOWN, targetId));
            newNode.setFatherPK(new NodePK(parentId, targetId));
            newNode.setCreatorId(userId);
            NodeDetail node;
            try {
              node = getNodeService().createNode(newNode);
            } catch (Exception e) {
              SilverLogger.getLogger(this)
                  .warn("Cannot create node {0} in path {1}: {2}", name, explicitPath,
                      e.getMessage());
              return "0";
            }
            parentId = node.getId();
          }
        }
        return parentId;
      });
    }

    private NodeService getNodeService() {
      return nodeService;
    }
  }

  private class PdfHistory {
    private boolean addPDFHistory;
    private boolean addPDFHistoryFirst;
    private String pdfHistoryName;

    public boolean isEnable() {
      return addPDFHistory;
    }

    public boolean isFirstAdding() {
      return addPDFHistoryFirst;
    }

    public String getFileName() {
      return pdfHistoryName;
    }

    public PdfHistory invoke() {
      // Add pdf history before instance attachments
      if (getTriggerParameter("addPDFHistory") != null) {
        addPDFHistory = StringUtil.getBooleanValue(getTriggerParameter("addPDFHistory").getValue());
        if (getTriggerParameter("addPDFHistoryFirst") != null) {
          addPDFHistoryFirst =
              StringUtil.getBooleanValue(getTriggerParameter("addPDFHistoryFirst").getValue());
        } else {
          addPDFHistoryFirst = true;
        }
        Parameter paramPDFName = getTriggerParameter("pdfHistoryName");
        if (paramPDFName != null) {
          pdfHistoryName = getTriggerParameter("pdfHistoryName").getValue();
        } else {
          pdfHistoryName = null;
        }
      } else {
        addPDFHistory = true;
        addPDFHistoryFirst = true;
        pdfHistoryName = null;
      }
      return this;
    }
  }
}