package ai.intelliswarm.swarmai.process;

import ai.intelliswarm.swarmai.agent.Agent;
import ai.intelliswarm.swarmai.event.SwarmEvent;
import ai.intelliswarm.swarmai.skill.SkillRegistry;
import ai.intelliswarm.swarmai.swarm.SwarmOutput;
import ai.intelliswarm.swarmai.task.Task;
import ai.intelliswarm.swarmai.task.output.TaskOutput;
import ai.intelliswarm.swarmai.tool.base.BaseTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * SwarmCoordinator — a distributed fan-out Process type.
 *
 * Execution flow:
 * <ol>
 *   <li><b>Discovery:</b> Execute the first task sequentially to discover targets (e.g., nmap host discovery).</li>
 *   <li><b>Parse targets:</b> Extract IP addresses from the discovery output.</li>
 *   <li><b>Fan-out:</b> Spawn parallel {@link SelfImprovingProcess} instances (one per target),
 *       each with its own analyst agent clone. Generated skills are shared via a shared {@link SkillRegistry}.</li>
 *   <li><b>Synthesize:</b> Collect all per-target results and combine them into a single master report.</li>
 * </ol>
 *
 * Key design points:
 * <ul>
 *   <li>Skills generated by any sub-process are visible to all others via the shared registry.</li>
 *   <li>Sub-process failures are logged and skipped — they do not fail the entire swarm.</li>
 *   <li>Each sub-process has a 10-minute timeout.</li>
 * </ul>
 */
public class SwarmCoordinator implements Process {

    private static final Logger logger = LoggerFactory.getLogger(SwarmCoordinator.class);

    private static final int DEFAULT_MAX_PARALLEL = 5;
    private static final long SUB_PROCESS_TIMEOUT_MINUTES = 10;

    /** Regex to extract IPv4 addresses from text. */
    private static final Pattern IP_PATTERN = Pattern.compile("\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b");

    private final List<Agent> agentTemplates;
    private final Agent reviewerAgent;
    private final ApplicationEventPublisher eventPublisher;
    private final int maxParallelAgents;
    private final int maxIterationsPerAgent;
    private final String qualityCriteria;
    private final SkillRegistry sharedSkillRegistry;

    /** Custom target pattern for non-IP target extraction (e.g., "COMPETITOR: Company Name"). */
    private Pattern targetPattern;
    private String targetPrefix;

    /**
     * Creates a new SwarmCoordinator.
     *
     * @param agentTemplates       agent templates; the first agent is used for discovery, the rest for per-target analysis
     * @param reviewerAgent        the reviewer/manager agent used for quality review in each sub-process
     * @param eventPublisher       Spring event publisher for swarm events
     * @param maxIterationsPerAgent max iterations for each per-target SelfImprovingProcess
     * @param maxParallelAgents    max number of parallel sub-processes (capped by available processors)
     * @param qualityCriteria      optional quality criteria passed to each sub-process reviewer
     */
    public SwarmCoordinator(List<Agent> agentTemplates, Agent reviewerAgent,
                            ApplicationEventPublisher eventPublisher,
                            int maxIterationsPerAgent, int maxParallelAgents,
                            String qualityCriteria) {
        this.agentTemplates = new ArrayList<>(agentTemplates);
        this.reviewerAgent = reviewerAgent;
        this.eventPublisher = eventPublisher;
        this.maxIterationsPerAgent = maxIterationsPerAgent > 0 ? maxIterationsPerAgent : 5;
        this.maxParallelAgents = maxParallelAgents > 0 ? maxParallelAgents : DEFAULT_MAX_PARALLEL;
        this.qualityCriteria = qualityCriteria;
        this.sharedSkillRegistry = new SkillRegistry();
    }

    /**
     * Sets a custom target prefix for parsing non-IP targets from discovery output.
     * When set, the coordinator will look for lines matching "PREFIX some text" and extract
     * the text after the prefix as a target name instead of extracting IP addresses.
     *
     * @param prefix the prefix string to match (e.g., "COMPETITOR:" for competitive research)
     */
    public void setTargetPattern(String prefix) {
        this.targetPrefix = prefix;
        // Pattern: PREFIX Some text here (until end of line)
        this.targetPattern = Pattern.compile("(?m)^\\s*" + Pattern.quote(prefix) + "\\s*(.+)$");
    }

    @Override
    public ProcessType getType() {
        return ProcessType.SWARM;
    }

    @Override
    public boolean isAsync() {
        return true;
    }

    @Override
    public void validateTasks(List<Task> tasks) {
        if (tasks == null || tasks.isEmpty()) {
            throw new IllegalArgumentException("At least one task (discovery) is required for SwarmCoordinator");
        }
        if (reviewerAgent == null) {
            throw new IllegalArgumentException("Reviewer agent is required for SwarmCoordinator");
        }
        if (agentTemplates.isEmpty()) {
            throw new IllegalArgumentException("At least one agent template is required for SwarmCoordinator");
        }
    }

    @Override
    public SwarmOutput execute(List<Task> tasks, Map<String, Object> inputs, String swarmId) {
        validateTasks(tasks);
        LocalDateTime startTime = LocalDateTime.now();

        publishEvent(SwarmEvent.Type.PROCESS_STARTED,
                "SwarmCoordinator started (maxParallel=" + maxParallelAgents +
                        ", maxIterPerAgent=" + maxIterationsPerAgent + ")",
                swarmId, Map.of("processType", "SWARM",
                        "maxParallelAgents", maxParallelAgents,
                        "maxIterationsPerAgent", maxIterationsPerAgent));

        List<TaskOutput> allOutputs = new ArrayList<>();

        // ====================================================================
        // Phase 1: Discovery — execute the first task sequentially
        // ====================================================================
        Task discoveryTask = tasks.get(0);
        Agent discoveryAgent = discoveryTask.getAgent() != null
                ? discoveryTask.getAgent()
                : agentTemplates.get(0);

        logger.info("SwarmCoordinator [{}]: Phase 1 — Discovery task starting", swarmId);
        publishEvent(SwarmEvent.Type.TASK_STARTED,
                "Discovery task started: " + discoveryTask.getId(), swarmId, Map.of());

        TaskOutput discoveryOutput;
        try {
            // Interpolate inputs into the discovery task
            if (inputs != null) {
                discoveryTask.interpolateDescription(inputs);
            }
            discoveryOutput = discoveryAgent.executeTask(discoveryTask, Collections.emptyList());
            allOutputs.add(discoveryOutput);

            logger.info("SwarmCoordinator [{}]: Discovery completed ({} chars, {} ms)",
                    swarmId,
                    discoveryOutput.getRawOutput() != null ? discoveryOutput.getRawOutput().length() : 0,
                    discoveryOutput.getExecutionTimeMs());

            publishEvent(SwarmEvent.Type.TASK_COMPLETED,
                    "Discovery task completed: " + discoveryTask.getId(), swarmId, Map.of());

        } catch (Exception e) {
            logger.error("SwarmCoordinator [{}]: Discovery task failed: {}", swarmId, e.getMessage());
            publishEvent(SwarmEvent.Type.TASK_FAILED,
                    "Discovery task failed: " + e.getMessage(), swarmId, Map.of());

            return SwarmOutput.builder()
                    .swarmId(swarmId)
                    .successful(false)
                    .taskOutputs(allOutputs)
                    .startTime(startTime)
                    .endTime(LocalDateTime.now())
                    .finalOutput("SwarmCoordinator failed: discovery task error — " + e.getMessage())
                    .metadata("stopReason", "discovery_failed")
                    .build();
        }

        // ====================================================================
        // Phase 2: Parse targets from discovery output
        // ====================================================================
        String discoveryText = discoveryOutput.getRawOutput();
        List<String> targets = parseTargets(discoveryText);

        if (targets.isEmpty()) {
            logger.warn("SwarmCoordinator [{}]: No targets found in discovery output", swarmId);
            return SwarmOutput.builder()
                    .swarmId(swarmId)
                    .successful(true)
                    .taskOutputs(allOutputs)
                    .startTime(startTime)
                    .endTime(LocalDateTime.now())
                    .finalOutput("Discovery completed but no targets were found.\n\nDiscovery output:\n" + discoveryText)
                    .metadata("stopReason", "no_targets_found")
                    .metadata("targetsFound", 0)
                    .build();
        }

        // Cap to maxParallelAgents
        if (targets.size() > maxParallelAgents) {
            logger.info("SwarmCoordinator [{}]: Capping targets from {} to {}", swarmId, targets.size(), maxParallelAgents);
            targets = targets.subList(0, maxParallelAgents);
        }

        logger.info("SwarmCoordinator [{}]: Phase 2 — Found {} targets: {}",
                swarmId, targets.size(), targets);

        // ====================================================================
        // Phase 3: Fan-out — spawn parallel sub-processes per target
        // ====================================================================
        logger.info("SwarmCoordinator [{}]: Phase 3 — Spawning {} parallel agents for targets: {}",
                swarmId, targets.size(), targets);

        publishEvent(SwarmEvent.Type.PROCESS_STARTED,
                "Spawning " + targets.size() + " parallel agents for targets: " + targets,
                swarmId, Map.of("phase", "fan-out", "targetCount", targets.size(),
                        "targets", targets));

        int threadPoolSize = Math.min(maxParallelAgents, Runtime.getRuntime().availableProcessors());
        ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);

        Map<String, SwarmOutput> targetResults;
        try {
            // Determine the analyst agent template (second agent if available, else first)
            Agent analystTemplate = agentTemplates.size() > 1
                    ? agentTemplates.get(1)
                    : agentTemplates.get(0);

            Map<String, CompletableFuture<SwarmOutput>> futureMap = new LinkedHashMap<>();

            for (String target : targets) {
                String subSwarmId = swarmId + "-target-" + target.replace(".", "_");

                CompletableFuture<SwarmOutput> future = CompletableFuture.supplyAsync(() -> {
                    try {
                        return executeSubProcess(target, subSwarmId, analystTemplate, discoveryOutput);
                    } catch (Exception e) {
                        logger.error("SwarmCoordinator [{}]: Sub-process for target {} failed: {}",
                                swarmId, target, e.getMessage(), e);
                        return SwarmOutput.builder()
                                .swarmId(subSwarmId)
                                .successful(false)
                                .finalOutput("Error analyzing target " + target + ": " + e.getMessage())
                                .metadata("target", target)
                                .metadata("error", e.getMessage())
                                .build();
                    }
                }, executor);

                futureMap.put(target, future);
            }

            // Wait for all sub-processes with timeout
            targetResults = new LinkedHashMap<>();
            for (Map.Entry<String, CompletableFuture<SwarmOutput>> entry : futureMap.entrySet()) {
                String target = entry.getKey();
                try {
                    SwarmOutput result = entry.getValue().get(SUB_PROCESS_TIMEOUT_MINUTES, TimeUnit.MINUTES);
                    targetResults.put(target, result);
                    logger.info("SwarmCoordinator [{}]: Target {} completed (successful={})",
                            swarmId, target, result.isSuccessful());
                } catch (TimeoutException e) {
                    logger.error("SwarmCoordinator [{}]: Sub-process for target {} timed out after {} minutes",
                            swarmId, target, SUB_PROCESS_TIMEOUT_MINUTES);
                    entry.getValue().cancel(true);
                    targetResults.put(target, SwarmOutput.builder()
                            .swarmId(swarmId + "-target-" + target.replace(".", "_"))
                            .successful(false)
                            .finalOutput("Timed out after " + SUB_PROCESS_TIMEOUT_MINUTES + " minutes analyzing " + target)
                            .metadata("target", target)
                            .metadata("error", "timeout")
                            .build());
                } catch (Exception e) {
                    logger.error("SwarmCoordinator [{}]: Sub-process for target {} failed: {}",
                            swarmId, target, e.getMessage());
                    targetResults.put(target, SwarmOutput.builder()
                            .swarmId(swarmId + "-target-" + target.replace(".", "_"))
                            .successful(false)
                            .finalOutput("Error analyzing target " + target + ": " + e.getMessage())
                            .metadata("target", target)
                            .metadata("error", e.getMessage())
                            .build());
                }
            }
        } finally {
            executor.shutdownNow();
        }

        // Collect all per-target task outputs
        for (SwarmOutput targetOutput : targetResults.values()) {
            allOutputs.addAll(targetOutput.getTaskOutputs());
        }

        // ====================================================================
        // Phase 4: Synthesize results into final report
        // ====================================================================
        logger.info("SwarmCoordinator [{}]: Phase 4 — Synthesizing results from {} targets", swarmId, targetResults.size());

        String synthesizedReport = synthesizeResults(swarmId, targetResults, tasks, inputs, allOutputs);

        LocalDateTime endTime = LocalDateTime.now();

        publishEvent(SwarmEvent.Type.PROCESS_COMPLETED,
                "SwarmCoordinator completed: " + targetResults.size() + " targets analyzed",
                swarmId, Map.of("targetsAnalyzed", targetResults.size(),
                        "successfulTargets", targetResults.values().stream().filter(SwarmOutput::isSuccessful).count(),
                        "skillsInRegistry", sharedSkillRegistry.size()));

        logger.info("SwarmCoordinator [{}]: Complete — {} targets, {} skills in shared registry",
                swarmId, targetResults.size(), sharedSkillRegistry.size());

        return SwarmOutput.builder()
                .swarmId(swarmId)
                .successful(true)
                .taskOutputs(allOutputs)
                .startTime(startTime)
                .endTime(endTime)
                .executionTime(Duration.between(startTime, endTime))
                .finalOutput(synthesizedReport)
                .metadata("processType", "SWARM")
                .metadata("targetsFound", targets.size())
                .metadata("targetsAnalyzed", targetResults.size())
                .metadata("successfulTargets",
                        targetResults.values().stream().filter(SwarmOutput::isSuccessful).count())
                .metadata("failedTargets",
                        targetResults.values().stream().filter(o -> !o.isSuccessful()).count())
                .metadata("sharedSkillsGenerated", sharedSkillRegistry.size())
                .metadata("registryStats", sharedSkillRegistry.getStats())
                .build();
    }

    /**
     * Execute a SelfImprovingProcess for a single target.
     * Clones the analyst agent with shared skills and creates a target-specific task.
     */
    private SwarmOutput executeSubProcess(String target, String subSwarmId,
                                           Agent analystTemplate, TaskOutput discoveryContext) {
        logger.info("SwarmCoordinator [{}]: Starting sub-process for target {}", subSwarmId, target);

        // Build a tools map from the analyst's base tools for skill injection
        Map<String, BaseTool> baseToolsMap = analystTemplate.getTools().stream()
            .collect(Collectors.toMap(BaseTool::getFunctionName, t -> t, (a, b) -> a));

        // Inject base tools into all shared skills so they can call tools.http_request etc.
        List<BaseTool> sharedSkills = new ArrayList<>(sharedSkillRegistry.getActiveSkills());
        for (BaseTool skill : sharedSkills) {
            if (skill instanceof ai.intelliswarm.swarmai.skill.GeneratedSkill gs) {
                Map<String, BaseTool> allTools = new java.util.HashMap<>(baseToolsMap);
                // Also add other skills so they can compose each other
                for (BaseTool other : sharedSkills) {
                    allTools.put(other.getFunctionName(), other);
                }
                gs.setAvailableTools(allTools);
            }
        }

        // Clone agents with shared skills
        Agent clonedAnalyst = analystTemplate.withAdditionalTools(sharedSkills);
        Agent clonedReviewer = reviewerAgent.withAdditionalTools(sharedSkills);

        // Create a target-specific task
        String sanitizedTarget = target.replaceAll("[^a-zA-Z0-9_\\-]", "_");
        String taskDescription;
        String taskExpectedOutput;

        if (targetPrefix != null) {
            // Custom target mode (e.g., competitive research, market analysis)
            taskDescription = "Perform deep analysis on: " + target + ". " +
                    "Research thoroughly using available tools: gather data, analyze findings, " +
                    "and produce a comprehensive report on this target.";
            taskExpectedOutput = "Comprehensive analysis of " + target + " with data-backed findings";
        } else {
            // Default: security/pentest mode
            taskDescription = "Enumerate and exploit services on " + target + ". " +
                    "Run nmap -sV --top-ports 100, then try exploitation tools " +
                    "(hydra, nikto, smbclient, etc.). Report all findings including " +
                    "open ports, service versions, vulnerabilities, and exploitation results.";
            taskExpectedOutput = "Detailed security analysis of " + target + " with " +
                    "open ports, services, vulnerabilities, and exploitation attempts";
        }

        Task targetTask = Task.builder()
                .id("analyze-" + sanitizedTarget)
                .description(taskDescription)
                .expectedOutput(taskExpectedOutput)
                .agent(clonedAnalyst)
                .build();

        // Create SelfImprovingProcess with the shared SkillRegistry
        SelfImprovingProcess subProcess = new SelfImprovingProcess(
                List.of(clonedAnalyst),
                clonedReviewer,
                eventPublisher,
                maxIterationsPerAgent,
                qualityCriteria,
                sharedSkillRegistry
        );

        // Execute with discovery output as context
        List<Task> subTasks = List.of(targetTask);
        Map<String, Object> subInputs = new HashMap<>();
        subInputs.put("target", target);
        subInputs.put("discoveryContext", discoveryContext.getRawOutput());

        return subProcess.execute(subTasks, subInputs, subSwarmId);
    }

    /**
     * Parse targets from the discovery output.
     * First tries custom target pattern (e.g., "COMPETITOR: Company Name") if configured,
     * then falls back to IP address extraction. Deduplicates and caps at maxParallelAgents.
     */
    List<String> parseTargets(String discoveryOutput) {
        if (discoveryOutput == null || discoveryOutput.isBlank()) {
            return Collections.emptyList();
        }

        List<String> targets = new ArrayList<>();

        // Try custom target pattern first (e.g., "COMPETITOR: Company Name")
        if (targetPattern != null) {
            Matcher m = targetPattern.matcher(discoveryOutput);
            while (m.find()) {
                String target = m.group(1).trim();
                if (!target.isEmpty() && !targets.contains(target)) {
                    targets.add(target);
                }
            }
        }

        // Fallback: if custom prefix was set but no matches found, try extracting
        // the prefix keyword from raw text more loosely (e.g., "TICKER" followed by colon or text)
        if (targets.isEmpty() && targetPrefix != null) {
            // Try extracting targets from the original query inputs
            // Look for uppercase words that look like tickers (2-5 uppercase letters/dots)
            Matcher tickerMatcher = Pattern.compile("\\b([A-Z][A-Z0-9.]{1,6})\\b").matcher(discoveryOutput);
            Set<String> seen = new LinkedHashSet<>();
            Set<String> skipWords = Set.of("DATA", "NOT", "AVAILABLE", "THE", "FOR", "AND", "THIS", "URL",
                "TICKER", "COMPETITOR", "HTTP", "HTTPS", "API", "GET", "POST", "JSON", "HTML", "SEC", "NYSE", "NASDAQ");
            while (tickerMatcher.find()) {
                String candidate = tickerMatcher.group(1);
                if (!skipWords.contains(candidate) && !seen.contains(candidate)) {
                    seen.add(candidate);
                }
            }
            targets.addAll(seen);
            if (!targets.isEmpty()) {
                logger.info("parseTargets: Prefix '{}' not found in output, extracted {} ticker-like targets: {}",
                    targetPrefix, targets.size(), targets);
            }
        }

        // Fall back to IP address extraction if no custom targets found
        if (targets.isEmpty()) {
            Matcher ipMatcher = IP_PATTERN.matcher(discoveryOutput);
            LinkedHashSet<String> uniqueIps = new LinkedHashSet<>();

            while (ipMatcher.find()) {
                String ip = ipMatcher.group(1);

                // Filter out broadcast/network addresses
                if (ip.endsWith(".0") || ip.endsWith(".255")) {
                    continue;
                }

                // Basic validation: each octet must be 0-255
                if (isValidIp(ip)) {
                    uniqueIps.add(ip);
                }
            }

            targets.addAll(uniqueIps);
        }

        // Cap at maxParallelAgents
        if (targets.size() > maxParallelAgents) {
            logger.info("Capping targets from {} to {}", targets.size(), maxParallelAgents);
            targets = targets.subList(0, maxParallelAgents);
        }

        return targets;
    }

    /**
     * Validate that each octet in an IP address is in the range 0-255.
     */
    private boolean isValidIp(String ip) {
        String[] octets = ip.split("\\.");
        if (octets.length != 4) return false;
        for (String octet : octets) {
            try {
                int value = Integer.parseInt(octet);
                if (value < 0 || value > 255) return false;
            } catch (NumberFormatException e) {
                return false;
            }
        }
        return true;
    }

    /**
     * Synthesize all per-target results into a single master report.
     * Uses the reviewer agent (or first agent template as report writer) to combine findings.
     */
    private String synthesizeResults(String swarmId, Map<String, SwarmOutput> targetResults,
                                      List<Task> originalTasks, Map<String, Object> inputs,
                                      List<TaskOutput> allOutputs) {
        // Build a combined context from all per-target outputs
        StringBuilder combinedFindings = new StringBuilder();
        combinedFindings.append("=== COMBINED PER-TARGET FINDINGS ===\n\n");

        int targetNum = 0;
        for (Map.Entry<String, SwarmOutput> entry : targetResults.entrySet()) {
            targetNum++;
            String target = entry.getKey();
            SwarmOutput output = entry.getValue();

            combinedFindings.append("--- TARGET ").append(targetNum).append(": ").append(target).append(" ---\n");
            combinedFindings.append("Status: ").append(output.isSuccessful() ? "SUCCESS" : "FAILED").append("\n");

            if (output.getFinalOutput() != null) {
                // Truncate per-target output to keep within context window
                String targetOutput = output.getFinalOutput();
                if (targetOutput.length() > 5000) {
                    targetOutput = targetOutput.substring(0, 5000) + "\n[... truncated ...]";
                }
                combinedFindings.append(targetOutput);
            } else if (!output.getTaskOutputs().isEmpty()) {
                for (TaskOutput to : output.getTaskOutputs()) {
                    if (to.getRawOutput() != null) {
                        String rawOut = to.getRawOutput();
                        if (rawOut.length() > 3000) {
                            rawOut = rawOut.substring(0, 3000) + "\n[... truncated ...]";
                        }
                        combinedFindings.append(rawOut);
                    }
                }
            } else {
                combinedFindings.append("No output available for this target.\n");
            }
            combinedFindings.append("\n\n");
        }

        // If there are additional tasks beyond discovery (e.g., a synthesis/report task), use them
        if (originalTasks.size() > 1) {
            // Use the last task as the synthesis task template
            Task synthesisTemplate = originalTasks.get(originalTasks.size() - 1);
            Agent synthesisAgent = synthesisTemplate.getAgent() != null
                    ? synthesisTemplate.getAgent()
                    : agentTemplates.get(0);

            String synthesisDescription;
            String synthesisExpected;

            if (targetPrefix != null) {
                // Custom target mode: generic synthesis
                synthesisDescription = "Combine the following per-target analysis findings into " +
                        "one comprehensive master report. " +
                        "Include an executive summary, per-target findings, comparative analysis, " +
                        "and actionable recommendations.\n\n" +
                        combinedFindings;
                synthesisExpected = "Comprehensive master report with " +
                        "executive summary, findings per target, comparisons, and recommendations";
            } else {
                // Default: security/pentest synthesis
                synthesisDescription = "Combine the following per-target security analysis findings into " +
                        "one comprehensive master penetration test report. " +
                        "Include an executive summary, per-target findings, " +
                        "risk ratings, and remediation recommendations.\n\n" +
                        combinedFindings;
                synthesisExpected = "Comprehensive master penetration test report with " +
                        "executive summary, findings per target, risk ratings, and recommendations";
            }

            Task synthesisTask = Task.builder()
                    .id("synthesis-" + swarmId)
                    .description(synthesisDescription)
                    .expectedOutput(synthesisExpected)
                    .agent(synthesisAgent)
                    .build();

            try {
                publishEvent(SwarmEvent.Type.TASK_STARTED,
                        "Synthesis task started", swarmId, Map.of());

                TaskOutput synthesisOutput = synthesisAgent.executeTask(synthesisTask, allOutputs);

                publishEvent(SwarmEvent.Type.TASK_COMPLETED,
                        "Synthesis task completed", swarmId, Map.of());

                return synthesisOutput.getRawOutput();
            } catch (Exception e) {
                logger.error("SwarmCoordinator [{}]: Synthesis task failed: {}", swarmId, e.getMessage());
                // Fall back to raw combined output
                return "SYNTHESIS FAILED: " + e.getMessage() + "\n\n" +
                        "Raw per-target findings:\n" + combinedFindings;
            }
        }

        // No synthesis task — return raw combined output
        return combinedFindings.toString();
    }

    private void publishEvent(SwarmEvent.Type type, String message, String swarmId, Map<String, Object> metadata) {
        if (eventPublisher != null) {
            Map<String, Object> meta = new HashMap<>(metadata);
            meta.put("processType", "SWARM");
            eventPublisher.publishEvent(new SwarmEvent(this, type, message, swarmId, meta));
        }
    }
}
