package be.iminds.ilabt.jfed.experimenter_gui.bugreporting;

import be.iminds.ilabt.jfed.bugreport.*;

import be.iminds.ilabt.jfed.bugreport.BugReport.ReportTarget;
import be.iminds.ilabt.jfed.bugreport.BugReport.ReportType;
import be.iminds.ilabt.jfed.call_log_output.LogOutput;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.connectivity_tester.ConnectivityTest;
import be.iminds.ilabt.jfed.experimenter_gui.JFedVersionInfo;
import be.iminds.ilabt.jfed.experimenter_gui.connectivity_tester.ConnectivityTester;
import be.iminds.ilabt.jfed.experimenter_gui.util.BugReportExperimentControllerListener;
import be.iminds.ilabt.jfed.highlevel.controller.JavaFXTaskThread;
import be.iminds.ilabt.jfed.highlevel.controller.TaskExecution;
import be.iminds.ilabt.jfed.highlevel.jobs.report.JobStateReport;
import be.iminds.ilabt.jfed.highlevel.jobs.report.StitchingJobReport;
import be.iminds.ilabt.jfed.highlevel.model.SfaModel;
import be.iminds.ilabt.jfed.highlevel.model.Slice;
import be.iminds.ilabt.jfed.highlevel.model.Sliver;
import be.iminds.ilabt.jfed.log.cache.AllApiCallsList;
import be.iminds.ilabt.jfed.log.cache.ApiCallDetailsRef;
import be.iminds.ilabt.jfed.logging.BugReportAppender;
import be.iminds.ilabt.jfed.logging.JFedLoggerManager;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.preferences.JFedGuiPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.KeyUtil;
import ch.qos.logback.classic.spi.ILoggingEvent;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.image.Image;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static ch.qos.logback.classic.spi.ThrowableProxyUtil.asString;
import static java.nio.charset.Charset.defaultCharset;
import static java.nio.file.Files.readAllLines;
import static java.time.ZonedDateTime.ofInstant;
import static javafx.embed.swing.SwingFXUtils.fromFXImage;
import static javax.imageio.ImageIO.write;
import static javax.xml.stream.XMLEventFactory.newInstance;
import static org.apache.commons.codec.binary.Base64.encodeBase64;

/**
 * This class contains references to all the information that must be included in a bugreport.
 * <p/>
 * It also extends the Task-class, which must be used to write the info to an outputstream. By extending from Task,
 * we are able to display the overall progress to the user.
 * <p/>
 * User: twalcari
 * Date: 6/13/2014
 * Time: 10:35
 */
public class BugReportComposer {
    private final static Logger LOG = LoggerFactory.getLogger(BugReportComposer.class);

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static final int SCREENSHOT_PROGRESS = 100;

    private final JavaFXTaskThread javaFXTaskThread;
    private final GeniUserProvider geniUserProvider;
    private final SfaModel sfaModel;
    private final JFedGuiPreferences jFedPreferences;
    private final JFedLoggerManager jFedLoggerManager;
    private final ConnectivityTester connectivityTester;
    private final BugReportExperimentControllerListener bugReportExperimentControllerListener;
    private final AllApiCallsList allApiCallsList;

    private StringProperty subject = new SimpleStringProperty();
    private SimpleObjectProperty<BugReport.ReportTarget> reportTarget = new SimpleObjectProperty<>();
    private SimpleObjectProperty<BugReport.ReportType> reportType = new SimpleObjectProperty<>();
    private ListProperty<GeniUrn> relatedTestbeds = new SimpleListProperty<>(FXCollections.observableArrayList());
    private BooleanProperty postOnPublicList = new SimpleBooleanProperty();

    private final StringProperty description = new SimpleStringProperty();
    private final StringProperty email = new SimpleStringProperty();
    private final ReadOnlyStringWrapper version = new ReadOnlyStringWrapper();
    private final ReadOnlyStringWrapper environment = new ReadOnlyStringWrapper();
    private final ReadOnlyStringWrapper reporterUrn = new ReadOnlyStringWrapper();
    private final ReadOnlyStringWrapper certificateExpiration = new ReadOnlyStringWrapper();

    private final ObjectProperty<Image> screenshot = new SimpleObjectProperty<>();


    @Inject
    public BugReportComposer(JavaFXTaskThread javaFXTaskThread, GeniUserProvider geniUserProvider,
                             SfaModel sfaModel, ConnectivityTester connectivityTester,
                             JFedGuiPreferences jFedPreferences, JFedLoggerManager jFedLoggerManager,
                             BugReportExperimentControllerListener bugReportExperimentControllerListener,
                             JFedVersionInfo jFedVersionInfo,
                             AllApiCallsList allApiCallsList) {
        this.finished = false;

        this.javaFXTaskThread = javaFXTaskThread;
        this.geniUserProvider = geniUserProvider;
        this.sfaModel = sfaModel;
        this.connectivityTester = connectivityTester;
        this.jFedPreferences = jFedPreferences;
        this.jFedLoggerManager = jFedLoggerManager;
        this.bugReportExperimentControllerListener = bugReportExperimentControllerListener;
        this.allApiCallsList = allApiCallsList;

        assert javaFXTaskThread != null;
        assert connectivityTester != null;

        //start connectivity tests if this hasn't happened yet
        this.connectivityTester.startTests();


        //now load the info into the disabled textfield
        this.version.set(jFedVersionInfo.getFullVersionString());

        this.environment.set(
                System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")
                        + " - Java " + System.getProperty("java.version")
                        + " (" + System.getProperty("java.vendor") + ")");

        final GeniUser loggedInGeniUser = this.geniUserProvider.getLoggedInGeniUser();

        this.reporterUrn.set(loggedInGeniUser.getUserUrnString());

        //try to get user e-mailaddress
        try {
            for (List<?> list : loggedInGeniUser.getClientCertificateChain().get(0)
                    .getSubjectAlternativeNames()) {
                if (Objects.equals(list.get(0), 1)) {  //if 1, then this is a rfc822Name-field
                    this.email.set(list.get(1).toString());
                    break;
                }
            }

            Date expirationDate =
                    getEarliestExpire(loggedInGeniUser.getClientCertificateChain());
            certificateExpiration.set(DATE_FORMAT.format(expirationDate));

        } catch (CertificateParsingException e) {
            e.printStackTrace();
        }


    }

    private Date getEarliestExpire(List<X509Certificate> certs) {
        Date res = null;
        for (X509Certificate cert : certs) {
            Date expire = cert.getNotAfter();
            if (res == null || res.after(expire))
                res = expire;
        }
        return res;
    }


    private static final int PROGRESS_OVERALL_INFO = 7;

    private int totalProgress;
    private int progress;
    private boolean finished;

    /**
     * Create the BugReport object that can be sent to the bugreport service
     *
     * The IDs of the calls will NOT be included, and neither the calls themselves.
     * The calls will be uploaded seperately, and the server will link them to the bugreport.
     *
     * This function expects the connectivityTests to have completed.
     *
     * @throws java.io.IOException
     */
    public BugReport createBugReport() {
        if (!isPreparationFinished())
            throw new IllegalStateException("Preparation has not finished!");

        //initialize logreader
        BugReportAppender bugReportAppender = jFedLoggerManager.getBugReportMemoryAppender();

        if (bugReportAppender == null)
            LOG.error("bugReportAppender \"BUGREPORT\" not found! Will not be able to add log entries to bug report.");

        progress = 0;
        //determine the total progress to be made
        totalProgress = PROGRESS_OVERALL_INFO
                + javaFXTaskThread.getCallsCount() * 10  //note: calls count for 10, but we don't add them here
                + (bugReportAppender == null ? 0 : bugReportAppender.getLength())
                + connectivityTester.getTestResults().size()
                + stitchingReportCount() * 50
                + (screenshot.get() != null ? SCREENSHOT_PROGRESS : 0);

        BugReportBuilder bugReportBuilder = new BugReportBuilder();
        bugReportBuilder.setDescription(description.get());
        bugReportBuilder.setMail(email.get());
        bugReportBuilder.setEnvironment(environment.get());
        bugReportBuilder.setVersion(version.get());
        bugReportBuilder.setReporterUrn(reporterUrn.get());
        bugReportBuilder.setLoggedInUserInfo(getLoggedInUserInfo());

        bugReportBuilder.setCommitTime(Instant.now());

        bugReportBuilder.setSubject(subject.get());
        bugReportBuilder.setReportTarget(reportTarget.get());
        bugReportBuilder.setReportType(reportType.get());
        bugReportBuilder.setRelatedTestbeds(relatedTestbeds.get());
        bugReportBuilder.setPostOnPublicList(postOnPublicList.get());

        if (jFedPreferences.getPropertiesFile() != null) {
            List<String> lines = null;
            try {
                lines = readAllLines(jFedPreferences.getPropertiesFile().toPath(), defaultCharset());

                String preferencesString = "";
                for (String line : lines) {
                    preferencesString += line + "\n";
                    bugReportBuilder.setPreferences(preferencesString);
                }
            } catch (IOException e) {
                LOG.error("Exception reading user preferences file in createBugReport()", e);
                bugReportBuilder.setPreferences("Exception reading user preferences file in createBugReport(): "+e.getMessage());
            }
        } else {
            bugReportBuilder.setPreferences("No properties file. (User without home dir?)\n");
        }

        progress += PROGRESS_OVERALL_INFO;



        //moved elsewhere
//        synchronized (javaFXTaskThread) {
//            List<ApiCallDetailsRef> allRefs = allApiCallsList.getCalls();
//            for (ApiCallDetailsRef ref : allRefs) {
//                SerializableApiCallDetails details = allApiCallsList.getCache().getApiCallDetails(ref);
//                SerializableApiCallDetailsWriter.writeApiCallDetails(details, xtw, eventFactory);
//                xtw.flush();
//
//                progress += 10;
//            }
//        }

        //include the list of call IDs, but not yet the calls themselves. This is only used for putting the number of calls in the bugreport email.
        synchronized (javaFXTaskThread) {
            bugReportBuilder.setApiCallDetailIds(
                    allApiCallsList.getCalls().stream()
                            .map(ApiCallDetailsRef::getId)
                            .collect(Collectors.toList())
            );
        }

        synchronized (bugReportExperimentControllerListener.getStitchingJobReports()) {
            for (StitchingJobReport stitchingReport : bugReportExperimentControllerListener.getStitchingJobReports()) {
                bugReportBuilder.addStitchingJobReport(convertJobReport(stitchingReport));

                progress += 50;
            }
        }

        for (Map.Entry<ConnectivityTest, ConnectivityTest.ConnectivityTestResult> entry :
                connectivityTester.getTestResults().entrySet()) {
            bugReportBuilder.addConnectivityTestResults(
                    convertConnectivityTest(entry.getKey(), entry.getValue())
            );

            progress++;
        }

        //write current slice information to the stream

        for (Slice slice : sfaModel.getSlices()) {
            bugReportBuilder.addSlice(convertSlice(slice));

            progress++;
        }


        if (screenshot.get() != null) {
            try {
                ByteArrayOutputStream screenshotBuffer = new ByteArrayOutputStream();
                write(fromFXImage(screenshot.get(), null), "png", screenshotBuffer);
                byte[] encoded = encodeBase64(screenshotBuffer.toByteArray());
                String screenshotString = new String(encoded);
                bugReportBuilder.setScreenshot(screenshotString);
            } catch (IOException ex) {
                LOG.warn("Error while writing bugreport screenshot: ignoring!", ex);
            }

            progress += SCREENSHOT_PROGRESS;
        }

        if (bugReportAppender != null) {
            List<ILoggingEvent> loggingEvents = bugReportAppender.getListCopy();
            for (ILoggingEvent loggingEvent : loggingEvents) {

                LogLine logLine = convertLogLine(loggingEvent);
                bugReportBuilder.addLogLine(logLine);
                progress++;

            }
        }

        finished = true;

        return bugReportBuilder.create();
    }

    public static interface ApiCallDetailsIterator {
        void process(ApiCallDetailsRef id, SerializableApiCallDetails detail);
    }

    private final Pattern bugReportPostUrlPattern = Pattern.compile(".*/bugreport/?$");
    private final Pattern bugReportCallPostUrlPattern = Pattern.compile(".*/bugreport/[0-9]*/call/?$");
    private final Pattern bugReportCallPutUrlPattern = Pattern.compile(".*/bugreport/[0-9]*/call/[0-9]*/?$");

    /** This detects and skips the (previous) bugreport calls */
    public void iterateOverApiCallDetails(ApiCallDetailsIterator it) {
        synchronized (javaFXTaskThread) {
            List<ApiCallDetailsRef> allRefs = allApiCallsList.getCalls();
            for (ApiCallDetailsRef ref : allRefs) {
                SerializableApiCallDetails details = allApiCallsList.getCache().getApiCallDetails(ref);

                boolean isBugReportCreateCall;
                if (details.getCallServerUrl() != null) {
                    Matcher bugReportCreateMatch1 = bugReportPostUrlPattern.matcher(details.getCallServerUrl());
                    Matcher bugReportCreateMatch2 = bugReportCallPostUrlPattern.matcher(details.getCallServerUrl());
                    Matcher bugReportCreateMatch3 = bugReportCallPutUrlPattern.matcher(details.getCallServerUrl());
                    isBugReportCreateCall = bugReportCreateMatch1.matches() || bugReportCreateMatch2.matches() || bugReportCreateMatch3.matches();
                } else {
                    isBugReportCreateCall = false;
                }

                if (!isBugReportCreateCall) {
                    it.process(ref, details);
                } else {
                    LOG.debug("Skipping call for BugReport, because it is a call from a previous BugReport");
                }

//                  SerializableApiCallDetailsWriter.writeApiCallDetails(details, xtw, eventFactory);
//                  xtw.flush();

                progress += 10;
            }
        }
    }

    private static be.iminds.ilabt.jfed.bugreport.StitchingJobReport convertJobReport(@Nonnull StitchingJobReport stitchingReport) {
        StitchingJobReportBuilder res = new StitchingJobReportBuilder();

        res.setName(stitchingReport.getName());

        for (JobStateReport jobStateReport : stitchingReport.getJobStateReports()) {
            StitchingJobStateReportBuilder jobStateBuilder = new StitchingJobStateReportBuilder();

            jobStateBuilder.setStart(jobStateReport.getStartTime());
            jobStateBuilder.setEnd(jobStateReport.getEndTime());
            jobStateBuilder.setName(jobStateReport.getStateName());
            jobStateBuilder.setStatus(Objects.toString(jobStateReport.getFinalStatus(), null));
            if (jobStateReport.getLogLines() != null) {
                jobStateBuilder.setLogEntries(
                        jobStateReport.getLogLines().stream()
                                .map(BugReportComposer::convertLogLine)
                                .collect(Collectors.toList())
                );
            }
            if (jobStateReport.getTaskExecutions() != null) {
                jobStateBuilder.setTaskExecutions(
                        jobStateReport.getTaskExecutions().stream()
                                .map(BugReportComposer::convertTaskExecutions)
                                .collect(Collectors.toList())
                );
            }

            res.addStitchingJobStateReport(jobStateBuilder.create());
        }


        return res.create();
    }


    private static be.iminds.ilabt.jfed.bugreport.Slice convertSlice(@Nonnull Slice slice) {
        SliceBuilder res = new SliceBuilder();

        res.setSliceUrn(slice.getUrnString());
        res.setExpirationDate(slice.getExpirationDate());

        if (slice.getStatus() != null) {
            res.setStatus(slice.getStatus().detailedToString());
        }

        for (Sliver sliver : slice.getSlivers()) {
            List<String> components = sliver.getComponentInfo().stream()
                    .filter(Objects::nonNull)
                    .map(c -> c.getClientId() == null ? c.getComponentId() : c.getClientId())
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());

            List<be.iminds.ilabt.jfed.bugreport.ErrorDetails> errorDetails = sliver.getStatus() == null ? null :
                    sliver.getStatus().getAllErrors().stream()
                            .filter(Objects::nonNull)
                            .map(e -> new be.iminds.ilabt.jfed.bugreport.ErrorDetails(
                                    e.getError(),
                                    e.getSliverUrn(),
                                    e.getComponentUrn(),
                                    Instant.ofEpochMilli(e.getTimestamp())
                            ))
                            .collect(Collectors.toList());

            String sliverUrn = sliver.getUrn() != null ?
                    sliver.getUrn().toString() :
                    "SLIVER-ON: " + sliver.getServer().getDefaultComponentManagerUrn();

            be.iminds.ilabt.jfed.bugreport.Sliver bugReportSliver =
                    new be.iminds.ilabt.jfed.bugreport.Sliver(
                            sliverUrn,
                            sliver.getStatusString(),
                            sliver.getStatus() == null ? null :
                                    Objects.toString(sliver.getStatus().getGlobalStatus(), null),
                            sliver.getExpirationDate(),
                            sliver.getManifestRspec() != null,
                            components,
                            errorDetails
                    );
            res.addSliver(bugReportSliver);
        }

        return res.create();
    }

    private static be.iminds.ilabt.jfed.bugreport.ConnectivityTestResult convertConnectivityTest(@Nonnull ConnectivityTest test,
                                                                                                 @Nullable ConnectivityTest.ConnectivityTestResult result) {

        be.iminds.ilabt.jfed.bugreport.ConnectivityTestResult.Status status = null;

        if (result != null) {
            switch (result.getStatus()) {
                case SUCCEEDED: { status = ConnectivityTestResult.Status.SUCCEEDED; break; }
                case WARNING: { status = ConnectivityTestResult.Status.WARNING; break; }
                case FAILED: { status = ConnectivityTestResult.Status.FAILED; break; }
                case SKIPPED: { status = ConnectivityTestResult.Status.SKIPPED; break; }
                default: { status = null; break; }
            }
        }

        be.iminds.ilabt.jfed.bugreport.ConnectivityTestResult res =
                new be.iminds.ilabt.jfed.bugreport.ConnectivityTestResult(
                        test.getName(),
                        status,
                        result == null ? null : result.getMessage(),
                        result.getExceptionString()
                );

        return res;
    }

    private static be.iminds.ilabt.jfed.bugreport.TaskExecutionReport convertTaskExecutions(TaskExecution<?> taskExecution) {
        List<Integer> apiCallDetailIds =
                taskExecution.getApiCallHistory().stream()
                        .filter(Objects::nonNull)
                        .map(ApiCallDetailsRef::getId)
                        .collect(Collectors.toList());

        TaskExecutionReport res = new TaskExecutionReport(
                taskExecution.getId(),
                taskExecution.getName(),
                taskExecution.getState() == null ? null : taskExecution.getState().name(),
                taskExecution.getTask() == null || taskExecution.getTask().getClass() == null ? null :
                        taskExecution.getTask().getClass().getName(),
                taskExecution.getRunStart() == null ? null : taskExecution.getRunStart().toInstant(),
                taskExecution.getRunStop() == null ? null : taskExecution.getRunStop().toInstant(),
                apiCallDetailIds,
                null
        );

        return res;
    }

    private static be.iminds.ilabt.jfed.bugreport.LogLine convertLogLine(LogOutput.LogEntry logEntry) {
        return new be.iminds.ilabt.jfed.bugreport.LogLine(
                Instant.ofEpochMilli(logEntry.getTimeMs()),
                null,
                Objects.toString(logEntry.getType(), null),
                logEntry.getText(),
                logEntry.getExceptionStackTrace(),
                null
        );
    }

    private static be.iminds.ilabt.jfed.bugreport.LogLine convertLogLine(@Nonnull ILoggingEvent loggingEvent) {
        return new be.iminds.ilabt.jfed.bugreport.LogLine(
                Instant.ofEpochMilli(loggingEvent.getTimeStamp()),
                loggingEvent.getLoggerName(),
                Objects.toString(loggingEvent.getLevel(), null),
                loggingEvent.getFormattedMessage(),
                loggingEvent.getThrowableProxy() == null ? null :
                        asString(loggingEvent.getThrowableProxy()),
                loggingEvent.getThreadName()
        );
    }

    public void reset() {
        progress = 0;
        totalProgress = 0;
        finished = false;
    }

    public boolean isFinished() {
        return finished;
    }

    public int getTotalProgress() {
        return totalProgress;
    }

    public int getProgress() {
        return progress;
    }

    public boolean isPreparationFinished() {
        return connectivityTester.getTestsFinished();

    }

    private String getLoggedInUserInfo() {
        String userInfoStr = null;
        try {
            if (geniUserProvider.isUserLoggedIn()) {
                GeniUser u = geniUserProvider.getLoggedInGeniUser();
                //TODO: certInfo would better contain info about the certificate such as all data that can be extracted.
                //      PEM does contain all that info but is not human readable.
                userInfoStr = u.getUserUrnString() + " @ " + u.getUserAuthorityServer().getDefaultComponentManagerUrn();
                if (u.getPrivateKey() == null)
                    userInfoStr += " WARNING, no private key!";
                try {
                    String pubkey =
                            KeyUtil.rsaPublicKeyToOpenSshAuthorizedKeysFormatData((RSAPublicKey) u.getClientCertificateChain().get(0).getPublicKey());
                    userInfoStr += " with pubkey " + pubkey;
                } catch (Exception | AssertionError e) {
                    userInfoStr += " ERROR getting pubkey " + e.getMessage();
                }
                try {
                    String certInfo = KeyUtil.x509certificateChainToPem(u.getClientCertificateChain());
                    userInfoStr += " with cert " + certInfo;
                } catch (Exception | AssertionError e) {
                    userInfoStr += " ERROR getting cert " + e.getMessage();
                }
            } else
                userInfoStr = "No user logged in";
        } catch (AssertionError e) {
            if (userInfoStr == null)
                userInfoStr = "NULL then ";
            userInfoStr += " AssertionError getting user info " + e.getMessage();
        } catch (Exception e) {
            if (userInfoStr == null)
                userInfoStr = "NULL then ";
            userInfoStr += " Exception getting user info " + e.getMessage();
        }
        return userInfoStr;
    }

    public String getDescription() {
        return description.get();
    }

    public StringProperty descriptionProperty() {
        return description;
    }

    public void setDescription(String description) {
        this.description.set(description);
    }

    public String getEmail() {
        return email.get();
    }

    public StringProperty emailProperty() {
        return email;
    }

    public void setEmail(String email) {
        this.email.set(email);
    }

    public String getVersion() {
        return version.get();
    }

    public ReadOnlyStringProperty versionProperty() {
        return version.getReadOnlyProperty();
    }

    public String getEnvironment() {
        return environment.get();
    }

    public ReadOnlyStringProperty environmentProperty() {
        return environment.getReadOnlyProperty();
    }


    public String getReporterUrn() {
        return reporterUrn.get();
    }

    public ReadOnlyStringProperty reporterUrnProperty() {
        return reporterUrn.getReadOnlyProperty();
    }

    public String getCertificateExpiration() {
        return certificateExpiration.get();
    }

    public ReadOnlyStringProperty certificateExpirationProperty() {
        return certificateExpiration.getReadOnlyProperty();
    }

    public ReadOnlyIntegerProperty callsCountProperty() {
        return javaFXTaskThread.callsCountProperty();
    }

    public int stitchingReportCount() {
        return bugReportExperimentControllerListener.getStitchingJobReports().size();
    }

    public Image getScreenshot() {
        return screenshot.get();
    }

    public ObjectProperty<Image> screenshotProperty() {
        return screenshot;
    }

    public void setScreenshot(Image screenshot) {
        this.screenshot.set(screenshot);
    }

    public String getSubject() {
        return this.subject.get();
    }

    public StringProperty subjectProperty() {
        return this.subject;
    }

    public void setSubject(String subject) {
        this.subject.set(subject);
    }

    public ReportTarget getReportTarget() {
        return this.reportTarget.get();
    }

    public SimpleObjectProperty<ReportTarget> reportTargetProperty() {
        return this.reportTarget;
    }

    public void setReportTarget(ReportTarget reportTarget) {
        this.reportTarget.set(reportTarget);
    }

    public ReportType getReportType() {
        return this.reportType.get();
    }

    public SimpleObjectProperty<ReportType> reportTypeProperty() {
        return this.reportType;
    }

    public void setReportType(ReportType reportType) {
        this.reportType.set(reportType);
    }

    public ObservableList<GeniUrn> getRelatedTestbeds() {
        return this.relatedTestbeds.get();
    }

    public ListProperty<GeniUrn> relatedTestbedsProperty() {
        return this.relatedTestbeds;
    }

    public void setRelatedTestbeds(@Nonnull List<GeniUrn> relatedTestbeds) {
        this.relatedTestbeds.setAll(relatedTestbeds);
    }

    public boolean isPostOnPublicList() {
        return this.postOnPublicList.get();
    }

    public BooleanProperty postOnPublicListProperty() {
        return this.postOnPublicList;
    }

    public void setPostOnPublicList(boolean postOnPublicList) {
        this.postOnPublicList.set(postOnPublicList);
    }
}
