"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api } from './api';
import { useArtifactLayout } from './use-artifact-layout';
import { useSpaces } from './use-spaces';
import { useDashboardSessions, getSavedLastSessionId } from './use-dashboard-sessions';
import { useMainAgentStream } from './use-main-agent-stream';
import { useRuntimeActions } from './use-runtime-actions';
import { useConfigPanel } from './use-config-panel';
import { useTrustMode } from './use-trust-mode';
import { useModelPicker } from './use-model-picker';

export function useDashboardState() {
  // ── Top-level state not owned by a leaf hook ──────────────────────────────
  const [state, setState] = useState(null);
  const [chat, setChat] = useState([
    {
      role: 'agent',
      text: 'Fresh workspace ready. I am the madarX Main Agent. No demo agents, cron jobs, reports, or old runtime data are loaded. Tell me what to create first.'
    }
  ]);
  const [usage, setUsage] = useState({ model: '', input: 0, output: 0, cache_read: 0, cache_creation: 0, context: 0 });
  const [sessionId, setSessionId] = useState(null);
  const [sessionArtifactId, setSessionArtifactId] = useState(null);
  const [chatHidden, setChatHidden] = useState(false);
  const [chatHydrated, setChatHydrated] = useState(false);
  // Hydrate selectedProjectId synchronously from localStorage so the very first
  // /api/state fetch is already filtered to the user's last project. Without
  // this, the dashboard fetches unfiltered (returning artifacts from every
  // project), then re-fetches once the auto-select effect picks 'personal',
  // causing a visible flash of cross-project artifacts in the main space.
  //
  // Fresh install (no saved value) defaults to the canonical 'personal' slug.
  // /api/state resolves slug → id server-side so the filter works before the
  // projects list has hydrated client-side.
  const [selectedProjectId, _setSelectedProjectId] = useState(() => {
    if (typeof window === 'undefined') return 'personal';
    try { return localStorage.getItem('madarx:selected-project') || 'personal'; } catch { return 'personal'; }
  });
  const setSelectedProjectId = useCallback((next) => {
    _setSelectedProjectId(next);
    if (typeof window !== 'undefined') {
      try {
        if (next) localStorage.setItem('madarx:selected-project', next);
        else localStorage.removeItem('madarx:selected-project');
      } catch { /* storage disabled */ }
    }
  }, []);
  const [voiceOpen, setVoiceOpen] = useState(false);

  // ── Leaf hooks ────────────────────────────────────────────────────────────
  const configPanel = useConfigPanel();
  const trustMode = useTrustMode();
  const modelPicker = useModelPicker();

  // ── Derived from modelPicker ───────────────────────────────────────────────
  const { selectedModel, setSelectedModel } = modelPicker;
  const ctxLimit = selectedModel?.startsWith('claude-') ? 200000 : 32768;
  const ctxPct = Math.min(100, Math.round(((usage.context || 0) / ctxLimit) * 100));

  // ── Existing hooks ────────────────────────────────────────────────────────
  function statePath() {
    return selectedProjectId ? `/api/state?project_id=${encodeURIComponent(selectedProjectId)}` : '/api/state';
  }

  async function refresh(signal) {
    setState(await api(statePath(), signal ? { signal } : undefined));
  }

  // Optimistic patch so dock + canvas update before the server move POST
  // settles. Pairs with use-spaces.js::moveArtifactToSpace.
  const patchArtifactSpace = useCallback((artifactId, nextSpaceId) => {
    setState((prev) => {
      if (!prev || !Array.isArray(prev.artifacts)) return prev;
      let changed = false;
      const artifacts = prev.artifacts.map((a) => {
        if (a.id !== artifactId) return a;
        if (a.space_id === nextSpaceId) return a;
        changed = true;
        return { ...a, space_id: nextSpaceId };
      });
      return changed ? { ...prev, artifacts } : prev;
    });
  }, []);

  const spaces = useSpaces({ onArtifactMoved: patchArtifactSpace, refresh });
  // Per-space artifact membership is derived from `artifact.space_id`, not a
  // client-side reconciler. Missing space_id (legacy rows backfilled on next
  // server access) collapses to 'default' so the UI is never empty.
  const activeSpaceArtifactIds = useMemo(() => {
    const list = (state?.artifacts || []).filter((a) => a.content);
    return list
      .filter((a) => (a.space_id || 'default') === spaces.activeSpaceId)
      .map((a) => a.id);
  }, [state?.artifacts, spaces.activeSpaceId]);
  const artifactLayout = useArtifactLayout({
    state,
    refresh,
    spaceArtifactIds: activeSpaceArtifactIds,
    activeSpaceId: spaces.activeSpaceId,
    moveArtifactToSpace: spaces.moveArtifactToSpace,
  });
  const sessions = useDashboardSessions({
    sessionId,
    setSessionId,
    setChat,
    setUsage,
    setSelectedModel,
    setSessionArtifactId,
  });
  const mainAgent = useMainAgentStream({
    chat,
    setChat,
    selectedModel,
    sessionId,
    setSessionId,
    sessionArtifactId,
    setSessionArtifactId,
    setUsage,
    refresh,
    refreshSessions: sessions.refreshSessions,
    bringToFront: artifactLayout.bringToFront,
    projectId: selectedProjectId,
    runs: state?.runs || null,
  });
  const runtimeActions = useRuntimeActions({ refresh, setChat });

  // ── Always-latest refs so mount-only effect reads current values ──────────
  // Direct render-body assignment is the React-recommended pattern for refs
  // that mirror the latest closure value. This lets the mount-only effect
  // read current values without re-subscribing and without eslint-disable.
  const selectedProjectIdRef = useRef(selectedProjectId);
  const mainAgentRef = useRef(mainAgent);
  const sessionsRef = useRef(sessions);
  const openConfigRef = useRef(null);
  const configPanelRef = useRef(configPanel);
  const trustModeRef = useRef(trustMode);
  const modelPickerRef = useRef(modelPicker);
  selectedProjectIdRef.current = selectedProjectId;
  mainAgentRef.current = mainAgent;
  sessionsRef.current = sessions;
  openConfigRef.current = configPanel.openConfig;
  configPanelRef.current = configPanel;
  trustModeRef.current = trustMode;
  modelPickerRef.current = modelPicker;

  // ── Trusted set of artifact iframe windows ────────────────────────────────
  const artifactFramesRef = useRef(null);
  if (artifactFramesRef.current === null) artifactFramesRef.current = new WeakSet();
  const registerArtifactFrame = useCallback((win) => {
    if (win) artifactFramesRef.current.add(win);
  }, []);

  // ── Auto-select default project ───────────────────────────────────────────
  // Runs when projects hydrate. Resolves slug → id (first mount may carry the
  // 'personal' slug from localStorage / default) so the canonical id is what we
  // store + persist. Skips when selectedProjectId already matches a project id.
  useEffect(() => {
    const projects = state?.projects;
    if (!Array.isArray(projects) || projects.length === 0) return;
    if (selectedProjectId && projects.some(p => p?.id === selectedProjectId)) return;
    const matchBySlug = selectedProjectId
      ? projects.find(p => p?.slug === selectedProjectId)
      : null;
    const personal = projects.find(p => p?.slug === 'personal' || p?.id === 'personal');
    const firstActive = projects.find(p => p?.status === 'active');
    const pick = matchBySlug || personal || firstActive || projects[0];
    if (pick?.id) setSelectedProjectId(pick.id);
  }, [state?.projects, selectedProjectId, setSelectedProjectId]);

  // ── Mount effect: poll + postMessage bridge + initial fetches ─────────────
  useEffect(() => {
    let cancelled = false;
    let inFlight = null;
    let pollInterval = null;
    let safetyInterval = null;
    let eventSource = null;
    let refreshTimer = null;

    // Slices the server publishes on `lib/live-events.js::publishStateChanged`.
    // Must stay in sync with STATE_SLICES there. Each name is a discrete SSE
    // event name (`state.<slice>.changed`).
    const LIVE_SLICES = ['agents', 'runs', 'artifacts', 'heartbeat', 'approvals',
      'connectors', 'schedules', 'sessions', 'local_actions', 'local_processes',
      'hermes_jobs', 'projects'];

    function currentStatePath() {
      const pid = selectedProjectIdRef.current;
      return pid ? `/api/state?project_id=${encodeURIComponent(pid)}` : '/api/state';
    }

    async function poll() {
      if (document.visibilityState === 'hidden' || inFlight) return;
      const controller = new AbortController();
      inFlight = controller;
      try {
        const next = await api(currentStatePath(), { signal: controller.signal });
        if (!cancelled) {
          setState(next);
          if (next?.main_agent_trust_mode) trustModeRef.current.setTrustMode(next.main_agent_trust_mode);
          // First-response flag flip: if live-deltas turned on between mounts,
          // swap the 5s poll for SSE + 60s safety. Idempotent — once SSE is
          // running we don't restart.
          if (next?.live_deltas_enabled && !eventSource) {
            startSseAndSafetyNet();
            if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
          }
        }
      } catch { /* best-effort polling */ }
      finally {
        if (inFlight === controller) inFlight = null;
      }
    }

    // Debounce coalesces bursts of state.*.changed events so the dashboard does
    // not refetch /api/state once per slice during a single mutation. 250ms is
    // short enough that humans still perceive updates as immediate.
    function scheduleRefresh() {
      if (refreshTimer) return;
      refreshTimer = setTimeout(() => {
        refreshTimer = null;
        poll();
      }, 250);
    }

    function startSseAndSafetyNet() {
      try {
        eventSource = new EventSource('/api/events/stream');
        for (const slice of LIVE_SLICES) {
          eventSource.addEventListener(`state.${slice}.changed`, scheduleRefresh);
        }
        // EventSource auto-reconnects on transport error. The 60s safety net
        // also catches missed mutations (e.g. a code path that forgets to emit).
        eventSource.addEventListener('error', () => { /* observe; auto-retry */ });
      } catch {
        eventSource = null;
      }
      safetyInterval = setInterval(poll, 60000);
    }

    // Always do the initial hydration call. If the response advertises
    // live-deltas, branch to SSE; otherwise fall back to the legacy 5s poll
    // (P3 soak week — `MADARX_DASHBOARD_LIVE_DELTAS` is off by default).
    poll().then(() => {
      if (cancelled || pollInterval || safetyInterval) return;
      // poll() already started SSE if the flag is on. If not, start the 5s loop.
      if (!eventSource) pollInterval = setInterval(poll, 5000);
    });

    function onArtifactMessage(ev) {
      if (ev.origin !== window.location.origin) return;
      if (!ev.source || !artifactFramesRef.current.has(ev.source)) return;
      const data = ev?.data;
      if (!data || typeof data !== 'object') return;
      if (data.type !== 'madarx:prompt') return;
      const text = typeof data.text === 'string' ? data.text.trim() : '';
      if (!text) return;
      const agent = mainAgentRef.current;
      if (!agent || agent.busy) return;
      agent.command(text);
    }
    window.addEventListener('message', onArtifactMessage);

    // Resync when the tab regains focus. Both polling and SSE branches skip
    // fetches while hidden (poll() guards on visibilityState; SSE events still
    // arrive but the safety interval is a backstop, not a catch-up). On
    // visibility-change we force-refetch once so the dashboard never lingers on
    // stale data after the user returns.
    function onVisibilityChange() {
      if (document.visibilityState !== 'visible') return;
      poll();
    }
    document.addEventListener('visibilitychange', onVisibilityChange);

    api('/api/config').then(d => {
      configPanelRef.current.setDesignStyle(d.designStyle ?? 'balanced');
      configPanelRef.current.setConfigTools(d.tools || []);
      if (d.model) {
        modelPickerRef.current.setSelectedModel(d.model);
        setUsage(u => ({ ...u, model: d.model }));
      }
    }).catch(() => {});
    api('/api/models').then(d => modelPickerRef.current.setModels(d.models || [])).catch(() => {});
    api('/api/config/env').then(d => configPanelRef.current.setConfigEnv(d.env || [])).catch(() => {});
    api('/api/settings/trust-mode').then(d => trustModeRef.current.setTrustMode(d.mode || 'approval_required')).catch(() => {});

    api('/api/chat/sessions').then(async d => {
      const list = d.sessions || [];
      const sx = sessionsRef.current;
      sx.setSessions(list);
      if (list.length) {
        const saved = getSavedLastSessionId();
        const restore = list.find(s => s.id === saved) || list[0];
        await sx.openSession(restore.id);
      }
    }).catch(() => {}).finally(() => {
      if (!cancelled) setChatHydrated(true);
    });

    return () => {
      cancelled = true;
      if (inFlight) inFlight.abort();
      if (pollInterval) clearInterval(pollInterval);
      if (safetyInterval) clearInterval(safetyInterval);
      if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = null; }
      if (eventSource) { try { eventSource.close(); } catch { /* ignore */ } }
      window.removeEventListener('message', onArtifactMessage);
      document.removeEventListener('visibilitychange', onVisibilityChange);
    };
    // Mount-only: all reads go through refs (selectedProjectIdRef, mainAgentRef,
    // sessionsRef, configPanelRef, trustModeRef, modelPickerRef), and writes use
    // useState setters which are exempt from the exhaustive-deps rule.
  }, []);

  // ── madarx:open-config window event ──────────────────────────────────────
  useEffect(() => {
    function handleOpenConfig() {
      openConfigRef.current?.();
    }
    window.addEventListener('madarx:open-config', handleOpenConfig);
    return () => window.removeEventListener('madarx:open-config', handleOpenConfig);
  }, []);

  // ── Derived state ─────────────────────────────────────────────────────────
  const initializing = state === null || !chatHydrated;
  const counts = state || { agents: [], runs: [], schedules: [], connectors: [], artifacts: [], hermes_jobs: [] };
  const approvalRun = useMemo(
    () => (counts.runs || []).find(r => r.kind === 'claude-code-dry-run' && r.status === 'approval-needed'),
    [counts.runs]
  );
  const activeHermesJobs = useMemo(
    () => (counts.hermes_jobs || []).filter(j => j.status === 'queued' || j.status === 'running'),
    [counts.hermes_jobs]
  );
  const pendingApprovals = counts.pending_approvals || [];
  const heartbeatTasks = counts.heartbeat_tasks || [];
  const localActions = counts.local_tool_audit || [];
  const localProcesses = counts.local_processes || [];
  const runtimeSignalCount = pendingApprovals.length + heartbeatTasks.length + localActions.length + localProcesses.length;
  const artifacts = useMemo(
    () => (counts.artifacts || []).filter(a => a.content),
    [counts.artifacts]
  );
  const visibleArtifacts = useMemo(
    () => artifacts.filter(a => (a.space_id || 'default') === spaces.activeSpaceId && !artifactLayout.minimizedIds[a.id]),
    [artifacts, spaces.activeSpaceId, artifactLayout.minimizedIds]
  );
  const dockedArtifacts = useMemo(
    () => artifacts.filter(a => (a.space_id || 'default') === spaces.activeSpaceId && artifactLayout.minimizedIds[a.id]),
    [artifacts, spaces.activeSpaceId, artifactLayout.minimizedIds]
  );
  const zForArtifact = useCallback((a) => {
    const stableIdx = artifacts.findIndex(x => x.id === a.id);
    return artifactLayout.cardZ?.[a.id] ?? (10 + Math.max(0, stableIdx));
  }, [artifacts, artifactLayout.cardZ]);
  const frontArtifactId = useMemo(
    () => visibleArtifacts.reduce((frontId, a) => {
      if (!frontId) return a.id;
      const front = visibleArtifacts.find(x => x.id === frontId);
      return zForArtifact(a) >= zForArtifact(front) ? a.id : frontId;
    }, null),
    [visibleArtifacts, zForArtifact]
  );

  return {
    // raw state
    state,
    chat,
    chatHydrated,
    chatHidden,
    setChatHidden,
    usage,
    setUsage,
    sessionId,
    sessionArtifactId,
    selectedProjectId,
    setSelectedProjectId,
    voiceOpen,
    setVoiceOpen,
    // derived
    initializing,
    counts,
    approvalRun,
    activeHermesJobs,
    pendingApprovals,
    heartbeatTasks,
    localActions,
    localProcesses,
    runtimeSignalCount,
    artifacts,
    visibleArtifacts,
    dockedArtifacts,
    zForArtifact,
    frontArtifactId,
    ctxLimit,
    ctxPct,
    // leaf hooks (spread so consumers can destructure anything)
    ...configPanel,
    ...trustMode,
    ...modelPicker,
    // existing hooks
    spaces,
    artifactLayout,
    sessions,
    mainAgent,
    runtimeActions,
    // frame registration
    registerArtifactFrame,
  };
}
