import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { Router as createRouter } from "express";
import * as pty from "node-pty";
import { v4 as uuidv4 } from "uuid";
import WebSocket, { WebSocketServer } from "ws";
import { isRuntimeTokenValid, validateToken } from "../middleware/auth.js";
import { createLogger } from "../utils/logger.js";
const log = createLogger("pty");
const DEFAULT_COLS = 80;
const DEFAULT_ROWS = 24;
const MAX_OUTPUT_BUFFER_CHARS = 200_000;
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
const DEFAULT_EXITED_TTL_MS = 5 * 60 * 1000;
const DEFAULT_CONNECT_TOKEN_TTL_MS = 60 * 1000;
const DEFAULT_MAX_PTY_SESSIONS = 8;
const NON_INTERACTIVE_UNIX_SHELLS = new Set([
    "false",
    "halt",
    "nologin",
    "reboot",
    "shutdown",
    "sync",
    "true",
]);
export const ptySessions = new Map();
const ptyConnectTokens = new Map();
/**
 * 解析 PTY 空闲清理时间。
 * 主流程：读取环境变量 -> 非法值回退默认值 -> 用于 WebSocket 全断开后的延迟清理。
 */
export function resolvePtyIdleTtlMs(env = process.env) {
    const raw = String(env.AWS_PTY_IDLE_TTL_MS || "").trim();
    if (!raw) {
        return DEFAULT_IDLE_TTL_MS;
    }
    const parsed = Number(raw);
    if (!Number.isFinite(parsed) || parsed < 1_000) {
        return DEFAULT_IDLE_TTL_MS;
    }
    return parsed;
}
/**
 * 选择默认 shell。
 * 主流程：Windows 优先 pwsh/powershell/cmd；Unix 优先 SHELL/bash/sh，返回可执行文件名与参数。
 */
export function resolveDefaultShell(platform = process.platform, env = process.env, existsSync = fs.existsSync) {
    if (platform === "win32") {
        const comSpec = String(env.ComSpec || env.COMSPEC || "").trim();
        return { shell: comSpec || "powershell.exe", args: [] };
    }
    const configuredShell = String(env.SHELL || "").trim();
    const shell = [configuredShell, "/bin/bash", "/bin/sh"].find((candidate) => isUsableUnixShellCandidate(candidate, existsSync));
    return { shell: shell || "/bin/sh", args: [] };
}
/**
 * 判断 Unix shell 候选是否适合交互式 PTY。
 * 具体逻辑：跳过 nologin/false 等会立即退出的系统 shell；绝对路径还必须真实存在。
 */
function isUsableUnixShellCandidate(candidate, existsSync) {
    const shell = candidate.trim();
    if (!shell) {
        return false;
    }
    const basename = path.basename(shell).toLowerCase();
    if (NON_INTERACTIVE_UNIX_SHELLS.has(basename)) {
        return false;
    }
    return !path.isAbsolute(shell) || existsSync(shell);
}
/**
 * 校验并解析 shell 选择。
 * 主流程：auto 使用默认 shell；显式 shell 必须命中 allowlist，避免前端任意执行二进制。
 */
export function resolveRequestedShell(requestedShell, platform = process.platform, env = process.env) {
    const normalized = String(requestedShell || "auto").trim();
    if (!normalized || normalized === "auto") {
        return resolveDefaultShell(platform, env);
    }
    const allowlist = new Set([
        "bash",
        "sh",
        "zsh",
        "fish",
        "pwsh",
        "pwsh.exe",
        "powershell",
        "powershell.exe",
        "cmd",
        "cmd.exe",
    ]);
    const basename = path.basename(normalized).toLowerCase();
    if (!allowlist.has(basename)) {
        throw new Error(`shell is not allowed: ${normalized}`);
    }
    return { shell: normalized, args: [] };
}
/**
 * 判断候选路径是否位于允许根目录中。
 * 具体逻辑：使用 path.relative 处理 Windows/Unix 分隔符，拒绝向上穿越和跨盘路径。
 */
export function isPathInsideRoot(candidatePath, rootPath) {
    const relative = path.relative(path.resolve(rootPath), path.resolve(candidatePath));
    return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function resolveAllowedWorkspaceRoots(env = process.env) {
    return String(env.AWS_PTY_ALLOWED_ROOTS || "")
        .split(path.delimiter)
        .map((item) => item.trim())
        .filter(Boolean)
        .map((item) => path.resolve(item));
}
/**
 * 解析并校验 PTY 工作目录。
 * 主流程：要求绝对目录存在；如果配置 AWS_PTY_ALLOWED_ROOTS，则必须位于允许根目录内。
 */
export function resolvePtyWorkspacePath(workspacePath, env = process.env) {
    const raw = String(workspacePath || "").trim();
    if (!raw) {
        throw new Error("workspacePath is required");
    }
    if (!path.isAbsolute(raw)) {
        throw new Error("workspacePath must be absolute");
    }
    const resolved = path.resolve(raw);
    const stat = fs.statSync(resolved);
    if (!stat.isDirectory()) {
        throw new Error("workspacePath must be an existing directory");
    }
    const allowedRoots = resolveAllowedWorkspaceRoots(env);
    if (allowedRoots.length > 0 && !allowedRoots.some((root) => isPathInsideRoot(resolved, root))) {
        throw new Error("workspacePath is outside allowed roots");
    }
    return resolved;
}
function normalizeDimension(value, fallback) {
    const parsed = Number(value);
    if (!Number.isInteger(parsed) || parsed <= 0) {
        return fallback;
    }
    return Math.min(parsed, 500);
}
function buildPtyEnv(baseEnv) {
    return {
        ...baseEnv,
        TERM: baseEnv.TERM || "xterm-256color",
        COLORTERM: baseEnv.COLORTERM || "truecolor",
    };
}
function appendOutputBuffer(session, data) {
    session.outputBuffer = `${session.outputBuffer}${data}`;
    if (session.outputBuffer.length > MAX_OUTPUT_BUFFER_CHARS) {
        session.outputBuffer = session.outputBuffer.slice(-MAX_OUTPUT_BUFFER_CHARS);
    }
}
function toSummary(session) {
    return {
        id: session.id,
        title: session.title,
        workspacePath: session.workspacePath,
        shell: session.shell,
        cols: session.cols,
        rows: session.rows,
        status: session.status,
        createdAt: session.createdAt,
        lastActiveAt: session.lastActiveAt,
        exitCode: session.exitCode,
        signal: session.signal,
        attachedClients: session.sockets.size,
        ownerUserId: session.ownerUserId,
        tenantId: session.tenantId,
        projectName: session.projectName,
    };
}
function sendJson(socket, payload) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify(payload));
    }
}
function broadcast(session, payload) {
    for (const socket of session.sockets) {
        sendJson(socket, payload);
    }
}
function scheduleIdleCleanup(session) {
    if (session.idleTimer) {
        clearTimeout(session.idleTimer);
    }
    session.idleTimer = setTimeout(() => {
        if (session.sockets.size === 0) {
            closePtySession(session.id, "idle-timeout");
        }
    }, resolvePtyIdleTtlMs());
}
function cancelIdleCleanup(session) {
    if (session.idleTimer) {
        clearTimeout(session.idleTimer);
        session.idleTimer = null;
    }
}
/**
 * 创建 PTY session。
 * 主流程：校验 cwd/shell -> spawn 真实 PTY -> 注册 output/exit 事件 -> 返回元数据给控制面。
 */
export function createPtySession(input) {
    if (ptySessions.size >= resolveMaxPtySessions()) {
        throw new Error("maximum PTY sessions reached");
    }
    const workspacePath = resolvePtyWorkspacePath(input.workspacePath);
    const shell = resolveRequestedShell(input.shell);
    const cols = normalizeDimension(input.cols, DEFAULT_COLS);
    const rows = normalizeDimension(input.rows, DEFAULT_ROWS);
    const id = `pty_${uuidv4()}`;
    const now = new Date().toISOString();
    const title = String(input.title || "").trim() || `Terminal ${ptySessions.size + 1}`;
    const ptyProcess = pty.spawn(shell.shell, shell.args, {
        name: "xterm-256color",
        cols,
        rows,
        cwd: workspacePath,
        env: buildPtyEnv(process.env),
    });
    const session = {
        id,
        title,
        workspacePath,
        shell: shell.shell,
        cols,
        rows,
        status: "running",
        createdAt: now,
        lastActiveAt: now,
        attachedClients: 0,
        ptyProcess,
        sockets: new Set(),
        outputBuffer: "",
        idleTimer: null,
        persistent: input.persistent === true,
        ownerUserId: String(input.ownerUserId || "").trim() || undefined,
        tenantId: String(input.tenantId || "").trim() || undefined,
        projectName: String(input.projectName || "").trim() || undefined,
    };
    ptyProcess.onData((data) => {
        session.lastActiveAt = new Date().toISOString();
        appendOutputBuffer(session, data);
        broadcast(session, { type: "output", data });
    });
    ptyProcess.onExit(({ exitCode, signal }) => {
        session.status = "exited";
        session.exitCode = exitCode;
        session.signal = signal;
        session.lastActiveAt = new Date().toISOString();
        broadcast(session, { type: "exit", exitCode, signal });
        if (session.sockets.size === 0 && !session.persistent) {
            scheduleIdleCleanup(session);
        }
        else if (session.persistent) {
            if (session.idleTimer) {
                clearTimeout(session.idleTimer);
            }
            session.idleTimer = setTimeout(() => {
                closePtySession(session.id, "exited-timeout");
            }, resolvePtyExitedTtlMs());
        }
    });
    ptySessions.set(id, session);
    if (!session.persistent) {
        scheduleIdleCleanup(session);
    }
    injectPtyPreCommand(session, input.preCommand);
    log.info(`created pty session id=${id}, cwd=${workspacePath}, shell=${shell.shell}`);
    return toSummary(session);
}
/**
 * 注入 PTY 前置命令。
 * 主流程：忽略空命令 -> 保持已有结尾换行 -> 否则追加一次回车，模拟用户输入执行。
 */
function injectPtyPreCommand(session, preCommand) {
    const command = typeof preCommand === "string" ? preCommand : "";
    if (!command.trim()) {
        return;
    }
    const payload = `${command.replace(/\n/g, "\r").replace(/\r+$/g, "")}\r`;
    try {
        session.ptyProcess.write(payload);
    }
    catch (error) {
        const err = error;
        log.warn(`failed to inject pty pre-command sessionId=${session.id}: ${err.message}`);
    }
}
function resolvePtyConnectTokenTtlMs(env = process.env) {
    const raw = String(env.AWS_PTY_CONNECT_TOKEN_TTL_MS || "").trim();
    if (!raw) {
        return DEFAULT_CONNECT_TOKEN_TTL_MS;
    }
    const parsed = Number(raw);
    if (!Number.isFinite(parsed) || parsed < 5_000 || parsed > 10 * 60 * 1000) {
        return DEFAULT_CONNECT_TOKEN_TTL_MS;
    }
    return parsed;
}
export function resolvePtyExitedTtlMs(env = process.env) {
    const raw = String(env.AWS_PTY_EXITED_TTL_MS || "").trim();
    if (!raw) {
        return DEFAULT_EXITED_TTL_MS;
    }
    const parsed = Number(raw);
    if (!Number.isFinite(parsed) || parsed < 1_000) {
        return DEFAULT_EXITED_TTL_MS;
    }
    return parsed;
}
export function resolveMaxPtySessions(env = process.env) {
    const raw = String(env.AWS_PTY_MAX_SESSIONS || "").trim();
    if (!raw) {
        return DEFAULT_MAX_PTY_SESSIONS;
    }
    const parsed = Number(raw);
    if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
        return DEFAULT_MAX_PTY_SESSIONS;
    }
    return parsed;
}
function readCreatePtySessionInput(body) {
    if (!body || typeof body !== "object") {
        return { workspacePath: undefined };
    }
    const record = body;
    return {
        workspacePath: record.workspacePath,
        title: record.title,
        shell: record.shell,
        cols: record.cols,
        rows: record.rows,
        persistent: record.persistent,
        preCommand: record.preCommand,
        ownerUserId: record.ownerUserId,
        tenantId: record.tenantId,
        projectName: record.projectName,
    };
}
export function listPtySessions() {
    return Array.from(ptySessions.values()).map(toSummary);
}
export function getPtySessionSummary(id) {
    const session = ptySessions.get(id);
    return session ? toSummary(session) : undefined;
}
/**
 * 生成短期 PTY 连接令牌。
 * 主流程：确认 session 存在 -> 生成随机 token -> 记录 session 绑定和过期时间。
 */
export function createPtyConnectToken(sessionId) {
    if (!ptySessions.has(sessionId)) {
        return undefined;
    }
    const token = `ptyws_${crypto.randomBytes(32).toString("base64url")}`;
    const expiresAtMs = Date.now() + resolvePtyConnectTokenTtlMs();
    ptyConnectTokens.set(token, { sessionId, expiresAt: expiresAtMs });
    return { token, expiresAt: new Date(expiresAtMs).toISOString() };
}
function consumePtyConnectToken(sessionId, token) {
    const entry = ptyConnectTokens.get(token);
    if (!entry) {
        return false;
    }
    if (entry.expiresAt < Date.now()) {
        ptyConnectTokens.delete(token);
        return false;
    }
    if (entry.sessionId !== sessionId) {
        return false;
    }
    ptyConnectTokens.delete(token);
    return true;
}
function deletePtyConnectTokensForSession(sessionId) {
    for (const [token, entry] of ptyConnectTokens.entries()) {
        if (entry.sessionId === sessionId) {
            ptyConnectTokens.delete(token);
        }
    }
}
/**
 * 关闭 PTY session。
 * 主流程：关闭连接 -> kill PTY -> 清理定时器和内存索引。
 */
export function closePtySession(id, reason = "closed") {
    const session = ptySessions.get(id);
    if (!session) {
        return false;
    }
    cancelIdleCleanup(session);
    deletePtyConnectTokensForSession(id);
    broadcast(session, { type: "status", status: "closing", reason });
    for (const socket of session.sockets) {
        socket.close(1000, reason);
    }
    session.sockets.clear();
    if (session.status === "running") {
        session.ptyProcess.kill();
    }
    ptySessions.delete(id);
    log.info(`closed pty session id=${id}, reason=${reason}`);
    return true;
}
export function closeAllPtySessions(reason = "shutdown") {
    for (const id of Array.from(ptySessions.keys())) {
        closePtySession(id, reason);
    }
}
export const ptyRouter = createRouter();
ptyRouter.get("/sessions", validateToken, (_req, res) => {
    res.json({ ok: true, sessions: listPtySessions() });
});
ptyRouter.post("/sessions", validateToken, (req, res) => {
    try {
        const session = createPtySession(readCreatePtySessionInput(req.body));
        res.status(201).json({ ok: true, session });
    }
    catch (error) {
        const err = error;
        res.status(400).json({ error: err.message });
    }
});
ptyRouter.get("/sessions/:sessionId", validateToken, (req, res) => {
    const session = getPtySessionSummary(req.params.sessionId);
    if (!session) {
        res.status(404).json({ error: "pty session not found" });
        return;
    }
    res.json({ ok: true, session });
});
ptyRouter.delete("/sessions/:sessionId", validateToken, (req, res) => {
    const closed = closePtySession(req.params.sessionId, "explicit-close");
    if (!closed) {
        res.status(404).json({ error: "pty session not found" });
        return;
    }
    res.json({ ok: true });
});
ptyRouter.post("/sessions/:sessionId/connect-token", validateToken, (req, res) => {
    const token = createPtyConnectToken(req.params.sessionId);
    if (!token) {
        res.status(404).json({ error: "pty session not found" });
        return;
    }
    res.json({ ok: true, ...token });
});
function parseWebSocketMessage(raw) {
    const text = raw.toString();
    try {
        return JSON.parse(text);
    }
    catch {
        return { type: "input", data: text };
    }
}
function handlePtySocket(session, socket) {
    cancelIdleCleanup(session);
    session.sockets.add(socket);
    session.lastActiveAt = new Date().toISOString();
    sendJson(socket, { type: "status", status: session.status, session: toSummary(session) });
    if (session.outputBuffer) {
        sendJson(socket, { type: "output", data: session.outputBuffer, replay: true });
    }
    socket.on("message", (raw) => {
        const message = parseWebSocketMessage(raw);
        if (!message || typeof message !== "object") {
            return;
        }
        const typed = message;
        const type = String(typed.type || "");
        session.lastActiveAt = new Date().toISOString();
        if (type === "input") {
            session.ptyProcess.write(String(typed.data || ""));
            return;
        }
        if (type === "resize") {
            const cols = normalizeDimension(typed.cols, session.cols);
            const rows = normalizeDimension(typed.rows, session.rows);
            session.cols = cols;
            session.rows = rows;
            session.ptyProcess.resize(cols, rows);
            return;
        }
        if (type === "close") {
            closePtySession(session.id, "client-close");
            return;
        }
        if (type === "ping") {
            sendJson(socket, { type: "pong" });
        }
    });
    socket.on("close", () => {
        session.sockets.delete(socket);
        session.lastActiveAt = new Date().toISOString();
        if (session.sockets.size === 0 && session.status === "running" && !session.persistent) {
            scheduleIdleCleanup(session);
        }
    });
}
function extractWsToken(request, requestUrl) {
    const queryToken = requestUrl.searchParams.get("token") || "";
    if (queryToken.trim()) {
        return queryToken.trim();
    }
    const headerToken = request.headers["x-runtime-token"];
    if (Array.isArray(headerToken)) {
        return headerToken[0]?.trim() || "";
    }
    if (typeof headerToken === "string") {
        return headerToken.trim();
    }
    const authorization = String(request.headers.authorization || "");
    const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
    return bearerMatch?.[1]?.trim() || "";
}
/**
 * 将 PTY WebSocket upgrade 挂到 HTTP server。
 * 主流程：匹配 /pty/sessions/:id/stream -> token 校验 -> session 校验 -> 交给 noServer WSS。
 */
export function attachPtyWebSocketServer(server) {
    const wss = new WebSocketServer({ noServer: true });
    server.on("upgrade", (request, socket, head) => {
        const host = request.headers.host || "localhost";
        const requestUrl = new URL(request.url || "/", `http://${host}`);
        const match = requestUrl.pathname.match(/^\/pty\/sessions\/([^/]+)\/stream$/);
        if (!match) {
            return;
        }
        const token = extractWsToken(request, requestUrl);
        const sessionId = decodeURIComponent(match[1] || "");
        if (!isRuntimeTokenValid(token) && !consumePtyConnectToken(sessionId, token)) {
            socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
            socket.destroy();
            return;
        }
        const session = ptySessions.get(sessionId);
        if (!session) {
            socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
            socket.destroy();
            return;
        }
        wss.handleUpgrade(request, socket, head, (ws) => {
            handlePtySocket(session, ws);
        });
    });
}
