import { Router, type Request, type Response } from 'express';
import { readFileSync } from 'fs';
import { createRequire } from 'module';
import type { BrowserSessionRepo, BrowserSessionProfile } from '../db/browser-session-repo.js';
import type { SessionManager } from '../engine/browser-session.js';
import {
  initMasterKey,
  generateUserDek,
  encryptUserDek,
  decryptUserDek,
  encryptStateBlob,
  decryptStateBlob,
} from '../crypto/sessions.js';
import { buildNovncPath } from './novnc-proxy.js';
import { logger } from '../logger.js';

const requireFromHere = createRequire(import.meta.url);

let cachedPlaywrightVersion: string | null = null;
function getPlaywrightVersion(): string {
  if (cachedPlaywrightVersion) return cachedPlaywrightVersion;
  try {
    const pkgPath = requireFromHere.resolve('playwright/package.json');
    const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string };
    cachedPlaywrightVersion = pkg.version;
  } catch {
    cachedPlaywrightVersion = 'unknown';
  }
  return cachedPlaywrightVersion;
}

interface Deps {
  sessRepo: BrowserSessionRepo;
  sessionManager: SessionManager | null;
  masterKeyPath: string;
  authActive?: boolean;
}

interface AuthedUser {
  id: string;
  role: string;
}

function getUser(req: Request): AuthedUser | null {
  return (req.user as AuthedUser | undefined) ?? null;
}

/** JSON-friendly subset of a profile. NEVER includes the encrypted blob. */
function serializeProfile(p: BrowserSessionProfile): {
  id: number;
  label: string;
  startUrl: string;
  matchPatterns: string[];
  storageOrigins: string[];
  loggedInSelector: string | null;
  loginUrlPatterns: string[];
  status: BrowserSessionProfile['status'];
  stateVersion: number;
  lastSavedAt: string | null;
  lastUsedAt: string | null;
  lastValidatedAt: string | null;
  lastError: string | null;
  createdAt: string;
  updatedAt: string;
} {
  return {
    id: p.id,
    label: p.label,
    startUrl: p.startUrl,
    matchPatterns: p.matchPatterns,
    storageOrigins: p.storageOrigins,
    loggedInSelector: p.loggedInSelector,
    loginUrlPatterns: p.loginUrlPatterns,
    status: p.status,
    stateVersion: p.stateVersion,
    lastSavedAt: p.lastSavedAt,
    lastUsedAt: p.lastUsedAt,
    lastValidatedAt: p.lastValidatedAt,
    lastError: p.lastError,
    createdAt: p.createdAt,
    updatedAt: p.updatedAt,
  };
}

/**
 * Ensure the user has a DEK, creating + persisting one if needed.
 * Returns the decrypted DEK (32 bytes) for use with state-blob encryption.
 */
export function ensureUserDek(deps: Deps, userId: string): Buffer {
  const master = initMasterKey(deps.masterKeyPath);
  let enc = deps.sessRepo.getUserDek(userId);
  if (!enc) {
    const dek = generateUserDek();
    enc = encryptUserDek(master, dek);
    deps.sessRepo.setUserDek(userId, enc);
    return dek;
  }
  return decryptUserDek(master, enc);
}

function isStringArray(v: unknown): v is string[] {
  return Array.isArray(v) && v.every(s => typeof s === 'string');
}

export function createBrowserSessionApi(deps: Deps): Router {
  const r = Router();
  const authActive = deps.authActive ?? true;

  // Auth gate — every request must have req.user.
  // In no-auth mode (authActive=false), fall back to a synthetic 'local' user
  // so the Browser Sessions panel works in local dev without OAuth.
  r.use((req: Request, res: Response, next) => {
    if (!authActive && !getUser(req)) {
      (req as any).user = { id: 'local', role: 'user' };
    }
    if (!getUser(req)) {
      res.status(401).json({ error: 'Unauthenticated' });
      return;
    }
    next();
  });

  // GET /profiles — list owned profiles
  r.get('/profiles', (req: Request, res: Response) => {
    const u = getUser(req)!;
    const profiles = deps.sessRepo.listProfilesByOwner(u.id).map(serializeProfile);
    res.json({ profiles });
  });

  // POST /profiles — create a new profile (status=pending, no blob yet)
  r.post('/profiles', (req: Request, res: Response) => {
    const u = getUser(req)!;
    const b = (req.body ?? {}) as Record<string, unknown>;

    if (typeof b['label'] !== 'string' || (b['label'] as string).length === 0) {
      res.status(400).json({ error: 'label is required and must be a non-empty string' });
      return;
    }
    if (typeof b['startUrl'] !== 'string' || (b['startUrl'] as string).length === 0) {
      res.status(400).json({ error: 'startUrl is required and must be a non-empty string' });
      return;
    }
    if (b['matchPatterns'] !== undefined && !isStringArray(b['matchPatterns'])) {
      res.status(400).json({ error: 'matchPatterns must be a string[]' });
      return;
    }
    if (b['storageOrigins'] !== undefined && !isStringArray(b['storageOrigins'])) {
      res.status(400).json({ error: 'storageOrigins must be a string[]' });
      return;
    }
    if (b['loginUrlPatterns'] !== undefined && !isStringArray(b['loginUrlPatterns'])) {
      res.status(400).json({ error: 'loginUrlPatterns must be a string[]' });
      return;
    }
    if (b['loggedInSelector'] !== undefined && b['loggedInSelector'] !== null && typeof b['loggedInSelector'] !== 'string') {
      res.status(400).json({ error: 'loggedInSelector must be a string or null' });
      return;
    }

    const id = deps.sessRepo.createProfile({
      ownerId: u.id,
      label: b['label'] as string,
      startUrl: b['startUrl'] as string,
      matchPatterns: isStringArray(b['matchPatterns']) ? b['matchPatterns'] : [],
      storageOrigins: isStringArray(b['storageOrigins']) ? b['storageOrigins'] : [],
      loggedInSelector: typeof b['loggedInSelector'] === 'string' ? b['loggedInSelector'] : null,
      loginUrlPatterns: isStringArray(b['loginUrlPatterns']) ? b['loginUrlPatterns'] : [],
    });

    deps.sessRepo.audit({
      actorUserId: u.id,
      ownerId: u.id,
      profileId: id,
      action: 'create',
      result: 'success',
    });

    const profile = deps.sessRepo.getProfileById(id, u.id);
    res.status(201).json({ profile: serializeProfile(profile!) });
  });

  // DELETE /profiles/:id — owner-only delete
  r.delete('/profiles/:id', (req: Request, res: Response) => {
    const u = getUser(req)!;
    const id = Number(req.params['id']);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }

    const ok = deps.sessRepo.deleteProfile(id, u.id);
    if (!ok) {
      // Either not found or not owned — same response either way.
      // Do NOT audit on failure (per checklist).
      res.status(404).json({ error: 'not found' });
      return;
    }

    deps.sessRepo.audit({
      actorUserId: u.id,
      ownerId: u.id,
      profileId: id,
      action: 'delete',
      result: 'success',
    });
    res.json({ ok: true });
  });

  // POST /profiles/:id/login — spawn an interactive noVNC session for the owner
  r.post('/profiles/:id/login', async (req: Request, res: Response) => {
    const u = getUser(req)!;
    const id = Number(req.params['id']);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const profile = deps.sessRepo.getProfileById(id, u.id);
    if (!profile) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    if (!deps.sessionManager) {
      res.status(503).json({ error: 'browser sessions unavailable (missing Xvfb/x11vnc/websockify)' });
      return;
    }
    try {
      const session = await deps.sessionManager.createLoginSession({ ownerId: u.id, profileId: id });
      // Navigate to start_url for the user (best-effort; do not fail the endpoint on goto error).
      try {
        const ctx = session.context;
        if (ctx) {
          const pages = ctx.pages();
          const page = pages.length > 0 ? pages[0]! : await ctx.newPage();
          await page.goto(profile.startUrl, { waitUntil: 'load', timeout: 60_000 });
        }
      } catch (gotoErr) {
        logger.warn(`[browser-session-api] login goto failed: ${(gotoErr as Error).message}`);
      }
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'login_start',
        result: 'success',
      });
      res.json({ sessionId: session.id, novncPath: buildNovncPath(session.id) });
    } catch (e) {
      const msg = (e as Error).message;
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'login_start',
        result: 'error',
        reason: msg,
      });
      res.status(500).json({ error: msg });
    }
  });

  // POST /profiles/:id/save — capture context.storageState(), encrypt, persist, destroy session
  r.post('/profiles/:id/save', async (req: Request, res: Response) => {
    const u = getUser(req)!;
    const id = Number(req.params['id']);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const sessionId = (req.body as { sessionId?: string } | undefined)?.sessionId;
    if (!sessionId) {
      res.status(400).json({ error: 'sessionId required' });
      return;
    }
    const profile = deps.sessRepo.getProfileById(id, u.id);
    if (!profile) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    if (!deps.sessionManager) {
      res.status(503).json({ error: 'browser sessions unavailable' });
      return;
    }

    const session = deps.sessionManager.getSession(sessionId);
    if (!session || session.kind !== 'login' || session.profileId !== id) {
      res.status(400).json({ error: 'login session does not match profile' });
      return;
    }

    try {
      if (!session.context) {
        throw new Error('login session has no browser context');
      }
      const state = await session.context.storageState();
      const dek = ensureUserDek(deps, u.id);
      const blob = encryptStateBlob(dek, JSON.stringify(state));
      deps.sessRepo.saveProfileBlob(id, blob, getPlaywrightVersion());
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'save',
        result: 'success',
      });
      await deps.sessionManager.destroySession(sessionId);
      const fresh = deps.sessRepo.getProfileById(id, u.id)!;
      res.json({ profile: serializeProfile(fresh) });
    } catch (e) {
      const msg = (e as Error).message;
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'save',
        result: 'error',
        reason: msg,
      });
      res.status(500).json({ error: msg });
    }
  });

  // POST /profiles/:id/cancel — abort an in-progress login session
  r.post('/profiles/:id/cancel', async (req: Request, res: Response) => {
    const u = getUser(req)!;
    const id = Number(req.params['id']);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const sessionId = (req.body as { sessionId?: string } | undefined)?.sessionId;
    if (!sessionId) {
      res.status(400).json({ error: 'sessionId required' });
      return;
    }
    const profile = deps.sessRepo.getProfileById(id, u.id);
    if (!profile || !deps.sessionManager) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    await deps.sessionManager.destroySession(sessionId);
    deps.sessRepo.audit({
      actorUserId: u.id,
      ownerId: u.id,
      profileId: id,
      action: 'login_cancel',
      result: 'success',
    });
    res.json({ ok: true });
  });

  // POST /profiles/:id/test — decrypt the saved state, drive a real headless
  // chromium against profile.startUrl, and update the profile's status based
  // on the auth-expiry heuristics in browser-session-expiry.
  r.post('/profiles/:id/test', async (req: Request, res: Response) => {
    const u = getUser(req)!;
    const id = Number(req.params['id']);
    if (!Number.isFinite(id)) {
      res.status(400).json({ error: 'invalid id' });
      return;
    }
    const profile = deps.sessRepo.getProfileById(id, u.id);
    if (!profile) {
      res.status(404).json({ error: 'not found' });
      return;
    }
    if (profile.encryptedStateBlob == null) {
      res.status(409).json({ error: 'profile has no saved state' });
      return;
    }

    // Lazy-load Playwright so the bridge layer doesn't require it at import time.
    let chromium: typeof import('playwright').chromium;
    try {
      const pw = await import('playwright');
      chromium = pw.chromium;
    } catch (e) {
      const msg = (e as Error).message;
      logger.warn(`[browser-session-api] playwright import failed: ${msg}`);
      res.status(503).json({ error: 'playwright not available' });
      return;
    }

    let stateJson: string;
    try {
      const dek = ensureUserDek(deps, u.id);
      stateJson = decryptStateBlob(dek, profile.encryptedStateBlob);
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'decrypt',
        result: 'success',
        reason: 'test',
      });
    } catch (e) {
      const msg = (e as Error).message;
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'decrypt',
        result: 'error',
        reason: msg,
      });
      res.status(500).json({ error: msg });
      return;
    }

    // detectAuthExpiry is loaded via dynamic import to avoid any circular-dep
    // surprises between the bridge and engine layers.
    const { detectAuthExpiry } = await import('../engine/browser-session-expiry.js');

    let browser: import('playwright').Browser | null = null;
    try {
      const { buildLaunchOptions, applyStealthInitScript } = await import('../engine/browser-launch.js');
      const { loadConfig } = await import('../config.js');
      browser = await chromium.launch(buildLaunchOptions(loadConfig().browser, true));
      const ctx = await browser.newContext({ storageState: JSON.parse(stateJson) });
      await applyStealthInitScript(ctx);
      const page = await ctx.newPage();
      const response = await page.goto(profile.startUrl, { waitUntil: 'load', timeout: 60_000 });
      const finalUrl = page.url();
      const statusCode = response?.status() ?? 0;
      const present = profile.loggedInSelector
        ? !!(await page.$(profile.loggedInSelector))
        : true;

      const verdict = detectAuthExpiry({
        profile: {
          loggedInSelector: profile.loggedInSelector,
          loginUrlPatterns: profile.loginUrlPatterns,
        },
        finalUrl,
        statusCode,
        loggedInSelectorPresent: present,
      });

      if (verdict.expired) {
        deps.sessRepo.markProfileStatus(id, 'expired', verdict.reason);
        deps.sessRepo.audit({
          actorUserId: u.id,
          ownerId: u.id,
          profileId: id,
          action: 'expire',
          result: 'success',
          reason: verdict.reason,
        });
      } else {
        deps.sessRepo.markProfileStatus(id, 'active', null);
        deps.sessRepo.audit({
          actorUserId: u.id,
          ownerId: u.id,
          profileId: id,
          action: 'test',
          result: 'success',
        });
      }

      res.json({ verdict, finalUrl, statusCode });
    } catch (e) {
      const msg = (e as Error).message;
      deps.sessRepo.audit({
        actorUserId: u.id,
        ownerId: u.id,
        profileId: id,
        action: 'test',
        result: 'error',
        reason: msg,
      });
      res.status(500).json({ error: msg });
    } finally {
      if (browser) {
        try {
          await browser.close();
        } catch (closeErr) {
          logger.warn(`[browser-session-api] browser.close failed: ${(closeErr as Error).message}`);
        }
      }
    }
  });

  return r;
}
