package dev.sonpx.loyalty.agent.core.executor;

import dev.sonpx.loyalty.agent.core.config.AgentProperties;
import dev.sonpx.loyalty.agent.exception.UnrecoverableToolExecutionException;
import dev.sonpx.loyalty.agent.service.AgentEventPublisher;
import dev.sonpx.loyalty.agent.service.ToolResultPresentationProcessor;
import io.modelcontextprotocol.client.McpSyncClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.stereotype.Component;

import java.io.EOFException;
import java.net.ConnectException;
import java.net.SocketException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.List;

/**
 * Wraps MCP tool callbacks with:
 * 1. Event publishing (tool running/done status)
 * 2. Error sanitization & Circuit breaker for unrecoverable errors
 * 3. Auto-reconnection on dropped SSE/MCP connection
 * 4. Result truncation (prevents context window overflow)
 * 5. Result presentation processing (agent_instruction handling)
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ToolInterceptor {

    private final ObjectProvider<ToolCallbackProvider> toolProviders;
    private final AgentEventPublisher eventPublisher;
    private final ToolResultPresentationProcessor toolResultProcessor;
    private final AgentProperties agentProperties;
    private final ObjectProvider<List<McpSyncClient>> mcpSyncClientsProvider;

    public List<ToolCallback> getWrappedTools(String sessionId, String messageId) {
        return getWrappedTools(sessionId, messageId, AgentToolScope.ALL);
    }

    public List<ToolCallback> getWrappedTools(String sessionId, String messageId, AgentToolScope scope) {
        List<ToolCallback> wrappedTools = new ArrayList<>();

        for (ToolCallbackProvider provider : toolProviders.stream().toList()) {
            for (ToolCallback tool : provider.getToolCallbacks()) {
                String toolName = tool.getToolDefinition().name();
                if (scope.allows(toolName)) {
                    wrappedTools.add(wrap(tool, sessionId, messageId));
                }
            }
        }
        log.debug("Loaded {} tools for scope {}", wrappedTools.size(), scope);
        return wrappedTools;
    }

    private ToolCallback wrap(ToolCallback tool, String sessionId, String messageId) {
        return new ToolCallback() {
            @Override
            public ToolDefinition getToolDefinition() {
                return tool.getToolDefinition();
            }

            @Override
            public String call(String toolInput) {
                String toolName = getToolDefinition().name();
                eventPublisher.toolRunning(sessionId, messageId, toolName);
                try {
                    String result = executeWithRetry(tool, toolInput, toolName);

                    // Unrecoverable raw system error responses (e.g. 404 HTML, connection reset)
                    if (result != null && isUnrecoverableErrorResponse(result)) {
                        log.error("Tool {} returned unrecoverable system error response: {}", toolName, result);
                        throw new UnrecoverableToolExecutionException(
                                "Tool " + toolName + " returned unrecoverable system error");
                    }

                    // Sanitize standard business/internal errors — don't leak stack traces to LLM
                    if (result != null && isErrorResponse(result)) {
                        log.warn("Tool {} returned error response, sanitizing", toolName);
                        return "{\"error\": \"Lỗi hệ thống khi gọi tool. Không thể xử lý yêu cầu. Vui lòng thử lại sau.\"}";
                    }

                    // Process presentation (agent_instruction handling)
                    String processed = toolResultProcessor.process(result);

                    // Truncate if result is too large for the model's context window
                    return truncateResult(processed, toolName);
                } catch (UnrecoverableToolExecutionException e) {
                    throw e;
                } catch (Exception e) {
                    log.error("Tool {} threw unrecoverable exception: {}", toolName, e.getMessage(), e);
                    throw new UnrecoverableToolExecutionException(
                            "Lỗi hệ thống không thể phục hồi khi gọi tool '" + toolName + "': " + e.getMessage(), e);
                } finally {
                    eventPublisher.toolDone(sessionId, messageId, toolName);
                }
            }
        };
    }

    private String executeWithRetry(ToolCallback tool, String toolInput, String toolName) throws Exception {
        try {
            return tool.call(toolInput);
        } catch (Exception e) {
            Throwable rootCause = getRootCause(e);
            if (isConnectionOrSessionError(rootCause)) {
                log.warn("Detected MCP connection/session drop on tool {}: {}. Attempting auto-reconnect and retry...",
                        toolName, rootCause.getMessage());
                attemptReconnect();
                try {
                    return tool.call(toolInput);
                } catch (Exception retryEx) {
                    log.error("Retry after reconnect failed for tool {}: {}", toolName, retryEx.getMessage());
                    throw new UnrecoverableToolExecutionException("Không thể khôi phục kết nối MCP khi gọi tool " + toolName, retryEx);
                }
            }
            throw e;
        }
    }

    private synchronized void attemptReconnect() {
        if (mcpSyncClientsProvider != null) {
            List<McpSyncClient> clients = mcpSyncClientsProvider.getIfAvailable();
            if (clients != null && !clients.isEmpty()) {
                for (McpSyncClient client : clients) {
                    try {
                        log.info("Re-initializing MCP Sync Client session...");
                        client.initialize();
                    } catch (Exception ex) {
                        log.warn("Failed to re-initialize MCP Sync Client: {}", ex.getMessage());
                    }
                }
            }
        }
    }

    private Throwable getRootCause(Throwable t) {
        if (t == null) return null;
        Throwable current = t;
        while (current.getCause() != null && current.getCause() != current) {
            current = current.getCause();
        }
        return current;
    }
    public boolean isConnectionOrSessionError(Throwable t) {
        if (t == null) {
            return false;
        }
        Throwable current = t;
        while (current != null) {
            if (current instanceof EOFException
                    || current instanceof ConnectException
                    || current instanceof SocketException
                    || current instanceof ClosedChannelException
                    || current.getClass().getName().contains("ConnectException")
                    || current.getClass().getName().contains("SocketException")) {
                return true;
            }
            String msg = current.getMessage();
            if (msg != null) {
                String lower = msg.toLowerCase();
                if (lower.contains("404")
                        || lower.contains("session not found")
                        || lower.contains("session invalid")
                        || lower.contains("connection reset")
                        || lower.contains("connection refused")
                        || lower.contains("stream closed")) {
                    return true;
                }
            }
            current = current.getCause();
        }
        return false;
    }

    private boolean isUnrecoverableErrorResponse(String result) {
        if (result == null) return false;
        String lower = result.toLowerCase();
        return lower.contains("404 session not found")
                || lower.contains("http status 404")
                || lower.contains("connection refused")
                || lower.contains("eofexception");
    }

    private boolean isErrorResponse(String result) {
        return (result.contains("java.lang.") && result.contains("Exception") && result.contains("org.springframework."))
                || result.contains("HTTP Status 500 – Internal Server Error");
    }

    /**
     * Truncate tool results that exceed the configured max chars.
     * Preserves the beginning of the result (most relevant data) and adds a truncation notice.
     */
    private String truncateResult(String result, String toolName) {
        if (result == null) {
            return result;
        }
        int maxChars = agentProperties.getMaxToolResultChars();
        if (result.length() <= maxChars) {
            return result;
        }

        log.info("Truncating tool result for {} from {} to {} chars", toolName, result.length(), maxChars);

        // Try to find a clean JSON break point (end of an object/array element)
        int cutPoint = maxChars;
        int lastBrace = result.lastIndexOf('}', maxChars);
        int lastBracket = result.lastIndexOf(']', maxChars);
        int cleanBreak = Math.max(lastBrace, lastBracket);
        if (cleanBreak > maxChars / 2) {
            cutPoint = cleanBreak + 1;
        }

        return result.substring(0, cutPoint)
                + "... (kết quả đã rút gọn, tổng " + result.length() + " ký tự)";
    }
}
