import { Router } from 'express';
import httpProxy from 'http-proxy';
const { createProxyServer } = httpProxy;
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import express from 'express';
import type { Server } from 'http';
import type { Server as HttpsServer } from 'https';
import type { SessionManager, BrowserSession } from '../engine/browser-session.js';
import type { UpgradeAuthChecker } from './auth.js';
import { logger } from '../logger.js';

/**
 * noVNC upgrade を許可するかの判定コールバック。
 * server.ts が Repository を見て構築する。novnc-proxy 側は Repository を
 * 知らずに済ませるための薄いインターフェース。
 */
export type NovncSessionAuthorizer = (
  session: BrowserSession,
  user: Express.User,
) => Promise<boolean>;

const __dirname = dirname(fileURLToPath(import.meta.url));

/** vendor/noVNC の Web 配布物 (vnc.html を含む) ディレクトリ */
function novncStaticDir(): string {
  return resolve(__dirname, '../../vendor/noVNC');
}

/**
 * vendor/noVNC/vnc.html が配置済みかをチェックする。
 * Browser タブの iframe が `/novnc/vnc.html` を読みに行くので、
 * これが無いと express.static が 404 を返し catch-all ハンドラが
 * `{"error":"Not found"}` を返してしまう。
 * Docker / 新規セットアップ時の取り違いを早めに検知するため、
 * API 層と起動時ログの両方で参照する。
 */
export function isNovncStaticInstalled(): boolean {
  return existsSync(resolve(novncStaticDir(), 'vnc.html'));
}

/**
 * noVNC クライアント URL を生成するヘルパー（全箇所で統一使用）。
 *
 * `path` は **絶対パス** (`/novnc/<sid>/websockify`) で渡す必要がある:
 * - noVNC v1.6.0 は `new URL(path, location.href)` で WebSocket URL を組み立てる。
 *   vnc.html 自体が `/novnc/vnc.html` 配下にあるため、相対パス
 *   (`novnc/<sid>/websockify`) を渡すと `/novnc/novnc/<sid>/websockify` と
 *   `/novnc/` が二重になり接続できなくなる。
 * - noVNC v1.5.0 系は単純文字列連結 (`/${path}`) で組み立てるため、絶対パスを渡すと
 *   `//novnc/...` と先頭スラッシュが二重になる。Express の upgrade ハンドラ側で
 *   `^\/+novnc\/` と寛容にマッチさせて両系統を吸収する。
 */
export function buildNovncPath(sessionId: string): string {
  return `/novnc/vnc.html?path=/novnc/${sessionId}/websockify&autoconnect=true&resize=scale`;
}

export function createNovncRouter(): Router {
  const router = Router();
  if (!isNovncStaticInstalled()) {
    logger.warn(
      '[novnc-proxy] vendor/noVNC/vnc.html が見つかりません。Browser タブの iframe で 404 が出ます。' +
      ' scripts/setup-novnc.sh を実行するか、Dockerfile で noVNC tarball を展開してください。',
    );
  }
  router.use(express.static(novncStaticDir()));
  return router;
}

/**
 * noVNC WebSocket プロキシをセットアップする。
 *
 * セキュリティ (auth 有効時):
 * - kind === 'pool' (CAPTCHA Pool): user.role === 'admin' のみ通す
 * - kind === 'task' (Task Session): authorizeSession コールバックでタスク
 *   visibility を判定し、見られるユーザーだけ通す
 * - 旧モデル (kind 未設定 / userId 直接マッチ): 従来どおり owner / admin
 * - authenticateUpgrade 未設定 (dev モード) は session 存在確認だけで通す
 */
export function setupNovncWebSocketProxy(
  // Both http.Server and https.Server emit the 'upgrade' event used for WSS,
  // so either type works here as a WebSocket proxy host.
  server: Server | HttpsServer,
  getSessionManager: () => SessionManager | null,
  authenticateUpgrade?: UpgradeAuthChecker,
  authorizeSession?: NovncSessionAuthorizer,
): void {
  const proxy = createProxyServer({ ws: true });

  proxy.on('error', (err, _req, res) => {
    logger.warn(`[novnc-proxy] WebSocket proxy error: ${err.message}`);
    // res is a net.Socket for WebSocket upgrades, not http.ServerResponse
    if (res && 'writeHead' in res && typeof res.writeHead === 'function') {
      try { res.writeHead(502); res.end(); } catch {}
    } else if (res && 'destroy' in res && typeof res.destroy === 'function') {
      res.destroy();
    }
  });

  server.on('upgrade', (req, socket, head) => {
    const url = req.url ?? '';
    // Match /novnc/:sessionId/websockify (先頭スラッシュ二重 //novnc/... も許容)
    // 旧 noVNC (v1.5.x) は単純文字列連結で URL を組むため、絶対パスを `path` に
    // 渡すと先頭が `//` で来る。新 (v1.6.0+) は `new URL()` で正規化して `/` 1個。
    const match = url.match(/^\/+novnc\/([^/]+)\/websockify/);
    if (!match) return; // Let other upgrade handlers (if any) handle it

    const sessionId = match[1]!;
    const sm = getSessionManager();
    if (!sm) {
      logger.warn(`[novnc-proxy] SessionManager not available, rejecting WebSocket for ${sessionId}`);
      socket.destroy();
      return;
    }

    const session = sm.getSession(sessionId);
    if (!session) {
      logger.warn(`[novnc-proxy] Unknown session ${sessionId}, rejecting WebSocket`);
      socket.destroy();
      return;
    }

    const performProxy = (): void => {
      proxy.ws(req, socket, head, {
        target: `http://127.0.0.1:${session.novncPort}`,
      });
    };

    if (!authenticateUpgrade) {
      // 認証無効モード: 従来どおり session 存在確認のみで接続を許可
      performProxy();
      return;
    }

    // 認証有効モード: cookie からユーザーを解決して session の種別ごとに認可
    authenticateUpgrade(req).then(async (user) => {
      if (!user) {
        logger.warn(`[novnc-proxy] Unauthenticated WebSocket attempt for session ${sessionId}, rejecting`);
        socket.destroy();
        return;
      }

      let allowed: boolean;
      if (authorizeSession) {
        // 新モデル: pool / task に応じて authorizeSession で判定
        try {
          allowed = await authorizeSession(session, user);
        } catch (err) {
          logger.warn(`[novnc-proxy] authorizeSession threw for session ${sessionId}: ${(err as Error).message}, rejecting`);
          socket.destroy();
          return;
        }
      } else {
        // 旧モデル fallback: owner-or-admin
        const isOwner = session.userId === user.id;
        const isAdmin = user.role === 'admin';
        allowed = isOwner || isAdmin;
      }

      if (!allowed) {
        logger.warn(`[novnc-proxy] User ${user.id} (role=${user.role}) denied access to session ${sessionId} (kind=${session.kind} taskId=${session.taskId ?? '-'} owner=${session.userId ?? '-'})`);
        socket.destroy();
        return;
      }

      performProxy();
    }).catch((err) => {
      logger.warn(`[novnc-proxy] Auth check failed for session ${sessionId}: ${(err as Error).message}`);
      socket.destroy();
    });
  });
}
