import { useCoreServices } from '@learnitive/context';
import {
  createProjectCodeMirror,
  resolveProjectEditorLanguage,
  selectLatexCodeMirrorLocation,
  type ProjectCodeMirrorSession,
} from '@learnitive/latex-codemirror';
import { projectFilePath } from '@learnitive/project-storage';
import { toast } from '@learnitive/ui-components';
import { WsPath } from '@learnitive/ws-path';
import { useAtomValue } from 'jotai';
import React from 'react';
import {
  persistSourceEditorFontSize,
  persistSourceEditorLineNumbers,
  persistSourceEditorSpellcheck,
  persistSourceEditorWordWrap,
  readSourceEditorPreferences,
  readSourceEditorSpellcheck,
} from '../editor-preferences';
import {
  PROJECT_EDITOR_FLUSH_EVENT,
  PROJECT_EDITOR_REVEAL_EVENT,
  consumeProjectEditorReveal,
  type ProjectEditorFlushRequest,
  type ProjectEditorRevealRequest,
} from '../project-compile-ui';
import { generateAutomaticCodeSuggestion } from '../code-ai';
import { ensureCompileAuthentication } from '../compile-auth-gate';
import { SourceEditorToolbar } from './source-editor-toolbar';

const PROJECT_TOOLBAR_TEST_IDS = {
  cursor: 'project-cursor-location',
  wordWrap: 'project-word-wrap-toggle',
  settings: 'project-source-editor-settings',
  fontSize: 'project-font-size',
  lineNumbers: 'project-line-numbers',
  spellcheck: 'project-spellcheck',
  fullscreen: 'project-fullscreen-toggle',
} as const;

export function ProjectCodeEditor({ wsPath, className = '', onOpenReviewThread }: { wsPath: string; className?: string; onOpenReviewThread?: (threadId: string) => void }) {
  const { fileSystem, projectAi, projectAiWriting, projectCompileOrchestrator, projectReview } = useCoreServices();
  const sectionRef = React.useRef<HTMLElement>(null);
  const hostRef = React.useRef<HTMLDivElement>(null);
  const sessionRef = React.useRef<ProjectCodeMirrorSession | undefined>(undefined);
  const pendingRef = React.useRef<string | undefined>(undefined);
  const canonicalRef = React.useRef('');
  const saveTimerRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const aiTimerRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const aiControllerRef = React.useRef<AbortController | undefined>(undefined);
  const cursorOffsetRef = React.useRef(0);
  const initialPreferencesRef = React.useRef<ReturnType<typeof readSourceEditorPreferences> | undefined>(undefined);
  if (!initialPreferencesRef.current) initialPreferencesRef.current = readSourceEditorPreferences();
  const initialPreferences = initialPreferencesRef.current;
  const [loaded, setLoaded] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [dirty, setDirty] = React.useState(false);
  const [wordWrap, setWordWrap] = React.useState(initialPreferences.wordWrap);
  const [showLineNumbers, setShowLineNumbers] = React.useState(initialPreferences.lineNumbers);
  const [fontSize, setFontSize] = React.useState(initialPreferences.fontSize);
  const [spellcheck, setSpellcheck] = React.useState(() => readSourceEditorSpellcheck('code'));
  const [fullscreen, setFullscreen] = React.useState(false);
  const [cursorLocation, setCursorLocation] = React.useState({ line: 1, column: 1 });
  const [historyState, setHistoryState] = React.useState({ canUndo: false, canRedo: false });
  const fileUpdateCount = useAtomValue(fileSystem.$fileContentUpdateCount);
  const reviewVersion = useAtomValue(projectReview.$version);
  const aiWritingSettings = useAtomValue(projectAiWriting.$settings);
  const compileState = useAtomValue(projectCompileOrchestrator.$state);
  const file = WsPath.assertFile(wsPath);
  const projectPath = projectFilePath(file.path);
  const language = resolveProjectEditorLanguage(file.path);
  const trackChangesEnabled = projectReview.getTrackChangesEnabled(file.wsName);
  const trackChangesRef = React.useRef(trackChangesEnabled);
  trackChangesRef.current = trackChangesEnabled;

  const automaticSuggestionsRef = React.useRef(aiWritingSettings.automaticSuggestions);
  automaticSuggestionsRef.current = aiWritingSettings.automaticSuggestions;

  const scheduleAutomaticCodeSuggestionRef = React.useRef<(next: string) => void>(() => undefined);

  const save = React.useCallback(async () => {
    if (trackChangesRef.current) return;
    const text = pendingRef.current;
    if (text === undefined) return;
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    setSaving(true);
    try {
      await fileSystem.writeFile(
        wsPath,
        new File([text], file.fileName, { type: 'text/plain;charset=utf-8' }),
      );
      pendingRef.current = undefined;
      setDirty(false);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : t.app.sourceEditorToolbar.saveFailed);
      throw error;
    } finally {
      setSaving(false);
    }
  }, [file.fileName, fileSystem, wsPath]);

  React.useEffect(() => {
    void projectReview.refresh(file.wsName);
  }, [file.wsName, projectReview]);

  const compile = React.useCallback(async () => {
    if (!(await ensureCompileAuthentication())) return;
    try {
      await projectCompileOrchestrator.compile(file.wsName, {
        activePath: file.path,
        prepare: save,
      });
    } catch {
      // Durable execution errors are shown by ProjectExecutionPane.
    }
  }, [file.path, file.wsName, projectCompileOrchestrator, save]);

  const scheduleAutomaticCodeSuggestion = React.useCallback((next: string) => {
    if (aiTimerRef.current) clearTimeout(aiTimerRef.current);
    aiControllerRef.current?.abort();
    if (automaticSuggestionsRef.current === 'off' || trackChangesRef.current) return;
    aiTimerRef.current = setTimeout(() => {
      aiTimerRef.current = undefined;
      if (automaticSuggestionsRef.current === 'off' || trackChangesRef.current) return;
      if (projectReview.getChanges(file.wsName, { status: 'pending', file: projectPath }).length > 0) return;
      const controller = new AbortController();
      aiControllerRef.current = controller;
      void (async () => {
        try {
          await save();
          const current = sessionRef.current?.view.state.doc.toString() ?? next;
          if (current !== next) return;
          const suggestion = await generateAutomaticCodeSuggestion({
            projectAi,
            filePath: projectPath,
            language: language.label,
            source: current,
            cursor: cursorOffsetRef.current,
            diagnostics: compileState.wsName===file.wsName?[compileState.error,compileState.result?.stderr,...(compileState.result?.diagnostics||[]).map((item)=>`${item.severity}${item.file?` ${item.file}${item.line?`:${item.line}`:''}`:''}: ${item.message}`)].filter(Boolean).join('\n'):'',
            signal: controller.signal,
          });
          if (!suggestion || controller.signal.aborted) return;
          const latest = sessionRef.current?.view.state.doc.toString();
          if (latest !== current) return;
          const thread = await projectReview.proposeSourceChange(file.wsName, {
            filePath: projectPath,
            from: suggestion.from,
            to: suggestion.to,
            originalText: suggestion.originalText,
            proposedText: suggestion.replacementText,
            author: { id: 'learnitive-code-auto', displayName: 'Learnitive AI · Automatic Code', kind: 'ai' },
            metadata: { source: 'ai', automatic: true, category: 'code', reason: suggestion.reason, confidence: suggestion.confidence },
          });
          onOpenReviewThread?.(thread.id);
        } catch (error) {
          if (!controller.signal.aborted) {
            // Automatic suggestions are intentionally quiet; configuration and
            // provider errors remain available through AI Settings/manual AI Review.
            void error;
          }
        } finally {
          if (aiControllerRef.current === controller) aiControllerRef.current = undefined;
        }
      })();
    }, 1600);
  }, [compileState, file.wsName, language.label, onOpenReviewThread, projectAi, projectPath, projectReview, save]);
  scheduleAutomaticCodeSuggestionRef.current = scheduleAutomaticCodeSuggestion;

  const reveal = React.useCallback((request: ProjectEditorRevealRequest) => {
    if (request.wsPath !== wsPath) return;
    const session = sessionRef.current;
    if (!session) return;
    selectLatexCodeMirrorLocation(session.view, request.line, request.column ?? 1, { focus: true });
  }, [wsPath]);

  React.useLayoutEffect(() => {
    if (!loaded) return;
    const pending = consumeProjectEditorReveal(wsPath);
    if (pending) reveal(pending);
  }, [loaded, reveal, wsPath]);

  React.useEffect(() => {
    const onReveal = (raw: Event) => reveal((raw as CustomEvent<ProjectEditorRevealRequest>).detail);
    window.addEventListener(PROJECT_EDITOR_REVEAL_EVENT, onReveal);
    return () => window.removeEventListener(PROJECT_EDITOR_REVEAL_EVENT, onReveal);
  }, [reveal]);

  React.useEffect(() => {
    const onFlush = (raw: Event) => {
      const event = raw as CustomEvent<ProjectEditorFlushRequest>;
      if (event.detail?.wsName !== file.wsName) return;
      event.detail.waitUntil(save());
    };
    window.addEventListener(PROJECT_EDITOR_FLUSH_EVENT, onFlush);
    return () => window.removeEventListener(PROJECT_EDITOR_FLUSH_EVENT, onFlush);
  }, [file.wsName, save]);

  React.useEffect(() => () => {
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    if (aiTimerRef.current) clearTimeout(aiTimerRef.current);
    aiControllerRef.current?.abort();
    void save();
  }, [save]);

  React.useEffect(() => {
    let cancelled = false;
    const load = async () => {
      if (dirty) return;
      const text = await fileSystem.readFileAsText(wsPath);
      if (cancelled || text === undefined || !hostRef.current) return;
      canonicalRef.current = text;
      const displayText = trackChangesRef.current
        ? projectReview.getProjectedSource(file.wsName, projectPath, text)
        : text;
      if (!sessionRef.current) {
        sessionRef.current = createProjectCodeMirror({
          doc: displayText,
          parent: hostRef.current,
          ariaLabel: `${language.label} editor`,
          language: language.id,
          softWrap: wordWrap,
          showLineNumbers,
          fontSize,
          spellcheck,
          onSave: () => void save(),
          onCompile: () => void compile(),
          onCursorChange: ({ offset, line, column }) => {
            cursorOffsetRef.current = offset;
            setCursorLocation({ line, column });
            projectAi.setEditorContext(file.wsName, { path: projectPath, line, column, offset });
          },
          onHistoryChange: setHistoryState,
          onOpenReviewThread: (threadId) => onOpenReviewThread?.(threadId),
          onChanges: (edits) => {
            if (!trackChangesRef.current) return;
            const canonical = canonicalRef.current;
            for (const edit of [...edits].sort((a, b) => b.from - a.from || b.to - a.to)) {
              projectReview.applyProjectedEdit(file.wsName, projectPath, canonical, edit);
            }
          },
          onChange: (next) => {
            if (trackChangesRef.current) return;
            pendingRef.current = next;
            setDirty(true);
            if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
            saveTimerRef.current = setTimeout(() => void save(), 450);
            scheduleAutomaticCodeSuggestionRef.current(next);
          },
        });
      } else {
        sessionRef.current.applyExternalText(displayText);
      }
      sessionRef.current.setReviewDecorations(projectReview.getEditorDecorations(file.wsName, projectPath, text));
      projectAi.setEditorContext(file.wsName, { path: projectPath, line: 1, column: 1, offset: 0 });
      setLoaded(true);
    };
    void load();
    return () => { cancelled = true; };
  }, [compile, dirty, file.wsName, fileSystem, fileUpdateCount, fontSize, language.id, language.label, onOpenReviewThread, projectAi, projectPath, projectReview, reviewVersion, save, showLineNumbers, spellcheck, wordWrap, wsPath]);

  React.useEffect(() => () => {
    sessionRef.current?.destroy();
    sessionRef.current = undefined;
  }, [wsPath]);

  React.useEffect(() => {
    sessionRef.current?.setSoftWrap(wordWrap);
    persistSourceEditorWordWrap(wordWrap);
  }, [wordWrap]);

  React.useEffect(() => {
    sessionRef.current?.setLineNumbers(showLineNumbers);
    persistSourceEditorLineNumbers(showLineNumbers);
  }, [showLineNumbers]);

  React.useEffect(() => {
    sessionRef.current?.setFontSize(fontSize);
    persistSourceEditorFontSize(fontSize);
  }, [fontSize]);

  React.useEffect(() => {
    sessionRef.current?.setSpellcheck(spellcheck);
    persistSourceEditorSpellcheck('code', spellcheck);
  }, [spellcheck]);

  React.useEffect(() => {
    const onFullscreenChange = () => {
      setFullscreen(document.fullscreenElement === sectionRef.current);
      sessionRef.current?.view.requestMeasure();
    };
    document.addEventListener('fullscreenchange', onFullscreenChange);
    return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
  }, []);

  const toggleFullscreen = async () => {
    const section = sectionRef.current;
    if (!section) return;
    if (document.fullscreenElement === section) await document.exitFullscreen();
    else await section.requestFullscreen();
  };

  const status = trackChangesEnabled
    ? 'Changes tracked'
    : saving
      ? t.app.sourceEditorToolbar.saving
      : dirty
        ? t.app.sourceEditorToolbar.unsaved
        : loaded
          ? t.app.sourceEditorToolbar.saved
          : t.app.sourceEditorToolbar.loading;

  return (
    <section
      ref={sectionRef}
      className={`flex min-h-0 flex-1 flex-col overflow-hidden bg-background ${className}`}
      data-testid="project-code-editor"
      data-editor-engine="project-code"
      data-ws-path={wsPath}
    >
      <SourceEditorToolbar
        languageLabel={language.label}
        path={file.path}
        cursor={cursorLocation}
        status={status}
        wordWrap={wordWrap}
        fontSize={fontSize}
        lineNumbers={showLineNumbers}
        spellcheck={spellcheck}
        fullscreen={fullscreen}
        canUndo={!trackChangesEnabled && historyState.canUndo}
        canRedo={!trackChangesEnabled && historyState.canRedo}
        onWordWrapChange={setWordWrap}
        onFontSizeChange={setFontSize}
        onLineNumbersChange={setShowLineNumbers}
        onSpellcheckChange={setSpellcheck}
        onFullscreen={toggleFullscreen}
        onUndo={() => {
          if (trackChangesEnabled) { toast.info('Reject a pending review change to undo it while Track Changes is on.'); return; }
          sessionRef.current?.undo();
        }}
        onRedo={() => { if (!trackChangesEnabled) sessionRef.current?.redo(); }}
        onSave={() => void save()}
        saveDisabled={!dirty || saving}
        testIds={PROJECT_TOOLBAR_TEST_IDS}
      />
      <div ref={hostRef} className="min-h-0 flex-1 overflow-hidden" />
    </section>
  );
}
