/*
 * Licensed to Apache Software Foundation (ASF) under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Apache Software Foundation (ASF) licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

import type { FastifyInstance, FastifyReply } from 'fastify';
import fastifyStatic from '@fastify/static';

import type { Config } from '../config.js';

// Anchor to process.cwd() (always the server package dir) rather than __dirname so
// the path is correct both in ts-node (src/plugins/) and compiled (dist/src/plugins/).
const WEB_DIST = join(process.cwd(), '..', 'web', 'dist');

export async function registerStatic(app: FastifyInstance, config: Config): Promise<void> {
  if (!existsSync(WEB_DIST)) {
    app.log.warn(`[static] web/dist not found at ${WEB_DIST} — SPA serving disabled (run npm run build in web/)`);
    return;
  }

  const indexPath = join(WEB_DIST, 'index.html');
  if (!existsSync(indexPath)) {
    app.log.warn(`[static] index.html not found at ${indexPath} — SPA serving disabled (run npm run build in web/)`);
    return;
  }

  let indexTemplate: string;
  try {
    indexTemplate = readFileSync(indexPath, 'utf8');
  } catch (error) {
    app.log.warn({ error }, `[static] failed to read ${indexPath} — SPA serving disabled`);
    return;
  }
  const runtimeIndex = indexTemplate
    .replace('<base href="/" />', `<base href="${config.baseHref}" />`)
    .replace('<meta name="canopy-base-path" content="/" />', `<meta name="canopy-base-path" content="${config.basePath}" />`);
  const sendIndex = async (reply: FastifyReply) => {
    reply.header('Content-Type', 'text/html; charset=utf-8');
    reply.header('Cache-Control', 'no-store, must-revalidate');
    reply.header('Pragma', 'no-cache');
    await reply.send(runtimeIndex);
  };

  await app.register(fastifyStatic, {
    root: WEB_DIST,
    prefix: '/',
    index: false,
    // SPA assets must always revalidate. Without these headers the browser
    // caches the index.html + the hash-named bundle, so a `npm run -w web build`
    // that swaps the bundle filename does NOT clear the browser's view of the
    // page — the cached HTML still points at the old hash. So index.html is
    // sent `no-store` (never cached; always refetched) and the hash-named
    // assets `no-cache, must-revalidate` (cached but revalidated each load,
    // so a swapped bundle is picked up while unchanged assets 304 cheaply).
    cacheControl: false,
    setHeaders: (res, path) => {
      if (path.endsWith('index.html')) {
        res.header('Cache-Control', 'no-store, must-revalidate');
        res.header('Pragma', 'no-cache');
      } else {
        res.header('Cache-Control', 'no-cache, must-revalidate');
      }
    },
    // Do NOT use wildcard here — we handle the SPA fallback manually below
    // to ensure /api, /auth, /monitoring, /healthz are NOT shadowed.
  });

  app.get('/', async (_request, reply) => sendIndex(reply));

  // SPA fallback: serve index.html for any path not matching API/auth/monitoring/healthz
  app.setNotFoundHandler(async (request, reply) => {
    const path = config.basePath === '/' ? request.url : request.url.slice(config.basePath.length) || '/';
    if (
      path.startsWith('/api/') ||
      path.startsWith('/auth/') ||
      path.startsWith('/monitoring/') ||
      path === '/healthz'
    ) {
      // Let these fall through to 404 naturally (they have real route handlers)
      await reply.status(404).send({ error: 'not_found' });
      return;
    }
    // SPA fallback — always re-validate the HTML.
    reply.header('Cache-Control', 'no-store, must-revalidate');
    reply.header('Pragma', 'no-cache');
    await sendIndex(reply);
  });
}
