import { useCallback, useEffect, useRef, useState } from 'react';
import { AppShell } from '@add-ideas/toolbox-shell-react';
import '@add-ideas/toolbox-shell-react/styles.css';

import { DiagnosticsPanel } from './components/DiagnosticsPanel.js';
import {
  ExportControls,
  type OneNoteExportFormat,
} from './components/ExportControls.js';
import { FileDropZone } from './components/FileDropZone.js';
import { HelpDialog } from './components/HelpDialog.js';
import { PackageInspector } from './components/PackageInspector.js';
import { PageReader } from './components/PageReader.js';
import {
  PackageNavigation,
  SingleSectionNavigation,
} from './components/SectionNavigation.js';
import type {
  OneNoteNotebookNodeDto,
  OneNotePackageDto,
  OneNotePackagedSectionDto,
  OneNotePageDto,
  OneNoteResourcePayloadDto,
  OneNoteSectionDto,
} from './onenote/model/dto.js';
import type { ParserDiagnostic } from './onenote/model/diagnostics.js';
import { selectSourceFiles } from './source-file.js';
import { toolboxApp } from './toolbox/manifest.js';
import {
  OneNoteWorkerClient,
  WorkerClientError,
} from './worker/onenote.client.js';

type LoadedSource =
  | {
      kind: 'one';
      sessionId: string;
      section: OneNoteSectionDto;
    }
  | {
      kind: 'onepkg';
      sessionId: string;
      notebook: OneNotePackageDto;
    };

type ApplicationState =
  | { phase: 'idle' }
  | { phase: 'reading'; fileName: string }
  | { phase: 'loaded'; fileName: string; source: LoadedSource }
  | {
      phase: 'error';
      title: string;
      message: string;
      detail?: string;
    };

type PageState =
  | { phase: 'empty' }
  | { phase: 'loading'; pageId: string }
  | { phase: 'loaded'; page: OneNotePageDto }
  | { phase: 'error'; message: string };

function uniqueDiagnostics(
  diagnostics: ParserDiagnostic[]
): ParserDiagnostic[] {
  const seen = new Set<string>();
  return diagnostics.filter((diagnostic) => {
    const key = JSON.stringify([
      diagnostic.severity,
      diagnostic.code,
      diagnostic.message,
      diagnostic.offset,
      diagnostic.structure,
      diagnostic.propertyId,
    ]);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function sectionsInNotebookOrder(
  notebook: OneNotePackageDto
): OneNotePackagedSectionDto[] {
  const sectionById = new Map(
    notebook.sections.map((section) => [section.id, section] as const)
  );
  const ordered: OneNotePackagedSectionDto[] = [];
  const seen = new Set<string>();
  const visit = (nodes: OneNoteNotebookNodeDto[]) => {
    for (const node of nodes) {
      if (node.kind === 'section-group') {
        visit(node.children);
        continue;
      }
      const section = sectionById.get(node.sectionId);
      if (section && !seen.has(section.id)) {
        ordered.push(section);
        seen.add(section.id);
      }
    }
  };
  visit(notebook.tree.nodes);
  ordered.push(...notebook.sections.filter((section) => !seen.has(section.id)));
  return ordered;
}

export interface OneNoteApplicationProps {
  createWorkerClient?: () => OneNoteWorkerClient;
}

export function OneNoteApplication({
  createWorkerClient = () => new OneNoteWorkerClient(),
}: OneNoteApplicationProps) {
  const [state, setState] = useState<ApplicationState>({ phase: 'idle' });
  const [pageState, setPageState] = useState<PageState>({ phase: 'empty' });
  const [selectedSectionId, setSelectedSectionId] = useState<
    string | undefined
  >();
  const activeClient = useRef<OneNoteWorkerClient | null>(null);
  const generation = useRef(0);
  const pageGeneration = useRef(0);

  const clearFile = useCallback(() => {
    generation.current += 1;
    pageGeneration.current += 1;
    activeClient.current?.terminate();
    activeClient.current = null;
    setSelectedSectionId(undefined);
    setPageState({ phase: 'empty' });
    setState({ phase: 'idle' });
  }, []);

  useEffect(() => {
    return () => {
      generation.current += 1;
      pageGeneration.current += 1;
      activeClient.current?.terminate();
    };
  }, []);

  const loadPage = async (
    sessionId: string,
    pageId: string,
    sectionId?: string
  ) => {
    const client = activeClient.current;
    const requestGeneration = ++pageGeneration.current;
    if (!client) {
      setPageState({
        phase: 'error',
        message: 'The local parser session is no longer available.',
      });
      return;
    }

    setPageState({ phase: 'loading', pageId });
    try {
      const response = await client.getPage(sessionId, pageId, sectionId);
      if (requestGeneration !== pageGeneration.current) return;
      if (response.type === 'failure') {
        setPageState({ phase: 'error', message: response.error.message });
        return;
      }
      setPageState({ phase: 'loaded', page: response.page });
    } catch (error) {
      if (requestGeneration !== pageGeneration.current) return;
      setPageState({
        phase: 'error',
        message:
          error instanceof WorkerClientError
            ? error.message
            : 'The selected page could not be read from the parser session.',
      });
    }
  };

  const openFiles = async (files: File[]) => {
    const selection = selectSourceFiles(files);
    if (!selection.ok) {
      setState({
        phase: 'error',
        title: selection.title,
        message: selection.message,
      });
      return;
    }

    generation.current += 1;
    pageGeneration.current += 1;
    const requestGeneration = generation.current;
    activeClient.current?.terminate();
    const client = createWorkerClient();
    activeClient.current = client;
    setSelectedSectionId(undefined);
    setPageState({ phase: 'empty' });
    setState({ phase: 'reading', fileName: selection.source.name });

    try {
      const buffers = await Promise.all(
        [selection.source, ...selection.cabinetParts].map((file) =>
          file.arrayBuffer()
        )
      );
      if (generation.current !== requestGeneration) return;
      const response = await client.parseSource(
        selection.kind,
        selection.source.name,
        buffers[0]!,
        selection.cabinetParts.map((part, index) => ({
          fileName: part.name,
          bytes: buffers[index + 1]!,
        }))
      );
      if (generation.current !== requestGeneration) return;

      if (response.type === 'failure') {
        client.terminate();
        if (activeClient.current === client) activeClient.current = null;
        setState({
          phase: 'error',
          title: 'Could not open this OneNote source',
          message: response.error.message,
          detail: response.error.structure
            ? `${response.error.code} · ${response.error.structure}`
            : response.error.code,
        });
        return;
      }

      if (response.type === 'section') {
        setState({
          phase: 'loaded',
          fileName: selection.source.name,
          source: {
            kind: 'one',
            sessionId: response.sessionId,
            section: response.section,
          },
        });
        const firstPage = response.section.pages[0];
        if (firstPage) void loadPage(response.sessionId, firstPage.id);
        return;
      }

      const orderedSections = sectionsInNotebookOrder(response.notebook);
      const preferredSection =
        orderedSections.find(
          (section) =>
            section.parseStatus === 'parsed' &&
            section.section !== undefined &&
            section.section.pages.length > 0
        ) ??
        orderedSections.find(
          (section) => section.parseStatus === 'parsed' && section.section
        ) ??
        orderedSections[0];
      setSelectedSectionId(preferredSection?.id);
      setState({
        phase: 'loaded',
        fileName: selection.source.name,
        source: {
          kind: 'onepkg',
          sessionId: response.sessionId,
          notebook: response.notebook,
        },
      });
      const firstPage = preferredSection?.section?.pages[0];
      if (firstPage) {
        void loadPage(response.sessionId, firstPage.id, preferredSection?.id);
      }
    } catch (error) {
      if (generation.current !== requestGeneration) return;
      client.terminate();
      if (activeClient.current === client) activeClient.current = null;
      setState({
        phase: 'error',
        title: 'Local parser stopped',
        message:
          error instanceof WorkerClientError
            ? error.message
            : 'The local file could not be handed to the parser worker.',
      });
    }
  };

  const selectedPageId =
    pageState.phase === 'loading'
      ? pageState.pageId
      : pageState.phase === 'loaded'
        ? pageState.page.id
        : undefined;
  const selectedPage =
    pageState.phase === 'loaded' ? pageState.page : undefined;

  const selectPackageSection = (sectionId: string) => {
    if (state.phase !== 'loaded' || state.source.kind !== 'onepkg') return;
    setSelectedSectionId(sectionId);
    const section = state.source.notebook.sections.find(
      (candidate) => candidate.id === sectionId
    );
    const firstPage = section?.section?.pages[0];
    if (firstPage) {
      void loadPage(state.source.sessionId, firstPage.id, sectionId);
    } else {
      pageGeneration.current += 1;
      setPageState({ phase: 'empty' });
    }
  };

  const previewSelectedResource = useCallback(
    async (resourceId: string): Promise<OneNoteResourcePayloadDto> => {
      const client = activeClient.current;
      if (!client || state.phase !== 'loaded') {
        throw new WorkerClientError(
          'session-not-available',
          'The local parser session is no longer available.'
        );
      }
      const response = await client.getResourcePreview(
        state.source.sessionId,
        resourceId,
        state.source.kind === 'onepkg' ? selectedSectionId : undefined
      );
      if (response.type === 'failure') {
        throw new WorkerClientError(
          response.error.code,
          response.error.message
        );
      }
      return response.resource;
    },
    [selectedSectionId, state]
  );

  const downloadSelectedResource = useCallback(
    async (resourceId: string, suggestedName?: string): Promise<void> => {
      const client = activeClient.current;
      if (!client || state.phase !== 'loaded') {
        throw new WorkerClientError(
          'session-not-available',
          'The local parser session is no longer available.'
        );
      }
      const response = await client.getResourceDownload(
        state.source.sessionId,
        resourceId,
        state.source.kind === 'onepkg' ? selectedSectionId : undefined
      );
      if (response.type === 'failure') {
        throw new WorkerClientError(
          response.error.code,
          response.error.message
        );
      }
      const resource = response.resource;
      const filename = safeDownloadFilename(
        resource.filename ??
          suggestedName ??
          `onenote-resource.${resource.extension ?? 'bin'}`
      );
      downloadBrowserFile(resource.bytes, resource.mediaType, filename);
    },
    [selectedSectionId, state]
  );

  const exportLoadedSource = useCallback(
    async (format: OneNoteExportFormat): Promise<void> => {
      const client = activeClient.current;
      if (!client || state.phase !== 'loaded') {
        throw new WorkerClientError(
          'session-not-available',
          'The local parser session is no longer available.'
        );
      }
      const response = await client.exportSession(
        state.source.sessionId,
        format
      );
      if (response.type === 'failure') {
        throw new WorkerClientError(
          response.error.code,
          response.error.message
        );
      }
      downloadBrowserFile(
        response.artifact.bytes,
        response.artifact.mediaType,
        safeDownloadFilename(response.artifact.filename)
      );
    },
    [state]
  );

  const visibleDiagnostics = uniqueDiagnostics(
    (() => {
      if (state.phase !== 'loaded') return [];
      const pageDiagnostics = selectedPage?.diagnostics ?? [];
      if (state.source.kind === 'one') {
        return [...state.source.section.diagnostics, ...pageDiagnostics];
      }
      const selectedSection = state.source.notebook.sections.find(
        (section) => section.id === selectedSectionId
      );
      return [
        ...state.source.notebook.diagnostics,
        ...(selectedSection?.diagnostics ?? []),
        ...(selectedSection?.section?.diagnostics ?? []),
        ...pageDiagnostics,
      ];
    })()
  );

  return (
    <main className="application">
      {state.phase === 'idle' ? (
        <FileDropZone onFiles={(files) => void openFiles(files)} />
      ) : null}

      {state.phase === 'reading' ? (
        <section className="status-card" aria-live="polite">
          <p className="status-card__label">Local parser worker</p>
          <h2>Opening {state.fileName}</h2>
          <p>
            Validating the source and extracting supported content in this
            browser.
          </p>
          <button type="button" onClick={clearFile}>
            Cancel
          </button>
        </section>
      ) : null}

      {state.phase === 'loaded' ? (
        <>
          <header className="source-toolbar">
            <div>
              <p className="eyebrow">
                {state.source.kind === 'one' ? 'OneNote section' : 'ONEPKG'}
              </p>
              <h2>{state.fileName}</h2>
            </div>
            <button type="button" onClick={clearFile}>
              Clear file
            </button>
          </header>

          {state.source.kind === 'onepkg' ? (
            <PackageInspector notebook={state.source.notebook} />
          ) : null}

          <ExportControls onExport={exportLoadedSource} />

          <div className="reader-workspace">
            {state.source.kind === 'one' ? (
              <SingleSectionNavigation
                section={state.source.section}
                selectedPageId={selectedPageId}
                onSelectPage={(pageId) =>
                  void loadPage(state.source.sessionId, pageId)
                }
              />
            ) : (
              <PackageNavigation
                tree={state.source.notebook.tree}
                sections={state.source.notebook.sections}
                selectedSectionId={selectedSectionId}
                selectedPageId={selectedPageId}
                onSelectSection={selectPackageSection}
                onSelectPage={(pageId) =>
                  void loadPage(
                    state.source.sessionId,
                    pageId,
                    selectedSectionId
                  )
                }
              />
            )}
            <PageReader
              page={selectedPage}
              loading={pageState.phase === 'loading'}
              error={
                pageState.phase === 'error' ? pageState.message : undefined
              }
              previewResource={previewSelectedResource}
              downloadResource={downloadSelectedResource}
            />
          </div>

          <DiagnosticsPanel diagnostics={visibleDiagnostics} />
        </>
      ) : null}

      {state.phase === 'error' ? (
        <section className="status-card status-card--error" role="alert">
          <p className="status-card__label">Could not open file</p>
          <h2>{state.title}</h2>
          <p>{state.message}</p>
          {state.detail ? <code>{state.detail}</code> : null}
          <div>
            <button type="button" onClick={clearFile}>
              Try another file
            </button>
          </div>
        </section>
      ) : null}

      <aside className="privacy-note">
        <strong>Local-only.</strong> Files are transferred to an in-browser
        worker for parsing and are not uploaded, persisted, or sent to
        analytics. Clearing ends the worker session.
      </aside>
    </main>
  );
}

function safeDownloadFilename(value: string): string {
  const basename = value.split(/[\\/]/u).at(-1) ?? value;
  const withoutUnsafeCharacters = [...basename.normalize('NFC')]
    .map((character) => {
      const code = character.codePointAt(0) ?? 0;
      return code <= 0x1f || code === 0x7f || '<>:"|?*'.includes(character)
        ? '_'
        : character;
    })
    .join('');
  const cleaned = withoutUnsafeCharacters.replace(/[. ]+$/gu, '').trim();
  return (cleaned || 'onenote-resource.bin').slice(0, 180);
}

function downloadBrowserFile(
  bytes: ArrayBuffer,
  mediaType: string,
  filename: string
): void {
  const url = URL.createObjectURL(new Blob([bytes], { type: mediaType }));
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = filename;
  anchor.rel = 'noopener';
  document.body.append(anchor);
  anchor.click();
  anchor.remove();
  window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}

export function App() {
  const [helpOpen, setHelpOpen] = useState(false);

  return (
    <AppShell
      app={toolboxApp}
      manifestUrl="./toolbox-app.json"
      helpAction={{ onClick: () => setHelpOpen(true) }}
    >
      <OneNoteApplication />
      {helpOpen ? <HelpDialog onClose={() => setHelpOpen(false)} /> : null}
    </AppShell>
  );
}
