import { readFileSync } from 'node:fs';
import path from 'node:path';

import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import dotenv from 'dotenv';
import { defineConfig } from 'electron-vite';
import type { PluginOption, ViteDevServer } from 'vite';
import { loadEnv } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

import {
  sharedOptimizeDeps,
  sharedRendererDefine,
  sharedRendererPlugins,
  sharedRollupOutput,
} from '../../plugins/vite/sharedRendererConfig';
import { externalRuntimeModules } from './external-runtime-deps.config.mjs';
import { getNativeExternalDependencies } from './native-deps.config.mjs';

/**
 * Force `base: '/'` in renderer config. The `electron-vite` preset
 * unconditionally rewrites base to `'./'` in production (with `enforce: 'pre'`),
 * which produces relative asset URLs like `../../assets/...`. Those break in
 * the popup window because its SPA URL (`/popup/agent/:aid/:tid`) is deep
 * enough that relative resolution lands at `/popup/assets/...` instead of the
 * actual `/assets/...`. Our `app://` protocol handler resolves absolute
 * `/assets/...` correctly regardless of URL depth.
 */
function forceAbsoluteBasePlugin(): PluginOption {
  return {
    name: 'electron-desktop-force-base',
    config(config) {
      config.base = '/';
    },
  };
}

/**
 * Rewrite SPA routes to their corresponding HTML entry so the electron-vite
 * dev server serves the right HTML when root is the monorepo root.
 *
 * - `/popup/*` → `/apps/desktop/popup.html` (topic popup SPA)
 * - `/`, `/index.html`, and everything else → `/apps/desktop/index.html`
 */
function electronDesktopHtmlPlugin(): PluginOption {
  return {
    configureServer(server: ViteDevServer) {
      server.middlewares.use((req, _res, next) => {
        const rawUrl = req.url ?? '';
        const pathname = rawUrl.split('?')[0];

        // Explicit document-entry requests — always rewrite.
        if (pathname === '/' || pathname === '/index.html') {
          req.url = '/apps/desktop/index.html';
          next();
          return;
        }
        if (pathname === '/overlay' || pathname === '/overlay.html') {
          req.url = '/apps/desktop/overlay.html';
          next();
          return;
        }
        if (pathname === '/popup.html') {
          req.url = '/apps/desktop/popup.html';
          next();
          return;
        }

        // For SPA deep links (e.g. `/popup/agent/A/T`) rewrite to the popup
        // HTML — but skip asset / module requests that happen to share the
        // prefix (e.g. `/popup/@vite/client` would have been generated by a
        // mis-resolved relative import).
        const lastSegment = pathname.split('/').pop() ?? '';
        const looksLikeAsset =
          lastSegment.includes('.') ||
          pathname.startsWith('/@') ||
          pathname.startsWith('/src/') ||
          pathname.startsWith('/node_modules/') ||
          pathname.startsWith('/apps/') ||
          pathname.startsWith('/packages/');

        if (!looksLikeAsset && (pathname === '/popup' || pathname.startsWith('/popup/'))) {
          req.url = '/apps/desktop/popup.html';
        }
        next();
      });
    },
    name: 'electron-desktop-html',
  };
}

const CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG = '__LOBECLOUD_DESKTOP_BUSINESS_FEATURES__';
const BUSINESS_CONST_MODULE_ID = '@lobechat/business-const';
const CLOUD_BUSINESS_CONST_MODULE_ID = '@cloud/business-const';
const DYNAMIC_BUSINESS_CONST_QUERY = '?lobe-cloud-desktop-business-const';

const createBusinessFeaturesBootstrapScript = () =>
  `globalThis[${JSON.stringify(CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG)}] = true;`;

const replaceBusinessFlagExport = (code: string, name: string, initializer: string) => {
  const pattern = new RegExp(`export\\s+(?:const|let|var)\\s+${name}\\s*=\\s*[\\s\\S]*?;`);

  return {
    code: code.replace(pattern, `export let ${name} = ${initializer};`),
    replaced: pattern.test(code),
  };
};

const injectDynamicBusinessFeatureFlag = (code: string) => {
  const businessFlag = replaceBusinessFlagExport(
    code,
    'ENABLE_BUSINESS_FEATURES',
    `Boolean(globalThis['${CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG}'])`,
  );
  const topicLinkFlag = replaceBusinessFlagExport(
    businessFlag.code,
    'ENABLE_TOPIC_LINK_SHARE',
    'ENABLE_BUSINESS_FEATURES',
  );

  if (!businessFlag.replaced) {
    throw new Error('Cannot find ENABLE_BUSINESS_FEATURES export in @cloud/business-const');
  }

  const topicLinkAssignment = topicLinkFlag.replaced
    ? '\n  ENABLE_TOPIC_LINK_SHARE = enabled;'
    : '';

  return `${topicLinkFlag.code}

const __lobeCloudDesktopBusinessFeaturesFlagKey = '${CLOUD_DESKTOP_BUSINESS_FEATURES_FLAG}';
const __lobeCloudDesktopApplyBusinessFeaturesFlag = (value) => {
  const enabled = Boolean(value);
  ENABLE_BUSINESS_FEATURES = enabled;${topicLinkAssignment}
  return enabled;
};

const __lobeCloudDesktopExistingDescriptor = Object.getOwnPropertyDescriptor(
  globalThis,
  __lobeCloudDesktopBusinessFeaturesFlagKey,
);
const __lobeCloudDesktopInitialValue = __lobeCloudDesktopExistingDescriptor?.get
  ? __lobeCloudDesktopExistingDescriptor.get.call(globalThis)
  : globalThis[__lobeCloudDesktopBusinessFeaturesFlagKey];

Object.defineProperty(globalThis, __lobeCloudDesktopBusinessFeaturesFlagKey, {
  configurable: true,
  get() {
    return ENABLE_BUSINESS_FEATURES;
  },
  set(value) {
    __lobeCloudDesktopApplyBusinessFeaturesFlag(value);
  },
});

__lobeCloudDesktopApplyBusinessFeaturesFlag(__lobeCloudDesktopInitialValue);
`;
};

function cloudDesktopBusinessConstPlugin(): PluginOption {
  return {
    enforce: 'pre',
    async resolveId(id, importer) {
      if (id !== BUSINESS_CONST_MODULE_ID) return;

      const resolved = await this.resolve(CLOUD_BUSINESS_CONST_MODULE_ID, importer, {
        skipSelf: true,
      });
      if (!resolved) throw new Error(`Cannot resolve ${CLOUD_BUSINESS_CONST_MODULE_ID}`);

      return `${resolved.id}${DYNAMIC_BUSINESS_CONST_QUERY}`;
    },
    load(id) {
      if (!id.endsWith(DYNAMIC_BUSINESS_CONST_QUERY)) return;

      const sourcePath = id.slice(0, -DYNAMIC_BUSINESS_CONST_QUERY.length);
      return injectDynamicBusinessFeatureFlag(readFileSync(sourcePath, 'utf8'));
    },
    name: 'lobe-cloud-desktop-business-const',
    transformIndexHtml() {
      return [
        {
          children: createBusinessFeaturesBootstrapScript(),
          injectTo: 'head-prepend',
          tag: 'script',
        },
      ];
    },
  };
}

dotenv.config();

const isDev = process.env.NODE_ENV === 'development';
const ROOT_DIR = path.resolve(__dirname, '../..');
const CLOUD_ROOT_DIR = path.resolve(__dirname, '../../..');
const isCloudDesktopBuild = process.env.CLOUD_DESKTOP === '1';
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';

Object.assign(process.env, loadEnv(mode, ROOT_DIR, ''));
const updateChannel = process.env.UPDATE_CHANNEL;
const desktopPackageJson = JSON.parse(
  readFileSync(path.resolve(__dirname, 'package.json'), 'utf8'),
) as { version: string };
const electronRuntimeExternals = ['electron'];
const mainProcessRuntimeExternals = [
  ...electronRuntimeExternals,
  ...externalRuntimeModules,
  'node-mac-permissions',
];
const externalNavigationHosts =
  process.env.DESKTOP_EXTERNAL_NAVIGATION_HOSTS ?? (isCloudDesktopBuild ? 'stripe.com' : '');

console.info(`[electron-vite.config.ts] Detected UPDATE_CHANNEL: ${updateChannel}`);
console.info(`[electron-vite.config.ts] Cloud desktop build: ${isCloudDesktopBuild}`);

const cloudTsconfigPathsPlugin = () =>
  ({
    ...tsconfigPaths({ projects: [path.resolve(CLOUD_ROOT_DIR, 'tsconfig.json')] }),
    name: 'lobe-cloud-desktop-tsconfig-paths',
  }) satisfies PluginOption;

export default defineConfig({
  main: {
    build: {
      minify: !isDev,
      outDir: 'dist/main',
      rolldownOptions: {
        // Native modules must be externalized to work correctly.
        // bufferutil and utf-8-validate are optional peer deps of ws that may not be installed.
        external: [
          ...mainProcessRuntimeExternals,
          ...getNativeExternalDependencies(),
          'bufferutil',
          'utf-8-validate',
        ],
        output: {
          // Prevent shared deps from being bundled into index.js to avoid side-effect pollution.
          // Pattern: when a module is imported by both the main bundle (statically) and a
          // dynamic-import chunk (lazy loader), rolldown places it in main and makes the
          // chunk back-reference `require("./index.js")`. Electron's main entry isn't in
          // Node's CJS cache, so that require recompiles `index.js` from scratch — which
          // re-runs `new App()` at top-level and triggers `protocol.registerSchemesAsPrivileged`
          // *after* the app is ready → throw.
          //
          // Same root cause as the original `debug` regression fixed in #11827. Isolate
          // each shared module into its own vendor chunk so both ends reference the vendor
          // chunk instead of back-referencing main.
          manualChunks(id) {
            if (id.includes('node_modules/debug')) {
              return 'vendor-debug';
            }

            // Small text/binary detection utilities in file-loaders/utils. Imported by
            // main (via `sniffBinaryFile`) and potentially by lazy loader chunks.
            // Explicitly enumerated to avoid catching `parser-utils.ts`, which pulls in
            // xmldom / yauzl / concat-stream — those belong in docx/pptx loader chunks.
            if (
              /packages\/file-loaders\/src\/utils\/(?:detectUtf16|isBinaryContent|isTextReadableFile)\.ts$/.test(
                id,
              )
            ) {
              return 'vendor-file-loaders-utils';
            }

            // jszip — imported by main (via some static path) AND by the docx loader chunk.
            // Without this, reading a .docx file throws the protocol re-init error.
            if (id.includes('node_modules/jszip')) {
              return 'vendor-jszip';
            }

            // Split i18n json resources by namespace (ns), not by locale.
            // Example: ".../resources/locales/zh-CN/common.json?import" -> "locales-common"
            const normalizedId = id.replaceAll('\\', '/').split('?')[0];
            const match = normalizedId.match(/\/locales\/[^/]+\/([^/]+)\.json$/);

            if (match?.[1]) return `locales-${match[1]}`;
          },
        },
      },
      sourcemap: isDev ? 'inline' : false,
    },
    define: {
      'process.env.DESKTOP_EXTERNAL_NAVIGATION_HOSTS': JSON.stringify(externalNavigationHosts),
      'process.env.UPDATE_CHANNEL': JSON.stringify(process.env.UPDATE_CHANNEL),
      'process.env.UPDATE_SERVER_URL': JSON.stringify(process.env.UPDATE_SERVER_URL),
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src/main'),
        '~common': path.resolve(__dirname, 'src/common'),
      },
    },
  },
  preload: {
    build: {
      minify: !isDev,
      outDir: 'dist/preload',
      rolldownOptions: {
        external: electronRuntimeExternals,
      },
      sourcemap: isDev ? 'inline' : false,
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src/main'),
        '~common': path.resolve(__dirname, 'src/common'),
      },
    },
  },
  renderer: {
    root: ROOT_DIR,
    build: {
      outDir: path.resolve(__dirname, 'dist/renderer'),
      rolldownOptions: {
        input: {
          main: path.resolve(__dirname, 'index.html'),
          overlay: path.resolve(__dirname, 'overlay.html'),
          popup: path.resolve(__dirname, 'popup.html'),
        },
        output: sharedRollupOutput,
      },
    },
    define: {
      ...sharedRendererDefine({ isMobile: false, isElectron: true }),
      __MAIN_VERSION__: JSON.stringify(desktopPackageJson.version),
    },
    optimizeDeps: sharedOptimizeDeps,
    plugins: [
      isCloudDesktopBuild && cloudTsconfigPathsPlugin(),
      isCloudDesktopBuild && cloudDesktopBusinessConstPlugin(),
      forceAbsoluteBasePlugin(),
      electronDesktopHtmlPlugin(),
      vanillaExtractPlugin(),
      ...(sharedRendererPlugins({ platform: 'desktop' }) as PluginOption[]),
    ],
    resolve: {
      dedupe: ['react', 'react-dom'],
      tsconfigPaths: !isCloudDesktopBuild,
    },
    // In dev the BrowserWindow loads `app://renderer/` and the Electron main process
    // proxies non-backend requests to this Vite dev server via `net.fetch`. The HMR
    // WebSocket still connects directly (browser → ws://localhost:<port>) — so the
    // port MUST be deterministic. `strictPort` fails fast on conflict instead of
    // silently sliding, and `clientPort` baked into the HMR injection has to match.
    server: {
      hmr: {
        clientPort: 5173,
        host: '127.0.0.1',
        protocol: 'ws',
      },
      // Force IPv4 so main-process `fetch` skips happy-eyeballs dual-stack
      // attempts that surface as ETIMEDOUT under cold-start request bursts.
      host: '127.0.0.1',
      port: 5173,
      strictPort: true,
    },
  },
});
