package tech.easyflow.agent.runtime.hitl;

import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.agent.entity.AgentHitlPending;
import tech.easyflow.agent.mapper.AgentHitlPendingMapper;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.runtime.ChatRuntimeContext;

import java.math.BigInteger;
import java.time.Instant;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Agent 工具审批 pending 持久化服务实现。
 */
@Service
public class AgentHitlPendingServiceImpl implements AgentHitlPendingService {

    private final AgentHitlPendingMapper pendingMapper;
    private final AgentRuntimeProperties properties;

    /**
     * 创建服务。
     *
     * @param pendingMapper pending Mapper
     * @param properties Agent 运行态配置
     */
    public AgentHitlPendingServiceImpl(AgentHitlPendingMapper pendingMapper,
                                       AgentRuntimeProperties properties) {
        this.pendingMapper = pendingMapper;
        this.properties = properties;
    }

    @Override
    public void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
        if (event == null || event.getPayload() == null) {
            return;
        }
        String resumeToken = stringValue(event.getPayload().get("resumeToken"));
        if (!StringUtils.hasText(resumeToken)) {
            return;
        }
        AgentHitlPending pending = findByToken(resumeToken);
        Date now = new Date();
        if (pending == null) {
            pending = new AgentHitlPending();
            pending.setResumeToken(resumeToken);
            pending.setCreated(now);
            pending.setCreatedBy(chatContext == null ? null : chatContext.getUserId());
            pending.setIsDeleted(0);
        }
        pending.setTenantId(chatContext == null ? null : chatContext.getTenantId());
        pending.setAgentId(resolveAgentId(event, chatContext));
        pending.setChatSessionId(chatContext == null ? null : chatContext.getSessionId());
        pending.setRuntimeSessionId(firstText(event.getSessionId(), stringValue(event.getPayload().get("sessionId"))));
        pending.setRequestId(requestId);
        pending.setToolCallId(firstText(event.getToolCallId(), stringValue(event.getPayload().get("toolCallId"))));
        pending.setToolName(stringValue(event.getPayload().get("toolName")));
        pending.setToolInputJson(mapValue(firstNonNull(event.getPayload().get("toolInput"), event.getPayload().get("input"))));
        pending.setStatus(AgentHitlPendingStatus.PENDING.name());
        pending.setExpiresAt(resolveExpiresAt(event));
        pending.setMetadataJson(metadata(event));
        pending.setModified(now);
        pending.setModifiedBy(chatContext == null ? null : chatContext.getUserId());
        pendingMapper.insertOrUpdate(pending);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AgentHitlPending approve(String resumeToken, BigInteger operatorId) {
        return consume(resumeToken, operatorId, AgentHitlPendingStatus.APPROVED, null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason) {
        return consume(resumeToken, operatorId, AgentHitlPendingStatus.REJECTED, reason);
    }

    @Override
    public void cancelByRequestId(String requestId, String reason) {
        if (!StringUtils.hasText(requestId)) {
            return;
        }
        List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
                .eq("request_id", requestId)
                .eq("status", AgentHitlPendingStatus.PENDING.name())
                .eq("is_deleted", 0));
        Date now = new Date();
        for (AgentHitlPending record : records) {
            record.setStatus(AgentHitlPendingStatus.CANCELLED.name());
            record.setRejectReason(reason);
            record.setConsumedAt(now);
            record.setModified(now);
            pendingMapper.update(record);
        }
    }

    @Override
    public void deleteByChatSessionId(BigInteger chatSessionId) {
        if (chatSessionId == null) {
            return;
        }
        List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
                .eq("chat_session_id", chatSessionId)
                .eq("is_deleted", 0));
        softDelete(records);
    }

    @Override
    public void deleteByRuntimeSessionId(String runtimeSessionId) {
        if (!StringUtils.hasText(runtimeSessionId)) {
            return;
        }
        List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
                .eq("runtime_session_id", runtimeSessionId)
                .eq("is_deleted", 0));
        softDelete(records);
    }

    private void softDelete(List<AgentHitlPending> records) {
        if (records == null || records.isEmpty()) {
            return;
        }
        Date now = new Date();
        for (AgentHitlPending record : records) {
            record.setIsDeleted(1);
            record.setModified(now);
            pendingMapper.update(record);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<AgentHitlPending> expirePending(int limit) {
        List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
                .eq("status", AgentHitlPendingStatus.PENDING.name())
                .eq("is_deleted", 0)
                .le("expires_at", new Date())
                .limit(Math.max(1, limit)));
        Date now = new Date();
        for (AgentHitlPending record : records) {
            record.setStatus(AgentHitlPendingStatus.EXPIRED.name());
            record.setRejectReason("审批超时，已自动拒绝");
            record.setConsumedAt(now);
            record.setModified(now);
            pendingMapper.update(record);
        }
        return records;
    }

    private AgentHitlPending consume(String resumeToken,
                                     BigInteger operatorId,
                                     AgentHitlPendingStatus targetStatus,
                                     String reason) {
        if (!StringUtils.hasText(resumeToken)) {
            throw new BusinessException("Agent 恢复令牌不能为空");
        }
        AgentHitlPending pending = findByToken(resumeToken);
        if (pending == null || Integer.valueOf(1).equals(pending.getIsDeleted())) {
            throw new BusinessException("Agent 审批请求不存在或已失效");
        }
        if (!AgentHitlPendingStatus.PENDING.name().equals(pending.getStatus())) {
            throw new BusinessException("Agent 审批请求已处理");
        }
        if (pending.getExpiresAt() != null && pending.getExpiresAt().before(new Date())) {
            markConsumed(pending, operatorId, AgentHitlPendingStatus.EXPIRED, "审批超时，已自动拒绝");
            throw new BusinessException("Agent 审批请求已过期");
        }
        if (!markConsumed(pending, operatorId, targetStatus, reason)) {
            throw new BusinessException("Agent 审批请求已处理");
        }
        pending.setStatus(targetStatus.name());
        pending.setRejectReason(reason);
        pending.setConsumedAt(new Date());
        pending.setModifiedBy(operatorId);
        return pending;
    }

    private boolean markConsumed(AgentHitlPending pending,
                                 BigInteger operatorId,
                                 AgentHitlPendingStatus targetStatus,
                                 String reason) {
        Date now = new Date();
        AgentHitlPending update = new AgentHitlPending();
        update.setStatus(targetStatus.name());
        update.setRejectReason(reason);
        update.setConsumedAt(now);
        update.setModified(now);
        update.setModifiedBy(operatorId);
        // 用 status=PENDING 作为消费条件，避免两个审批请求同时把同一个 token 消费两次。
        return pendingMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", pending.getId())
                .eq("status", AgentHitlPendingStatus.PENDING.name())
                .eq("is_deleted", 0)) > 0;
    }

    private AgentHitlPending findByToken(String resumeToken) {
        return pendingMapper.selectOneByQuery(QueryWrapper.create()
                .eq("resume_token", resumeToken)
                .eq("is_deleted", 0)
                .limit(1));
    }

    private BigInteger resolveAgentId(AgentRuntimeEvent event, ChatRuntimeContext chatContext) {
        String agentId = firstText(event.getAgentId(), stringValue(event.getPayload().get("agentId")));
        if (StringUtils.hasText(agentId)) {
            try {
                return new BigInteger(agentId);
            } catch (NumberFormatException ignored) {
                return chatContext == null ? null : chatContext.getAssistantId();
            }
        }
        return chatContext == null ? null : chatContext.getAssistantId();
    }

    private Map<String, Object> metadata(AgentRuntimeEvent event) {
        Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.putAll(event.getMetadata() == null ? Map.of() : event.getMetadata());
        Object approvalMetadata = event.getPayload().get("approvalMetadata");
        if (approvalMetadata instanceof Map<?, ?> map) {
            map.forEach((key, value) -> metadata.put(String.valueOf(key), value));
        }
        return metadata;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> mapValue(Object value) {
        if (value instanceof Map<?, ?> map) {
            Map<String, Object> result = new LinkedHashMap<>();
            map.forEach((key, item) -> result.put(String.valueOf(key), item));
            return result;
        }
        return new LinkedHashMap<>();
    }

    private Date dateValue(Object value) {
        if (value instanceof Date date) {
            return date;
        }
        String text = stringValue(value);
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return Date.from(Instant.parse(text));
        } catch (RuntimeException ignored) {
            return null;
        }
    }

    private Date resolveExpiresAt(AgentRuntimeEvent event) {
        Date eventExpiresAt = dateValue(event.getPayload().get("expiresAt"));
        if (eventExpiresAt != null) {
            return eventExpiresAt;
        }
        return Date.from(Instant.now().plus(properties.getHitlPendingTimeout()));
    }

    private Object firstNonNull(Object left, Object right) {
        return left == null ? right : left;
    }

    private String firstText(String left, String right) {
        return StringUtils.hasText(left) ? left : right;
    }

    private String stringValue(Object value) {
        return value == null ? null : String.valueOf(value);
    }
}
