package com.gzzm.lobster.api;

import com.gzzm.lobster.common.JsonUtil;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.PendingRequestStatus;
import com.gzzm.lobster.common.RunExitReason;
import com.gzzm.lobster.common.RunStatus;
import com.gzzm.lobster.common.StreamEventType;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.identity.UserContextHolder;
import com.gzzm.lobster.pending.PendingActionResolver;
import com.gzzm.lobster.pending.PendingRequest;
import com.gzzm.lobster.pending.PendingRequestDao;
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.ThreadDao;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Pending request response and query API.
 */
@Service
public class PendingRequestApi {

    @Inject private PendingRequestService pendingService;
    @Inject private PendingRequestDao pendingDao;
    @Inject private ThreadService threadService;
    @Inject private AgentRuntime agentRuntime;
    @Inject private PendingActionResolver actionResolver;
    @Inject private RunDao runDao;
    @Inject private ThreadDao threadDao;
    @Inject private RunStreamEventService runStreamEventService;

    @Service(url = "/ai/api/pending-requests/{$0}/resolve", method = HttpMethod.post)
    public Map<String, Object> resolve(String requestId, String action, String responseJson) throws Exception {
        UserContext user = UserContextHolder.require();
        Map<String, Object> payload = responseJson == null || responseJson.isEmpty()
                ? new LinkedHashMap<String, Object>() : JsonUtil.fromJsonToMap(responseJson);
        PendingRequest preflight = pendingService.get(requestId);
        if (preflight == null) throw new LobsterException("pending.not_found", "Request not found");
        if (!user.getUserId().equals(preflight.getUserId())) {
            throw new LobsterException("pending.forbidden", "Pending request not owned by current user");
        }
        if (preflight.getStatus() != PendingRequestStatus.open) {
            throw new LobsterException("pending.state", "Pending request already " + preflight.getStatus());
        }
        ThreadRoom thread = threadService.requireOwnedThread(user, preflight.getThreadId());
        Map<String, Object> pendingPayload = parsePayload(preflight.getPayloadJson());
        String sourceRunId = preflight.getSourceRunId();
        if ((sourceRunId == null || sourceRunId.isEmpty()) && pendingPayload.get("sourceRunId") != null) {
            sourceRunId = String.valueOf(pendingPayload.get("sourceRunId"));
        }
        Run sourceRun = (sourceRunId == null || sourceRunId.isEmpty())
                ? null : requireSourceRun(user.getUserId(), thread.getThreadId(), sourceRunId);
        if (isMaxTurnsContinue(pendingPayload) && "CONFIRM".equalsIgnoreCase(action)
                && !isWaitingForContinuation(sourceRun)) {
            throw new LobsterException("run.state", "source run is not waiting for continuation");
        }

        PendingRequestService.ResolveResult rr = pendingService.resolve(requestId, user, action, payload);
        boolean actionSideEffectApplied = false;
        try {
            PendingRequest pr = rr.getRequest();
            if (isMaxTurnsContinue(pendingPayload) && !"CONFIRM".equalsIgnoreCase(action)) {
                if (sourceRun != null && isCancellable(sourceRun)) {
                    agentRuntime.cancel(sourceRun.getRunId());
                }
                Map<String, Object> out = new LinkedHashMap<>();
                out.put("requestId", requestId);
                out.put("runId", sourceRunId == null ? "" : sourceRunId);
                out.put("exitReason", "cancelled");
                out.put("continued", false);
                return out;
            }

            String userInput = rr.getNewUserInput();
            PendingActionResolver.ActionResult actionResult =
                    actionResolver.executeWithResultIfApplicable(pr, user, action, payload);
            if (actionResult != null) {
                actionSideEffectApplied = actionResult.isSideEffectApplied();
                userInput = userInput + "\n" + actionResult.getMessage();
            }
            if (isMaxTurnsContinue(pendingPayload)) {
                userInput = "[用户确认继续执行] 已达到 maxTurnsPerRun 后继续当前任务。"
                        + "请从当前 transcript 状态继续，不要重复已经完成的工具动作。";
            }

            boolean maxTurnsContinue = isMaxTurnsContinue(pendingPayload);
            RunRequest runReq = newContinuationRunRequest(thread, user, userInput,
                    requestId, sourceRun);
            if (sourceRunId != null && !sourceRunId.isEmpty()) {
                runReq = runReq.asResumeContinuation(sourceRunId, !maxTurnsContinue);
            }
            String runId = agentRuntime.startDetached(runReq);
            Map<String, Object> out = new LinkedHashMap<>();
            out.put("requestId", requestId);
            out.put("runId", runId);
            out.put("sourceRunId", sourceRunId == null ? "" : sourceRunId);
            out.put("exitReason", "running");
            out.put("continued", maxTurnsContinue);
            return out;
        } catch (Throwable t) {
            if (actionSideEffectApplied) {
                finishSourceRunAfterAppliedActionFailure(sourceRun, thread, t);
            } else {
                pendingService.reopen(requestId);
                restoreSourceRunWaiting(sourceRun, thread, user.getUserId());
            }
            throw t;
        }
    }

    private boolean isMaxTurnsContinue(Map<String, Object> payload) {
        return payload != null && "max_turns_continue".equals(String.valueOf(payload.get("kind")));
    }

    private Run requireSourceRun(String userId, String threadId, String sourceRunId) throws Exception {
        if (sourceRunId == null || sourceRunId.isEmpty()) {
            throw new LobsterException("run.source_missing", "Pending request has no source run");
        }
        Run run = runDao.getRun(sourceRunId);
        if (run == null) throw new LobsterException("run.not_found", "source run not found");
        if (!threadId.equals(run.getThreadId()) || !userId.equals(run.getUserId())) {
            throw new LobsterException("run.forbidden", "source run does not belong to pending thread");
        }
        return run;
    }

    private boolean isCancellable(Run run) {
        return run != null && (run.getStatus() == RunStatus.running
                || run.getStatus() == RunStatus.waiting_user);
    }

    private boolean isWaitingForContinuation(Run run) {
        return run != null && run.getStatus() == RunStatus.waiting_user;
    }

    private RunRequest newContinuationRunRequest(ThreadRoom thread, UserContext user,
                                                 String userInput, String requestId,
                                                 Run sourceRun) {
        Map<String, Object> sourcePayload = sourceRun == null
                ? new LinkedHashMap<String, Object>()
                : parsePayload(sourceRun.getRequestPayloadJson());
        boolean kbEnabled = bool(sourcePayload.get("kbEnabled"));
        String kbMode = str(sourcePayload.get("kbMode"), "auto");
        List<String> kbScopeIds = stringList(sourcePayload.get("kbScopeIds"));

        // Continuations should inherit execution settings from the suspended run
        // without replaying the original attachment prelude as a new user turn.
        // The original user attachments are already durable in the transcript.
        return new RunRequest(thread, user, userInput, "pending_resolve", requestId,
                (List<String>) null, (List<String>) null, kbEnabled, kbMode, kbScopeIds);
    }

    private void restoreSourceRunWaiting(Run sourceRun, ThreadRoom thread, String userId) {
        if (sourceRun == null || thread == null || sourceRun.getRunId() == null) return;
        try {
            sourceRun.setStatus(RunStatus.waiting_user);
            sourceRun.setExitReason(null);
            sourceRun.setEndedAt(null);
            sourceRun.setHeartbeatAt(new java.util.Date());
            runDao.save(sourceRun);
            ThreadRoom current = threadDao.getThread(thread.getThreadId());
            String activeRunId = current == null ? null : current.getActiveRunId();
            if (activeRunId == null || activeRunId.isEmpty()) {
                threadDao.claimActiveRun(sourceRun.getRunId(), new java.util.Date(),
                        thread.getThreadId(), userId);
            } else if (!sourceRun.getRunId().equals(activeRunId)) {
                threadDao.replaceActiveRun(sourceRun.getRunId(), new java.util.Date(),
                        thread.getThreadId(), userId, activeRunId);
            }
        } catch (Throwable ignore) { /* best-effort compensation */ }
    }

    private void finishSourceRunAfterAppliedActionFailure(Run sourceRun, ThreadRoom thread, Throwable cause) {
        if (sourceRun == null || sourceRun.getRunId() == null) return;
        try {
            java.util.Date now = new java.util.Date();
            sourceRun.setStatus(RunStatus.error);
            sourceRun.setExitReason(RunExitReason.error);
            sourceRun.setEndedAt(now);
            sourceRun.setHeartbeatAt(now);
            sourceRun.setErrorCode(cause instanceof LobsterException
                    ? ((LobsterException) cause).getCode() : "pending.continuation_failed_after_action");
            sourceRun.setErrorMessage(truncate("确认后的外部动作已执行，但续接 run 启动失败: "
                    + (cause == null ? "unknown" : cause.getMessage()), 2000));
            runDao.save(sourceRun);
            if (thread != null) {
                threadDao.clearActiveRunIfMatches(now, thread.getThreadId(), sourceRun.getRunId());
            }
            recordSourceRunErrorTerminal(sourceRun);
        } catch (Throwable ignore) { /* best-effort compensation */ }
    }

    private void recordSourceRunErrorTerminal(Run sourceRun) {
        if (sourceRun == null || sourceRun.getRunId() == null || runStreamEventService == null) return;
        try {
            long seq = runStreamEventService.maxSeq(sourceRun.getRunId()) + 1L;
            Map<String, Object> err = new LinkedHashMap<>();
            err.put("threadId", sourceRun.getThreadId());
            err.put("runId", sourceRun.getRunId());
            err.put("code", sourceRun.getErrorCode());
            err.put("message", sourceRun.getErrorMessage());
            runStreamEventService.record(sourceRun.getUserId(), sourceRun.getThreadId(),
                    sourceRun.getRunId(), seq, StreamEvent.of(StreamEventType.error, err));

            Map<String, Object> ended = new LinkedHashMap<>();
            ended.put("threadId", sourceRun.getThreadId());
            ended.put("runId", sourceRun.getRunId());
            ended.put("exitReason", RunExitReason.error.name());
            runStreamEventService.record(sourceRun.getUserId(), sourceRun.getThreadId(),
                    sourceRun.getRunId(), seq + 1L, StreamEvent.of(StreamEventType.run_ended, ended));
        } catch (Throwable ignore) { /* best-effort terminal projection */ }
    }

    private String truncate(String s, int max) {
        if (s == null || max <= 0 || s.length() <= max) return s;
        return s.substring(0, max);
    }

    private Map<String, Object> parsePayload(String json) {
        if (json == null || json.isEmpty()) return new LinkedHashMap<>();
        try {
            Map<String, Object> p = JsonUtil.fromJsonToMap(json);
            return p == null ? new LinkedHashMap<String, Object>() : p;
        } catch (Throwable t) {
            return new LinkedHashMap<>();
        }
    }

    private static boolean bool(Object value) {
        if (value instanceof Boolean) return (Boolean) value;
        return value != null && Boolean.parseBoolean(String.valueOf(value));
    }

    private static String str(Object value, String fallback) {
        if (value == null) return fallback;
        String s = String.valueOf(value);
        return s.isEmpty() ? fallback : s;
    }

    private static List<String> stringList(Object value) {
        List<String> out = new ArrayList<>();
        if (!(value instanceof Iterable)) return out;
        for (Object item : (Iterable<?>) value) {
            if (item != null) out.add(String.valueOf(item));
        }
        return out;
    }

    @Service(url = "/ai/api/me/pending-requests", method = HttpMethod.all)
    public Map<String, Object> myPending(Integer offset, Integer limit) throws Exception {
        UserContext user = UserContextHolder.require();
        int o = offset == null ? 0 : offset;
        int l = limit == null ? 20 : Math.min(50, limit);
        List<PendingRequest> list = pendingDao.listOpenByUser(user.getUserId(), PendingRequestStatus.open, o, l);
        List<Map<String, Object>> items = new ArrayList<>();
        for (PendingRequest p : list) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("requestId", p.getRequestId());
            row.put("threadId", p.getThreadId());
            row.put("type", p.getType().name());
            row.put("title", p.getTitle());
            row.put("prompt", p.getPrompt());
            row.put("status", p.getStatus().name());
            row.put("createTime", p.getCreateTime());
            items.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("items", items);
        out.put("offset", o);
        out.put("limit", l);
        return out;
    }
}
