/**
 * SSR Server - Standalone Node.js server for executing server.js modules
 * Runs on a separate port, completely outside of Vite
 * Uses Blockbuster's approach: @swc/core for parsing + importFromString for execution
 */

import { readFileSync } from 'fs';
import * as http from 'http';

import { parse } from '@swc/core';
import { parseHTML } from 'linkedom';
import { importFromString } from 'module-from-string';

const PREVIEW_SSR_PORT = process.env.PREVIEW_SSR_PORT ? parseInt(process.env.PREVIEW_SSR_PORT, 10) : 3001;
const EXTERNAL_IMPORTS_BLOCK_START = '/* EXTERNAL_IMPORTS_START */';
const EXTERNAL_IMPORTS_BLOCK_END = '/* EXTERNAL_IMPORTS_END */';

interface GlobalDesign {
  color: {
    title: { hex: string };
    body: { hex: string };
    button: { hex: string };
    link: { hex: string };
    background: { hex: string };
  };
}

interface RenderContext {
  externalizationEnabled: boolean;
  globalDesign?: GlobalDesign;
  [key: string]: unknown;
}

interface RenderResult {
  html: string;
}

interface ServerModule {
  init(): {
    render: (context: RenderContext, data: unknown) => Promise<RenderResult>;
  };
  default?: unknown;
}

interface ImportSpecifier {
  local: {
    value: string;
  };
}

interface ImportDeclaration {
  type: string;
  source: {
    value: string;
  };
  specifiers: ImportSpecifier[];
}

interface ParsedModule {
  body: ImportDeclaration[];
}

interface ExternalImportsResult {
  transformedModuleData: string;
  externalModulesAsGlobals: Record<string, unknown>;
}

interface RenderRequest {
  modulePath: string;
  context?: RenderContext;
  data?: unknown;
}

interface MockWindowInstantSite {
  getSiteId: () => void;
  getAppPublicToken: () => void;
  getAppPublicConfig: () => void;
  openSearchPage: () => void;
  onTileLoaded: { add: () => void };
  onTileUnloaded: { add: () => void };
}

interface MockWindowEcwid {
  getOwnerId: (storeId?: number) => number;
  formatCurrency: () => void;
  getAppPublicToken: () => void;
  getAppPublicConfig: () => void;
  getInitializedWidgets: () => void;
  getTrackingConsent: () => void;
  getStorefrontLang: () => void;
  getVisitorLocation: () => void;
  openPage: () => void;
  setTrackingConsent: () => void;
  OnApiLoaded: { add: () => void };
  OnCartChanged: { add: () => void };
  OnConsentChanged: { add: () => void };
  OnPageLoad: { add: () => void };
  OnPageLoaded: { add: () => void };
  OnPageSwitch: { add: () => void };
  OnSetProfile: { add: () => void };
  Customer: {
    get: () => void;
    signOut: () => void;
  };
  Cart: {
    addProduct: () => void;
    clear: () => void;
    get: () => void;
    removeProduct: () => void;
    removeProducts: () => void;
    calculateTotal: () => void;
  };
}

interface MockWindow {
  instantsite: MockWindowInstantSite;
  Ecwid: MockWindowEcwid;
  [key: string]: unknown;
}

// Minimal default global design to mimic Blockbuster defaults when not provided
const DEFAULT_GLOBAL_DESIGN: GlobalDesign = {
  color: {
    title: { hex: '#191919ff' },
    body: { hex: '#191919ff' },
    button: { hex: '#191919ff' },
    link: { hex: '#1a7ac4ff' },
    background: { hex: '#f4f4f4ff' },
  },
};

// ============================================================================
// Mock Window Setup (matching Blockbuster)
// ============================================================================

const mockFn = () => {};
const mockEcwidApiPromise = { add: mockFn };

function createMockInstantSite() {
  return {
    getSiteId: mockFn,
    getAppPublicToken: mockFn,
    getAppPublicConfig: mockFn,
    openSearchPage: mockFn,
    onTileLoaded: mockEcwidApiPromise,
    onTileUnloaded: mockEcwidApiPromise,
  };
}

function createMockEcwid(storeId?: number) {
  return {
    // defaults to 0 to prevent @lightspeed/ecom-headless initialization errors
    getOwnerId: () => storeId ?? 0,
    formatCurrency: mockFn,
    getAppPublicToken: mockFn,
    getAppPublicConfig: mockFn,
    getInitializedWidgets: mockFn,
    getTrackingConsent: mockFn,
    getStorefrontLang: mockFn,
    getVisitorLocation: mockFn,
    openPage: mockFn,
    setTrackingConsent: mockFn,
    OnApiLoaded: mockEcwidApiPromise,
    OnCartChanged: mockEcwidApiPromise,
    OnConsentChanged: mockEcwidApiPromise,
    OnPageLoad: mockEcwidApiPromise,
    OnPageLoaded: mockEcwidApiPromise,
    OnPageSwitch: mockEcwidApiPromise,
    OnSetProfile: mockEcwidApiPromise,
    Customer: {
      get: mockFn,
      signOut: mockFn,
    },
    Cart: {
      addProduct: mockFn,
      clear: mockFn,
      get: mockFn,
      removeProduct: mockFn,
      removeProducts: mockFn,
      calculateTotal: mockFn,
    },
  };
}

/**
 * Creates a mock window object for SSR using linkedom,
 * including mocked `instantsite` and `Ecwid` objects to prevent errors
 * from server-side code that references them (matching Blockbuster).
 */
function createMockWindow(storeId?: number): MockWindow {
  const { window } = parseHTML('');
  const mockWindow = window as unknown as MockWindow;

  mockWindow.instantsite = createMockInstantSite();
  mockWindow.Ecwid = createMockEcwid(storeId);

  return mockWindow;
}

// ============================================================================
// External Imports Handling (Blockbuster approach)
// ============================================================================

function getExternalModuleName(source: string): string {
  // Remove \0 prefix and ?commonjs-external suffix
  // e.g., "\0@vue/compiler-dom?commonjs-external" -> "@vue/compiler-dom"
  const suffix = '?commonjs-external';
  return source.split(suffix)[0].slice(1);
}

function getModuleImportIdentifier(specifiers: ImportSpecifier[]): string {
  // Extract the local variable names from import specifiers
  // e.g., [{ local: { value: 'e' } }] -> 'e'
  return specifiers.map(({ local }) => local.value).join(', ');
}

async function transformExternalModulesToGlobals(body: ImportDeclaration[]): Promise<Record<string, unknown>> {
  // Parse the AST and extract import declarations
  const globals: Record<string, unknown> = {};

  const importPromises = body.map(async (node) => {
    if (node.type === 'ImportDeclaration') {
      const { source, specifiers } = node;
      const moduleName = getExternalModuleName(source.value);
      const importIdentifier = getModuleImportIdentifier(specifiers);

      // Import the module and add it to globals with the variable name
      try {
        globals[importIdentifier] = await import(moduleName);
      } catch (error: unknown) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        console.warn(`Failed to import ${moduleName}:`, errorMessage);
        globals[importIdentifier] = {};
      }
    }
  });

  await Promise.all(importPromises);

  return globals;
}

async function replaceExternalImportsWithGlobals(fullModuleData: string): Promise<ExternalImportsResult> {
  // Find the EXTERNAL_IMPORTS block
  const externalImportsStart = fullModuleData.indexOf(EXTERNAL_IMPORTS_BLOCK_START);
  const externalImportsEnd = fullModuleData.indexOf(EXTERNAL_IMPORTS_BLOCK_END);

  if (externalImportsStart === -1 || externalImportsEnd === -1) {
    return {
      transformedModuleData: fullModuleData,
      externalModulesAsGlobals: {},
    };
  }

  // Extract the external imports code block
  const externalImportsCode = fullModuleData.slice(externalImportsStart, externalImportsEnd);
  const remainingModuleCode = fullModuleData.slice(externalImportsEnd + EXTERNAL_IMPORTS_BLOCK_END.length);

  // Parse the imports code with @swc/core
  try {
    const parsed = (await parse(externalImportsCode)) as ParsedModule;
    const { body } = parsed;
    const externalModulesAsGlobals = await transformExternalModulesToGlobals(body);

    return {
      transformedModuleData: remainingModuleCode,
      externalModulesAsGlobals,
    };
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    console.warn('Failed to parse external imports:', errorMessage);
    return {
      transformedModuleData: remainingModuleCode,
      externalModulesAsGlobals: {},
    };
  }
}

// ============================================================================
// Globals Creation
// ============================================================================

function createGlobals(window: unknown): Record<string, unknown> {
  // Create base globals for SSR execution
  // With useCurrentGlobal: true, window properties (document, location, etc.)
  // will be accessible from the current global context
  return {
    window,
    fetch,
    TextEncoder,
    TextDecoder,
    URLSearchParams,
  };
}

async function loadServerModule(modulePath: string, storeId?: number): Promise<ServerModule> {
  try {
    // Read the file from disk
    const moduleData = readFileSync(modulePath, 'utf8');

    // Create mock window with storeId (matching Blockbuster)
    const mockWindow = createMockWindow(storeId);
    const baseGlobals = createGlobals(mockWindow);

    // Set global document and window for Vue setup functions
    // This ensures that when Vue components' setup functions run, they can access document
    const globalThisWithProps = globalThis as Record<string, unknown>;
    const previousGlobalDocument = globalThisWithProps.document;
    const previousGlobalWindow = globalThisWithProps.window;
    globalThisWithProps.document = (mockWindow as Record<string, unknown>).document;
    globalThisWithProps.window = mockWindow;

    try {
      // Replace external imports with globals (Blockbuster approach)
      const { transformedModuleData, externalModulesAsGlobals }
        = await replaceExternalImportsWithGlobals(moduleData);

      // Combine base globals with external modules
      const globals = {
        ...externalModulesAsGlobals,
        ...baseGlobals,
      };

      // Execute the module with importFromString (Blockbuster approach)
      // Use useCurrentGlobal: true to avoid property descriptor conflicts with window properties
      const module = await importFromString(transformedModuleData, { globals, useCurrentGlobal: true });

      // Return the module exports
      return module.default || module;
    } finally {
      // Restore previous global state
      globalThisWithProps.document = previousGlobalDocument;
      globalThisWithProps.window = previousGlobalWindow;
    }
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    console.error('Error loading server module:', errorMessage);
    throw error;
  }
}

// ============================================================================
// HTTP Server
// ============================================================================

const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
  // Enable CORS
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');

  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.statusCode = 200;
    res.end();
    return;
  }

  // POST /load-server-module - Load module and return HTML
  if (req.method === 'POST' && req.url === '/load-server-module') {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });

    req.on('end', async () => {
      try {
        const { modulePath, context: rawContext, data } = JSON.parse(body) as RenderRequest;

        if (!modulePath) {
          res.statusCode = 400;
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ error: 'modulePath is required' }));
          return;
        }

        // Normalize context
        const context: RenderContext = {
          externalizationEnabled: true,
          ...rawContext,
        };
        if (!context.globalDesign || !context.globalDesign.color) {
          context.globalDesign = DEFAULT_GLOBAL_DESIGN;
        }

        // Load the server module
        const serverModule = await loadServerModule(modulePath);

        // Call init() to get the render function
        const { render } = serverModule.init();

        // Call render with context and data
        const result = await render(context, data || { defaults: {}, background: {}, externalContent: {} });

        res.statusCode = 200;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ success: true, html: result.html }));
      } catch (error: unknown) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        console.error('Error loading server module:', error);
        res.statusCode = 500;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({
          error: 'Failed to load server module',
          message: errorMessage,
        }));
      }
    });
    return;
  }

  // 404
  res.statusCode = 404;
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ error: 'Not found' }));
});

/**
 * Start the SSR server and return a promise that resolves when it's listening
 */
export function startServer(): Promise<{ server: http.Server; port: number }> {
  return new Promise((resolve) => {
    server.listen(PREVIEW_SSR_PORT, () => {
      const actualPort = (server.address() as { port: number }).port;
      console.log(`🚀 SSR Server running on http://localhost:${actualPort}`);
      resolve({ server, port: actualPort });
    });
  });
}
