import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { wasm } from '@rollup/plugin-wasm';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import inject from '@rollup/plugin-inject';
import topLevelAwait from 'vite-plugin-top-level-await';
import { VitePWA } from 'vite-plugin-pwa';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import buildConfig from './build.config';

function resolveAppVersion() {
  if (process.env.VITE_APP_VERSION) return process.env.VITE_APP_VERSION;
  try {
    // --match 'v*' filters out non-semver tags (e.g. `redesign-p0-done`,
    // `pre-redesign`) so they never shadow the version-derivation chain.
    // Without it, `git describe` would pick the nearest tag of any shape and
    // the regex below would fall through to `return raw`, leaking the human
    // tag name into __APP_VERSION__ on the Welcome screen.
    const raw = execSync("git describe --tags --match 'v*' --always --dirty", {
      stdio: ['ignore', 'pipe', 'ignore'],
    })
      .toString()
      .trim();
    const m = raw.match(/^(v?\d+\.\d+)\.\d+-(\d+)-g([0-9a-f]+)(-dirty)?$/);
    if (m) {
      const base = m[1];
      const patch = m[2];
      const hash = m[3];
      const dirty = m[4] ?? '';
      return `${base}.${patch}+g${hash}${dirty}`;
    }
    return raw;
  } catch {
    const pkg = JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8'));
    return `v${pkg.version}`;
  }
}

const copyFiles = {
  targets: [
    // Element-call ships MediaPipe vision_wasm (~9.5 MB) for video-call
    // background blur. Vojo's DM calls are voice-only (see
    // useSwitchOrStartDmCall.ts → createCallEmbed(..., voiceOnly=true)),
    // so the blur wasm is dead weight in both the web bundle and the APK.
    // The `transform` handler returning null skips the file at copy time —
    // it never enters dist/ in the first place. (Two separate targets
    // because viteStaticCopy's `transform` only accepts file-level globs,
    // not directories; EC's dist has the assets/ subdir.)
    {
      src: 'node_modules/@element-hq/element-call-embedded/dist/{index.html,config.json}',
      dest: 'public/element-call',
    },
    {
      src: 'node_modules/@element-hq/element-call-embedded/dist/assets/*',
      dest: 'public/element-call/assets',
      transform: {
        encoding: 'buffer',
        handler: (content, filename) =>
          filename.includes('vision_wasm_internal') ? null : content,
      },
    },
    {
      src: 'node_modules/pdfjs-dist/build/pdf.worker.min.mjs',
      dest: '',
      rename: 'pdf.worker.min.js',
    },
    {
      src: 'netlify.toml',
      dest: '',
    },
    {
      src: 'config.json',
      dest: '',
    },
    {
      src: 'public/manifest.json',
      dest: '',
    },
    {
      src: 'public/res/android',
      dest: 'public/',
    },
    {
      src: 'public/res/svg/vojo.svg',
      dest: 'public/android/',
    },
    {
      src: 'public/locales',
      dest: 'public/',
    },
    {
      src: 'public/privacy.html',
      dest: '',
    },
    {
      src: 'public/delete-account.html',
      dest: '',
    },
  ],
};

// Dev-only overlay for runtime config. The SPA fetches `/config.json` at boot
// to read homeserver list, bot presets, push gateway config, etc. — production
// ships this file unmodified from `~/vojo/cinny/config.json` on the server.
// Locally we want per-developer overrides (e.g. `bots[id=telegram].experience.url`
// → `http://localhost:8081/` for a widget dev server) WITHOUT touching the
// committed `config.json`. This middleware reads optional `config.local.json`
// at the project root and overlays it on top, then serves the merged JSON.
//
// Merge contract:
//   - top-level fields: shallow override (local wins).
//   - `bots[]`: merged by `id` — the local entry shallow-merges over the base
//     entry with the same id, so `{ id: "telegram", experience: {...} }` is
//     enough to override one field of an existing bot. Bots that exist only
//     in local are appended as-is.
//
// Production builds ignore this plugin (`apply: 'serve'`) so prod
// `config.json` is served untouched by Caddy. `config.local.json` is in
// `.gitignore` and never deployed.
function mergeBotsById(base, local) {
  if (!Array.isArray(local)) return base;
  if (!Array.isArray(base)) return local;
  const byId = new Map();
  base.forEach((bot) => {
    if (bot && typeof bot.id === 'string') byId.set(bot.id, bot);
  });
  local.forEach((overlay) => {
    if (!overlay || typeof overlay.id !== 'string') return;
    const baseBot = byId.get(overlay.id);
    byId.set(overlay.id, baseBot ? { ...baseBot, ...overlay } : overlay);
  });
  return [...byId.values()];
}

function mergeRuntimeConfig(base, local) {
  if (!local || typeof local !== 'object') return base;
  const merged = { ...base, ...local };
  if (Array.isArray(local.bots) || Array.isArray(base?.bots)) {
    merged.bots = mergeBotsById(base?.bots, local.bots);
  }
  return merged;
}

function serveLocalConfigOverlay() {
  return {
    name: 'vite-plugin-serve-local-config-overlay',
    apply: 'serve',
    configureServer(server) {
      server.middlewares.use('/config.json', (req, res, next) => {
        const localPath = path.resolve('config.local.json');
        if (!fs.existsSync(localPath)) {
          next();
          return;
        }
        try {
          const baseRaw = fs.readFileSync(path.resolve('config.json'), 'utf8');
          const localRaw = fs.readFileSync(localPath, 'utf8');
          const merged = mergeRuntimeConfig(JSON.parse(baseRaw), JSON.parse(localRaw));
          res.setHeader('Content-Type', 'application/json');
          res.setHeader('Cache-Control', 'no-cache');
          res.end(JSON.stringify(merged));
        } catch (err) {
          next(err);
        }
      });
    },
  };
}

// Dev-mirror for the Caddy directives that serve static legal pages in
// prod:
//   try_files {path} {path}.html /index.html  (extensionless URLs)
//   redir /<page>/* /<page> permanent         (collapse subpaths)
// vite-plugin-static-copy maps a single fileMap key per target, so it only
// answers /<page>.html — every other shape (/<page>, /<page>/,
// /<page>/lobby) falls into vite's SPA fallback, cinny's router reads the
// prefix as a Matrix space alias, and redirects to /<page>/lobby. This
// middleware short-circuits the entire /<page>* prefix to the static file.
// Add a new entry here when a new static legal/info page is introduced.
function serveStaticPagesDevMirror() {
  const pages = [
    { prefix: '/privacy', file: 'public/privacy.html' },
    { prefix: '/delete-account', file: 'public/delete-account.html' },
  ];
  return {
    name: 'vite-plugin-serve-static-pages-dev-mirror',
    apply: 'serve',
    configureServer(server) {
      pages.forEach(({ prefix, file }) => {
        server.middlewares.use(prefix, (_req, res, next) => {
          const filePath = path.resolve(file);
          if (!fs.existsSync(filePath)) {
            next();
            return;
          }
          res.setHeader('Content-Type', 'text/html; charset=utf-8');
          res.setHeader('Cache-Control', 'no-cache');
          res.end(fs.readFileSync(filePath));
        });
      });
    },
  };
}

function serverMatrixSdkCryptoWasm(wasmFilePath) {
  return {
    name: 'vite-plugin-serve-matrix-sdk-crypto-wasm',
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        if (req.url === wasmFilePath) {
          const resolvedPath = path.join(
            path.resolve(),
            '/node_modules/@matrix-org/matrix-sdk-crypto-wasm/pkg/matrix_sdk_crypto_wasm_bg.wasm'
          );

          if (fs.existsSync(resolvedPath)) {
            res.setHeader('Content-Type', 'application/wasm');
            res.setHeader('Cache-Control', 'no-cache');

            const fileStream = fs.createReadStream(resolvedPath);
            fileStream.pipe(res);
          } else {
            res.writeHead(404);
            res.end('File not found');
          }
        } else {
          next();
        }
      });
    },
  };
}

export default defineConfig({
  appType: 'spa',
  publicDir: false,
  base: buildConfig.base,
  define: {
    __APP_VERSION__: JSON.stringify(resolveAppVersion()),
  },
  server: {
    port: 8080,
    // Vojo's electron:dev wires `wait-on tcp:8080` against this exact port,
    // and `electron/main.ts` hard-codes `http://localhost:8080` as DEV_URL.
    // Without `strictPort: true` Vite silently falls forward to :8081 when
    // :8080 is taken (stale dev process, widget dev-server, Caddy), and
    // Electron blindly waits for / loads the original port — wrong content
    // or hang. Fail-fast is the better default for dev.
    strictPort: true,
    host: true,
    fs: {
      // Allow serving files from one level up to the project root
      allow: ['..'],
    },
  },
  plugins: [
    serveLocalConfigOverlay(),
    serveStaticPagesDevMirror(),
    serverMatrixSdkCryptoWasm('/node_modules/.vite/deps/pkg/matrix_sdk_crypto_wasm_bg.wasm'),
    topLevelAwait({
      // The export name of top-level await promise for each chunk module
      promiseExportName: '__tla',
      // The function to generate import names of top-level await promise in each chunk module
      promiseImportName: (i) => `__tla_${i}`,
    }),
    viteStaticCopy(copyFiles),
    vanillaExtractPlugin(),
    wasm(),
    react(),
    VitePWA({
      srcDir: 'src',
      filename: 'sw.ts',
      strategies: 'injectManifest',
      injectRegister: false,
      manifest: false,
      injectManifest: {
        injectionPoint: undefined,
      },
      devOptions: {
        enabled: true,
        type: 'module',
      },
    }),
  ],
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
      plugins: [
        // Enable esbuild polyfill plugins
        NodeGlobalsPolyfillPlugin({
          process: false,
          buffer: true,
        }),
      ],
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
    copyPublicDir: false,
    rollupOptions: {
      plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
      output: {
        // Conservative vendor splitting: carve only the large, stable,
        // leaf-ish dependency families out of the single ~1 MB-gzip app
        // chunk into their own cacheable chunks. The win is cache stability
        // — an app-code redeploy no longer busts the matrix-js-sdk / slate
        // bytes for returning users, and the two load in parallel with the
        // entry. We deliberately do NOT split react / folds / react-aria:
        // they are boot-critical and tightly coupled, and aggressive vendor
        // splitting under topLevelAwait + wasm can reorder init and break
        // shared singletons (element-web likewise avoids hand-rolled vendor
        // cacheGroups). Heavy *feature* code is code-split via React.lazy in
        // Router.tsx instead — that's what actually shrinks first load.
        manualChunks(id) {
          // Pin the heavy emoji PICKER dataset (~506 KB `compact.json` + the
          // `emoji-data.ts` module that processes it) to its own chunk so it
          // stays OFF the initial bundle — only the lazy Room / Settings chunks
          // import it (on first room open / picker mount). Runs BEFORE the
          // node_modules guard so the src module is pinned too.
          if (id.includes('/plugins/emoji-data') || id.includes('emojibase-data/en/compact')) {
            return 'emoji-data';
          }
          // Pin the LIGHT emoji module (shortcode lookups + `getHexcodeForEmoji`)
          // and its shortcode maps to a SEPARATE chunk. These are genuinely on the
          // boot path — the HTML parser (react-custom-html-parser, reachable from
          // RoomNavItem → InviteUserPrompt) statically imports them. Pinning them
          // here is what KEEPS `emoji-data` off boot: without it Rollup folds the
          // light module into the `emoji-data` chunk (it's a dependency of
          // `emoji-data.ts`), and the entry's boot import of the light functions
          // then drags the 506 KB `compact.json` into the initial load with it.
          // `.ts` is matched specifically so this never catches `emoji-data.ts`
          // (already returned above).
          if (id.includes('/plugins/emoji.ts') || id.includes('emojibase-data/en/shortcodes')) {
            return 'emoji-shortcodes';
          }
          if (!id.includes('node_modules')) return undefined;
          // Conservative vendor splitting: carve only the large, stable,
          // leaf-ish dependency families out of the single app chunk into their
          // own cacheable chunks. The win is cache stability — an app-code
          // redeploy no longer busts the matrix-js-sdk / slate bytes for
          // returning users. We deliberately do NOT split react / folds /
          // react-aria: they are boot-critical and tightly coupled, and
          // aggressive vendor splitting under topLevelAwait + wasm can reorder
          // init and break shared singletons (element-web likewise avoids
          // hand-rolled vendor cacheGroups).
          if (
            id.includes('matrix-js-sdk') ||
            id.includes('@matrix-org/') ||
            id.includes('matrix-events-sdk') ||
            id.includes('matrix-widget-api')
          ) {
            return 'matrix-sdk';
          }
          if (/[\\/]node_modules[\\/](slate|slate-react|slate-dom|slate-history)[\\/]/.test(id)) {
            return 'editor';
          }
          return undefined;
        },
      },
    },
  },
});
