'use client';

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {loyalyAiRepository} from '@/features/loyaly-ai/repositories/loyalyAiRepository';
import {
  applyStreamedPart,
  createConversation,
  createMessage,
  titleFromPrompt,
  toHistory,
} from '@/features/loyaly-ai/services/loyalyAiService';
import type {
  ChatMessage,
  Conversation,
  ConversationSummary,
} from '@/features/loyaly-ai/types/chat';

/**
 * All Loyaly AI state, mounted in app/providers.tsx — ABOVE the route tree.
 *
 * Two things force that placement:
 *  1. Route changes. The panel lives in the workspace shell, which the App
 *     Router preserves across sibling navigations, so it would survive
 *     /dashboard → /staff on its own.
 *  2. The tablet breakpoint. Crossing it swaps the inline panel for a
 *     slide-over, which genuinely unmounts the surface. Holding state up here
 *     means the presentation can change without losing a conversation, a
 *     half-typed message, or a stream in flight.
 *
 * Every component below is a pure view over this context.
 */

export type PanelMode = 'normal' | 'expanded' | 'fullscreen';

interface LoyalyAiValue {
  /** Inline panel visibility, above the laptop breakpoint. */
  isOpen: boolean;
  setIsOpen: (v: boolean) => void;
  toggle: () => void;
  /** Slide-over visibility, below it. */
  isSlideOverOpen: boolean;
  setSlideOverOpen: (v: boolean) => void;

  panelMode: PanelMode;
  setPanelMode: (mode: PanelMode) => void;
  toggleExpand: () => void;
  toggleFullscreen: () => void;
  panelWidth: number;
  setPanelWidth: (w: number) => void;

  conversation: Conversation;
  history: ConversationSummary[];
  isHistoryOpen: boolean;
  setHistoryOpen: (v: boolean) => void;

  /** The composer value. Kept here so navigation never drops it. */
  draft: string;
  setDraft: (v: string) => void;

  send: (text: string) => void;
  stop: () => void;
  isStreaming: boolean;

  newChat: () => void;
  openConversation: (id: string) => void;
}

const LoyalyAiContext = createContext<LoyalyAiValue | null>(null);

const STORAGE_MODE_KEY = 'loyaly_ai_panel_mode';
const STORAGE_WIDTH_KEY = 'loyaly_ai_panel_width';

export function LoyalyAiProvider({children}: {children: React.ReactNode}) {
  const [isOpen, setIsOpen] = useState(true);
  const [isSlideOverOpen, setSlideOverOpen] = useState(false);
  const [isHistoryOpen, setHistoryOpen] = useState(false);
  const [draft, setDraft] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);

  const [panelMode, setPanelModeState] = useState<PanelMode>('normal');
  const [panelWidth, setPanelWidthState] = useState<number>(440);

  // Sync saved localStorage settings post-hydration to eliminate SSR mismatch
  useEffect(() => {
    const savedMode = localStorage.getItem(STORAGE_MODE_KEY);
    if (savedMode === 'expanded' || savedMode === 'fullscreen' || savedMode === 'normal') {
      setPanelModeState(savedMode as PanelMode);
    }
    const savedWidth = localStorage.getItem(STORAGE_WIDTH_KEY);
    if (savedWidth) {
      const parsed = parseInt(savedWidth, 10);
      if (!isNaN(parsed) && parsed >= 420 && parsed <= 1800) {
        setPanelWidthState(parsed);
      }
    }
  }, []);

  const prevModeRef = useRef<PanelMode>('normal');

  const setPanelMode = useCallback((mode: PanelMode) => {
    setPanelModeState((prev) => {
      prevModeRef.current = prev;
      if (typeof window !== 'undefined') {
        localStorage.setItem(STORAGE_MODE_KEY, mode);
      }
      return mode;
    });
  }, []);

  const setPanelWidth = useCallback((w: number) => {
    const clamped = Math.max(420, Math.min(w, typeof window !== 'undefined' ? window.innerWidth * 0.85 : 1400));
    setPanelWidthState(clamped);
    if (typeof window !== 'undefined') {
      localStorage.setItem(STORAGE_WIDTH_KEY, clamped.toString());
    }
  }, []);

  const toggleExpand = useCallback(() => {
    setPanelModeState((prev) => {
      const next = prev === 'expanded' ? 'normal' : 'expanded';
      if (typeof window !== 'undefined') {
        localStorage.setItem(STORAGE_MODE_KEY, next);
      }
      return next;
    });
  }, []);

  const toggleFullscreen = useCallback(() => {
    setPanelModeState((prev) => {
      const next = prev === 'fullscreen' ? (prevModeRef.current === 'fullscreen' ? 'normal' : prevModeRef.current) : 'fullscreen';
      prevModeRef.current = prev;
      if (typeof window !== 'undefined') {
        localStorage.setItem(STORAGE_MODE_KEY, next);
      }
      return next;
    });
  }, []);

  // Every conversation this session, newest state included. The active one is
  // referenced by id rather than held separately, so there is no way for the
  // list and the open chat to disagree about the same conversation.
  const [conversations, setConversations] = useState<Conversation[]>(() => [
    createConversation(),
  ]);
  const [activeId, setActiveId] = useState<string>(
    () => conversations[0]?.id ?? '',
  );

  // Aborts the stream in flight. A ref because it is machinery, not state:
  // nothing renders differently because a controller exists.
  const abortRef = useRef<AbortController | null>(null);

  const conversation = useMemo(
    () => conversations.find((c) => c.id === activeId) ?? conversations[0],
    [conversations, activeId],
  );

  const history = useMemo(() => toHistory(conversations), [conversations]);

  /** Update one conversation in place, stamping updatedAt. */
  const patchConversation = useCallback(
    (id: string, update: (c: Conversation) => Conversation) => {
      setConversations((prev) =>
        prev.map((c) =>
          c.id === id ? {...update(c), updatedAt: new Date().toISOString()} : c,
        ),
      );
    },
    [],
  );

  const stop = useCallback(() => {
    abortRef.current?.abort();
    abortRef.current = null;
    setIsStreaming(false);
    // Clear the streaming flag on whatever was being written, or the cursor
    // blinks forever on an answer that stopped arriving.
    setConversations((prev) =>
      prev.map((c) => ({
        ...c,
        messages: c.messages.map((m) =>
          m.isStreaming ? {...m, isStreaming: false} : m,
        ),
      })),
    );
  }, []);

  const send = useCallback(
    (text: string) => {
      const prompt = text.trim();
      if (!prompt || isStreaming) return;

      const targetId = conversation.id;
      const userMessage = createMessage('user', [{type: 'text', text: prompt}]);
      const assistantMessage = createMessage('assistant', [], {
        isStreaming: true,
      });

      patchConversation(targetId, (c) => ({
        ...c,
        // Named from the first prompt, so history is readable without opening
        // anything. Later prompts do not rename it — a conversation that
        // renamed itself as it went would be impossible to find again.
        title: c.messages.length === 0 ? titleFromPrompt(prompt) : c.title,
        messages: [...c.messages, userMessage, assistantMessage],
      }));
      setDraft('');
      setIsStreaming(true);

      const controller = new AbortController();
      abortRef.current = controller;

      void (async () => {
        try {
          const stream = loyalyAiRepository.streamReply({
            prompt,
            history: conversation.messages,
            signal: controller.signal,
          });
          for await (const part of stream) {
            patchConversation(targetId, (c) => ({
              ...c,
              messages: c.messages.map((m) =>
                m.id === assistantMessage.id ? applyStreamedPart(m, part) : m,
              ),
            }));
          }
        } finally {
          // Runs on success, on abort and on failure. Whatever happened, the
          // message must stop claiming to be mid-stream.
          if (abortRef.current === controller) abortRef.current = null;
          patchConversation(targetId, (c) => ({
            ...c,
            messages: c.messages.map((m) =>
              m.id === assistantMessage.id ? {...m, isStreaming: false} : m,
            ),
          }));
          setIsStreaming(false);
        }
      })();
    },
    [conversation, isStreaming, patchConversation],
  );

  const newChat = useCallback(() => {
    stop();
    setConversations((prev) => {
      // Reuse the current one when it is already empty. Otherwise every click
      // of New chat strands another blank conversation in memory.
      const current = prev.find((c) => c.id === activeId);
      if (current && current.messages.length === 0) return prev;
      const fresh = createConversation();
      setActiveId(fresh.id);
      return [fresh, ...prev];
    });
    setDraft('');
    setHistoryOpen(false);
  }, [activeId, stop]);

  const openConversation = useCallback(
    (id: string) => {
      stop();
      setActiveId(id);
      setHistoryOpen(false);
    },
    [stop],
  );

  const toggle = useCallback(() => setIsOpen((v) => !v), []);

  const value = useMemo<LoyalyAiValue>(
    () => ({
      isOpen,
      setIsOpen,
      toggle,
      isSlideOverOpen,
      setSlideOverOpen,
      panelMode,
      setPanelMode,
      toggleExpand,
      toggleFullscreen,
      panelWidth,
      setPanelWidth,
      conversation,
      history,
      isHistoryOpen,
      setHistoryOpen,
      draft,
      setDraft,
      send,
      stop,
      isStreaming,
      newChat,
      openConversation,
    }),
    [
      isOpen,
      toggle,
      isSlideOverOpen,
      panelMode,
      setPanelMode,
      toggleExpand,
      toggleFullscreen,
      panelWidth,
      setPanelWidth,
      conversation,
      history,
      isHistoryOpen,
      draft,
      send,
      stop,
      isStreaming,
      newChat,
      openConversation,
    ],
  );

  return (
    <LoyalyAiContext value={value}>{children}</LoyalyAiContext>
  );
}

export function useLoyalyAi(): LoyalyAiValue {
  const ctx = useContext(LoyalyAiContext);
  if (!ctx) {
    throw new Error('useLoyalyAi must be used inside <LoyalyAiProvider>');
  }
  return ctx;
}

/** Re-exported so components do not reach past the provider for a type. */
export type {ChatMessage};
