package com.github.catatafishen.agentbridge.client;

import com.github.catatafishen.agentbridge.acp.protocol.PromptRequest;
import com.github.catatafishen.agentbridge.bridge.SessionOption;
import com.github.catatafishen.agentbridge.model.Model;
import com.github.catatafishen.agentbridge.model.PromptResponse;
import com.github.catatafishen.agentbridge.model.SessionUpdate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.function.Consumer;

/**
 * Unified abstract base class for all agent types: ACP-based (Copilot, Junie, Kiro, OpenCode, Hermes Agent)
 * and non-ACP (Claude CLI, Anthropic Direct).
 * <p>
 * Replaces the {@link ClientConnector} interface and {@code bridge.AgentClient} interface.
 * Extended by {@code AcpClient} and {@code AbstractClaudeAgentClient}.
 */
public abstract class AbstractClient {

    // ─── Identity ────────────────────────────────────

    /**
     * Unique agent ID. e.g. "copilot", "junie", "claude-cli"
     */
    public abstract String agentId();

    /**
     * Display name for UI. e.g. "GitHub Copilot", "Claude (CLI)"
     */
    public abstract String displayName();

    // ─── Lifecycle ───────────────────────────────────

    /**
     * Start the agent process and perform handshake.
     */
    // S112: throws Exception is intentional — subclasses (AcpClient, ClaudeClient, etc.) throw
    // different checked exceptions during process startup. Narrowing would be a breaking API change.
    public abstract void start() throws Exception; // NOSONAR java:S112

    /**
     * Gracefully stop the agent process.
     */
    public abstract void stop();

    /**
     * Clear any persisted session/thread ID so the next session starts fresh.
     * Called when the user explicitly starts a new conversation. Default: no-op.
     */
    public void clearPersistedSession() {
    }

    /**
     * Whether the agent process is alive and initialized.
     */
    public abstract boolean isConnected();

    /**
     * Whether this client is alive and usable. Defaults to {@link #isConnected()}.
     */
    public boolean isHealthy() {
        return isConnected();
    }

    /**
     * Returns the filesystem path to the current session's working directory,
     * or {@code null} if no session is active or the client doesn't expose one.
     * <p>
     * Used by the Files tab in the side panel to list session artifacts
     * (plans, events, checkpoints, research files, etc.).
     */
    public @Nullable java.nio.file.Path getSessionDirectory() {
        return null;
    }

    /**
     * Close/stop the agent. Alias for {@link #stop()}.
     */
    public void close() {
        stop();
    }

    // ─── Sessions ────────────────────────────────────

    /**
     * Create a new conversation session.
     *
     * @param cwd working directory for the session
     * @return session ID
     */
    public abstract String createSession(String cwd) throws Exception; // NOSONAR java:S112 — see start()

    /**
     * Returns conversation history replayed by the agent during the most recent
     * {@code session/load} call, or {@code null} if no history was replayed (or
     * the session was created fresh via {@code session/new}).
     * <p>
     * When non-null and non-empty, the agent has conversation context from the
     * loaded session and the UI layer does NOT need to inject a compressed summary.
     * When null, injection should be used as a fallback.
     */
    public @Nullable List<SessionUpdate> getLoadedSessionHistory() {
        return null;
    }

    /**
     * Cancel an in-progress prompt turn.
     */
    public abstract void cancelSession(String sessionId);

    /**
     * Drops the cached current session ID without sending any cancellation request.
     * <p>
     * Used after detecting a corrupted session (e.g. an agent that returns
     * {@code end_turn} with no content). Clearing the cached ID forces the next
     * {@link #createSession} call to open a fresh session via the full load/new
     * flow instead of hitting the early-return "reuse" path.
     * </p>
     * <p>
     * The default implementation is a no-op; subclasses with a cached session ID
     * should override this method.
     * </p>
     */
    public void dropCurrentSession() {
    }

    // ─── Prompts ─────────────────────────────────────

    /**
     * Send a prompt and receive streaming updates.
     *
     * @param request  the prompt content and metadata
     * @param onUpdate callback for each streamed session update
     * @return the final prompt response when the turn completes
     */
    public abstract PromptResponse sendPrompt(PromptRequest request,
                                              Consumer<SessionUpdate> onUpdate) throws Exception; // NOSONAR java:S112 — see start()

    // ─── Modes (built-in interaction modes, e.g. default/agent/plan/autopilot) ──

    /**
     * Built-in mode slug to activate by default, or null for the agent's own default.
     */
    public @Nullable String defaultModeSlug() {
        return null;
    }

    /**
     * Available built-in modes (populated from session/new after connection).
     */
    public List<AgentMode> getAvailableModes() {
        return List.of();
    }

    /**
     * Returns the currently selected mode slug (user override or default).
     */
    public @Nullable String getCurrentModeSlug() {
        return defaultModeSlug();
    }

    /**
     * Sets the current mode slug. No-op for agents that don't support mode selection.
     */
    public void setCurrentModeSlug(@Nullable String slug) {
        // no-op
    }

    // ─── Agents (custom agent definitions, e.g. intellij-default/intellij-explore) ──

    /**
     * Custom agent slug to activate by default, or null to skip custom agent selection.
     */
    public @Nullable String defaultAgentSlug() {
        return null;
    }

    /**
     * Available custom agents for this connector. Override to expose agent selection.
     */
    public List<AgentMode> getAvailableAgents() {
        return List.of();
    }

    /**
     * Returns the currently selected custom agent slug (user override or default).
     */
    public @Nullable String getCurrentAgentSlug() {
        return defaultAgentSlug();
    }

    /**
     * Sets the current custom agent slug. No-op for agents that don't support agent selection.
     */
    public void setCurrentAgentSlug(@Nullable String slug) {
        // no-op
    }

    /**
     * Returns the effective slug to send as {@code modeSlug} in session/prompt.
     * Custom agent slug takes priority over built-in mode slug.
     */
    public @Nullable String getEffectiveModeSlug() {
        String agent = getCurrentAgentSlug();
        if (agent != null && !agent.isEmpty()) return agent;
        return getCurrentModeSlug();
    }

    // ─── Config options (e.g. effort/thinking level, returned by session/new) ───

    /**
     * Available config options for the current session (populated after session creation).
     */
    public List<AgentConfigOption> getAvailableConfigOptions() {
        return List.of();
    }

    /**
     * Sets a config option value for the session.
     */
    public void setConfigOption(@NotNull String sessionId, @NotNull String configId, @NotNull String valueId) {
        // no-op
    }

    // ─── Models ──────────────────────────────────────

    /**
     * Available models for this agent. Empty list if not supported.
     */
    public abstract List<Model> getAvailableModels();

    /**
     * The agent-reported currently selected model ID from session/new, or {@code null} if not provided.
     */
    public @Nullable String getCurrentModelId() {
        return null;
    }

    /**
     * Returns the ID of the currently active session if one exists (e.g. from eager creation at
     * startup), without creating a new one. Returns {@code null} if no session has been created yet.
     * <p>
     * Use this to apply changes (e.g. model selection) to an already-created session before the
     * first prompt is sent. Default: {@code null}.
     */
    public @Nullable String getActiveSessionId() {
        return null;
    }

    /**
     * Set the model for a session. No-op if agent doesn't support model selection.
     */
    public abstract void setModel(String sessionId, String modelId);

    /**
     * How models are displayed in the UI.
     */
    public ModelDisplayMode modelDisplayMode() {
        return ModelDisplayMode.NONE;
    }

    /**
     * Extract a multiplier/tier string from model metadata (e.g. "2x").
     */
    public @Nullable String getModelMultiplier(@NotNull Model model) {
        return null;
    }

    /**
     * Returns the pricing multiplier label for the given model ID (e.g. "1x", "2x"),
     * or {@code null} if the multiplier is not available for this model.
     * Default implementation looks up the model in {@link #getAvailableModels()} and calls
     * {@link #getModelMultiplier(Model)}.
     */
    @Nullable
    public String getModelMultiplier(@NotNull String modelId) {
        for (Model m : getAvailableModels()) {
            if (modelId.equals(m.id())) {
                return getModelMultiplier(m);
            }
        }
        return null;
    }

    /**
     * Whether this client supports per-model premium-request multipliers.
     * Default: {@code false}.
     */
    public boolean supportsMultiplier() {
        return false;
    }

    /**
     * Whether this agent's model data can be grouped by provider in the model picker.
     * When {@code true}, the UI renders models in collapsible provider sections
     * with a favorites feature.
     */
    public boolean supportsModelGrouping() {
        return false;
    }

    // ─── Session Options ─────────────────────────────

    /**
     * Returns session option descriptors that this client supports beyond model selection.
     * Default: empty.
     */
    @NotNull
    public List<SessionOption> listSessionOptions() {
        return List.of();
    }

    /**
     * Apply a session option value for the given session.
     * Default: no-op.
     */
    public void setSessionOption(@NotNull String sessionId, @NotNull String key, @NotNull String value) {
        // no-op
    }

    // ─── Permission Handling ─────────────────────────

    /**
     * Register a listener for tool permission prompts.
     * Default: no-op for clients that handle permissions internally.
     */
    public void setPermissionRequestListener(Consumer<PermissionPrompt> listener) {
        // no-op by default
    }

    /**
     * Notify that a sub-agent task is now active or has completed. No-op by default.
     */
    public void setSubAgentActive(boolean active) {
        // no-op by default
    }

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

    /**
     * Returns the default project file shortcuts for this agent.
     * Default: empty.
     */
    @NotNull
    public List<com.github.catatafishen.agentbridge.settings.ProjectFilesSettings.FileEntry>
    getDefaultProjectFiles() {
        return List.of();
    }

    /**
     * Checks whether this agent is authenticated and ready to accept prompts.
     *
     * <p>This is a liveness check only — the plugin never inspects local credential
     * stores (files, OS keychain, etc.). Authentication state is observed at runtime
     * from the agent's own error responses; see {@code docs/AUTH-HANDLING.md}.</p>
     *
     * @return {@code null} if the agent is started, or a human-readable error message otherwise
     */
    @Nullable
    public String checkAuthentication() {
        return isHealthy() ? null : "Agent not started";
    }

    // ─── Auth ────────────────────────────────────────

    /**
     * Authentication method info for sign-in commands, or null if not applicable.
     */
    @Nullable
    public com.github.catatafishen.agentbridge.bridge.AuthMethod getAuthMethod() {
        return null;
    }

    // ─── Display ─────────────────────────────────────

    /**
     * A mode (agent persona) available for this connector.
     *
     * @param slug        the mode identifier used in protocol requests
     * @param name        human-readable display name
     * @param description optional description of what this mode does
     */
    public record AgentMode(@NotNull String slug, @NotNull String name, @Nullable String description) {
    }

    public record AgentConfigOption(
        @NotNull String id,
        @NotNull String label,
        @Nullable String description,
        @NotNull List<AgentConfigOptionValue> values,
        @Nullable String selectedValueId
    ) {
    }

    public record AgentConfigOptionValue(@NotNull String id, @NotNull String label) {
    }

    /**
     * How model information is shown in the UI.
     */
    public enum ModelDisplayMode {
        /**
         * Don't show model info.
         */
        NONE,
        /**
         * Show model name.
         */
        NAME,
        /**
         * Show token count per turn.
         */
        TOKEN_COUNT,
        /**
         * Show multiplier (1x, 2x, 10x).
         */
        MULTIPLIER
    }

    /**
     * Callback for permission prompts.
     */
    public interface PermissionPrompt {
        String toolCallId();

        String toolName();

        @Nullable String arguments();

        List<String> options();

        void allow(String optionId);

        void deny(String reason);
    }
}
