/**
 * Trace Parser — parses Skyramp trace format and HAR files into
 * structured entries for analysis enrichment.
 */
import * as fs from "fs";
import * as path from "path";
import { KNOWN_AUTH_HEADERS } from "./workspaceAuth.js";
const FLOW_GAP_MS = 30_000;
// Redact-only headers: scrub these from traces but don't treat them as extractable auth headers.
// set-cookie carries session tokens; auth-token / client-secret / access-token are generic secrets.
const REDACT_ONLY_HEADERS = new Set(["set-cookie", "auth-token", "client-secret", "access-token"]);
// Build redaction check from the shared KNOWN_AUTH_HEADERS set (workspaceAuth.ts) plus
// the redact-only extras. Uses exact matching — KNOWN_AUTH_HEADERS covers all known variants.
function isSensitiveHeader(name) {
    const lower = name.toLowerCase();
    return KNOWN_AUTH_HEADERS.has(lower) || REDACT_ONLY_HEADERS.has(lower);
}
function groupIntoFlows(entries) {
    if (entries.length === 0)
        return [];
    const sorted = [...entries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
    const flows = [];
    let current = [sorted[0]];
    let flowIdx = 1;
    for (let i = 1; i < sorted.length; i++) {
        const prev = new Date(sorted[i - 1].timestamp).getTime();
        const curr = new Date(sorted[i].timestamp).getTime();
        if (curr - prev > FLOW_GAP_MS) {
            const first = new Date(current[0].timestamp).getTime();
            const last = new Date(current[current.length - 1].timestamp).getTime();
            flows.push({ flowId: `flow-${flowIdx++}`, entries: current, durationMs: last - first });
            current = [];
        }
        current.push(sorted[i]);
    }
    if (current.length > 0) {
        const first = new Date(current[0].timestamp).getTime();
        const last = new Date(current[current.length - 1].timestamp).getTime();
        flows.push({ flowId: `flow-${flowIdx}`, entries: current, durationMs: last - first });
    }
    return flows;
}
function sanitizeHeaders(headers) {
    const result = {};
    for (const [k, v] of Object.entries(headers)) {
        result[k] = isSensitiveHeader(k) ? "<redacted>" : v;
    }
    return result;
}
function parseSkyrampTrace(data) {
    return data.map((entry) => {
        const rawQp = entry.QueryParams || entry.queryParams;
        let queryParams;
        if (rawQp && typeof rawQp === "object" && !Array.isArray(rawQp) && Object.keys(rawQp).length > 0) {
            queryParams = {};
            for (const [k, v] of Object.entries(rawQp)) {
                queryParams[k] = Array.isArray(v) ? v.map(String) : [String(v)];
            }
        }
        return {
            method: (entry.method || entry.Method || entry.request?.method || "GET").toUpperCase(),
            path: entry.path || entry.Path || entry.url || entry.request?.url || "/",
            statusCode: entry.statusCode || entry.StatusCode || entry.status || entry.response?.status || 0,
            requestHeaders: entry.request?.headers ? sanitizeHeaders(entry.request.headers) : undefined,
            requestBody: summarizeBody(entry.request?.body ?? entry.requestBody ?? entry.RequestBody),
            queryParams,
            responseHeaders: entry.response?.headers ? sanitizeHeaders(entry.response.headers) : undefined,
            responseBody: summarizeBody(entry.response?.body ?? entry.responseBody ?? entry.ResponseBody),
            timestamp: entry.timestamp || entry.Timestamp || entry.request?.timestamp || "",
        };
    });
}
function parseHarTrace(harData) {
    const entries = harData?.log?.entries || [];
    return entries.map((entry) => {
        const req = entry.request || {};
        const res = entry.response || {};
        const url = new URL(req.url || "http://localhost/");
        const reqHeaders = {};
        for (const h of req.headers || [])
            reqHeaders[h.name] = h.value;
        const resHeaders = {};
        for (const h of res.headers || [])
            resHeaders[h.name] = h.value;
        let reqBody;
        if (req.postData?.text) {
            try {
                reqBody = JSON.parse(req.postData.text);
            }
            catch {
                reqBody = req.postData.text;
            }
        }
        let resBody;
        if (res.content?.text) {
            try {
                resBody = JSON.parse(res.content.text);
            }
            catch {
                resBody = res.content.text;
            }
        }
        const queryParams = {};
        url.searchParams.forEach((v, k) => {
            if (queryParams[k]) {
                queryParams[k].push(v);
            }
            else {
                queryParams[k] = [v];
            }
        });
        return {
            method: (req.method || "GET").toUpperCase(),
            path: url.pathname,
            statusCode: res.status || 0,
            requestHeaders: sanitizeHeaders(reqHeaders),
            requestBody: summarizeBody(reqBody),
            queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
            responseHeaders: sanitizeHeaders(resHeaders),
            responseBody: summarizeBody(resBody),
            timestamp: entry.startedDateTime || "",
        };
    });
}
function summarizeBody(body) {
    if (!body)
        return undefined;
    const str = typeof body === "string" ? body : JSON.stringify(body);
    if (str.length > 2000) {
        if (typeof body === "object" && body !== null) {
            const keys = Object.keys(body);
            return { _summarized: true, keys, _truncatedAt: 2000 };
        }
        return str.slice(0, 2000) + "...<truncated>";
    }
    return body;
}
export async function parseTraceFile(filePath) {
    const raw = fs.readFileSync(filePath, "utf-8");
    let data;
    try {
        data = JSON.parse(raw);
    }
    catch {
        return { entries: [], userFlows: [], format: "unknown" };
    }
    let entries;
    let format;
    if (data?.log?.entries) {
        entries = parseHarTrace(data);
        format = "har";
    }
    else if (Array.isArray(data)) {
        entries = parseSkyrampTrace(data);
        format = "skyramp";
    }
    else if (data?.traces && Array.isArray(data.traces)) {
        entries = parseSkyrampTrace(data.traces);
        format = "skyramp";
    }
    else {
        return { entries: [], userFlows: [], format: "unknown" };
    }
    const userFlows = groupIntoFlows(entries);
    return { entries, userFlows, format };
}
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", "coverage", "__pycache__", ".venv", "venv"]);
/** Known test-artifact directories where testbot-generated traces are written. */
const TRACE_SCAN_DIRS = [".skyramp", "tests", "test", "e2e", "playwright"];
/**
 * Recursively scan a directory for files matching a predicate, up to maxDepth levels.
 */
function scanDir(dir, predicate, maxDepth, results) {
    if (maxDepth < 0)
        return;
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const entry of entries) {
        if (entry.isDirectory()) {
            if (!SKIP_DIRS.has(entry.name)) {
                scanDir(path.join(dir, entry.name), predicate, maxDepth - 1, results);
            }
        }
        else if (entry.isFile() && predicate(entry.name)) {
            results.push(path.join(dir, entry.name));
        }
    }
}
/**
 * Scan only known test-artifact directories for trace files.
 * Root-level files are checked at depth 0; named test-artifact subdirs are scanned
 * at full depth. This prevents picking up committed demo assets (e.g. frontend/public/traces/).
 */
function scanTraceArtifactDirs(repositoryPath, predicate, results) {
    // Root-level files only (depth 0)
    scanDir(repositoryPath, predicate, 0, results);
    // Named test-artifact subdirectories (full depth)
    for (const dir of TRACE_SCAN_DIRS) {
        const full = path.join(repositoryPath, dir);
        if (fs.existsSync(full)) {
            scanDir(full, predicate, 5, results);
        }
    }
}
/**
 * Discover trace JSON files in a repository path.
 */
export function discoverTraceFiles(repositoryPath) {
    const found = [];
    // Fixed-name candidates at repo root
    for (const name of ["skyramp_traces.json", "traces.json", "trace.json", "traces.har", "trace.har"]) {
        const full = `${repositoryPath}/${name}`;
        if (fs.existsSync(full))
            found.push(full);
    }
    // Recursive scan scoped to test-artifact dirs: any *trace*.json|har, excluding scenario/test output files
    const isTraceJson = (name) => /\.(json|har)$/i.test(name) &&
        /trace/i.test(name) &&
        !/^scenario_/i.test(name) &&
        !/_test\.(json|har)$/i.test(name);
    scanTraceArtifactDirs(repositoryPath, isTraceJson, found);
    // Deduplicate and sort for deterministic ordering
    return [...new Set(found)].sort();
}
/**
 * Discover Playwright recording zip files in a repository path.
 */
export function discoverPlaywrightZips(repositoryPath) {
    const found = [];
    // Match: *playwright*.zip OR *_trace.zip (skyramp_export_zip output) OR trace.zip
    const isPlaywrightZip = (name) => /\.zip$/i.test(name) && (/playwright/i.test(name) ||
        /_trace\.zip$/i.test(name) ||
        name.toLowerCase() === "trace.zip");
    scanTraceArtifactDirs(repositoryPath, isPlaywrightZip, found);
    return [...new Set(found)].sort();
}
