import { _serialize as serialize } from '@qwik.dev/core/internal';
import { parentPort } from 'node:worker_threads';
import {
  renderQwikMiddleware,
  resolveRequestHandlers,
} from '../middleware/request-handler/resolve-request-handlers-core';
import { trimInternalPathname } from '../middleware/request-handler/request-path';
import type { ServerRequestEvent } from '../middleware/request-handler/types';
import { runQwikRouter } from '../middleware/request-handler/user-response';
import { getRouteLoaderValues, getRouteLoaders } from '../runtime/src/route-loaders';
import { loadRoute } from '../runtime/src/routing';
import type {
  SsgHandlerOptions,
  SsgRoute,
  SsgWorkerRenderResult,
  StaticStreamWriter,
  System,
  WorkerInputMessage,
  WorkerOutputMessage,
} from './types';

interface StaticWorkerThreadDeps {
  loadRoute: typeof loadRoute;
  renderQwikMiddleware: typeof renderQwikMiddleware;
  resolveRequestHandlers: typeof resolveRequestHandlers;
  trimInternalPathname: typeof trimInternalPathname;
  runQwikRouter: typeof runQwikRouter;
}

interface WorkerThreadDeps extends StaticWorkerThreadDeps {
  serialize: typeof serialize;
}

const staticWorkerThreadDeps: StaticWorkerThreadDeps = {
  loadRoute,
  renderQwikMiddleware,
  resolveRequestHandlers,
  trimInternalPathname,
  runQwikRouter,
};

export async function workerThread(sys: System) {
  const opts = sys.getOptions();
  const pendingPromises = new Set<Promise<any>>();
  const deps: WorkerThreadDeps = {
    ...staticWorkerThreadDeps,
    serialize,
  };

  // Prevent unhandled errors/rejections from crashing the worker thread.
  // SSR rendering can throw asynchronously (e.g., qwik's logErrorAndStop)
  // in microtasks not connected to any promise chain.
  process.on('uncaughtException', (e) => {
    console.error('Worker uncaught exception (suppressed):', e.message);
  });
  process.on('unhandledRejection', (e) => {
    console.error('Worker unhandled rejection (suppressed):', e instanceof Error ? e.message : e);
  });

  const onMessage = async (msg: WorkerInputMessage): Promise<WorkerOutputMessage> => {
    switch (msg.type) {
      case 'render': {
        return new Promise<SsgWorkerRenderResult>((resolve) => {
          workerRender(sys, opts, msg, pendingPromises, resolve, deps).catch((e) => {
            console.error('Error during render', msg.pathname, e);
            resolve({
              type: 'render',
              pathname: msg.pathname,
              url: '',
              ok: false,
              error: {
                message: e instanceof Error ? e.message : String(e),
                stack: e instanceof Error ? e.stack : undefined,
              },
              filePath: null,
              contentType: null,
              resourceType: null,
            });
          });
        });
      }
      case 'close': {
        if (pendingPromises.size) {
          const promises = Array.from(pendingPromises);
          pendingPromises.clear();
          await Promise.all(promises);
        }
        return { type: 'close' };
      }
    }
  };

  parentPort?.on('message', async (msg: WorkerInputMessage) => {
    try {
      parentPort?.postMessage(await onMessage(msg));
    } catch (e) {
      // Send error result back instead of crashing the worker
      if (msg.type === 'render') {
        const error = e instanceof Error ? e : new Error(String(e));
        parentPort?.postMessage({
          type: 'render',
          pathname: msg.pathname,
          url: '',
          ok: false,
          error: { message: error.message, stack: error.stack },
          filePath: null,
          contentType: null,
          resourceType: null,
        } satisfies WorkerOutputMessage);
      } else {
        console.error('Worker message handler error', e);
      }
    }
    if (msg.type === 'close') {
      parentPort?.close();
    }
  });
}

async function workerRender(
  sys: System,
  opts: SsgHandlerOptions,
  staticRoute: SsgRoute,
  pendingPromises: Set<Promise<any>>,
  callback: (result: SsgWorkerRenderResult) => void,
  deps: WorkerThreadDeps
) {
  // pathname and origin already normalized at this point
  const url = new URL(staticRoute.pathname, opts.origin);

  const result: SsgWorkerRenderResult = {
    type: 'render',
    pathname: staticRoute.pathname,
    url: url.href,
    ok: false,
    error: null,
    filePath: null,
    contentType: null,
    resourceType: null,
  };

  try {
    let routeWriter: StaticStreamWriter | null = null;
    let closeResolved: (v?: any) => void;
    const closePromise = new Promise((closePromiseResolve) => {
      closeResolved = closePromiseResolve;
    });

    const request = new Request(url);

    const requestCtx: ServerRequestEvent<void> = {
      mode: 'static',
      locale: undefined,
      url,
      request,
      env: {
        get(key) {
          return sys.getEnv(key);
        },
      },
      platform: sys.platform,
      getClientConn: () => {
        return {};
      },
      getWritableStream: (status, headers, _, _r, requestEv) => {
        // The not-found page renders with a 404 status but is still written as a static asset.
        const isNotFoundPage = url.pathname.endsWith('/404.html');
        result.ok = (status >= 200 && status < 300) || (isNotFoundPage && status === 404);

        if (!result.ok) {
          // not ok, don't write anything
          return createNoopWritableStream() as any;
        }

        result.contentType = (headers.get('Content-Type') || '').toLowerCase();
        const isHtml = result.contentType.includes('text/html');
        const routeFilePath = sys.getRouteFilePath(url.pathname, isHtml);

        if (isHtml) {
          result.resourceType = 'page';
        }

        const hasRouteWriter = isHtml ? opts.emitHtml !== false : true;
        const writeLoaderDataEnabled = isHtml && opts.emitData !== false;

        const stream = new WritableStream<Uint8Array>({
          async start() {
            try {
              if (hasRouteWriter || writeLoaderDataEnabled) {
                // ensure the containing directory is created
                await sys.ensureDir(routeFilePath);
              }

              if (hasRouteWriter) {
                // create a write stream for the static file if enabled
                routeWriter = sys.createWriteStream(routeFilePath);
                routeWriter.on('error', (e) => {
                  console.error(e);
                  routeWriter = null;
                  result.error = {
                    message: e.message,
                    stack: e.stack,
                  };
                });
              }
            } catch (e: any) {
              console.error('Error during stream start', staticRoute.pathname, e);
              routeWriter = null;
              result.error = {
                message: String(e),
                stack: e.stack || '',
              };
            }
          },
          write(chunk) {
            try {
              if (routeWriter) {
                // write to the static file if enabled
                routeWriter.write(Buffer.from(chunk.buffer));
              }
            } catch (e: any) {
              console.error('Error during stream write', staticRoute.pathname, e);
              routeWriter = null;
              result.error = {
                message: String(e),
                stack: e.stack || '',
              };
            }
          },
          async close() {
            const writePromises: Promise<any>[] = [];

            try {
              if (writeLoaderDataEnabled && !isNotFoundPage) {
                const routeLoaders = getRouteLoaders(requestEv);
                const loaderValues = getRouteLoaderValues(requestEv);
                const manifestHash = (opts.manifest as any)?.manifestHash || 'dev';
                // Write individual per-loader files for static loaders (expires === 0)
                for (const loader of routeLoaders) {
                  if (loader.__expires !== 0) {
                    continue;
                  }
                  const data = loaderValues[loader.__id];
                  if (data === undefined) {
                    continue;
                  }
                  const loaderFilePath = sys.getLoaderFilePath(
                    url.pathname,
                    loader.__id,
                    manifestHash
                  );
                  const loaderWriter = sys.createWriteStream(loaderFilePath);
                  loaderWriter.on('error', (e) => {
                    console.error(e);
                    result.error = {
                      message: e.message,
                      stack: e.stack,
                    };
                  });

                  // Match the LoaderResponse envelope { d, r, e } produced by loaderHandler
                  // so the client deserializer reads the same shape from static and dynamic responses.
                  const serialized = await deps.serialize({ d: data });
                  loaderWriter.write(serialized);

                  writePromises.push(
                    new Promise<void>((resolve) => {
                      result.filePath = routeFilePath;
                      loaderWriter.end(resolve);
                    })
                  );
                }
              }

              if (routeWriter) {
                // close the static file if there is one
                writePromises.push(
                  new Promise<void>((resolve) => {
                    // set the static file path for the result
                    result.filePath = routeFilePath;
                    routeWriter!.end(resolve);
                  }).finally(closeResolved)
                );
              }

              if (writePromises.length > 0) {
                await Promise.all(writePromises);
              }
            } catch (e: any) {
              console.error('Error during stream close', staticRoute.pathname, e);
              routeWriter = null;
              result.error = {
                message: String(e),
                stack: e.stack || '',
              };
            }
          },
        });
        return stream;
      },
    };

    const promise = requestHandlerForSsg(requestCtx, opts, deps)
      .then((rsp) => {
        if (rsp != null) {
          return rsp.completion.then((r) => {
            if (routeWriter) {
              return closePromise.then(() => r);
            }
            return r;
          });
        }
      })
      .then((e: any) => {
        if (e !== undefined) {
          if (isRedirectMessage(e)) {
            // TODO We should render a html page for redirects too
            // that would require refactoring redirects
            return;
          }
          if (e instanceof Error) {
            result.error = {
              message: e.message,
              stack: e.stack,
            };
          } else {
            result.error = {
              message: String(e),
              stack: undefined,
            };
          }
          console.error('Error during request handling', staticRoute.pathname, e);
        }
      })
      .catch((e) => {
        console.error('Unhandled error during request handling', staticRoute.pathname, e);
        result.error = {
          message: String(e),
          stack: e.stack || '',
        };
      })
      .finally(() => {
        pendingPromises.delete(promise);
        callback(result);
      });

    pendingPromises.add(promise);
  } catch (e: any) {
    console.error('Error during render', staticRoute.pathname, e);
    if (e instanceof Error) {
      result.error = {
        message: e.message,
        stack: e.stack,
      };
    } else {
      result.error = {
        message: String(e),
        stack: undefined,
      };
    }
    callback(result);
  }
}

/** Create a fresh no-op WritableStream (must be a real instance for pipeTo checks). */
const createNoopWritableStream = () => new WritableStream();

function isRedirectMessage(value: unknown) {
  return (
    typeof value === 'object' &&
    value !== null &&
    (value as { constructor?: { name?: string } }).constructor?.name === 'RedirectMessage'
  );
}

async function requestHandlerForSsg<T>(
  serverRequestEv: ServerRequestEvent<T>,
  opts: SsgHandlerOptions,
  deps: WorkerThreadDeps
) {
  const { render, checkOrigin, qwikRouterConfig } = opts;

  if (!qwikRouterConfig) {
    throw new Error('qwikRouterConfig is required.');
  }

  const pathname = deps.trimInternalPathname(serverRequestEv.url.pathname);
  if (pathname === '/.well-known' || pathname.startsWith('/.well-known/')) {
    return null;
  }

  const { routes, serverPlugins, cacheModules } = qwikRouterConfig;
  const loadedRoute = await deps.loadRoute(routes, cacheModules, pathname);
  const requestHandlers = deps.resolveRequestHandlers(
    serverPlugins,
    loadedRoute,
    serverRequestEv.request.method,
    checkOrigin ?? true,
    deps.renderQwikMiddleware(render)
  );

  // The forced 404.html prerender is a miss by design — render it even under fallthrough so the
  // static asset exists for the adapter/host to serve.
  if (qwikRouterConfig.fallthrough && loadedRoute.$notFound$ && !pathname.endsWith('/404.html')) {
    return null;
  }

  const rebuildRouteInfo = async (url: URL) => {
    const cleanPathname = deps.trimInternalPathname(url.pathname);
    const loadedRoute = await deps.loadRoute(routes, cacheModules, cleanPathname);
    const requestHandlers = deps.resolveRequestHandlers(
      serverPlugins,
      loadedRoute,
      serverRequestEv.request.method,
      checkOrigin ?? true,
      deps.renderQwikMiddleware(render)
    );
    return {
      loadedRoute,
      requestHandlers,
    };
  };

  return deps.runQwikRouter(
    serverRequestEv,
    loadedRoute,
    requestHandlers,
    rebuildRouteInfo,
    qwikRouterConfig.basePathname
  );
}
