package com.github.catatafishen.agentbridge.client.claude;

import com.github.catatafishen.agentbridge.model.ContentBlock;
import com.github.catatafishen.agentbridge.model.Model;
import com.github.catatafishen.agentbridge.model.SessionUpdate;
import com.github.catatafishen.agentbridge.client.AbstractClient;
import com.github.catatafishen.agentbridge.client.ClientException;
import com.github.catatafishen.agentbridge.services.ToolDefinition;
import com.github.catatafishen.agentbridge.services.ToolRegistry;
import com.github.catatafishen.agentbridge.settings.ProjectFilesSettings;
import com.google.gson.JsonObject;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.regex.Pattern;

/**
 * Shared base for Claude-based {@link AbstractClient} implementations.
 *
 * <p>Holds constants, session-lifecycle maps, and utility methods common to
 * {@link ClaudeClient} (subprocess via {@code claude} CLI).
 */
abstract class AbstractClaudeClient extends AbstractClient {

    private static final Logger LOG = Logger.getInstance(AbstractClaudeClient.class);

    @Nullable
    protected final ToolRegistry registry;

    protected AbstractClaudeClient(@Nullable ToolRegistry registry) {
        this.registry = registry;
    }

    // ── Claude model defaults ────────────────────────────────────────────────

    protected static final String DEFAULT_MODEL = "claude-sonnet-4-6";

    // ── JSON field names ─────────────────────────────────────────────────────

    protected static final String FIELD_TYPE = "type";
    protected static final String FIELD_CONTENT = "content";
    protected static final String FIELD_INPUT = "input";

    // ── Session state ────────────────────────────────────────────────────────

    protected final Map<String, String> sessionModels = new ConcurrentHashMap<>();
    private final Map<String, Map<String, String>> sessionOptions = new ConcurrentHashMap<>();
    protected final Map<String, AtomicBoolean> sessionCancelled = new ConcurrentHashMap<>();
    protected volatile boolean started = false;

    // ── AbstractClient — shared concrete implementations ────────────────

    @Override
    public void setModel(@NotNull String sessionId, @NotNull String modelId) {
        sessionModels.put(sessionId, modelId);
        LOG.info("Model set to " + modelId + " for session " + sessionId);
    }

    @Override
    public void setSessionOption(@NotNull String sessionId, @NotNull String key, @NotNull String value) {
        sessionOptions.computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>()).put(key, value);
        LOG.info("Session option [" + key + "] set to '" + value + "' for session " + sessionId);
    }

    @Nullable
    protected String getSessionOption(@NotNull String sessionId, @NotNull String key) {
        Map<String, String> opts = sessionOptions.get(sessionId);
        return opts != null ? opts.get(key) : null;
    }

    @Override
    public List<Model> getAvailableModels() {
        return List.of();
    }

    // ── Shared utilities ─────────────────────────────────────────────────────

    @NotNull
    protected String resolveModel(@NotNull String sessionId, @Nullable String model) {
        if (model != null && !model.isEmpty()) return model;
        String stored = sessionModels.get(sessionId);
        return (stored != null && !stored.isEmpty()) ? stored : DEFAULT_MODEL;
    }

    protected void ensureStarted() throws ClientException {
        if (!started) throw new ClientException(getClass().getSimpleName() + " not started", null, false);
    }

    // ── Tool name normalisation ──────────────────────────────────────────────

    @NotNull
    public String normalizeToolName(@NotNull String name) {
        if (name.startsWith("mcp__agentbridge__")) {
            return name.substring("mcp__agentbridge__".length());
        }
        return name;
    }

    // ── Project files configuration ───────────────────────────────────────────

    @Override
    @NotNull
    public List<ProjectFilesSettings.FileEntry> getDefaultProjectFiles() {
        List<ProjectFilesSettings.FileEntry> entries = new ArrayList<>();
        entries.add(new ProjectFilesSettings.FileEntry("CLAUDE.md", "CLAUDE.md", false, "Claude"));
        return entries;
    }

    // ── sessionUpdate emission ───────────────────────────────────────────────

    protected void emitToolCallStart(@NotNull String toolUseId, @NotNull String toolName,
                                     @NotNull JsonObject input,
                                     @Nullable Consumer<SessionUpdate> onUpdate) {
        if (onUpdate == null) return;
        String normalized = normalizeToolName(toolName);
        String args = (!input.isEmpty()) ? input.toString() : null;
        // "subagent_type" is the current field name (Claude Code >= 2.1.63 / Task tool renamed to Agent)
        // "agent_type" is the legacy field name; keep checking both for backwards compatibility
        String agentType = null;
        if (input.has("subagent_type")) {
            agentType = input.get("subagent_type").getAsString();
        } else if (input.has("agent_type")) {
            agentType = input.get("agent_type").getAsString();
        }
        String subAgentDesc = agentType != null && input.has("description") ? input.get("description").getAsString() : null;
        String subAgentPrompt = agentType != null && input.has("prompt") ? input.get("prompt").getAsString() : null;
        SessionUpdate.ToolKind kind = SessionUpdate.ToolKind.OTHER;
        if (registry != null) {
            ToolDefinition tool = registry.findById(normalized);
            if (tool != null) {
                kind = SessionUpdate.ToolKind.fromCategory(tool.category());
            }
        }
        onUpdate.accept(new SessionUpdate.ToolCall(
            toolUseId, normalized, normalized, kind, args, null,
            agentType, subAgentDesc, subAgentPrompt, null));
    }

    protected void emitToolCallEnd(@NotNull String toolUseId, @NotNull String result,
                                   boolean success,
                                   @Nullable Consumer<SessionUpdate> onUpdate) {
        if (onUpdate == null) return;
        if (success) {
            onUpdate.accept(new SessionUpdate.ToolCallUpdate(toolUseId, SessionUpdate.ToolCallStatus.COMPLETED, result, null, null));
        } else {
            onUpdate.accept(new SessionUpdate.ToolCallUpdate(toolUseId, SessionUpdate.ToolCallStatus.FAILED, null, result, null));
        }
    }

    private static final Pattern RATE_LIMIT_PATTERN =
        Pattern.compile("hit.*limit|rate.?limit|usage.*limit", Pattern.CASE_INSENSITIVE);

    protected static boolean isRateLimitError(@NotNull String errorText) {
        return RATE_LIMIT_PATTERN.matcher(errorText).find();
    }

    protected void emitBannerEvent(@NotNull String message, @NotNull SessionUpdate.BannerLevel level,
                                   @NotNull SessionUpdate.ClearOn clearOn,
                                   @Nullable Consumer<SessionUpdate> onUpdate) {
        if (onUpdate == null) return;
        onUpdate.accept(new SessionUpdate.Banner(message, level, clearOn));
    }

    protected void emitRateLimitBanner(@NotNull String message,
                                       @Nullable Consumer<SessionUpdate> onUpdate) {
        emitBannerEvent(message, SessionUpdate.BannerLevel.WARNING, SessionUpdate.ClearOn.NEXT_SUCCESS, onUpdate);
    }

    protected void emitThought(@NotNull String text, @Nullable Consumer<SessionUpdate> onUpdate) {
        if (onUpdate == null || text.isEmpty()) return;
        onUpdate.accept(new SessionUpdate.AgentThoughtChunk(List.of(new ContentBlock.Text(text))));
    }
}
