package com.github.catatafishen.agentbridge.client.claude;

import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * Prepends plugin instructions to an agent-specific config file before the agent process starts.
 *
 * <p>Some agents (e.g. Copilot CLI) ignore the MCP {@code instructions} field from the initialize
 * response and instead read instructions from a config file at a well-known path. This class
 * handles the file-mutation workaround for those agents. The target path is specified per-profile
 * via {@link com.github.catatafishen.agentbridge.services.AgentProfile#getPrependInstructionsTo()}.</p>
 *
 * <p>Thread-safe: uses a class-level lock so concurrent calls don't race.</p>
 */
public final class InstructionsManager {

    private static final Logger LOG = Logger.getInstance(InstructionsManager.class);

    /**
     * Sentinel used to detect a prior prepend and avoid duplicating instructions.
     */
    public static final String INSTRUCTIONS_SENTINEL =
        "---\n> ⚙️ Auto-generated by AgentBridge — do not edit this section";
    private static final String INSTRUCTIONS_END =
        "> End of auto-generated instructions\n---";

    /**
     * Legacy sentinel prefixes written by older versions of the plugin (before the rename
     * to "AgentBridge" in commit 21c2014e). We still recognise them so in-place regeneration
     * can upgrade existing files to the current content instead of duplicating the block.
     */
    private static final String[] LEGACY_SENTINEL_MARKERS = {
        "> ⚙️ Auto-generated by IDE Agent for Copilot",
        "> ⚙️ Auto-generated by AgentBridge"
    };

    private static final Object LOCK = new Object();

    private InstructionsManager() {
    }

    /**
     * Workaround: prepend plugin instructions to a file on the filesystem.
     * This is a fallback for agents that ignore the ACP {@code instructions} field
     * and don't process in-conversation messages (session/message). For agents like
     * Junie that support session/message, prefer injecting instructions via
     * {@link AgentConfig#getSessionInstructions()} in {@code AcpClient.createSession()}.
     *
     * <p>Thread-safe: uses a class-level lock so concurrent calls don't race.</p>
     */
    public static void ensureInstructions(@Nullable String projectBasePath,
                                          @NotNull String relativeTargetPath,
                                          @NotNull String additionalInstructions) {
        if (projectBasePath == null || relativeTargetPath.isEmpty()) return;

        synchronized (LOCK) {
            try {
                Path targetFile = Path.of(projectBasePath, relativeTargetPath);
                String pluginInstructions = buildInstructions(additionalInstructions);

                if (Files.isRegularFile(targetFile)) {
                    String existing = Files.readString(targetFile, StandardCharsets.UTF_8);
                    String spliced = spliceInPlace(existing, pluginInstructions);
                    if (spliced != null) {
                        if (spliced.equals(existing)) {
                            return; // already up-to-date
                        }
                        Files.writeString(targetFile, spliced, StandardCharsets.UTF_8);
                        LOG.info("Refreshed plugin instructions in " + targetFile);
                        return;
                    }
                    Files.writeString(targetFile, pluginInstructions + "\n\n" + existing, StandardCharsets.UTF_8);
                    LOG.info("Prepended plugin instructions to " + targetFile);
                } else {
                    Path parent = targetFile.getParent();
                    if (parent != null) {
                        Files.createDirectories(parent);
                    }
                    Files.writeString(targetFile, pluginInstructions, StandardCharsets.UTF_8);
                    LOG.info("Created " + targetFile + " with plugin instructions");
                }
            } catch (IOException e) {
                LOG.warn("Failed to ensure instructions in " + relativeTargetPath, e);
            }
        }
    }

    @NotNull
    private static String buildInstructions(@NotNull String additionalInstructions) {
        StringBuilder sb = new StringBuilder(INSTRUCTIONS_SENTINEL).append("\n\n");
        try (InputStream is = InstructionsManager.class.getResourceAsStream("/default-startup-instructions.md")) {
            if (is != null) {
                sb.append(new String(is.readAllBytes(), StandardCharsets.UTF_8));
            } else {
                LOG.warn("Resource /default-startup-instructions.md not found in classpath");
                sb.append("You are running inside an IntelliJ IDEA plugin with IDE tools accessible via MCP.");
            }
        } catch (IOException e) {
            LOG.error("Failed to read /default-startup-instructions.md from classpath", e);
            sb.append("You are running inside an IntelliJ IDEA plugin with IDE tools accessible via MCP.");
        }
        if (!additionalInstructions.isBlank()) {
            sb.append("\n\n").append(additionalInstructions);
        }
        sb.append("\n\n").append(INSTRUCTIONS_END);
        return sb.toString();
    }

    /**
     * If the existing file already has an auto-generated block (current or legacy sentinel),
     * return the file with that block replaced in-place by {@code newBlock}. Returns
     * {@code null} if no recognised block is present (caller should prepend instead).
     *
     * <p>The block start is identified by a {@code ---} fence line immediately followed by a
     * line containing one of {@link #LEGACY_SENTINEL_MARKERS}. The block end is the next
     * occurrence of {@link #INSTRUCTIONS_END}. If the start is found but the end is not
     * (corrupt or partially-written file from an older buggy version), we splice to the
     * first blank line after the start so we don't delete unrelated user content.</p>
     */
    @Nullable
    static String spliceInPlace(@NotNull String existing, @NotNull String newBlock) {
        int startIdx = findBlockStart(existing);
        if (startIdx < 0) return null;

        int endMarker = existing.indexOf(INSTRUCTIONS_END, startIdx);
        int endIdx;
        if (endMarker >= 0) {
            endIdx = endMarker + INSTRUCTIONS_END.length();
        } else {
            // No end marker -- conservative: replace up to the next blank line (or EOF).
            int blank = existing.indexOf("\n\n", startIdx);
            endIdx = (blank >= 0) ? blank : existing.length();
        }

        String before = existing.substring(0, startIdx);
        String after = existing.substring(endIdx);
        return before + newBlock + after;
    }

    private static int findBlockStart(@NotNull String existing) {
        // Preferred: current sentinel as a whole (includes leading `---\n`).
        int idx = existing.indexOf(INSTRUCTIONS_SENTINEL);
        if (idx >= 0) return idx;

        // Legacy: look for `---\n> ⚙️ Auto-generated by …` with any recognised marker.
        for (String marker : LEGACY_SENTINEL_MARKERS) {
            int markerIdx = existing.indexOf(marker);
            if (markerIdx >= 0) {
                // Walk back to the `---` fence immediately preceding it (allow one \n between).
                int fence = existing.lastIndexOf("---", markerIdx);
                if (fence >= 0) {
                    String between = existing.substring(fence + 3, markerIdx);
                    if (between.equals("\n") || between.isBlank()) {
                        return fence;
                    }
                }
            }
        }
        return -1;
    }
}
