/*
 * Copyright 2008-2026 Async-IO.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.atmosphere.spring.boot;

import org.atmosphere.admin.AtmosphereAdmin;
import org.atmosphere.admin.a2a.TaskController;
import org.atmosphere.admin.ai.AiRuntimeController;
import org.atmosphere.admin.ai.GovernanceController;
import org.atmosphere.admin.coordinator.CoordinatorController;
import org.atmosphere.admin.flow.FlowController;
import org.atmosphere.admin.mcp.McpController;
import org.atmosphere.admin.metrics.MetricsController;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;

import java.time.Instant;
import java.util.List;
import java.util.Map;

/**
 * REST controller exposing the Atmosphere Admin control plane read operations.
 * All endpoints are under {@code /api/admin/}.
 *
 * @since 4.0
 */
@AutoConfiguration
@RestController
@RequestMapping("/api/admin")
@ConditionalOnBean(AtmosphereAdmin.class)
public class AtmosphereAdminEndpoint {

    private final AtmosphereAdmin admin;
    private final org.springframework.core.env.Environment env;

    public AtmosphereAdminEndpoint(
            AtmosphereAdmin admin,
            org.springframework.core.env.Environment env) {
        this.admin = admin;
        this.env = env;
    }

    /**
     * Consulted on every mutating call — not cached — so an operator
     * can flip {@code atmosphere.admin.http-write-enabled=false} at
     * runtime (Spring's {@code @RefreshScope} or a live
     * {@code ConfigFileApplicationListener}) and every subsequent
     * write rejects without a restart. Resolved fresh per request; the
     * cost is one environment lookup per guardWrite call — negligible
     * next to the servlet dispatch.
     */
    boolean writeEnabled() {
        return Boolean.parseBoolean(
                env.getProperty("atmosphere.admin.http-write-enabled", "false"));
    }

    /**
     * Authorization gate for every mutating admin REST endpoint. Three
     * layers, evaluated in order:
     * <ol>
     *   <li><strong>Feature flag</strong> —
     *       {@code atmosphere.admin.http-write-enabled} must be {@code true}.
     *       Returns {@code 403} when disabled.</li>
     *   <li><strong>Authenticated principal</strong> — the servlet
     *       {@code HttpServletRequest.getUserPrincipal()} must be non-null
     *       and non-blank. Returns {@code 401} when anonymous.</li>
     *   <li><strong>{@link org.atmosphere.admin.ControlAuthorizer}</strong>
     *       installed on {@link AtmosphereAdmin} — an operator may plug in a
     *       role/scope/tenant check. Returns {@code 403} on deny.</li>
     * </ol>
     *
     * <p>Every decision (allowed or denied) is recorded in the control audit
     * log so operators see the complete picture.</p>
     *
     * <p>Default deny per Correctness Invariant #6. The earlier
     * feature-flag-only gate let any anonymous caller mutate state once the
     * flag was flipped for machine access — the principal check closes that
     * hole.</p>
     */
    private ResponseEntity<Map<String, Object>> guardWrite(
            HttpServletRequest request, String action, String target) {
        if (!writeEnabled()) {
            admin.auditLog().record(null, action + ".denied.flag", target, false,
                    "atmosphere.admin.http-write-enabled=false");
            return ResponseEntity.status(403).body(Map.of(
                    "error", "Admin write operations disabled",
                    "hint", "Set atmosphere.admin.http-write-enabled=true to enable"));
        }
        // Principal resolution order mirrors AiEndpointHandler:
        //   1. servlet getUserPrincipal() (Spring Security, Jakarta Security)
        //   2. Atmosphere's own AuthInterceptor attribute (token-validated Principal)
        //   3. ai.userId request attribute (framework-set fallback)
        String principalName = null;
        if (request != null) {
            var principal = request.getUserPrincipal();
            if (principal != null && principal.getName() != null
                    && !principal.getName().isBlank()) {
                principalName = principal.getName();
            } else if (request.getAttribute("org.atmosphere.auth.principal")
                    instanceof java.security.Principal attrPrincipal
                    && attrPrincipal.getName() != null
                    && !attrPrincipal.getName().isBlank()) {
                principalName = attrPrincipal.getName();
            } else if (request.getAttribute("ai.userId") instanceof String s
                    && !s.isBlank()) {
                principalName = s;
            }
        }
        if (principalName == null) {
            admin.auditLog().record(null, action + ".denied.anonymous", target, false, null);
            return ResponseEntity.status(401).body(Map.of(
                    "error", "Authentication required for admin write operations"));
        }
        var authorizer = admin.authorizer();
        if (!authorizer.authorize(action, target, principalName)) {
            admin.auditLog().record(principalName, action + ".denied.authz", target, false, null);
            return ResponseEntity.status(403).body(Map.of(
                    "error", "Forbidden"));
        }
        return null;
    }

    /**
     * Resolve the authenticated caller's name using the same order as
     * {@code guardWrite}. Must only be called on a request that already
     * passed guardWrite — so principalName is guaranteed non-null.
     */
    private static String resolvePrincipalName(HttpServletRequest request) {
        var principal = request.getUserPrincipal();
        if (principal != null && principal.getName() != null
                && !principal.getName().isBlank()) {
            return principal.getName();
        }
        if (request.getAttribute("org.atmosphere.auth.principal")
                instanceof java.security.Principal attrPrincipal
                && attrPrincipal.getName() != null) {
            return attrPrincipal.getName();
        }
        if (request.getAttribute("ai.userId") instanceof String s) {
            return s;
        }
        return "unknown";
    }

    // ── System Overview ──

    @GetMapping("/overview")
    public Map<String, Object> overview() {
        return admin.overview();
    }

    // ── Framework ──

    @GetMapping("/broadcasters")
    public List<Map<String, Object>> listBroadcasters() {
        return admin.framework().listBroadcasters();
    }

    @GetMapping("/broadcasters/detail")
    public ResponseEntity<Map<String, Object>> getBroadcaster(@RequestParam("id") String id) {
        return admin.framework().getBroadcaster(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/resources")
    public List<Map<String, Object>> listResources() {
        return admin.framework().listResources();
    }

    @GetMapping("/handlers")
    public List<Map<String, Object>> listHandlers() {
        return admin.framework().listHandlers();
    }

    @GetMapping("/interceptors")
    public List<Map<String, Object>> listInterceptors() {
        return admin.framework().listInterceptors();
    }

    // ── Framework Write Operations ──

    @PostMapping("/broadcasters/broadcast")
    public ResponseEntity<Map<String, Object>> broadcast(
            HttpServletRequest request,
            @RequestBody Map<String, String> body) {
        var id = body.get("broadcasterId");
        var message = body.get("message");
        var denied = guardWrite(request, "broadcast", id);
        if (denied != null) return denied;
        if (id == null || message == null) {
            return ResponseEntity.badRequest().body(
                    Map.of("error", "missing 'broadcasterId' or 'message' field"));
        }
        var principalName = resolvePrincipalName(request);
        var success = admin.framework().broadcast(id, message);
        admin.auditLog().record(principalName, "broadcast", id, success, message);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "broadcast sent"));
        }
        return ResponseEntity.notFound().build();
    }

    @PostMapping("/broadcasters/unicast")
    public ResponseEntity<Map<String, Object>> unicast(
            HttpServletRequest request,
            @RequestBody Map<String, String> body) {
        var id = body.get("broadcasterId");
        var uuid = body.get("uuid");
        var message = body.get("message");
        var target = id != null && uuid != null ? id + "/" + uuid : id;
        var denied = guardWrite(request, "unicast", target);
        if (denied != null) return denied;
        if (id == null || uuid == null || message == null) {
            return ResponseEntity.badRequest().body(
                    Map.of("error", "missing 'broadcasterId', 'uuid', or 'message' field"));
        }
        var principalName = resolvePrincipalName(request);
        var success = admin.framework().unicast(id, uuid, message);
        admin.auditLog().record(principalName, "unicast", target, success, message);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "unicast sent"));
        }
        return ResponseEntity.notFound().build();
    }

    @DeleteMapping("/broadcasters/destroy")
    public ResponseEntity<Map<String, Object>> destroyBroadcaster(
            HttpServletRequest request,
            @RequestParam("id") String id) {
        var denied = guardWrite(request, "destroy_broadcaster", id);
        if (denied != null) return denied;
        var principalName = resolvePrincipalName(request);
        var success = admin.framework().destroyBroadcaster(id);
        admin.auditLog().record(principalName, "destroy_broadcaster", id, success, null);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "broadcaster destroyed"));
        }
        return ResponseEntity.notFound().build();
    }

    @DeleteMapping("/resources/{uuid}")
    public ResponseEntity<Map<String, Object>> disconnectResource(
            HttpServletRequest request,
            @PathVariable("uuid") String uuid) {
        var denied = guardWrite(request, "disconnect", uuid);
        if (denied != null) return denied;
        var principalName = resolvePrincipalName(request);
        var success = admin.framework().disconnectResource(uuid);
        admin.auditLog().record(principalName, "disconnect", uuid, success, null);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "resource disconnected"));
        }
        return ResponseEntity.notFound().build();
    }

    @PostMapping("/resources/{uuid}/resume")
    public ResponseEntity<Map<String, Object>> resumeResource(
            HttpServletRequest request,
            @PathVariable("uuid") String uuid) {
        var denied = guardWrite(request, "resume", uuid);
        if (denied != null) return denied;
        var principalName = resolvePrincipalName(request);
        var success = admin.framework().resumeResource(uuid);
        admin.auditLog().record(principalName, "resume", uuid, success, null);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "resource resumed"));
        }
        return ResponseEntity.notFound().build();
    }

    // ── A2A Task Write Operations ──

    @PostMapping("/tasks/{taskId}/cancel")
    public ResponseEntity<Map<String, Object>> cancelTask(
            HttpServletRequest request,
            @PathVariable("taskId") String taskId) {
        var denied = guardWrite(request, "cancel_task", taskId);
        if (denied != null) return denied;
        TaskController controller = admin.taskController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        var principalName = resolvePrincipalName(request);
        var success = controller.cancelTask(taskId);
        admin.auditLog().record(principalName, "cancel_task", taskId, success, null);
        if (success) {
            return ResponseEntity.ok(Map.of("status", "task canceled"));
        }
        return ResponseEntity.notFound().build();
    }

    // ── AI Streaming Session Operations ──

    /**
     * Cancel an in-flight LLM call on an AI streaming session WITHOUT
     * disconnecting the underlying transport. Surfaces
     * {@link org.atmosphere.ai.AiStreamingSession#cancelInflight} to operators
     * who want to abort just the upstream call (refunding tokens, releasing
     * approval futures) while keeping the socket open for follow-up turns.
     *
     * <p>For "kill the whole connection" semantics, prefer
     * {@code DELETE /resources/{uuid}} which fires a full transport
     * disconnect — that path automatically triggers cancelInflight via the
     * removeAllForResource fan-out, but also closes the socket.</p>
     *
     * @param request the servlet request (for guardWrite + audit)
     * @param uuid    the AtmosphereResource UUID whose AI sessions to cancel
     * @return 200 with the count of sessions cancelled, 404 when no AI
     *     sessions are registered for that uuid
     */
    @PostMapping("/sessions/{uuid}/cancel-inflight")
    public ResponseEntity<Map<String, Object>> cancelSessionInflight(
            HttpServletRequest request,
            @PathVariable("uuid") String uuid) {
        var denied = guardWrite(request, "cancel_inflight", uuid);
        if (denied != null) return denied;
        var principalName = resolvePrincipalName(request);

        // Pre-check: is there a session registered? AiStreamingSession does
        // not expose a public "size" accessor for the per-uuid list, so we
        // route through the static cancel path which is itself idempotent —
        // a 404 here is informational, not load-bearing.
        var hadSessions = org.atmosphere.ai.AiStreamingSession
                .resourceHasActiveSessions(uuid);
        org.atmosphere.ai.AiStreamingSession.cancelInflightForResource(uuid);
        admin.auditLog().record(principalName, "cancel_inflight", uuid, hadSessions, null);
        if (hadSessions) {
            return ResponseEntity.ok(Map.of(
                    "status", "in-flight LLM call cancelled",
                    "uuid", uuid));
        }
        return ResponseEntity.notFound().build();
    }

    // ── Agents ──

    @GetMapping("/agents")
    public List<Map<String, Object>> listAgents() {
        return admin.agents().listAgents();
    }

    @GetMapping("/agents/{name}")
    public ResponseEntity<Map<String, Object>> getAgent(@PathVariable("name") String name) {
        return admin.agents().getAgent(name)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/agents/{name}/sessions")
    public List<Map<String, Object>> listAgentSessions(@PathVariable("name") String name) {
        return admin.agents().listSessions(name);
    }

    // ── Coordinators ──

    @GetMapping("/coordinators")
    public ResponseEntity<List<Map<String, Object>>> listCoordinators() {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listCoordinators());
    }

    @GetMapping("/coordinators/{name}/fleet")
    public ResponseEntity<Map<String, Object>> getFleet(@PathVariable("name") String name) {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return controller.getFleet(name)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/journal")
    public ResponseEntity<List<Map<String, Object>>> queryJournal(
            @RequestParam(value = "coordinationId", required = false) String coordinationId,
            @RequestParam(value = "agent", required = false) String agentName,
            @RequestParam(value = "since", required = false) String since,
            @RequestParam(value = "until", required = false) String until,
            @RequestParam(value = "limit", defaultValue = "100") int limit) {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        Instant sinceInstant = null;
        Instant untilInstant = null;
        try {
            if (since != null) sinceInstant = Instant.parse(since);
            if (until != null) untilInstant = Instant.parse(until);
        } catch (java.time.format.DateTimeParseException e) {
            return ResponseEntity.badRequest().body(List.of(Map.of(
                    "error", "Invalid timestamp: " + e.getMessage())));
        }
        return ResponseEntity.ok(
                controller.queryJournal(coordinationId, agentName, sinceInstant, untilInstant, limit));
    }

    @GetMapping("/journal/{coordinationId}")
    public ResponseEntity<List<Map<String, Object>>> getJournalEvents(
            @PathVariable("coordinationId") String coordinationId) {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(
                controller.queryJournal(coordinationId, null, null, null, 0));
    }

    @GetMapping("/journal/{coordinationId}/log")
    public ResponseEntity<String> getJournalLog(
            @PathVariable("coordinationId") String coordinationId) {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return controller.getJournalLog(coordinationId)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // ── Agent-to-Agent Flow Graph ──

    @GetMapping("/flow")
    public ResponseEntity<Map<String, Object>> renderFlow(
            @RequestParam(value = "lookbackMinutes", defaultValue = "0") int lookbackMinutes) {
        FlowController controller = admin.flowController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of("nodes", List.of(), "edges", List.of()));
        }
        return ResponseEntity.ok(controller.renderFlow(lookbackMinutes));
    }

    @GetMapping("/flow/{coordinationId}")
    public ResponseEntity<Map<String, Object>> renderRun(
            @PathVariable("coordinationId") String coordinationId) {
        FlowController controller = admin.flowController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of("nodes", List.of(), "edges", List.of()));
        }
        return ResponseEntity.ok(controller.renderRun(coordinationId));
    }

    // ── Workflow Authoring ──

    @GetMapping("/workflow")
    public ResponseEntity<List<org.atmosphere.admin.workflow.WorkflowManifest>> listWorkflows() {
        org.atmosphere.admin.workflow.WorkflowController controller = admin.workflowController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.list());
    }

    @GetMapping("/workflow/{id}")
    public ResponseEntity<org.atmosphere.admin.workflow.WorkflowManifest> getWorkflow(
            @PathVariable("id") String id) {
        org.atmosphere.admin.workflow.WorkflowController controller = admin.workflowController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return controller.get(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping("/workflow")
    public ResponseEntity<Map<String, Object>> saveWorkflow(
            @org.springframework.web.bind.annotation.RequestBody
            org.atmosphere.admin.workflow.WorkflowManifest manifest,
            HttpServletRequest request) {
        var guard = guardWrite(request, "workflow.write", manifest.id());
        if (guard != null) {
            return guard;
        }
        org.atmosphere.admin.workflow.WorkflowController controller = admin.workflowController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of(
                    "error", "Workflow controller not wired"));
        }
        try {
            var saved = controller.save(manifest, resolvePrincipal(request));
            return ResponseEntity.ok(Map.of(
                    "id", saved.id(),
                    "name", saved.name(),
                    "version", saved.version(),
                    "updatedAt", saved.updatedAt().toString()));
        } catch (org.atmosphere.admin.workflow.WorkflowStoreException wse) {
            if (wse.kind() == org.atmosphere.admin.workflow.WorkflowStoreException.Kind.VERSION_CONFLICT) {
                return ResponseEntity.status(409).body(Map.of(
                        "error", wse.getMessage(),
                        "kind", wse.kind().name()));
            }
            return ResponseEntity.status(500).body(Map.of(
                    "error", wse.getMessage(),
                    "kind", wse.kind().name()));
        } catch (IllegalArgumentException iae) {
            return ResponseEntity.badRequest().body(Map.of("error", iae.getMessage()));
        } catch (SecurityException se) {
            return ResponseEntity.status(403).body(Map.of("error", se.getMessage()));
        }
    }

    @DeleteMapping("/workflow/{id}")
    public ResponseEntity<Map<String, Object>> deleteWorkflow(
            @PathVariable("id") String id,
            HttpServletRequest request) {
        var guard = guardWrite(request, "workflow.delete", id);
        if (guard != null) {
            return guard;
        }
        org.atmosphere.admin.workflow.WorkflowController controller = admin.workflowController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of(
                    "error", "Workflow controller not wired"));
        }
        try {
            controller.delete(id, resolvePrincipal(request));
            return ResponseEntity.ok(Map.of("deleted", id));
        } catch (SecurityException se) {
            return ResponseEntity.status(403).body(Map.of("error", se.getMessage()));
        }
    }

    private String resolvePrincipal(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        var principal = request.getUserPrincipal();
        if (principal != null && principal.getName() != null && !principal.getName().isBlank()) {
            return principal.getName();
        }
        var attr = request.getAttribute("ai.userId");
        return attr != null ? attr.toString() : null;
    }

    // ── Eval Dashboard ──

    @GetMapping("/evals/runs")
    public ResponseEntity<List<org.atmosphere.admin.evals.EvalRun>> listEvalRuns(
            @RequestParam(value = "baseline", required = false) String baseline) {
        org.atmosphere.admin.evals.EvalController controller = admin.evalController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(
                baseline != null ? controller.listRuns(baseline) : controller.listRuns());
    }

    @GetMapping("/evals/baselines")
    public ResponseEntity<List<Map<String, Object>>> baselineSummary() {
        org.atmosphere.admin.evals.EvalController controller = admin.evalController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.baselineSummary());
    }

    @GetMapping("/evals/runs/{id}")
    public ResponseEntity<org.atmosphere.admin.evals.EvalRun> getEvalRun(
            @PathVariable("id") String id) {
        org.atmosphere.admin.evals.EvalController controller = admin.evalController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return controller.getRun(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping("/evals/runs")
    public ResponseEntity<Map<String, Object>> recordEvalRun(
            @org.springframework.web.bind.annotation.RequestBody
            org.atmosphere.admin.evals.EvalRun run,
            HttpServletRequest request) {
        var guard = guardWrite(request, "evals.write", run.baseline());
        if (guard != null) {
            return guard;
        }
        org.atmosphere.admin.evals.EvalController controller = admin.evalController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of(
                    "error", "Eval controller not wired"));
        }
        try {
            var saved = controller.record(run, resolvePrincipal(request));
            return ResponseEntity.ok(Map.of(
                    "id", saved.id(),
                    "baseline", saved.baseline(),
                    "passed", saved.passed()));
        } catch (IllegalArgumentException | IllegalStateException invalid) {
            return ResponseEntity.badRequest().body(Map.of("error", invalid.getMessage()));
        } catch (SecurityException se) {
            return ResponseEntity.status(403).body(Map.of("error", se.getMessage()));
        }
    }

    @DeleteMapping("/evals/runs/{id}")
    public ResponseEntity<Map<String, Object>> deleteEvalRun(
            @PathVariable("id") String id,
            HttpServletRequest request) {
        var guard = guardWrite(request, "evals.delete", id);
        if (guard != null) {
            return guard;
        }
        org.atmosphere.admin.evals.EvalController controller = admin.evalController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of(
                    "error", "Eval controller not wired"));
        }
        try {
            controller.delete(id, resolvePrincipal(request));
            return ResponseEntity.ok(Map.of("deleted", id));
        } catch (SecurityException se) {
            return ResponseEntity.status(403).body(Map.of("error", se.getMessage()));
        }
    }

    // ── A2A Tasks ──

    @GetMapping("/tasks")
    public ResponseEntity<List<Map<String, Object>>> listTasks(
            @RequestParam(value = "contextId", required = false) String contextId) {
        TaskController controller = admin.taskController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listTasks(contextId));
    }

    @GetMapping("/tasks/{taskId}")
    public ResponseEntity<Map<String, Object>> getTask(@PathVariable("taskId") String taskId) {
        TaskController controller = admin.taskController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return controller.getTask(taskId)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    // ── AI Runtimes ──

    @GetMapping("/runtimes")
    public ResponseEntity<List<Map<String, Object>>> listRuntimes() {
        AiRuntimeController controller = admin.aiRuntimeController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listRuntimes());
    }

    @GetMapping("/runtimes/active")
    public ResponseEntity<Map<String, Object>> getActiveRuntime() {
        AiRuntimeController controller = admin.aiRuntimeController();
        if (controller == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(controller.getActiveRuntime());
    }

    // ── Governance Policies ──

    @GetMapping("/governance/policies")
    public ResponseEntity<List<Map<String, Object>>> listGovernancePolicies() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listPolicies());
    }

    @GetMapping("/governance/summary")
    public ResponseEntity<Map<String, Object>> governanceSummary() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of("policyCount", 0, "sources", List.of()));
        }
        return ResponseEntity.ok(controller.summary());
    }

    /**
     * Governance plane health snapshot — kill switch state, dry-run counters,
     * and per-policy hash fingerprints for supply-chain drift detection.
     * Read-only; no authorizer required.
     */
    @GetMapping("/governance/health")
    public ResponseEntity<Map<String, Object>> governanceHealth() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of(
                    "killSwitch", Map.of("armed", false),
                    "policies", List.of(),
                    "dryRuns", List.of(),
                    "slos", List.of()));
        }
        return ResponseEntity.ok(controller.healthMap());
    }

    /**
     * Hot-reload a policy wrapped in {@code SwappablePolicy}. Request body
     * carries {@code {swapName, yaml}}; response carries the outgoing /
     * incoming delegate identity. MUTATING — requires authenticated +
     * authorized caller at the HTTP layer.
     *
     * <p>400 on malformed YAML or unknown swap name; 200 with swap summary
     * on success.</p>
     */
    @PostMapping("/governance/reload")
    public ResponseEntity<Map<String, Object>> governanceReload(
            HttpServletRequest request,
            @RequestBody(required = false) Map<String, Object> body) {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of("error", "governance controller not installed"));
        }
        var swapName = stringField(body, "swapName");
        var denied = guardWrite(request, "governance.reload", swapName);
        if (denied != null) return denied;

        var yaml = stringField(body, "yaml");
        var principalName = resolvePrincipalName(request);
        try {
            var result = controller.reloadSwappable(swapName, yaml);
            admin.auditLog().record(principalName, "governance.reload", swapName, true, null);
            return ResponseEntity.ok(result);
        } catch (IllegalArgumentException e) {
            admin.auditLog().record(principalName, "governance.reload.invalid",
                    swapName, false, e.getMessage());
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }

    /**
     * Arm the operator kill switch. Request body carries
     * {@code {reason, operator}} — subsequent turns deny until disarm.
     * MUTATING — gated by {@code guardWrite}.
     */
    @PostMapping("/governance/kill-switch/arm")
    public ResponseEntity<Map<String, Object>> governanceKillSwitchArm(
            HttpServletRequest request,
            @RequestBody(required = false) Map<String, Object> body) {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of("error", "governance controller not installed"));
        }
        var reason = stringField(body, "reason");
        var denied = guardWrite(request, "governance.kill_switch.arm", reason);
        if (denied != null) return denied;

        var operator = stringField(body, "operator");
        var principalName = resolvePrincipalName(request);
        try {
            var result = controller.armKillSwitch(reason,
                    operator != null ? operator : principalName);
            admin.auditLog().record(principalName, "governance.kill_switch.arm",
                    reason, true, operator);
            return ResponseEntity.ok(result);
        } catch (IllegalArgumentException e) {
            admin.auditLog().record(principalName, "governance.kill_switch.arm.invalid",
                    reason, false, e.getMessage());
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        } catch (IllegalStateException e) {
            admin.auditLog().record(principalName, "governance.kill_switch.arm.unavailable",
                    reason, false, e.getMessage());
            return ResponseEntity.status(409).body(Map.of("error", e.getMessage()));
        }
    }

    /** Disarm the kill switch. No body required. MUTATING — gated by {@code guardWrite}. */
    @PostMapping("/governance/kill-switch/disarm")
    public ResponseEntity<Map<String, Object>> governanceKillSwitchDisarm(HttpServletRequest request) {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.status(503).body(Map.of("error", "governance controller not installed"));
        }
        var denied = guardWrite(request, "governance.kill_switch.disarm", null);
        if (denied != null) return denied;

        var principalName = resolvePrincipalName(request);
        try {
            var result = controller.disarmKillSwitch();
            admin.auditLog().record(principalName, "governance.kill_switch.disarm",
                    null, true, null);
            return ResponseEntity.ok(result);
        } catch (IllegalStateException e) {
            admin.auditLog().record(principalName, "governance.kill_switch.disarm.unavailable",
                    null, false, e.getMessage());
            return ResponseEntity.status(409).body(Map.of("error", e.getMessage()));
        }
    }

    /**
     * Extract a string field from a loosely-typed JSON body. Returns null
     * for missing key, null value, or non-string type. Never coerces via
     * {@code String.valueOf} — a missing field must not become the literal
     * string {@code "null"} that trips blank-check validation.
     */
    private static String stringField(Map<String, Object> body, String key) {
        if (body == null) return null;
        var value = body.get(key);
        if (value instanceof String s && !s.isBlank()) {
            return s;
        }
        return null;
    }

    /**
     * Compliance export in the {@code agt verify} schema shape —
     * cross-framework findings + per-framework coverage counts. External
     * compliance tooling that already consumes MS's Agent Compliance
     * package format can round-trip this output.
     */
    @GetMapping("/governance/agt-verify")
    public ResponseEntity<Map<String, Object>> governanceAgtVerify() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of(
                    "schemaVersion", "agt-verify/1",
                    "findings", List.of(),
                    "summary", Map.of()));
        }
        return ResponseEntity.ok(controller.agtVerifyExport());
    }

    /**
     * Recent policy decisions (ring-buffered). {@code limit} defaults to 100;
     * capped at the log's configured capacity. Read-only — no authorizer
     * guard required.
     */
    @GetMapping("/governance/decisions")
    public ResponseEntity<List<Map<String, Object>>> governanceDecisions(
            @RequestParam(value = "limit", defaultValue = "100") int limit) {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listRecentDecisions(limit));
    }

    /**
     * OWASP Agentic AI Top 10 (Dec 2025) self-assessment. Read-only. Pairs
     * with the {@code agt verify} CLI payload shape — external compliance
     * tooling that targets MS's Agent Compliance package can consume this
     * endpoint as the Atmosphere equivalent evidence source.
     */
    @GetMapping("/governance/owasp")
    public ResponseEntity<Map<String, Object>> governanceOwasp() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of("framework", "OWASP Agentic AI Top 10 (December 2025)",
                    "rows", List.of(), "total_rows", 0));
        }
        return ResponseEntity.ok(controller.owaspMatrix());
    }

    /**
     * Compliance matrices — EU AI Act / HIPAA / SOC2 self-assessments
     * parallel to the OWASP matrix. Read-only. Returns a map keyed by
     * framework name ({@code EU_AI_ACT}, {@code HIPAA}, {@code SOC2})
     * with one matrix per framework in the same shape as {@code /owasp}.
     */
    @GetMapping("/governance/compliance")
    public ResponseEntity<Map<String, Object>> governanceCompliance() {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of());
        }
        return ResponseEntity.ok(controller.complianceMatrices());
    }

    /**
     * Microsoft Agent Governance Toolkit {@code POST /check}-compatible
     * decision endpoint. External gateways (Envoy, Kong, Azure APIM)
     * that already speak to MS's ASGI policy provider can point at
     * this endpoint to use Atmosphere as the decision service.
     * Payload: {@code {"agent_id": "...", "action": "...", "context": {...}}}.
     * Response: {@code {"allowed": bool, "decision": "...", "reason": "...",
     *                   "matched_policy": "...", "evaluation_ms": N}}.
     * Read-only — no authorizer guard required (this does not mutate state).
     */
    @PostMapping("/governance/check")
    public ResponseEntity<Map<String, Object>> governanceCheck(@RequestBody(required = false) Map<String, Object> payload) {
        GovernanceController controller = admin.governanceController();
        if (controller == null) {
            // MS /check wire compat — gateways routing on `allowed` keep
            // working even on deployments that haven't wired the
            // governance plane. Shape matches the shared
            // GovernanceController.unconfiguredAllowPayload() helper so
            // Spring and Quarkus return byte-identical JSON here.
            return ResponseEntity.ok(GovernanceController.unconfiguredAllowPayload());
        }
        return ResponseEntity.ok(controller.check(payload));
    }

    /**
     * Signed commitment records emitted by the coordination journal.
     * Read-only. Operators render this stream as a verifiable audit
     * trail — each entry carries the Ed25519 proof
     * scheme / key id / signature so downstream SIEMs can verify
     * against the publicly-published coordinator key.
     *
     * <p>Returns {@link org.atmosphere.admin.coordinator.CommitmentRecordView}
     * — a typed record rather than a loose {@code Map<String, Object>}
     * so shape drift surfaces at compile time.</p>
     */
    @GetMapping("/governance/commitments")
    public ResponseEntity<List<org.atmosphere.admin.coordinator.CommitmentRecordView>> governanceCommitments(
            @RequestParam(value = "limit", defaultValue = "100") int limit) {
        CoordinatorController controller = admin.coordinatorController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listCommitmentRecords(limit));
    }

    // ── MCP Registry ──

    @GetMapping("/mcp/tools")
    public ResponseEntity<List<Map<String, Object>>> listMcpTools() {
        McpController controller = admin.mcpController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listTools());
    }

    @GetMapping("/mcp/resources")
    public ResponseEntity<List<Map<String, Object>>> listMcpResources() {
        McpController controller = admin.mcpController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listResources());
    }

    @GetMapping("/mcp/prompts")
    public ResponseEntity<List<Map<String, Object>>> listMcpPrompts() {
        McpController controller = admin.mcpController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listPrompts());
    }

    // ── Metrics / Observability ──

    @GetMapping("/metrics")
    public ResponseEntity<Map<String, Object>> metricsSnapshot() {
        MetricsController controller = admin.metricsController();
        if (controller == null) {
            return ResponseEntity.ok(Map.of("error", "Micrometer not on classpath"));
        }
        return ResponseEntity.ok(controller.snapshot());
    }

    @GetMapping("/metrics/all")
    public ResponseEntity<List<Map<String, Object>>> allMeters() {
        MetricsController controller = admin.metricsController();
        if (controller == null) {
            return ResponseEntity.ok(List.of());
        }
        return ResponseEntity.ok(controller.listMeters());
    }

    // ── Audit Log ──

    @GetMapping("/audit")
    public List<org.atmosphere.admin.ControlAuditLog.AuditEntry> listAuditEntries(
            @RequestParam(value = "limit", defaultValue = "100") int limit) {
        return admin.auditLog().entries(limit);
    }
}
