import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import { readdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

// ────────────────────────────────────────────────────────────────────────
// Sticker manifest plugin
// ────────────────────────────────────────────────────────────────────────
//
// Exposes a virtual module `virtual:sticker-manifest` that emits a list of
// sticker filenames present in `public/stickers/` at build time. The client
// imports this list to render the Stickers tab without paying for a runtime
// directory listing or shipping a separately-maintained manifest file.
//
// Why a virtual module rather than `import.meta.glob`:
//   Vite's docs explicitly state that files in `public/` should not be
//   imported — they're served as-is, untouched by the build. So glob globs
//   against `public/` don't work. A virtual module is the documented escape
//   hatch when we want build-time access to public assets without giving up
//   their stable, hash-free URLs.
//
// Stable URLs matter here because the SERVER ALSO needs to read these files
// during export (see server.js → resolveImageSource). If we used /src/assets/
// the URLs would be hashed in prod and the server couldn't recover the
// original filename. Keeping the stickers in public/ means the URL is
// `/stickers/<filename>` everywhere, and the server reads from
// `public/stickers/` (dev) or `dist/stickers/` (prod).
//
// On change: in dev, the plugin watches the directory and triggers an HMR
// reload when files are added or removed. Adding a sticker means: drop the
// file in, the next page render picks it up. No restart.
function stickerManifestPlugin() {
  const VIRTUAL_ID = 'virtual:sticker-manifest';
  const RESOLVED_ID = '\0' + VIRTUAL_ID;
  const STICKERS_DIR = join(__dirname, 'public', 'stickers');
  const VALID_EXT = /\.(png|webp|jpe?g|svg)$/i;

  function readStickerFilenames() {
    if (!existsSync(STICKERS_DIR)) return [];
    try {
      return readdirSync(STICKERS_DIR).filter((f) => VALID_EXT.test(f)).sort();
    } catch {
      return [];
    }
  }

  return {
    name: 'sticker-manifest',
    resolveId(id) {
      if (id === VIRTUAL_ID) return RESOLVED_ID;
      return null;
    },
    load(id) {
      if (id !== RESOLVED_ID) return null;
      const filenames = readStickerFilenames();
      // The manifest is just the filename list. URL construction is the
      // client's job (always `/stickers/<filename>`); doing it here would
      // bake in assumptions about the public path prefix.
      return `export const STICKER_FILES = ${JSON.stringify(filenames)};\n`;
    },
    configureServer(server) {
      // HMR: when files in public/stickers/ change, invalidate the virtual
      // module so the next import re-runs and the client sees the new list.
      // Vite's default file watcher already watches public/ for plain file
      // serving; we just need to react to the events.
      server.watcher.add(STICKERS_DIR);
      const onChange = (path) => {
        if (!path.startsWith(STICKERS_DIR)) return;
        const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
        if (mod) {
          server.moduleGraph.invalidateModule(mod);
          server.ws.send({ type: 'full-reload' });
        }
      };
      server.watcher.on('add', onChange);
      server.watcher.on('unlink', onChange);
      // We deliberately don't react to 'change' — a sticker file being
      // overwritten with new bytes doesn't change the manifest (still the
      // same filename); the browser will re-fetch when the user reloads.
    },
  };
}

export default defineConfig({
  plugins: [
    react(),
    stickerManifestPlugin(),
    VitePWA({
      registerType: 'prompt',
      // We register the SW manually via `useRegisterSW` in PWAInstall, so the plugin
      // shouldn't inject a registration script.
      injectRegister: false,
      includeAssets: ['favicon.svg', 'pwa-192x192.svg', 'pwa-512x512.svg'],
      manifest: {
        name: 'Apparel Designer',
        short_name: 'ApparelDesigner',
        description: 'T-shirt customization editor',
        theme_color: '#38bdf8',
        background_color: '#ffffff',
        display: 'standalone',
        orientation: 'any',
        scope: '/',
        start_url: '/',
        icons: [
          { src: 'pwa-192x192.svg', sizes: '192x192', type: 'image/svg+xml' },
          { src: 'pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml' },
          { src: 'pwa-512x512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        // Stickers live in /stickers/ and may be many in number. Don't
        // precache them — the whole point of `loading="lazy"` on the
        // <img> tags is to skip fetching stickers the user never sees,
        // which precaching would undo. They're served via the
        // `sticker-library` runtimeCaching rule below instead.
        globIgnores: ['**/stickers/**'],
        // Note: we deliberately do NOT raise `maximumFileSizeToCacheInBytes`
        // here. The default 2 MiB cap is a useful regression alarm — if
        // a future change pushes any chunk past it, the build fails loudly
        // and we know to either split further or investigate why a chunk
        // grew. Earlier this file overrode the cap to 5 MiB because the
        // pre-split bundle was 2.32 MB; that override was removed once
        // `build.rollupOptions.output.manualChunks` (below in this config)
        // brought every chunk under the default cap.
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/cdn\.huggingface\.co\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'transformers-models',
              expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 30 },
              cacheableResponse: { statuses: [0, 200] },
            },
          },
          {
            urlPattern: /^https:\/\/cdn-lfs\.huggingface\.co\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'transformers-lfs',
              expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 30 },
              cacheableResponse: { statuses: [0, 200] },
            },
          },
          {
            urlPattern: /^\/uploads\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'uploaded-images',
              expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 },
            },
          },
          {
            urlPattern: /^\/api\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-responses',
              expiration: { maxEntries: 50, maxAgeSeconds: 300 },
              cacheableResponse: { statuses: [0, 200] },
              networkTimeoutSeconds: 3,
            },
          },
          {
            urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'google-fonts',
              expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
            },
          },
          {
            urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'gstatic-fonts',
              expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 },
            },
          },
          {
            // Stickers are static brand assets — once fetched, they're
            // safe to cache aggressively. CacheFirst minimizes network
            // traffic when the user reopens the editor. Capacity (200)
            // is comfortably more than a typical library; new stickers
            // pushed via a deploy will displace old ones on access.
            urlPattern: /^\/stickers\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'sticker-library',
              expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 },
              cacheableResponse: { statuses: [0, 200] },
            },
          },
        ],
      },
    }),
  ],
  server: {
    port: 3000,
    proxy: {
      '/api': { target: 'http://localhost:3001', changeOrigin: true },
      '/uploads': { target: 'http://localhost:3001', changeOrigin: true },
      '/exports': { target: 'http://localhost:3001', changeOrigin: true },
    },
  },
  build: {
    outDir: 'dist',
    // ────────────────────────────────────────────────────────────────
    // Manual chunk splitting
    // ────────────────────────────────────────────────────────────────
    //
    // Why we split rather than ship one bundle
    // ─────────────────────────────────────────
    // 1. Workbox precache cap. The PWA plugin's default
    //    `maximumFileSizeToCacheInBytes` is 2 MiB. A single bundle
    //    containing Konva + react-konva + @huggingface/transformers +
    //    react + filerobot + everything else easily clears 2.3 MB,
    //    which fails the precache step. Splitting keeps every chunk
    //    comfortably under the cap without needing to override it.
    //
    // 2. Browser cache reuse across deploys. When app code changes
    //    (the common case during active development), only the
    //    `index` chunk's hash flips; vendor chunks (Konva, React,
    //    transformers) keep their old hash and stay cached in users'
    //    browsers. That means returning users only re-download the
    //    relatively-small app chunk, not the 1-MB-each library
    //    chunks that haven't changed.
    //
    // 3. Parallel download. Browsers can fetch multiple chunks
    //    concurrently on HTTP/2, so loading 4 × 600 KB chunks is
    //    typically faster than 1 × 2.3 MB chunk on broadband, even
    //    before considering caching.
    //
    // What goes where
    // ───────────────
    //   transformers/      @huggingface/transformers \u2014 the in-browser
    //                     ML runtime used for background removal.
    //                     Biggest single dependency. Only the
    //                     inference engine is bundled here; the
    //                     actual ML model weights stream from the
    //                     huggingface CDN at runtime (see
    //                     `transformers-models` / `transformers-lfs`
    //                     runtimeCaching rules above for offline
    //                     re-use).
    //   konva/            konva + react-konva + use-image \u2014 the canvas
    //                     stack. All three are needed together
    //                     because react-konva wraps konva and
    //                     use-image wraps it for React consumption.
    //                     Grouping prevents one from being inlined
    //                     into a chunk that doesn't otherwise need
    //                     the others.
    //   react-vendor/     react + react-dom \u2014 stable, every visitor
    //                     needs them, perfect for long-term caching.
    //   filerobot/        react-filerobot-image-editor \u2014 mid-size,
    //                     used only when the user opens the advanced
    //                     image editor. Worth its own chunk so the
    //                     rest of the app doesn't pay for it.
    //   index/            Everything else: app code, react-select,
    //                     styled-components, zod, uuid, plus all
    //                     the small utilities. This stays the
    //                     "main" chunk that changes with each deploy.
    //
    // What we deliberately did NOT split
    // ──────────────────────────────────
    // The smaller deps (react-select, styled-components, zod, uuid,
    // @emotion/is-prop-valid) stay in the index chunk on purpose.
    // Pulling them out would create chunks under 50 KB each, which
    // adds HTTP overhead (per-request connection cost, separate
    // hash entries in the precache manifest) without meaningfully
    // helping cache reuse. The "many small chunks" antipattern is
    // worse than a moderately-sized index chunk that includes the
    // long tail.
    //
    // If a future change makes one of these grow significantly (say,
    // a styled-components major upgrade that doubles its size), it
    // becomes worth pulling into its own chunk \u2014 but only then.
    //
    // Maintenance: when adding a new heavy dependency
    // ───────────────────────────────────────────────
    // 1. Build and check `dist/assets/` for which chunk it landed in.
    // 2. If it bloated the `index` chunk past comfort (rule of thumb:
    //    if index alone is > 1 MiB), add a new entry to manualChunks
    //    targeting that package name.
    // 3. The 2 MiB workbox cap acts as the loud alarm \u2014 if you forget,
    //    the build fails and tells you exactly which chunk overflowed.
    rollupOptions: {
      output: {
        // Rolldown (Vite 8) only accepts a function here — object form was Rollup-only.
        manualChunks(id) {
          const groups = [
            ['transformers', '@huggingface/transformers'],
            ['konva', 'konva', 'react-konva', 'use-image'],
            ['react-vendor', 'react', 'react-dom'],
            ['filerobot', 'react-filerobot-image-editor'],
          ];
          for (const [chunk, ...packages] of groups) {
            for (const pkg of packages) {
              if (id.includes(`node_modules/${pkg}/`) || id.includes(`node_modules/${pkg}\\`)) {
                return chunk;
              }
            }
          }
        },
      },
    },
  },
});
