import express, { type Application, type Request, type Response } from 'express';
import { logger } from '../logger.js';
import { parseTaskId } from './validation.js';
import { canViewTask, resolveSpaceAccess } from './local-api-helpers.js';
import { type LocalTasksDeps, resolveTaskWriteGate } from './local-tasks-shared.js';
import {
  resolvePythonPackagesConfig,
  parseAndValidateSpec,
  assertNotShadowing,
} from '../engine/python-packages.js';
import { isBwrapAvailable } from '../engine/tools/sandbox.js';
import { addPackageToSpace } from './space-python-packages-service.js';

/**
 * Package-request mechanism (PR3): list a task's recorded Python package
 * requests, and approve/deny a pending one.
 *
 * SECURITY MODEL:
 *   - Listing needs task VIEW permission.
 *   - Deciding needs task WRITE permission (owner / admin / space editor) — an
 *     agent can NEVER approve its own request.
 *   - Approving installs ONLY through the shared installSpacePackages path
 *     (bwrap-isolated / wheels-only / fixed index / stdlib+preinstalled
 *     shadowing guards / index URL never leaked), and re-validates the stored
 *     spec (defense in depth). The install is idempotent (content-addressed +
 *     name-deduped), so a double approve never double-installs.
 */
export function registerLocalTaskPackageRequestRoutes(app: Application, deps: LocalTasksDeps): void {
  const { repo } = deps;

  // List the package requests recorded for a task (RequestPackage declarations).
  app.get('/api/local/tasks/:taskId/package-requests', async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) { res.status(400).json({ error: 'Invalid task ID' }); return; }
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      if (!canViewTask(req, res, task, resolveSpaceAccess(repo, task, viewer))) return;
      res.json({ packageRequests: repo.listPackageRequestsByTask(String(taskId)) });
    } catch (err) {
      logger.error(`Package requests list API error: ${err}`);
      res.status(500).json({ error: 'Failed to list package requests' });
    }
  });

  // Approve (install into the space overlay + resume) or deny a pending package
  // request. Requires task write permission.
  app.post('/api/local/tasks/:taskId/package-requests/:reqId/decide', express.json(), async (req: Request, res: Response) => {
    try {
      const taskId = parseTaskId(req.params.taskId);
      if (taskId === null) { res.status(400).json({ error: 'Invalid task ID' }); return; }
      const reqId = String(req.params.reqId);
      const decision = (req.body as { decision?: unknown })?.decision;
      if (decision !== 'approve' && decision !== 'deny') {
        res.status(400).json({ error: 'decision must be "approve" or "deny"' });
        return;
      }
      const viewer = req.user as Express.User | undefined;
      const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
      const gate = await resolveTaskWriteGate(repo, viewer, task ? { ownerId: task.ownerId, spaceId: task.spaceId } : null);
      if (gate !== 'allow') {
        res.status(gate === 'view-only' ? 403 : 404)
          .json({ error: gate === 'view-only' ? 'No permission to decide package requests' : 'Task not found' });
        return;
      }
      const pr = repo.getPackageRequest(reqId);
      if (!pr || pr.taskId !== String(taskId)) { res.status(404).json({ error: 'Package request not found' }); return; }
      if (pr.status !== 'pending') { res.status(409).json({ error: `Already decided (${pr.status})` }); return; }

      const decidedBy = viewer?.id ?? null;

      if (decision === 'deny') {
        // CAS: if a concurrent approve already claimed it, don't resume-as-denied.
        if (!repo.decidePackageRequest(reqId, { status: 'denied', decidedBy })) {
          res.status(409).json({ error: 'Already decided' });
          return;
        }
        const resumed = pr.jobId ? repo.resumePackageRequestJob(pr.jobId) : 0;
        res.json({ ok: true, decision, spec: pr.spec, resumed: resumed > 0 });
        return;
      }

      // ---- approve ----
      // Validate everything cheap BEFORE claiming so a bad config doesn't burn
      // the pending state.
      if (!pr.spaceId) {
        // Requests are only recorded with a space (worker gate), so this is a
        // defensive guard: without a space there is nowhere to persist the
        // desired-state / overlay.
        res.status(422).json({ error: 'This request has no workspace to install into.' });
        return;
      }
      const cfg = resolvePythonPackagesConfig(deps.opts.getPythonPackagesConfig?.());
      if (!cfg.enabled) {
        res.status(400).json({ error: 'Python package management is disabled (config: python_packages.enabled)' });
        return;
      }
      // Re-validate the stored spec (defense in depth — the DB holds a string).
      let parsed;
      try {
        parsed = parseAndValidateSpec(pr.spec);
        assertNotShadowing(parsed.normalizedName);
      } catch (e) {
        res.status(400).json({ error: `invalid stored spec: ${(e as Error).message}` });
        return;
      }
      const bwrapAvailable = await isBwrapAvailable();
      if (!bwrapAvailable) {
        // Installs are fail-closed: never run pip without the isolated sandbox.
        res.status(503).json({ error: 'The bwrap sandbox is unavailable on this server, so packages cannot be installed safely.' });
        return;
      }

      // CLAIM the request (CAS pending→approved) BEFORE installing. This closes
      // the race where a concurrent deny marks it denied while we install: if
      // the claim loses (another decide won), we abort WITHOUT installing. On
      // install failure we revert to pending so it can be retried.
      if (!repo.decidePackageRequest(reqId, { status: 'approved', decidedBy })) {
        res.status(409).json({ error: 'Already decided' });
        return;
      }

      // ownerId is only a fallback arg to spaceKeyFor (ignored when a space id
      // is present), but fetch it for parity with the admin form.
      const space = await repo.getSpace(pr.spaceId, viewer ? { viewer } : undefined);
      let result;
      try {
        result = await addPackageToSpace({
          repo,
          cfg,
          spaceId: pr.spaceId,
          ownerId: space?.ownerId ?? null,
          parsed,
          bwrapAvailable,
        });
      } catch (e) {
        repo.revertPackageRequestToPending(reqId);
        throw e;
      }
      if ('installError' in result || 'tooMany' in result || 'versionConflict' in result) {
        // Revert so the (still-parked) job can be re-approved / denied after a fix.
        repo.revertPackageRequestToPending(reqId);
        if ('tooMany' in result) {
          res.status(400).json({ error: `too many packages (max ${cfg.maxPackagesPerSpace})` });
        } else if ('versionConflict' in result) {
          res.status(409).json({ error: `already pinned to a different version: ${result.versionConflict}. Change it in Settings → Python first.` });
        } else {
          res.status(422).json({ error: result.installError });
        }
        return;
      }
      // `conflict` (already installed) OR `ok` are both success: the overlay
      // already contains the wheel, so the request is satisfied. The claim above
      // already set status=approved.
      const resumed = pr.jobId ? repo.resumePackageRequestJob(pr.jobId) : 0;
      logger.info(`[local-tasks] package request approved task=${taskId} spec=${pr.spec} space=${pr.spaceId}`);
      res.json({ ok: true, decision, spec: pr.spec, resumed: resumed > 0 });
    } catch (err) {
      logger.error(`Package request decide API error: ${err}`);
      res.status(500).json({ error: 'Failed to decide package request' });
    }
  });
}
