Add PatchSetCreatedEvent listener and finalize key features
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
new file mode 100644
index 0000000..57ec722
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
@@ -0,0 +1,57 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import lombok.Getter;
+
+@Singleton
+@Getter
+public class Configuration {
+
+    public static final String OPENAI_DOMAIN = "https://api.openai.com";
+    public static final String DEFAULT_GPT_MODEL = "gpt-3.5-turbo";
+    public static final String DEFAULT_GPT_PROMPT = "Act as a Code Review Helper, please review this patch set: ";
+    public static final String NOT_CONFIGURED_ERROR_MSG = "%s is not configured";
+
+    private final PluginConfig cfg;
+
+    private final String gptDomain;
+    private final String gptToken;
+    private final String gptModel;
+    private final String gptPrompt;
+    private final int gptMaxTokens;
+
+    private final String gerritAuthBaseUrl;
+    private final String gerritUserName;
+    private final String gerritPassword;
+
+    private final boolean patchSetReduction;
+    private final int maxReviewLines;
+
+    @Inject
+    Configuration(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
+        cfg = cfgFactory.getFromGerritConfig(pluginName);
+        gptDomain = cfg.getString("gptUrl", OPENAI_DOMAIN);
+        gptToken = getValidatedOrThrow("gptToken");
+        gptModel = cfg.getString("gptModel", DEFAULT_GPT_MODEL);
+        gptPrompt = cfg.getString("gptPrompt", DEFAULT_GPT_PROMPT);
+        gptMaxTokens = cfg.getInt("gptMaxTokens", 4096);
+        gerritAuthBaseUrl = getValidatedOrThrow("gerritAuthBaseUrl");
+        gerritUserName = getValidatedOrThrow("gerritUserName");
+        gerritPassword = getValidatedOrThrow("gerritPassword");
+        patchSetReduction = cfg.getBoolean("patchSetReduction", false);
+        maxReviewLines = cfg.getInt("maxReviewLines", 1000);
+    }
+
+    private String getValidatedOrThrow(String key) {
+        String value = cfg.getString(key);
+        if (value == null) {
+            throw new RuntimeException(String.format(NOT_CONFIGURED_ERROR_MSG, key));
+        }
+        return value;
+    }
+}
+
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
new file mode 100644
index 0000000..28725cb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
@@ -0,0 +1,13 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.events.EventListener;
+import com.google.inject.AbstractModule;
+
+public class Module extends AbstractModule {
+
+    @Override
+    protected void configure() {
+        DynamicSet.bind(binder(), EventListener.class).to(PatchSetCreated.class);
+    }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
new file mode 100644
index 0000000..3feeecf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
@@ -0,0 +1,52 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+public class PatchSetCreated implements EventListener {
+
+    private final PatchSetReviewer reviewer;
+
+    @Inject
+    public PatchSetCreated(PatchSetReviewer reviewer) {
+        this.reviewer = reviewer;
+    }
+
+    @Override
+    public void onEvent(Event event) {
+        if (!(event instanceof PatchSetCreatedEvent)) {
+            log.debug("The event is not a PatchSetCreatedEvent, it is: {}", event);
+            return;
+        }
+
+        log.info("Processing event: {}", event);
+
+        PatchSetCreatedEvent createdPatchSetEvent = (PatchSetCreatedEvent) event;
+        String projectName = createdPatchSetEvent.getProjectNameKey().get();
+        String branchName = createdPatchSetEvent.getBranchNameKey().shortName();
+        String changeKey = createdPatchSetEvent.getChangeKey().get();
+
+        log.info("Processing patch set for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey);
+
+        String reviewId = String.join("~", projectName, branchName, changeKey);
+        // Execute the potentially time-consuming operation asynchronously
+        CompletableFuture.runAsync(() -> {
+            try {
+                reviewer.review(reviewId);
+            } catch (IOException | InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("Failed to submit review for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey, e);
+            } catch (Throwable e) {
+                log.error("Failed to submit review for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey, e);
+            }
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
new file mode 100644
index 0000000..e3a5057
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -0,0 +1,100 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
+import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Singleton
+public class PatchSetReviewer {
+
+    private static final String SPLIT_REVIEW_MSG = "Too many changes. Please consider splitting into patches smaller than %s lines for review.";
+
+    private static final int COMMENT_BATCH_SIZE = 25;
+
+    private final Configuration configuration;
+    private final GerritClient gerritClient;
+    private final OpenAiClient openAiClient;
+
+    @Inject
+    public PatchSetReviewer(Configuration configuration, GerritClient gerritClient, OpenAiClient openAiClient) {
+        this.configuration = configuration;
+        this.gerritClient = gerritClient;
+        this.openAiClient = openAiClient;
+    }
+
+    public void review(String changeId) throws IOException, InterruptedException {
+        log.info("Starting to review patch set: changeId={}", changeId);
+
+        String patchSet = gerritClient.getPatchSet(changeId);
+        if (configuration.isPatchSetReduction()) {
+            patchSet = reducePatchSet(patchSet);
+            log.debug("Reduced patch set: {}", patchSet);
+        }
+
+        String reviewSuggestion = getReviewSuggestion(changeId, patchSet);
+        List<String> reviewBatches = splitReviewIntoBatches(reviewSuggestion);
+
+        for (String reviewBatch : reviewBatches) {
+            gerritClient.postComment(changeId, reviewBatch);
+            log.debug("Posted review batch: {}", reviewBatch);
+        }
+
+        log.info("Finished reviewing patch set: changeId={}", changeId);
+
+    }
+
+    private List<String> splitReviewIntoBatches(String review) {
+        List<String> batches = new ArrayList<>();
+        String[] lines = review.split("\n");
+
+        StringBuilder batch = new StringBuilder();
+        for (int i = 0; i < lines.length; i++) {
+            batch.append(lines[i]).append("\n");
+            if ((i + 1) % COMMENT_BATCH_SIZE == 0) {
+                batches.add(batch.toString());
+                batch = new StringBuilder();
+            }
+        }
+        if (batch.length() > 0) {
+            batches.add(batch.toString());
+        }
+        log.info("Review batches created: {}", batches.size());
+        return batches;
+    }
+
+    private String reducePatchSet(String patchSet) {
+        Set<String> skipPrefixes = new HashSet<>(Arrays.asList(
+                "import", "-", "+package", "+import", "From", "Date:", "Subject:",
+                "Change-Id:", "diff --git", "index", "---", "+++", "@@", "Binary files differ"
+        ));
+
+        return Arrays.stream(patchSet.split("\n"))
+                .map(line -> line.replace("\t", "").replace("    ", ""))
+                .filter(line -> skipPrefixes.stream().noneMatch(line::startsWith))
+                .filter(line -> !line.trim().isEmpty())
+                .collect(Collectors.joining("\n"));
+    }
+
+    private String getReviewSuggestion(String changeId, String patchSet) throws IOException, InterruptedException {
+        log.info("Starting review for changeId: {}", changeId);
+
+        List<String> patchLines = Arrays.asList(patchSet.split("\n"));
+        if (patchLines.size() > configuration.getMaxReviewLines()) {
+            log.warn("Patch set too large. Skipping review. changeId: {}", changeId);
+            return String.format(SPLIT_REVIEW_MSG, configuration.getMaxReviewLines());
+        }
+
+        String reviewSuggestion = openAiClient.ask(patchSet);
+        log.info("Review completed for changeId: {}", changeId);
+        return reviewSuggestion;
+
+    }
+}
+