import express, { Request, Response, NextFunction } from 'express';
import { Repository } from '../db/repository.js';
import { logger } from '../logger.js';
import { loadConfig } from '../config.js';
import { requireAuth, requireAdmin, resolveOrgIds } from './auth.js';
import { mergeSshConfig } from '../ssh/config.js';
import { isKeyConfigured } from '../mcp/crypto.js';
import {
  bootstrapSystemDek,
  verifySystemDek,
  encryptPrivateKey as sshEncryptPrivateKey,
  decryptPrivateKey as sshDecryptPrivateKey,
  computeKeyFingerprint as sshComputeKeyFingerprint,
  formatPublicKey as sshFormatPublicKey,
  generateKeypair as sshGenerateKeypair,
  type GeneratedKeyType as SshGeneratedKeyType,
} from '../ssh/crypto.js';
import { resolveConsoleTaskSpace } from './console-space-resolver.js';
import { createConnectionRepo } from '../ssh/connection-repo.js';
import { createGrantsRepo } from '../ssh/grants-repo.js';
import { createAuditRepo } from '../ssh/audit-repo.js';
import { createAbuseRepo } from '../ssh/abuse-repo.js';
import { createAccessResolver } from '../ssh/access.js';
import { maintenance as sshMaintenance } from '../ssh/maintenance.js';
import { createAdminRateLimiter, FORCE_UNLOCK_LIMIT } from '../ssh/admin-rate-limit.js';
import {
  sshTest,
  sshExec,
  sshUpload,
  sshDownload,
  openShellChannel,
  type ResolvedConnection as SshResolvedConnection,
} from '../ssh/session.js';
import { SessionRegistry } from '../ssh/console-registry.js';
import { createSshUserRouter, createSshAdminRouter, type SshApiDeps } from './ssh-api.js';
import { setSshSubsystem, preflight as sshPreflight, type SshSubsystem } from '../engine/tools/ssh.js';
import { listAgentConsoleSessions, closeConsoleSessionByUser } from '../engine/tools/ssh-console.js';
import { __setActiveSessionLookup } from '../engine/agent-loop.js';
import {
  createConsoleStatusRouter,
  createConsoleSessionRouter,
  createConsoleSessionsRouter,
  createConsoleSessionCloseRouter,
  type SimpleTask,
  type SimpleUser,
} from './console-ws-api.js';
import { createConsoleAdminRouter } from './console-admin-api.js';

/**
 * Deps the SSH Console WebSocket upgrade hook + status endpoints need.
 * Captured by setupSshSubsystem and consumed by startCoreServer to wire the
 * WS upgrade onto the http.Server it eventually creates. Null when SSH is
 * disabled / failed init.
 */
export interface SshConsoleDeps {
  registry: SessionRegistry;
  resolveUserFromUpgrade: (req: import('http').IncomingMessage) => Promise<SimpleUser | null>;
  resolveTask: (taskId: string, user: SimpleUser) => Promise<SimpleTask | null>;
  resolveSshAccess: (
    user: SimpleUser,
    session: import('../ssh/console-session.js').ConsoleSession,
    task: SimpleTask,
  ) => Promise<boolean>;
  denyPatterns: import('./console-ws-api.js').DenyPatternProvider;
}

export interface SshSubsystemDeps {
  repo: Repository;
  authActive: boolean;
  authenticateUpgrade: import('./auth.js').UpgradeAuthChecker | undefined;
}

/**
 * Wire the SSH subsystem (connection / grants / audit / abuse repos, access
 * resolver, session registry, crypto wrappers) and mount its admin/user REST
 * routers + Console status/session/admin routers. Gated on ssh.enabled AND
 * MCP_ENCRYPTION_KEY.
 *
 * Returns the SshConsoleDeps for startCoreServer to attach the Console WS
 * upgrade hook, or null when SSH is disabled / fails to initialise.
 *
 * Extracted verbatim from createCoreServer (server.ts); see
 * server-subsystems.test.ts for the gate characterization.
 */
export function setupSshSubsystem(app: express.Application, deps: SshSubsystemDeps): SshConsoleDeps | null {
  const { repo, authActive, authenticateUpgrade } = deps;

  // Phase 5 (SSH Console): sshConsole is captured here so that
  // startCoreServer() can wire the WS upgrade hook to the http.Server
  // it eventually creates. null when SSH is disabled / failed init.
  let sshConsole: SshConsoleDeps | null = null;

  const sshConfig = mergeSshConfig(loadConfig().ssh);
  if (!sshConfig.enabled) {
    setSshSubsystem(null);
    __setActiveSessionLookup(null);
  } else if (!isKeyConfigured()) {
    logger.warn('[ssh] MCP_ENCRYPTION_KEY not configured — SSH features disabled');
    setSshSubsystem(null);
    __setActiveSessionLookup(null);
  } else {
    try {
      bootstrapSystemDek(repo.getDb());
      verifySystemDek(repo.getDb());

        const connectionRepo = createConnectionRepo(repo.getDb());
        const grantsRepo = createGrantsRepo(repo.getDb());
        const auditRepo = createAuditRepo(repo.getDb());
        const abuseRepo = createAbuseRepo(repo.getDb(), {
          windowMinutes: sshConfig.abuseWindowMinutes,
          failureThreshold: sshConfig.abuseFailureThreshold,
          lockMinutes: sshConfig.abuseLockMinutes,
        });
        const accessResolver = createAccessResolver(grantsRepo, {
          adminBypassesGrants: sshConfig.adminBypassesGrants,
        });
        const forceUnlockLimiter = createAdminRateLimiter(FORCE_UNLOCK_LIMIT);

        // Hoisted above sshDeps so the grant-revocation hook can call
        // sessionRegistry.revokeAccessFor (introduced for Phase 5 hardening:
        // kick active WS viewers when their grant is deleted).
        const sessionRegistry = new SessionRegistry({
          idleTimeoutMs: sshConfig.console.idleTimeoutSeconds * 1000,
          maxSessionDurationMs: sshConfig.console.maxSessionDurationSeconds * 1000,
          maxSessionsPerConnection: sshConfig.console.maxSessionsPerConnection,
        });

        const sshDeps: SshApiDeps = {
          db: repo.getDb(),
          authActive,
          requireAuth: authActive ? requireAuth : (_req, _res, next) => next(),
          requireAdmin: authActive ? requireAdmin : (_req, _res, next) => next(),
          getUserId: (req) => (req.user as { id?: string } | undefined)?.id ?? null,
          isAdmin: (req) => (req.user as { role?: string } | undefined)?.role === 'admin',
          getOrgIds: (req) => ((req.user as { orgIds?: string[] } | undefined)?.orgIds ?? []),
          getSpace: (spaceId, viewer) => repo.getSpace(spaceId, { viewer }),
          connectionRepo,
          grantsRepo,
          auditRepo,
          abuseRepo,
          accessResolver,
          maintenance: sshMaintenance,
          forceUnlockLimiter,
          encryptKeyMaterial: (ownerId, pem, passphrase, spaceId) => {
            const { blob, keyVersion } = sshEncryptPrivateKey(repo.getDb(), ownerId, pem, spaceId);
            const passphraseBlob = passphrase
              ? sshEncryptPrivateKey(repo.getDb(), ownerId, passphrase, spaceId).blob
              : null;
            const fingerprint = sshComputeKeyFingerprint(pem, passphrase);
            const publicKey = sshFormatPublicKey(pem, passphrase);
            return { blob, passphraseBlob, keyVersion, fingerprint, publicKey };
          },
          decryptKeyMaterial: (ownerId, blob, spaceId) => sshDecryptPrivateKey(repo.getDb(), ownerId, blob, spaceId),
          decryptPassphrase: (ownerId, blob, spaceId) =>
            blob ? sshDecryptPrivateKey(repo.getDb(), ownerId, blob, spaceId) : null,
          generateKeypair: (keyType: SshGeneratedKeyType) => sshGenerateKeypair(keyType),
          derivePublicKey: (ownerId, blob, passphraseBlob, spaceId) => {
            const pem = sshDecryptPrivateKey(repo.getDb(), ownerId, blob, spaceId);
            const pass = passphraseBlob
              ? sshDecryptPrivateKey(repo.getDb(), ownerId, passphraseBlob, spaceId)
              : null;
            try {
              return sshFormatPublicKey(pem, pass);
            } finally {
              pem.fill(0);
              if (pass) pass.fill(0);
            }
          },
          sshTester: {
            async test({ connection, decryptedKey, passphrase, timeoutMs }) {
              const conn: SshResolvedConnection = {
                id: connection.id,
                ownerId: connection.ownerId,
                host: connection.host,
                port: connection.port,
                username: connection.username,
                privateKeyPem: decryptedKey,
                passphrase: passphrase ?? undefined,
                hostKeyB64: connection.hostKeyB64,
                hostKeyVerified: connection.hostKeyVerifiedAt !== null,
                allowPrivate:
                  sshConfig.allowPrivateAddresses || connection.allowPrivateAddresses,
              };
              return sshTest({ connection: conn, timeoutMs });
            },
          },
          connectionTestTimeoutMs: sshConfig.callTimeoutSeconds * 1000,
          onAccessRevoked: ({ connectionId, userId }) =>
            sessionRegistry.revokeAccessFor({
              connectionId,
              userId,
              reason: 'access_revoked',
            }),
        };

        app.use('/api/ssh/admin', express.json(), createSshAdminRouter(sshDeps));
        app.use('/api/ssh', express.json(), createSshUserRouter(sshDeps));

        // Phase 7: register the SSH tool subsystem so SshExec / SshUpload /
        // SshDownload tools can access the same repos / session primitives /
        // crypto wrappers that the HTTP layer uses. sessionRegistry is
        // constructed above (hoisted so sshDeps.onAccessRevoked can use it).
        // Captured in a local const (not just passed to setSshSubsystem)
        // so the user-initiated console-session REST endpoint can call the
        // shared openConsoleSession core with the EXACT same `sub` the
        // agent-facing console tools use — no second SshSubsystem.
        const sshSubsystem: SshSubsystem = {
          connectionRepo,
          auditRepo,
          abuseRepo,
          accessResolver,
          decryptKeyMaterial: (ownerId, blob, spaceId) =>
            sshDecryptPrivateKey(repo.getDb(), ownerId, blob, spaceId),
          decryptPassphrase: (ownerId, blob, spaceId) =>
            blob ? sshDecryptPrivateKey(repo.getDb(), ownerId, blob, spaceId) : null,
          getUserAccess: (userId) => {
            const user = repo.getUserById(userId);
            const isAdmin = user?.role === 'admin';
            const orgIds = resolveOrgIds(repo, userId);
            return { isAdmin, orgIds };
          },
          sshExec,
          sshUpload,
          sshDownload,
          maintenance: sshMaintenance,
          config: sshConfig,
          sessionRegistry,
          openShellChannel,
        };
        setSshSubsystem(sshSubsystem);

        // Phase 4 (SSH Console): wire the registry into agent-loop so
        // buildSystemPrompt can auto-inject the live screen tail into
        // the LLM system prompt for movements that allow SshConsole*.
        // Task 5: a task can now hold several live sessions at once (one
        // per connection), so this hands prompt.ts every live session for
        // the task; prompt.ts scopes the injected blocks down to the
        // current movement's allowedSshConnections.
        __setActiveSessionLookup((taskId) => listAgentConsoleSessions(sshSubsystem, taskId));

        // Phase 5 (SSH Console): start the periodic sweep so idle /
        // duration-cap sessions actually get closed. Without this, the
        // registry just holds sessions until shutdown.
        sessionRegistry.startSweepTimer(60_000);

        // Phase 5 (SSH Console): when SSH maintenance mode activates
        // (master-key rotation), close all live console sessions. They
        // would otherwise hold a decrypted DEK reference past the
        // rewrap window. The reason 'maintenance' is surfaced to the
        // WS client as the close cause.
        sshMaintenance.onEnter(async () => {
          const all = sessionRegistry.listAll();
          for (const s of all) {
            await sessionRegistry.closeForTask(s.localTaskId, 'maintenance');
          }
        });

        // Capture WS / status deps for startCoreServer to wire up.
        const consoleDeps: SshConsoleDeps = {
          registry: sessionRegistry,
          resolveUserFromUpgrade: async (req) => {
            if (!authenticateUpgrade) {
              // No-auth single-user mode: synthesize a stable `local` admin
              // user so the Console terminal WS attaches (admin role makes
              // the null-owner no-auth task visible in resolveTask). Mirrors
              // the Console REST routers and notifications-api.
              return { id: 'local', role: 'admin' };
            }
            const u = await authenticateUpgrade(req);
            return u ? { id: u.id, role: u.role } : null;
          },
          resolveTask: async (taskId, user) => {
            const idNum = Number(taskId);
            if (!Number.isFinite(idNum)) return null;
            const viewer: Express.User = {
              id: user.id,
              email: '',
              name: null,
              avatarUrl: null,
              role: (user.role === 'admin' ? 'admin' : 'user'),
              status: 'active',
              orgIds: resolveOrgIds(repo, user.id),
              defaultVisibility: 'private',
              defaultVisibilityOrgId: null,
            };
            const task = await repo.getLocalTask(idNum, { viewer });
            if (!task) return null;
            // Per-space isolation (spec §11) + IDOR guard: resolve the task's
            // effective space (personal-WS NULL+owner→personal space, worker
            // parity) AND require the ACTING USER to be a genuine owner/member
            // of it before it gates SSH-console access. Task visibility (which
            // let `user` reach here) is NOT space membership — a public/org
            // task must not let a non-member open/attach a console on that
            // space's hosts. null = fail closed / legacy space-less path.
            const spaceId = await resolveConsoleTaskSpace(
              repo,
              { ownerId: task.ownerId ?? null, spaceId: task.spaceId ?? null },
              { id: user.id },
              (msg) => logger.warn(`[ssh:console] resolveTask task=${task.id}: ${msg}`),
            );
            return {
              id: String(task.id),
              ownerId: task.ownerId ?? '',
              visibility: task.visibility,
              pieceName: task.pieceName,
              spaceId,
            };
          },
          resolveSshAccess: async (user, session, task) => {
            const connection = connectionRepo.resolveConnection(session.connectionId);
            if (!connection) return false;
            const orgIds = resolveOrgIds(repo, user.id);
            const decision = accessResolver.resolveAccess({
              connection,
              userId: user.id,
              isAdmin: user.role === 'admin',
              // Use the task's actual piece name so piece-specific grants in
              // ssh_connection_grants match (applies_to_all_pieces=0 case).
              // Bug pre-fix: hardcoded '' silently failed every piece-scoped grant.
              pieceName: task.pieceName,
              orgIds,
              // Per-space isolation (spec §11): the WS re-attach gate must
              // honor the task's space like SshExec/open, else re-attaching to
              // a workspace connection is denied with space_mismatch.
              spaceId: task.spaceId ?? null,
            });
            return decision.allowed;
          },
          denyPatterns: {
            async getPatterns(connectionId: string) {
              const c = connectionRepo.resolveConnection(connectionId);
              if (!c) return { deny: [], allow: [] };
              const split = (s: string | null): string[] =>
                s ? s.split('\n').map((x) => x.trim()).filter((x) => x.length > 0) : [];
              return {
                deny: split(c.commandDenyPatterns),
                allow: split(c.commandAllowPatterns),
              };
            },
          },
        };
        sshConsole = consoleDeps;

        // REST status endpoint: /api/local/tasks/:taskId/console/status
        app.use(
          '/api',
          createConsoleStatusRouter({
            registry: sessionRegistry,
            authActive,
            requireAuth: authActive ? requireAuth : (_req: Request, _res: Response, next: NextFunction) => next(),
            resolveTask: consoleDeps.resolveTask,
            // Per-session authz recheck on the most-recent session before it is
            // advertised as active (task visibility ≠ connection authz).
            resolveSshAccess: consoleDeps.resolveSshAccess,
          }),
        );

        // REST sessions-list endpoint: GET
        // /api/local/tasks/:taskId/console/sessions. Filters per session via
        // resolveSshAccess (the IDOR guard) and joins connection labels only
        // for the sessions that survive the filter.
        app.use(
          '/api',
          createConsoleSessionsRouter({
            registry: sessionRegistry,
            authActive,
            requireAuth: authActive ? requireAuth : (_req: Request, _res: Response, next: NextFunction) => next(),
            resolveTask: consoleDeps.resolveTask,
            resolveSshAccess: consoleDeps.resolveSshAccess,
            resolveConnectionLabel: (connectionId) =>
              connectionRepo.resolveConnection(connectionId)?.label ?? null,
          }),
        );

        // REST manual-close endpoint: POST
        // /api/local/tasks/:taskId/console/session/close. Owner/admin or the
        // session's starter may close; closeConsoleSessionByUser records the
        // real actor in the audit log (user_close).
        app.use(
          '/api',
          createConsoleSessionCloseRouter({
            registry: sessionRegistry,
            authActive,
            requireAuth: authActive ? requireAuth : (_req: Request, _res: Response, next: NextFunction) => next(),
            resolveTask: consoleDeps.resolveTask,
            closeSession: (args) => closeConsoleSessionByUser(sshSubsystem, args),
          }),
        );

        // REST user-initiated session-open endpoint:
        // POST /api/local/tasks/:taskId/console/session. Reuses the same
        // SshSubsystem + preflight the console tools use; the access gate
        // runs inside openConsoleSession against task.pieceName.
        // NOTE: no express.json() here — the router is mounted on the whole
        // /api prefix, so a mount-level parser (default limit 100kb) would
        // run for EVERY /api request and 413 large bodies before the
        // route-specific parsers (e.g. the task-attachment limit) ever ran.
        // The session route carries its own scoped json() parser.
        app.use(
          '/api',
          createConsoleSessionRouter({
            sub: sshSubsystem,
            preflight: sshPreflight,
            authActive,
            requireAuth: authActive ? requireAuth : (_req: Request, _res: Response, next: NextFunction) => next(),
            resolveTask: consoleDeps.resolveTask,
          }),
        );

        // Phase 6 (SSH Console): admin list + kill endpoints. The
        // `/api/admin` prefix already has `express.json()` mounted above
        // (see Admin user management API), so POST bodies parse correctly.
        app.use(
          '/api/admin',
          createConsoleAdminRouter({
            registry: sessionRegistry,
            requireAdmin: authActive ? requireAdmin : (_req: Request, _res: Response, next: NextFunction) => next(),
          }),
        );

        logger.info('[ssh] subsystem initialised');
    } catch (e) {
      logger.error(`[ssh] init failed err=${String(e)}`);
      setSshSubsystem(null);
      __setActiveSessionLookup(null);
    }
  }

  return sshConsole;
}
