package com.sino.mci.inbound.engine;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sino.mci.flow.entity.FlowDefinition;
import com.sino.mci.flow.repository.FlowDefinitionMapper;
import com.sino.mci.inbound.entity.FlowExecutionTrace;
import com.sino.mci.inbound.entity.MessageEvent;
import com.sino.mci.inbound.intent.IntentResult;
import com.sino.mci.inbound.repository.FlowExecutionTraceMapper;
import com.sino.mci.inbound.repository.MessageEventMapper;
import com.sino.mci.send.dto.SendRequest;
import com.sino.mci.send.service.SendService;
import com.sino.mci.shared.util.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Component
@RequiredArgsConstructor
public class FlowEngine {

    private final FlowDefinitionMapper flowDefinitionMapper;
    private final MessageEventMapper messageEventMapper;
    private final IntentRecognitionService intentRecognitionService;
    private final WebhookPushService webhookPushService;
    private final FlowExecutionTraceMapper flowExecutionTraceMapper;
    private final SendService sendService;

    public void executeInboundFlow(Long eventId) {
        MessageEvent event = messageEventMapper.selectById(eventId);
        if (event == null) {
            log.warn("Message event not found: {}", eventId);
            return;
        }

        trace(event, null, "flow_start", null, event.getEventId(), null, 1, now(), null);

        FlowDefinition flow = findPublishedFlow(event.getTenantCode());
        Long flowDefinitionId = flow != null ? flow.getId() : null;
        if (flow == null) {
            log.info("No published inbound flow for tenant {}, skipping flow execution", event.getTenantCode());
            markProcessed(event, flowDefinitionId);
            return;
        }

        try {
            List<FlowNode> nodes = FlowDefinitionParser.parseNodes(flow.getDefinitionJson());
            List<FlowEdge> edges = FlowDefinitionParser.parseEdges(flow.getDefinitionJson());
            if (nodes.isEmpty()) {
                log.warn("Flow {} has no nodes", flow.getFlowCode());
                markProcessed(event, flowDefinitionId);
                return;
            }

            FlowNode receiveNode = nodes.stream()
                    .filter(n -> "receive".equals(n.type()))
                    .findFirst()
                    .orElse(null);
            if (receiveNode == null) {
                log.warn("Flow {} has no receive node", flow.getFlowCode());
                markProcessed(event, flowDefinitionId);
                return;
            }

            Map<String, FlowNode> nodeMap = nodes.stream()
                    .collect(Collectors.toMap(FlowNode::id, n -> n));
            Map<String, List<FlowEdge>> adjacency = edges.stream()
                    .collect(Collectors.groupingBy(FlowEdge::source));

            Map<String, Object> businessContext = parseBusinessContext(event.getBusinessContext());
            FlowContext context = new FlowContext(event, businessContext);

            FlowNode current = receiveNode;
            while (current != null) {
                long nodeStart = now();
                NodeResult result;
                try {
                    result = executeNode(current, context, flowDefinitionId, adjacency, nodeStart);
                } catch (Exception e) {
                    trace(event, flowDefinitionId, current.type(), JsonUtils.toJson(current.config()),
                            current.id(), e.getMessage(), 2, nodeStart, e.getMessage());
                    log.error("Node execution failed: eventId={}, nodeId={}, nodeType={}",
                            event.getEventId(), current.id(), current.type(), e);
                    return;
                }

                if (result.terminal() || result.nextNodeId() == null) {
                    if (result.success()) {
                        markProcessed(event, flowDefinitionId);
                    }
                    return;
                }

                current = nodeMap.get(result.nextNodeId());
                if (current == null) {
                    log.warn("Next node {} not found in flow {}", result.nextNodeId(), flow.getFlowCode());
                    return;
                }
            }
        } catch (Exception e) {
            log.error("Flow execution failed for event {}", eventId, e);
        }
    }

    private NodeResult executeNode(FlowNode node, FlowContext context, Long flowDefinitionId,
                                   Map<String, List<FlowEdge>> adjacency, long startMs) {
        String nodeConfigJson = JsonUtils.toJson(node.config());
        MessageEvent event = context.getEvent();

        switch (node.type()) {
            case "receive" -> {
                Object expectedChannel = node.config().get("channel_type");
                if (expectedChannel != null && !expectedChannel.toString().equalsIgnoreCase(event.getChannelType())) {
                    throw new IllegalStateException("Channel type mismatch: expected " + expectedChannel
                            + ", actual " + event.getChannelType());
                }
                trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                        event.getEventId(), event.getChannelType(), 1, startMs, null);
                return new NodeResult(firstTarget(adjacency.get(node.id())), false, true, "ok");
            }
            case "intent" -> {
                IntentResult intent = intentRecognitionService.recognize(event);
                context.setIntent(intent);
                String intentJson = JsonUtils.toJson(intent);
                trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                        event.getContent(), intentJson, 1, startMs, null);
                return new NodeResult(firstTarget(adjacency.get(node.id())), false, true, intentJson);
            }
            case "condition" -> {
                String expression = node.config().get("expression") != null
                        ? node.config().get("expression").toString() : "";
                if (expression.isBlank()) {
                    String nextNodeId = firstTarget(adjacency.get(node.id()));
                    trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                            expression, "empty -> first", 1, startMs, null);
                    return new NodeResult(nextNodeId, false, true, "empty");
                }
                boolean expressionResult = SpelConditionEvaluator.evaluate(expression, context);
                String nextNodeId = selectConditionTarget(adjacency.get(node.id()), expressionResult);
                trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                        expression, String.valueOf(expressionResult), 1, startMs, null);
                return new NodeResult(nextNodeId, false, true, String.valueOf(expressionResult));
            }
            case "webhook" -> {
                String customUrl = node.config().get("url") != null
                        ? node.config().get("url").toString() : null;
                webhookPushService.pushToBusiness(event.getTenantCode(), event, context.getIntent(), customUrl);
                trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                        event.getEventId(), "pushed", 1, startMs, null);
                return new NodeResult(null, true, true, "pushed");
            }
            case "reply" -> {
                SendRequest request = buildReplyRequest(event, node.config());
                sendService.send(event.getTenantCode(), request);
                trace(event, flowDefinitionId, node.type(), nodeConfigJson,
                        event.getEventId(), JsonUtils.toJson(request), 1, startMs, null);
                return new NodeResult(null, true, true, "sent");
            }
            default -> throw new IllegalArgumentException("Unsupported node type: " + node.type());
        }
    }

    private String firstTarget(List<FlowEdge> edges) {
        if (edges == null || edges.isEmpty()) {
            return null;
        }
        return edges.get(0).target();
    }

    private String selectConditionTarget(List<FlowEdge> edges, boolean result) {
        if (edges == null || edges.isEmpty()) {
            return null;
        }
        String expectedLabel = String.valueOf(result);
        return edges.stream()
                .filter(e -> expectedLabel.equalsIgnoreCase(e.label()))
                .findFirst()
                .map(FlowEdge::target)
                .orElseGet(() -> edges.get(0).target());
    }

    private SendRequest buildReplyRequest(MessageEvent event, Map<String, Object> config) {
        Object contentObj = config.get("content");
        Map<String, Object> content;
        if (contentObj instanceof Map<?, ?> map) {
            content = new HashMap<>();
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                content.put(String.valueOf(entry.getKey()), entry.getValue());
            }
        } else {
            content = new HashMap<>();
            content.put("type", "text");
            content.put("body", contentObj != null ? contentObj.toString() : "");
        }

        SendRequest request = new SendRequest();
        request.setContentType(String.valueOf(content.get("type")));
        request.setContent(content);

        Map<String, Object> channelStrategy = new HashMap<>();
        channelStrategy.put("channel_type", event.getChannelType());
        request.setChannelStrategy(channelStrategy);

        if (event.getConversationId() != null) {
            request.setTargetType("conversation");
            request.setTargetValue(Map.of("conversation_id", event.getConversationId()));
        } else if (event.getPersonId() != null) {
            request.setTargetType("person");
            request.setTargetValue(Map.of("person_id", event.getPersonId()));
        } else {
            throw new IllegalStateException("Cannot reply: message event has no conversation_id or person_id");
        }
        return request;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> parseBusinessContext(String businessContextJson) {
        if (businessContextJson == null || businessContextJson.isBlank()) {
            return new LinkedHashMap<>();
        }
        Map<String, Object> context = JsonUtils.fromJson(businessContextJson, Map.class);
        return context != null ? context : new LinkedHashMap<>();
    }

    private FlowDefinition findPublishedFlow(String tenantCode) {
        List<FlowDefinition> flows = flowDefinitionMapper.selectList(
                new LambdaQueryWrapper<FlowDefinition>()
                        .eq(FlowDefinition::getTenantCode, tenantCode)
                        .eq(FlowDefinition::getFlowType, 1)
                        .eq(FlowDefinition::getStatus, 1)
                        .orderByDesc(FlowDefinition::getUpdateTime));
        return flows.isEmpty() ? null : flows.get(0);
    }

    private void markProcessed(MessageEvent event, Long flowDefinitionId) {
        event.setIsProcessed(1);
        messageEventMapper.updateById(event);
        trace(event, flowDefinitionId, "mark_processed", null, event.getEventId(), "processed", 1, now(), null);
    }

    private void trace(MessageEvent event, Long flowDefinitionId, String nodeType, String nodeConfig,
                       String input, String output, int status, long startMs, String errorMsg) {
        FlowExecutionTrace record = new FlowExecutionTrace();
        record.setEventId(event.getEventId());
        record.setFlowDefinitionId(flowDefinitionId);
        record.setNodeType(nodeType);
        record.setNodeConfig(nodeConfig);
        record.setInput(input);
        record.setOutput(output);
        record.setStatus(status);
        record.setDurationMs((int) (now() - startMs));
        record.setErrorMsg(errorMsg);
        flowExecutionTraceMapper.insert(record);
    }

    private long now() {
        return System.currentTimeMillis();
    }

    private record NodeResult(String nextNodeId, boolean terminal, boolean success, String output) {
    }
}
