import express, { Router } from 'express';
import { canManageSpace } from './visibility.js';
import { viewerOf } from './space-viewer.js';
import { logger } from '../logger.js';
import {
  resolvePythonPackagesConfig,
  parseAndValidateSpec,
  assertNotShadowing,
  installSpacePackages,
  spaceKeyFor,
  pipPreflight,
  type ParsedSpec,
} from '../engine/python-packages.js';
import { isBwrapAvailable } from '../engine/tools/sandbox.js';
import type { SpaceApiDeps } from './space-api.js';
import {
  readState,
  withSpaceLock,
  addPackageToSpace,
  type PersistedState,
} from './space-python-packages-service.js';

/**
 * Per-space Python package management (admin form entry point).
 *
 * GET  /:id/python-packages         — viewer: list installed + feature state
 * POST /:id/python-packages         — canManageSpace: add a wheel + rebuild overlay
 * DELETE /:id/python-packages/:name — canManageSpace: remove + rebuild overlay
 *
 * The persisted desired-state (a JSON array on spaces.python_packages) is the
 * source of truth; each mutation rebuilds the whole content-addressed overlay
 * and atomically repoints `current`. Installs run out-of-band in a separate,
 * network-enabled bwrap with no workspace/secret access (see python-packages.ts).
 */

// PersistedState / readState / withSpaceLock / addPackageToSpace now live in
// space-python-packages-service.ts so the admin form and agent-approval paths
// share ONE mutex + ONE desired-state read-modify-write.

export function registerSpacePythonPackagesRoutes(router: Router, deps: SpaceApiDeps): void {
  const { repo } = deps;
  const jsonParser = express.json({ limit: '256kb' });

  function cfg() {
    return resolvePythonPackagesConfig(deps.getPythonPackagesConfig?.());
  }

  // Installs REQUIRE the isolated bwrap sandbox (fail-closed). Report pip + bwrap
  // readiness so the UI can explain why the form is unavailable.
  async function computePreflight(enabled: boolean): Promise<{ ok: boolean; reason?: string }> {
    if (!enabled) return { ok: false, reason: 'feature disabled' };
    if (!(await isBwrapAvailable())) {
      return { ok: false, reason: 'The bwrap sandbox is unavailable on this server, so packages cannot be installed safely.' };
    }
    return pipPreflight();
  }

  // GET — viewer may read the list + feature state.
  router.get('/:id/python-packages', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const c = cfg();
    const state = readState(repo.getSpacePythonPackages(space.id));
    const preflight = await computePreflight(c.enabled);
    // NOTE: indexUrl is deliberately NOT returned. A private index can embed
    // credentials (https://user:token@host/simple); this endpoint is readable by
    // any space viewer, so leaking it would expose the token to non-managers.
    res.json({
      enabled: c.enabled,
      maxPackagesPerSpace: c.maxPackagesPerSpace,
      preflight,
      packages: state.packages,
    });
  });

  // POST { spec } — canManageSpace: add one package and rebuild the overlay.
  router.post('/:id/python-packages', jsonParser, async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
    if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
      return res.status(403).json({ error: 'forbidden' });
    }
    const c = cfg();
    if (!c.enabled) return res.status(400).json({ error: 'Python package management is disabled (config: python_packages.enabled)' });

    let parsed: ParsedSpec;
    try {
      parsed = parseAndValidateSpec(String(req.body?.spec ?? ''));
      assertNotShadowing(parsed.normalizedName);
    } catch (e) {
      return res.status(400).json({ error: (e as Error).message });
    }

    const pre = await computePreflight(true);
    if (!pre.ok) return res.status(503).json({ error: pre.reason });

    try {
      const result = await addPackageToSpace({
        repo,
        cfg: c,
        spaceId: space.id,
        ownerId: space.ownerId,
        parsed,
        bwrapAvailable: await isBwrapAvailable(),
      });

      if ('conflict' in result) return res.status(409).json({ error: `already installed: ${parsed.normalizedName}` });
      if ('versionConflict' in result) return res.status(409).json({ error: `already pinned to a different version: ${result.versionConflict}. Remove it first.` });
      if ('tooMany' in result) return res.status(400).json({ error: `too many packages (max ${c.maxPackagesPerSpace})` });
      if ('installError' in result) return res.status(422).json({ error: result.installError });
      logger.info(`[space-api] python-package added space=${space.id} pkg=${parsed.spec}`);
      res.json({ packages: result.state.packages, lockHash: result.state.lockHash });
    } catch (err) {
      logger.error(`[space-api] python-package POST failed space=${space.id} err=${(err as Error).message}`);
      res.status(500).json({ error: 'internal error' });
    }
  });

  // DELETE /:name — canManageSpace: remove one package and rebuild the overlay.
  router.delete('/:id/python-packages/:name', async (req, res) => {
    const viewer = viewerOf(req, deps.authActive);
    const space = await repo.getSpace(req.params.id, { viewer });
    if (!space) return res.status(404).json({ error: 'not found' });
    const memberRole = repo.getSpaceMemberRole(space.id, viewer.id);
    if (!canManageSpace(viewer, { ownerId: space.ownerId }, memberRole)) {
      return res.status(403).json({ error: 'forbidden' });
    }
    const c = cfg();
    if (!c.enabled) return res.status(400).json({ error: 'Python package management is disabled (config: python_packages.enabled)' });
    const name = String(req.params.name || '').toLowerCase();

    try {
      const result = await withSpaceLock(space.id, async () => {
        const state = readState(repo.getSpacePythonPackages(space.id));
        const remaining = state.packages.filter((p) => p.name !== name);
        if (remaining.length === state.packages.length) return { notFound: true as const };
        const specs: ParsedSpec[] = remaining.map((p) => parseAndValidateSpec(p.spec));
        const install = await installSpacePackages({
          pkgRoot: c.dir,
          spaceKey: spaceKeyFor(space.id, space.ownerId),
          specs,
          cfg: c,
          bwrapAvailable: await isBwrapAvailable(),
        });
        if (!install.ok) return { installError: install.error ?? 'rebuild failed' };
        const next: PersistedState = {
          packages: remaining,
          lockHash: install.lockHash,
          updatedAt: new Date().toISOString(),
        };
        repo.setSpacePythonPackages(space.id, JSON.stringify(next));
        return { ok: true as const, state: next };
      });

      if ('notFound' in result) return res.status(404).json({ error: `not installed: ${name}` });
      if ('installError' in result) return res.status(422).json({ error: result.installError });
      logger.info(`[space-api] python-package removed space=${space.id} pkg=${name}`);
      res.json({ packages: result.state.packages, lockHash: result.state.lockHash });
    } catch (err) {
      logger.error(`[space-api] python-package DELETE failed space=${space.id} err=${(err as Error).message}`);
      res.status(500).json({ error: 'internal error' });
    }
  });
}
