package com.gzzm.lobster.api;

import com.gzzm.lobster.common.JsonUtil;
import com.gzzm.lobster.common.IdGenerator;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.RunStatus;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.identity.UserContextHolder;
import com.gzzm.lobster.pending.PendingRequest;
import com.gzzm.lobster.pending.PendingRequestService;
import com.gzzm.lobster.run.Run;
import com.gzzm.lobster.run.RunDao;
import com.gzzm.lobster.run.RunStreamEventService;
import com.gzzm.lobster.runtime.AgentRuntime;
import com.gzzm.lobster.runtime.RunRequest;
import com.gzzm.lobster.runtime.StreamEvent;
import com.gzzm.lobster.thread.ThreadRoom;
import com.gzzm.lobster.thread.ThreadService;
import net.cyan.arachne.HttpMethod;
import net.cyan.arachne.annotation.Service;
import net.cyan.nest.annotation.Inject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Run trigger and status API.
 *
 * <p>POST /ai/api/runs creates a backend run. SSE is served only by
 * {@link RunStreamServlet} and only observes existing runs.
 */
@Service
public class RunApi {

    @Inject private ThreadService threadService;
    @Inject private AgentRuntime agentRuntime;
    @Inject private RunDao runDao;
    @Inject private RunStreamEventService runStreamEventService;
    @Inject private PendingRequestService pendingRequestService;

    @Service(url = "/ai/api/runs", method = HttpMethod.post)
    public Map<String, Object> runSync(String threadId, String userInput, String attachments,
                                       String attachedResourceIds,
                                       Boolean kbEnabled, String kbMode, String kbScopeIds,
                                       String clientRequestId) throws Exception {
        UserContext user = UserContextHolder.require();
        ThreadRoom thread = threadService.requireOwnedThread(user, threadId);
        String requestKey = normalizeClientRequestId(clientRequestId);
        Run idempotent = findByClientRequestId(user.getUserId(), thread.getThreadId(), requestKey);
        if (idempotent != null) {
            Map<String, Object> out = toRunMap(agentRuntime.reconcileRunLease(idempotent));
            out.put("reused", true);
            out.put("idempotent", true);
            return out;
        }
        Run active = agentRuntime.activeRunForThread(thread.getThreadId());
        if (active != null) {
            Map<String, Object> out = toRunMap(active);
            out.put("reused", true);
            return out;
        }
        Map<String, Object> pendingReuse = pendingReuse(thread.getThreadId());
        if (pendingReuse != null) return pendingReuse;
        String candidateRunId = IdGenerator.runId();
        RunRequest req = new RunRequest(thread, user, userInput, "user_input", null,
                parseAttachments(attachments), parseAttachments(attachedResourceIds),
                Boolean.TRUE.equals(kbEnabled), kbMode, parseAttachments(kbScopeIds))
                .withClientRequestId(requestKey)
                .withRunId(candidateRunId);
        String runId = agentRuntime.startDetached(req);
        Run run = runDao.getRun(runId);
        Map<String, Object> out = run == null ? new LinkedHashMap<>() : toRunMap(agentRuntime.reconcileRunLease(run));
        boolean reusedByStartRace = !candidateRunId.equals(runId)
                || (run != null && requestKey != null
                && !requestKey.equals(run.getClientRequestId()));
        if (out.isEmpty()) {
            out.put("runId", runId);
            out.put("threadId", thread.getThreadId());
            out.put("status", "running");
        }
        out.put("reused", reusedByStartRace);
        if (reusedByStartRace && run != null && requestKey != null
                && requestKey.equals(run.getClientRequestId())) {
            out.put("idempotent", true);
        }
        return out;
    }

    @Service(url = "/ai/api/runs/{$0}/cancel", method = HttpMethod.post)
    public Map<String, Object> cancel(String runId) throws Exception {
        UserContext user = UserContextHolder.require();
        Run run = runDao.getRun(runId);
        if (run == null) throw new LobsterException("run.not_found", "run not found");
        threadService.requireOwnedThread(user, run.getThreadId());
        if (run.getStatus() != RunStatus.running && run.getStatus() != RunStatus.waiting_user) {
            return toRunMap(run);
        }
        agentRuntime.cancel(runId);
        Run updated = runDao.getRun(runId);
        return toRunMap(updated == null ? run : updated);
    }

    @Service(url = "/ai/api/runs/status", method = HttpMethod.all)
    public Map<String, Object> getRun(String runId) throws Exception {
        UserContext user = UserContextHolder.require();
        Run run = runDao.getRun(runId);
        if (run == null) throw new LobsterException("run.not_found", "run not found");
        threadService.requireOwnedThread(user, run.getThreadId());
        run = agentRuntime.reconcileRunLease(run);
        return toRunMap(run);
    }

    @Service(url = "/ai/api/threads/{$0}/runs/active", method = HttpMethod.all)
    public Map<String, Object> getActiveRun(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        threadService.requireOwnedThread(user, threadId);
        Run active = agentRuntime.activeRunForThread(threadId);
        List<Run> runs = runDao.listByThread(threadId);
        Run latest = null;
        if (runs != null) {
            for (Run r : runs) {
                if (r == null) continue;
                r = agentRuntime.reconcileRunLease(r);
                if (latest == null) latest = r;
                if (active == null && r != null && (r.getStatus() == RunStatus.running
                        || r.getStatus() == RunStatus.waiting_user)) {
                    active = r;
                    break;
                }
            }
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("run", active == null ? null : toRunMap(active));
        out.put("latestRun", latest == null ? null : toRunMap(latest));
        return out;
    }

    @Service(url = "/ai/api/runs/{$0}/events", method = HttpMethod.all)
    public Map<String, Object> getRunEvents(String runId, Long afterSeq) throws Exception {
        UserContext user = UserContextHolder.require();
        Run run = runDao.getRun(runId);
        if (run == null) throw new LobsterException("run.not_found", "run not found");
        threadService.requireOwnedThread(user, run.getThreadId());
        run = agentRuntime.reconcileRunLease(run);
        long after = afterSeq == null ? 0L : afterSeq;
        if (runStreamEventService == null) {
            throw new LobsterException("run_event.persistence_unavailable",
                    "Run stream event persistence is unavailable");
        }
        List<StreamEvent> events = runStreamEventService.listAfter(runId, after);
        List<Map<String, Object>> items = new ArrayList<>();
        for (StreamEvent ev : events) {
            if (ev == null) continue;
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("type", ev.getType().name());
            item.put("payload", ev.getPayload());
            items.add(item);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("run", toRunMap(run));
        out.put("events", items);
        out.put("lastEventSeq", runStreamEventService.maxSeq(runId));
        return out;
    }

    @SuppressWarnings("unchecked")
    private List<String> parseAttachments(String raw) {
        if (raw == null || raw.isEmpty()) return Collections.emptyList();
        String s = raw.trim();
        if (s.startsWith("[") && s.endsWith("]")) {
            try {
                List<Object> arr = JsonUtil.fromJson(s, List.class);
                if (arr == null) return Collections.emptyList();
                List<String> out = new ArrayList<>(arr.size());
                for (Object o : arr) if (o != null) out.add(String.valueOf(o));
                return out;
            } catch (Throwable ignore) {
                // Fall back to CSV for older clients.
            }
        }
        return Arrays.asList(s.split(","));
    }

    private Map<String, Object> toRunMap(Run run) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("runId", run.getRunId());
        row.put("threadId", run.getThreadId());
        row.put("clientRequestId", run.getClientRequestId());
        row.put("continuationSourceRunId", run.getContinuationSourceRunId());
        row.put("modelId", run.getModelId());
        row.put("status", run.getStatus() == null ? null : run.getStatus().name());
        row.put("exitReason", run.getExitReason() == null ? null : run.getExitReason().name());
        row.put("turns", run.getTurns());
        row.put("errorCode", run.getErrorCode());
        row.put("errorMessage", run.getErrorMessage());
        row.put("cancelReason", run.getCancelReason());
        row.put("cancelRequestedAt", run.getCancelRequestedAt());
        row.put("startedAt", run.getStartedAt());
        row.put("heartbeatAt", run.getHeartbeatAt());
        row.put("endedAt", run.getEndedAt());

        AgentRuntime.RunLeaseSnapshot lease = agentRuntime.runLease(run.getRunId());
        boolean running = run.getStatus() == RunStatus.running;
        boolean waiting = run.getStatus() == RunStatus.waiting_user;
        boolean active = (running || waiting) && lease.isActive();
        long heartbeatAtMs = heartbeatAtMs(run, lease);
        long heartbeatAgeMs = heartbeatAtMs <= 0 ? -1L : Math.max(0L, System.currentTimeMillis() - heartbeatAtMs);
        boolean stale = running && !active && heartbeatAgeMs >= lease.getStaleAfterMs();
        String runState = waiting ? "waiting_user"
                : (!running ? "ended" : (active ? "active" : (stale ? "stale" : "detached")));
        row.put("active", active);
        row.put("runState", runState);
        row.put("stale", stale);
        row.put("heartbeatAgeMs", heartbeatAgeMs);
        row.put("leaseStaleAfterMs", lease.getStaleAfterMs());
        long lastEventSeq = runEventCursor(run.getRunId());
        row.put("lastEventSeq", lastEventSeq);
        // Terminal/waiting runs are already represented by transcript + pending
        // state. Running runs may still have in-flight deltas that are only in
        // the persisted stream projection, including detached/cross-worker
        // runs, so a fresh page must replay them from the beginning. The
        // frontend still uses its local cursor when it has one.
        row.put("replayAfterSeq", running ? 0L : lastEventSeq);
        return row;
    }

    private Map<String, Object> pendingReuse(String threadId) throws Exception {
        if (pendingRequestService == null) return null;
        List<PendingRequest> open = pendingRequestService.listOpenByThread(threadId);
        if (open == null || open.isEmpty()) return null;
        PendingRequest pending = open.get(0);
        String sourceRunId = pending == null ? null : pending.getSourceRunId();
        if (sourceRunId != null && !sourceRunId.isEmpty()) {
            Run source = runDao.getRun(sourceRunId);
            if (source != null && threadId.equals(source.getThreadId())) {
                Map<String, Object> out = toRunMap(agentRuntime.reconcileRunLease(source));
                out.put("reused", true);
                out.put("pendingRequestId", pending.getRequestId());
                out.put("pending", true);
                return out;
            }
        }
        throw new LobsterException("run.pending_open",
                "Thread has an open pending request; resolve it before starting a new run");
    }

    private Run findByClientRequestId(String userId, String threadId, String clientRequestId) throws Exception {
        if (clientRequestId == null || clientRequestId.isEmpty()) return null;
        List<Run> runs = runDao.listByClientRequestId(userId, threadId, clientRequestId);
        return runs == null || runs.isEmpty() ? null : runs.get(0);
    }

    private String normalizeClientRequestId(String raw) {
        if (raw == null) return null;
        String s = raw.trim();
        if (s.isEmpty()) return null;
        return s.length() <= 120 ? s : s.substring(0, 120);
    }

    private long runEventCursor(String runId) {
        if (runStreamEventService == null) {
            throw new LobsterException("run_event.persistence_unavailable",
                    "Run stream event persistence is unavailable");
        }
        return runStreamEventService.maxSeq(runId);
    }

    private long heartbeatAtMs(Run run, AgentRuntime.RunLeaseSnapshot lease) {
        long inProcess = lease == null ? 0L : lease.getHeartbeatAtMs();
        Date heartbeatAt = run == null ? null : run.getHeartbeatAt();
        long persisted = heartbeatAt == null ? 0L : heartbeatAt.getTime();
        if (persisted <= 0L && run != null && run.getStartedAt() != null) {
            persisted = run.getStartedAt().getTime();
        }
        return Math.max(inProcess, persisted);
    }
}
