import type {
  AggregateUsage,
  AIProviderRegistryStatusEntry,
  ChatInputDuringExecutionMode,
  ChatMessage,
} from '@genesislcap/foundation-ai';
import type { PayloadAction } from '@genesislcap/foundation-redux';
import { createSlice } from '@genesislcap/foundation-redux';
import type { AgentConfig } from '../config/config';
import type { AiAssistantAnimation, AiAssistantState, SuggestionsState } from '../main/main.types';
import { emptyUsage, sumUsage } from '../utils/sum-usage';

/**
 * A single in-flight per-call chat-input override pushed by a `requestSubAgent`
 * or `requestInteraction` call. Tracked as an array (not a counter) so the
 * slice can survive pop-in/pop-out and so a listener that connects
 * mid-execution can compute the effective mode without having seen the start
 * event.
 *
 * @internal
 */
export interface InputOverride {
  /** Unique per-invocation id, paired with the start/stop events. */
  id: string;
  /** The mode requested for this invocation. */
  mode: ChatInputDuringExecutionMode;
}

/**
 * Shape of a single AI assistant session's serializable state.
 *
 * @internal
 */
export interface AiAssistantSessionState {
  messages: ChatMessage[];
  state: AiAssistantState;
  showToolCalls: boolean;
  showThinkingSteps: boolean;
  showNarration: boolean;
  showAgentSwitchIndicator: boolean;
  enabledAnimations: AiAssistantAnimation[];
  suggestionsState: SuggestionsState;
  contextTokens: number | undefined;
  contextLimit: number | undefined;
  /**
   * Cost and per-bucket token totals across every chat turn in this session,
   * including sub-agent turns and spend banked by a compaction.
   *
   * One field rather than a cost scalar plus a token scalar: they are derived from
   * the same walk of the transcript, so keeping them together makes it impossible
   * for the two to disagree about which turns they counted.
   */
  sessionUsage: AggregateUsage;
  /** Active model id (e.g. `claude-sonnet-4-6`), resolved on connect. */
  activeModel: string | undefined;
  /**
   * Name of the AI provider used on the most recent turn — drives "current"
   * marker in the settings panel. `undefined` until the first turn runs.
   */
  activeProviderName: string | undefined;
  /**
   * Snapshot of every registered provider's status, in registration order.
   * Populated on connect (or on settings-open if not yet loaded) and reused
   * for the lifetime of the session — provider registration is static.
   */
  providerStatuses: AIProviderRegistryStatusEntry[];
  activeAgent: Omit<AgentConfig, 'toolHandlers'> | undefined;
  /**
   * Name of the agent the user has pinned via the picker. `null` means the
   * orchestrator chooses each turn (Auto). Survives lifecycle events but
   * resets on page refresh. Only changed by explicit user picker action (or
   * the host's `setAgent` API) — never by the orchestrator's auto-pin on
   * stateful activation. See {@link flowOwnerAgentName} for that.
   */
  pinnedAgentName: string | null;
  /**
   * Name of the agent that currently owns a guided flow — set by the
   * orchestrator when a stateful agent's `onActivate` fires, cleared by
   * `releaseAgent` or session teardown. While non-null, routing precedence
   * goes to this agent and the picker is locked. Independent of
   * {@link pinnedAgentName}: a flow can wrap up and release without
   * disturbing the user's sticky picker choice.
   */
  flowOwnerAgentName: string | null;
  /**
   * The user prompt that started the current guided flow (GENC-1351 §5.9) —
   * captured when a flow-owner locks in, cleared when it releases. Persisted only
   * into a *non-resumable tombstone* (not the normal snapshot), where it pre-fills
   * the composer on return so the user can re-run the task. Slice-backed (like
   * {@link flowOwnerAgentName}) so both element instances agree and it survives
   * pop-in/out.
   */
  flowActivationPrompt: string | null;
  /**
   * Whether the agent picker slide-out panel is currently open. Lives in the
   * slice so the bubble-dialog and popout-panel instances stay in sync as the
   * user pops in and out.
   */
  agentPickerOpen: boolean;
  /**
   * Whether the lifecycle (session) menu slide-out panel is open. Mirrors
   * `agentPickerOpen` — kept in the slice so panel state survives pop-in/out.
   */
  sessionMenuOpen: boolean;
  /**
   * Whether a manual compaction's async summary is in flight (GENC-1351 §5.7).
   * In the slice (not element-local) so the "Compacting…" transcript view and
   * input-disable stay in sync across pop-in/out, like the panel flags above.
   */
  compacting: boolean;
  /**
   * Whether a page-reload restore is in flight — `provider.load()` hasn't
   * resolved yet (GENC-1351 §6). In the slice (like `compacting`) so the
   * "Restoring…" transcript view + input-disable survive pop-in/out. Invisible
   * for synchronous providers (resolves before paint); matters for server-backed
   * ones (S3 latency), where the disable also **prevents a mid-load send from
   * being clobbered** when the restored history loads.
   */
  restoring: boolean;
  /** Draft text in the input box — preserved across pop-in/pop-out cycles. */
  inputValue: string;
  /** Live trace from a currently-executing sub-agent. Cleared on sub-agent-stop. */
  liveSubAgentTrace: ChatMessage[];
  /** Name of the currently-executing sub-agent, or null when idle. */
  liveSubAgentName: string | null;
  /**
   * In-flight per-call chat-input overrides pushed by `requestSubAgent` or
   * `requestInteraction` calls. The most restrictive entry (`'hidden'` >
   * `'disabled'`) wins; an empty list means the agent-level
   * `chatInputDuringExecution` applies.
   */
  inputOverrides: InputOverride[];
}

/**
 * The subset of session state restored verbatim from a persisted session
 * (page-reload restore). Agent *identity* (`activeAgent`) and per-agent machine
 * snapshots are applied separately by the host — resolve the name against the
 * live agents array, then hand the snapshot to the agent's `init({ restored })`.
 *
 * @internal
 */
export interface LoadSessionPayload {
  messages: ChatMessage[];
  pinnedAgentName: string | null;
  flowOwnerAgentName: string | null;
  contextTokens: number | undefined;
  contextLimit: number | undefined;
  activeModel: string | undefined;
  activeProviderName: string | undefined;
}

/**
 * Build a fresh default session state. A factory (not just a shared const) so
 * each call returns independent collections — `resetSession` can hand back a
 * clean slate without aliasing the initial state's arrays.
 */
export function createDefaultSessionState(): AiAssistantSessionState {
  return {
    messages: [],
    state: 'idle',
    showToolCalls: false,
    showThinkingSteps: false,
    showNarration: false,
    showAgentSwitchIndicator: false,
    enabledAnimations: [],
    suggestionsState: { status: 'idle' },
    contextTokens: undefined,
    contextLimit: undefined,
    sessionUsage: emptyUsage(),
    activeModel: undefined,
    activeProviderName: undefined,
    providerStatuses: [],
    activeAgent: undefined,
    pinnedAgentName: null,
    flowOwnerAgentName: null,
    flowActivationPrompt: null,
    agentPickerOpen: false,
    sessionMenuOpen: false,
    compacting: false,
    restoring: false,
    inputValue: '',
    liveSubAgentTrace: [],
    liveSubAgentName: null,
    inputOverrides: [],
  };
}

export const defaultSessionState: AiAssistantSessionState = createDefaultSessionState();

export const aiAssistantSlice = createSlice({
  name: 'aiAssistant',
  initialState: defaultSessionState,
  reducers: {
    setMessages(state, action: PayloadAction<ChatMessage[]>) {
      state.messages = action.payload;
    },
    setState(state, action: PayloadAction<AiAssistantState>) {
      state.state = action.payload;
    },
    setShowToolCalls(state, action: PayloadAction<boolean>) {
      state.showToolCalls = action.payload;
    },
    setShowThinkingSteps(state, action: PayloadAction<boolean>) {
      state.showThinkingSteps = action.payload;
    },
    setShowNarration(state, action: PayloadAction<boolean>) {
      state.showNarration = action.payload;
    },
    setShowAgentSwitchIndicator(state, action: PayloadAction<boolean>) {
      state.showAgentSwitchIndicator = action.payload;
    },
    setEnabledAnimations(state, action: PayloadAction<AiAssistantAnimation[]>) {
      state.enabledAnimations = action.payload;
    },
    setSuggestionsState(state, action: PayloadAction<SuggestionsState>) {
      state.suggestionsState = action.payload;
    },
    setContextTokens(state, action: PayloadAction<number | undefined>) {
      state.contextTokens = action.payload;
    },
    setContextLimit(state, action: PayloadAction<number | undefined>) {
      state.contextLimit = action.payload;
    },
    setSessionUsage(state, action: PayloadAction<AggregateUsage>) {
      state.sessionUsage = action.payload;
    },
    setActiveModel(state, action: PayloadAction<string | undefined>) {
      state.activeModel = action.payload;
    },
    setActiveProviderName(state, action: PayloadAction<string | undefined>) {
      state.activeProviderName = action.payload;
    },
    setProviderStatuses(state, action: PayloadAction<AIProviderRegistryStatusEntry[]>) {
      state.providerStatuses = action.payload;
    },
    setActiveAgent(state, action: PayloadAction<Omit<AgentConfig, 'toolHandlers'> | undefined>) {
      state.activeAgent = action.payload;
    },
    setPinnedAgentName(state, action: PayloadAction<string | null>) {
      state.pinnedAgentName = action.payload;
    },
    setFlowOwnerAgentName(state, action: PayloadAction<string | null>) {
      state.flowOwnerAgentName = action.payload;
    },
    setFlowActivationPrompt(state, action: PayloadAction<string | null>) {
      state.flowActivationPrompt = action.payload;
    },
    setAgentPickerOpen(state, action: PayloadAction<boolean>) {
      state.agentPickerOpen = action.payload;
    },
    setSessionMenuOpen(state, action: PayloadAction<boolean>) {
      state.sessionMenuOpen = action.payload;
    },
    setCompacting(state, action: PayloadAction<boolean>) {
      state.compacting = action.payload;
    },
    setRestoring(state, action: PayloadAction<boolean>) {
      state.restoring = action.payload;
    },
    setInputValue(state, action: PayloadAction<string>) {
      state.inputValue = action.payload;
    },
    setLiveSubAgentTrace(state, action: PayloadAction<ChatMessage[]>) {
      state.liveSubAgentTrace = action.payload;
    },
    setLiveSubAgentName(state, action: PayloadAction<string | null>) {
      state.liveSubAgentName = action.payload;
    },
    addInputOverride(state, action: PayloadAction<InputOverride>) {
      state.inputOverrides.push(action.payload);
    },
    removeInputOverride(state, action: PayloadAction<{ id: string }>) {
      state.inputOverrides = state.inputOverrides.filter((o) => o.id !== action.payload.id);
    },
    /**
     * Bulk-hydrate the restorable fields of a persisted session (page-reload
     * restore) and set `state` back to `idle`. Agent identity and the per-agent
     * machine snapshot are applied separately by the host — see
     * `LoadSessionPayload`.
     */
    loadSession(state, action: PayloadAction<LoadSessionPayload>) {
      const p = action.payload;
      state.messages = p.messages;
      state.pinnedAgentName = p.pinnedAgentName;
      state.flowOwnerAgentName = p.flowOwnerAgentName;
      // Derive the totals from the restored transcript rather than trusting a stored
      // counter. Restore dispatches this action directly, bypassing the host's
      // `messages` setter — the thing that normally recomputes — so a stored total
      // would sit stale (historically at 0) until the next turn arrived. The
      // transcript is the source of truth: per-message usage survives the snapshot,
      // and a compacted summary carries the usage of the turns it replaced.
      state.sessionUsage = sumUsage(p.messages);
      state.contextTokens = p.contextTokens;
      state.contextLimit = p.contextLimit;
      state.activeModel = p.activeModel;
      state.activeProviderName = p.activeProviderName;
      state.state = 'idle';
    },
    /**
     * Wipe the session back to a fresh default — the "Clear" / "New chat" action.
     * The user's **display preferences** (tool-call / thinking / narration / agent-switch
     * visibility + animations) are deliberately preserved across Clear: they are settings,
     * not conversation state, and are persisted independently of the snapshot (GENC-1351).
     * Without this they'd flip to their all-off defaults in the live store on Clear, since
     * `clearSession` re-applies no baseline and the idempotent setters suppress a re-apply.
     */
    resetSession(state) {
      const preservedPrefs = {
        showToolCalls: state.showToolCalls,
        showThinkingSteps: state.showThinkingSteps,
        showNarration: state.showNarration,
        showAgentSwitchIndicator: state.showAgentSwitchIndicator,
        enabledAnimations: state.enabledAnimations,
      };
      Object.assign(state, createDefaultSessionState(), preservedPrefs);
    },
  },
  selectors: {},
});
