import { useState, useEffect, useCallback, useRef } from 'react';
import type { Document, Chunk, EmbeddedChunk, ModelState } from './types';
import { loadAllModels, isAllModelsReady } from './pipeline/models';
import { chunkDocument, extractTitle } from './pipeline/chunking';
import { embedDocChunksBatch } from './pipeline/embeddings';
import { BM25Index } from './pipeline/bm25';
import { runPipeline } from './pipeline/orchestrator';
import type { PipelineState } from './components/PipelineView';
import QueryInput from './components/QueryInput';
import ModelStatus from './components/ModelStatus';
import PipelineView from './components/PipelineView';
import DocumentManager from './components/DocumentManager';

const SAMPLE_DOCS = [
  'api-design-principles.md',
  'distributed-systems-overview.md',
  'machine-learning-primer.md',
  'history-of-coffee.md',
];

const SHOWCASE_CARDS = [
  {
    title: 'Faithful to qmd',
    body: 'BM25, vector search, query expansion, RRF fusion, and reranking follow the upstream retrieval recipe instead of flattening everything into one model call.',
  },
  {
    title: 'Browser-native bits',
    body: 'Transformers.js and WebGPU run the pipeline locally, cache model weights in the browser, and expose each stage so the search system stays inspectable.',
  },
];

const INDEX_BATCH_SIZE = 8;

const INITIAL_PIPELINE: PipelineState = {
  expansion: { status: 'idle' },
  search: { status: 'idle' },
  rrf: { status: 'idle' },
  rerank: { status: 'idle' },
  blend: { status: 'idle' },
};

function upsertDocuments(current: Document[], incoming: Document[]): Document[] {
  const merged = new Map(current.map((doc) => [doc.id, doc]));
  for (const doc of incoming) {
    merged.set(doc.id, doc);
  }
  return [...merged.values()];
}

function ShowcaseCard({ title, body }: { title: string; body: string }) {
  return (
    <div
      style={{
        padding: '0.9rem 1rem',
        background: 'var(--bg-card)',
        border: '1px solid var(--border)',
        borderRadius: '10px',
        boxShadow: '0 2px 12px var(--shadow)',
      }}
    >
      <div
        style={{
          marginBottom: '0.35rem',
          fontSize: '0.74rem',
          fontWeight: 700,
          letterSpacing: '0.08em',
          textTransform: 'uppercase',
          color: '#4285F4',
        }}
      >
        {title}
      </div>
      <p
        style={{
          margin: 0,
          fontSize: '0.84rem',
          lineHeight: 1.6,
          color: 'var(--text-secondary)',
        }}
      >
        {body}
      </p>
    </div>
  );
}

function App() {
  const [models, setModels] = useState<ModelState[]>([
    { name: 'embedding', status: 'pending', progress: 0 },
    { name: 'reranker', status: 'pending', progress: 0 },
    { name: 'expansion', status: 'pending', progress: 0 },
  ]);
  const [documents, setDocuments] = useState<Document[]>([]);
  const [chunks, setChunks] = useState<Chunk[]>([]);
  const [embeddedChunks, setEmbeddedChunks] = useState<EmbeddedChunk[]>([]);
  const [bm25Index, setBm25Index] = useState<BM25Index | null>(null);
  const [pipeline, setPipeline] = useState<PipelineState>(INITIAL_PIPELINE);
  const [indexing, setIndexing] = useState(false);
  const [indexingProgress, setIndexingProgress] = useState({ completed: 0, total: 0 });
  const [query, setQuery] = useState('');
  const [intent, setIntent] = useState<string | undefined>();
  const [dark, setDark] = useState(() =>
    document.documentElement.getAttribute('data-theme') === 'dark',
  );
  const searchRunIdRef = useRef(0);

  const embeddingReady = models.find((model) => model.name === 'embedding')?.status === 'ready';

  useEffect(() => {
    loadAllModels((state) => {
      setModels((prev) => prev.map((model) => (
        model.name === state.name ? state : model
      )));
    }).catch(console.error);
  }, []);

  useEffect(() => {
    async function loadSampleDocs() {
      try {
        const loadedDocs = await Promise.all(
          SAMPLE_DOCS.map(async (filename) => {
            const response = await fetch(`/eval-docs/${filename}`);
            const body = await response.text();
            const title = extractTitle(body, filename);
            return { id: filename, title, body, filepath: filename };
          }),
        );
        setDocuments((prev) => upsertDocuments(prev, loadedDocs));
      } catch (error) {
        console.error(error);
      }
    }

    loadSampleDocs();
  }, []);

  useEffect(() => {
    if (documents.length === 0) {
      setChunks([]);
      setEmbeddedChunks([]);
      setBm25Index(null);
      setIndexing(false);
      setIndexingProgress({ completed: 0, total: 0 });
      return;
    }

    const nextChunks = documents.flatMap((doc) => chunkDocument(doc));
    setChunks(nextChunks);
    setBm25Index(new BM25Index(nextChunks));
  }, [documents]);

  useEffect(() => {
    let cancelled = false;

    if (!embeddingReady || chunks.length === 0) {
      setEmbeddedChunks([]);
      setIndexing(false);
      setIndexingProgress({ completed: 0, total: chunks.length });
      return () => {
        cancelled = true;
      };
    }

    async function embedChunks() {
      setIndexing(true);
      setIndexingProgress({ completed: 0, total: chunks.length });

      const embedded: EmbeddedChunk[] = [];
      for (let i = 0; i < chunks.length; i += INDEX_BATCH_SIZE) {
        const batch = chunks.slice(i, i + INDEX_BATCH_SIZE);
        const embeddings = await embedDocChunksBatch(
          batch.map((chunk) => ({ title: chunk.title, text: chunk.text })),
        );

        if (cancelled) return;

        for (let j = 0; j < batch.length; j++) {
          const chunk = batch[j];
          const embedding = embeddings[j];
          if (!chunk || !embedding) continue;
          embedded.push({ ...chunk, embedding });
        }

        setIndexingProgress({
          completed: Math.min(i + batch.length, chunks.length),
          total: chunks.length,
        });
      }

      if (cancelled) return;

      setEmbeddedChunks(embedded);
      setIndexing(false);
    }

    embedChunks().catch((error) => {
      if (cancelled) return;
      console.error(error);
      setEmbeddedChunks([]);
      setIndexing(false);
    });

    return () => {
      cancelled = true;
    };
  }, [chunks, embeddingReady]);

  const handleUpload = useCallback(async (files: FileList) => {
    const uploadedDocs = await Promise.all(
      Array.from(files).map(async (file) => {
        const body = await file.text();
        const title = extractTitle(body, file.name);
        return { id: file.name, title, body, filepath: file.name };
      }),
    );

    setDocuments((prev) => upsertDocuments(prev, uploadedDocs));
  }, []);

  const handlePaste = useCallback((text: string, filename: string) => {
    const title = extractTitle(text, filename);
    setDocuments((prev) => upsertDocuments(prev, [
      { id: filename, title, body: text, filepath: filename },
    ]));
  }, []);

  const handleSearch = useCallback(async (searchQuery: string, searchIntent?: string) => {
    if (!bm25Index || embeddedChunks.length === 0) return;

    const runId = ++searchRunIdRef.current;
    setQuery(searchQuery);
    setIntent(searchIntent);
    setPipeline(INITIAL_PIPELINE);

    const generator = runPipeline({
      query: searchQuery,
      intent: searchIntent,
      embeddedChunks,
      bm25Index,
    });

    for await (const event of generator) {
      if (searchRunIdRef.current !== runId) return;

      setPipeline((prev) => ({
        ...prev,
        [event.stage]: {
          status: event.status,
          ...('data' in event ? { data: event.data } : {}),
          ...('error' in event ? { error: event.error } : {}),
        },
      }));
    }
  }, [bm25Index, embeddedChunks]);

  const allReady = isAllModelsReady() && embeddedChunks.length > 0 && !indexing;

  const toggleTheme = useCallback(() => {
    setDark((prev) => {
      const next = !prev;
      document.documentElement.setAttribute('data-theme', next ? 'dark' : 'light');
      localStorage.setItem('qmd-theme', next ? 'dark' : 'light');
      return next;
    });
  }, []);

  return (
    <div
      style={{
        fontFamily: 'system-ui, -apple-system, sans-serif',
        maxWidth: 1400,
        margin: '0 auto',
        padding: '1.25rem 1rem 2rem',
      }}
    >
      <style>{`
        .showcase-grid {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 0.85rem;
          margin-top: 1rem;
        }

        @media (max-width: 900px) {
          .showcase-grid {
            grid-template-columns: 1fr;
          }
        }
      `}</style>

      <header style={{ marginBottom: '1.5rem' }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: '1rem' }}>
          <div style={{ flex: 1 }}>
            <div
              style={{
                marginBottom: '0.4rem',
                fontSize: '0.74rem',
                fontWeight: 700,
                letterSpacing: '0.08em',
                textTransform: 'uppercase',
                color: '#4285F4',
              }}
            >
              QMD in the browser
            </div>
            <h1 style={{ margin: 0, fontSize: '1.7rem', color: 'var(--text)' }}>
              QMD Web Sandbox
            </h1>
            <p style={{ margin: '0.45rem 0 0', color: 'var(--text-secondary)', fontSize: '0.9rem', lineHeight: 1.65, maxWidth: 860 }}>
              A browser-native sandbox that recreates the core{' '}
              <a href="https://github.com/tobi/qmd" target="_blank" rel="noopener noreferrer" style={{ color: '#4285F4', textDecoration: 'none' }}>qmd</a>
              {' '}retrieval pipeline with Transformers.js, while making the local WebGPU execution path visible.
              Documents are chunked, embedded, searched, fused, reranked, and inspected entirely in the browser.
            </p>
            <div
              style={{
                marginTop: '0.7rem',
                display: 'inline-flex',
                alignItems: 'center',
                gap: '0.35rem',
                flexWrap: 'wrap',
              }}
            >
              {[
                { label: 'WebGPU', color: '#4285F4' },
                { label: 'Local cache', color: '#34a853' },
                { label: 'Transparent pipeline', color: '#00897b' },
              ].map(badge => (
                <span
                  key={badge.label}
                  style={{
                    padding: '0.25rem 0.55rem',
                    borderRadius: '999px',
                    border: `1px solid ${badge.color}30`,
                    background: `${badge.color}10`,
                    color: badge.color,
                    fontSize: '0.72rem',
                    fontWeight: 600,
                    fontFamily: 'system-ui, -apple-system, sans-serif',
                    whiteSpace: 'nowrap',
                  }}
                >
                  {badge.label}
                </span>
              ))}
            </div>
          </div>

          <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexShrink: 0 }}>
            <a
              href="https://github.com/tobi/qmd"
              target="_blank"
              rel="noopener noreferrer"
              style={{
                fontSize: '0.78rem',
                color: 'var(--text-secondary)',
                textDecoration: 'none',
                padding: '0.35rem 0.7rem',
                border: '1px solid var(--border)',
                borderRadius: '999px',
                fontFamily: 'system-ui, -apple-system, sans-serif',
                background: 'var(--bg-card)',
              }}
              onMouseEnter={(event) => { event.currentTarget.style.color = '#4285F4'; }}
              onMouseLeave={(event) => { event.currentTarget.style.color = 'var(--text-secondary)'; }}
            >
              Original qmd
            </a>
            <button
              onClick={toggleTheme}
              title={dark ? 'Switch to light mode' : 'Switch to dark mode'}
              style={{
                background: 'var(--bg-card)',
                border: '1px solid var(--border)',
                borderRadius: '999px',
                padding: '0.35rem 0.6rem',
                cursor: 'pointer',
                fontSize: '1rem',
                lineHeight: 1,
                color: 'var(--text)',
              }}
            >
              {dark ? '\u2600' : '\u263E'}
            </button>
          </div>
        </div>

        <div className="showcase-grid">
          {SHOWCASE_CARDS.map((card) => (
            <ShowcaseCard key={card.title} title={card.title} body={card.body} />
          ))}
        </div>
      </header>

      <ModelStatus models={models} />

      {indexing && (
        <div
          style={{
            padding: '0.6rem 1rem',
            background: 'var(--indexing-bg)',
            borderRadius: 8,
            marginBottom: '1rem',
            fontSize: '0.84rem',
            color: 'var(--text)',
            border: '1px solid var(--border)',
          }}
        >
          Indexing local chunks in the browser ({indexingProgress.completed}/{indexingProgress.total})...
        </div>
      )}

      <QueryInput onSearch={handleSearch} disabled={!allReady} />

      {query && <PipelineView state={pipeline} query={query} intent={intent} />}

      <DocumentManager
        documents={documents.map((doc) => ({ id: doc.id, title: doc.title, filepath: doc.filepath }))}
        onUpload={handleUpload}
        onPaste={handlePaste}
      />
    </div>
  );
}

export default App;
