/**
 * `module-functions` adapter — surfaces module-level exported functions
 * as synthetic service classes via the candidate-step pipeline.
 *
 * Why this exists: PowerSync / hand-rolled Drizzle / Supabase-queries
 * style codebases keep their business logic in modules of exported
 * async functions:
 *
 *   // db/queries/smc-rounds.ts
 *   export async function smcCreateRound(db, params) { ... }
 *   export async function smcGetRound(db, id) { ... }
 *   export async function smcUpdateRoundStatus(db, id, status) { ... }
 *
 * The class walker only sees classes — module functions are invisible
 * to it. But GitNexus traces cross-method call flows AND captures
 * function terminals as `Process` nodes (`Function:<filePath>:<fnName>`).
 * That signal is already in `facts.processes` at the point the
 * orchestrator finishes structural extraction.
 *
 * This adapter consumes the process terminals, groups them by file,
 * and synthesizes one service class per file — exactly mirroring the
 * `express-routes` pattern. The synthetic class body feeds into the
 * existing `extractCandidateSteps` classifier, so module-function
 * services get pre-classified timelines just like real class methods.
 *
 * Phase 0 of `2026-05-13-NORMALISE-PHASE.md`.
 *
 * Filed in proposal section §3.
 */
/**
 * Parse a `Function:<filePath>:<fnName>` terminalId. Returns null if
 * the ID doesn't match (e.g. it's a class method).
 *
 * Examples:
 *   "Function:db/queries/smc-rounds.ts:smcCreateRound" → { filePath, functionName }
 *   "Method:UserController:create" → null
 */
export function parseFunctionTerminalId(id) {
    // Format: Function:<filePath>:<functionName>. The filePath may contain
    // colons (drive letters on Windows, ports in URLs — rare in practice
    // but defensive). The functionName is always the LAST `:`-separated
    // segment. Anchor on `Function:` prefix.
    if (!id.startsWith('Function:'))
        return null;
    const rest = id.slice('Function:'.length);
    const lastColon = rest.lastIndexOf(':');
    if (lastColon < 0)
        return null;
    const filePath = rest.slice(0, lastColon);
    const functionName = rest.slice(lastColon + 1);
    if (!filePath || !functionName)
        return null;
    return { filePath, functionName };
}
/**
 * Walk `facts.processes`-shaped input for function terminals. Returns a
 * map: filePath → Set<functionName>. Skips:
 *   - Non-Function: terminals (class methods, etc.)
 *   - Files outside common source extensions
 *   - Files in node_modules / dist / build output
 */
export function groupTerminalsByFile(processes) {
    const out = new Map();
    for (const p of processes) {
        const parsed = parseFunctionTerminalId(p.terminalId);
        if (!parsed)
            continue;
        // Skip files that look like generated / vendored output.
        if (/node_modules|\.d\.ts$|dist\/|build\//.test(parsed.filePath))
            continue;
        // Stick to backend source extensions. `.tsx`/`.jsx` are React/JSX
        // files — those are frontend components + hooks, NOT backend
        // service modules. The react-views adapter handles them; this
        // adapter must not double-claim them as services.
        if (!/\.(ts|js|mjs|cjs)$/.test(parsed.filePath))
            continue;
        if (/\.d\.ts$/.test(parsed.filePath))
            continue;
        let set = out.get(parsed.filePath);
        if (!set) {
            set = new Set();
            out.set(parsed.filePath, set);
        }
        set.add(parsed.functionName);
    }
    return out;
}
/**
 * Find an exported top-level function in source. Handles four common
 * declaration shapes:
 *
 *   export async function <name>(...) { ... }
 *   export function <name>(...) { ... }
 *   export const <name> = async (...) => { ... }
 *   export const <name> = (...) => { ... }
 *
 * Returns null if not found or if the declaration's body can't be
 * isolated (single-expression arrow `=> expr` without a brace block).
 */
export function findExportedFunctionBody(source, fnName) {
    // Shape A: `export (async )? function <fnName>(...) { ... }`
    // The space-or-newline between `function` and the name allows for
    // multiline declarations with intervening whitespace.
    const funcRe = new RegExp(`\\bexport\\s+(async\\s+)?function\\s+${escapeRegex(fnName)}\\s*\\(`, 'm');
    const funcMatch = funcRe.exec(source);
    if (funcMatch) {
        const openParenIdx = funcMatch.index + funcMatch[0].length - 1;
        const closeParenIdx = findMatchingParen(source, openParenIdx);
        if (closeParenIdx < 0)
            return null;
        // Body opening brace = first `{` after `)` at angle-bracket depth 0
        // (skips `{` inside a return type generic like `Promise<{ ... }>`).
        const braceIdx = findBodyBrace(source, closeParenIdx + 1);
        if (braceIdx < 0)
            return null;
        return extractBody(source, funcMatch.index, braceIdx, !!funcMatch[1]);
    }
    // Shape B: `export const <fnName> = (async )? (...) [: Type]? => { ... }`
    const constRe = new RegExp(`\\bexport\\s+const\\s+${escapeRegex(fnName)}\\s*=\\s*(async\\s+)?\\(`, 'm');
    const constMatch = constRe.exec(source);
    if (constMatch) {
        const openParenIdx = constMatch.index + constMatch[0].length - 1;
        const closeParenIdx = findMatchingParen(source, openParenIdx);
        if (closeParenIdx < 0)
            return null;
        // After the `)`, expect optionally `: <ReturnType>` then `=>` then `{`.
        // The return type can contain `>`, `<`, `,`, generics, etc., so we
        // can't use a simple regex. Walk forward through angle-bracket-aware
        // chars until we hit `=>`.
        const arrowIdx = findArrowAfterParen(source, closeParenIdx + 1);
        if (arrowIdx < 0)
            return null;
        // After `=>`, skip whitespace, expect `{`.
        let i = arrowIdx + 2;
        while (i < source.length && /\s/.test(source[i]))
            i++;
        if (source[i] !== '{')
            return null;
        return extractBody(source, constMatch.index, i, !!constMatch[1]);
    }
    return null;
}
/**
 * Find the first `{` after `fromIdx` that's at angle-bracket depth 0.
 * This skips over generic-parameter braces like `Promise<{rows: Row[]}>`
 * which would otherwise be mistakenly identified as the function body.
 */
function findBodyBrace(source, fromIdx) {
    let angleDepth = 0;
    let strChar = null;
    for (let i = fromIdx; i < source.length; i++) {
        const c = source[i];
        if (strChar) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === strChar)
                strChar = null;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            strChar = c;
            continue;
        }
        if (c === '<')
            angleDepth++;
        else if (c === '>')
            angleDepth = Math.max(0, angleDepth - 1);
        else if (c === '{' && angleDepth === 0)
            return i;
    }
    return -1;
}
/**
 * Find the `=>` after `fromIdx` at angle-bracket depth 0. Lets us walk
 * past a `: Type<...>` return-type annotation on a const arrow form.
 */
function findArrowAfterParen(source, fromIdx) {
    let angleDepth = 0;
    let strChar = null;
    for (let i = fromIdx; i < source.length - 1; i++) {
        const c = source[i];
        const next = source[i + 1];
        if (strChar) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === strChar)
                strChar = null;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            strChar = c;
            continue;
        }
        if (c === '<')
            angleDepth++;
        else if (c === '>')
            angleDepth = Math.max(0, angleDepth - 1);
        else if (c === '=' && next === '>' && angleDepth === 0)
            return i;
        // Bail if we hit `{` or `;` at depth 0 without seeing `=>` — not an arrow form.
        else if ((c === '{' || c === ';') && angleDepth === 0)
            return -1;
    }
    return -1;
}
function extractBody(source, declStartIdx, openBraceIdx, isAsync) {
    const closeBraceIdx = findMatchingBrace(source, openBraceIdx);
    if (closeBraceIdx < 0)
        return null;
    const body = source.slice(openBraceIdx + 1, closeBraceIdx);
    const startLine = source.slice(0, declStartIdx).split('\n').length;
    const endLine = source.slice(0, closeBraceIdx + 1).split('\n').length;
    return {
        methodName: '', // filled by caller; we don't have the name here
        isAsync,
        body,
        sourceLineRange: [startLine, endLine],
    };
}
/**
 * Wrap a list of module functions in a synthetic class body. The output
 * is suitable for `walkMethodBodies` / `extractCandidateSteps` to
 * consume. Each function becomes a `<methodName>(...) { <body> }` member.
 *
 * Mirrors `synthesizeControllerClass` in `express-routes.ts`.
 */
export function synthesizeServiceClass(serviceName, functions) {
    const lines = [];
    lines.push(`class ${serviceName} {`);
    for (const f of functions) {
        const asyncWord = f.isAsync ? 'async ' : '';
        lines.push(`  ${asyncWord}${f.methodName}() {`);
        const bodyLines = f.body.split('\n').map((l) => '  ' + l);
        lines.push(...bodyLines);
        lines.push('  }');
    }
    lines.push('}');
    return lines.join('\n');
}
/**
 * Detect signals that a TS/JS module is client-side (React Native,
 * browser SPA, etc.) and therefore should NOT be classified as a Node
 * backend service by the module-functions synthesiser.
 *
 * Motivation (2026-05-15, surfaced by the idle-meta × marrbox cell of
 * the 9-cell comparison matrix): idle_meta is a React Native app.
 * Its `TokenStorageService.ts` uses `AsyncStorage` (RN client storage,
 * not available in Node). The module-functions synthesiser turned it
 * into a backend service with `TARGET RUNTIME: Node.js`, and realize
 * emitted code that would have crashed at runtime. marrbox correctly
 * refused (STUB002, "AsyncStorage not available in Node.js runtime")
 * and the verifier surfaced the spec gap — but the *upstream* fix is
 * to not extract client-side modules as backend services in the first
 * place.
 *
 * Detection is conservative: only flag when STRONG client-only signals
 * are present. Plain React (used dual-side in Next.js etc.) is NOT a
 * signal on its own. Detected signals are returned alongside the bool
 * so callers can surface them in diagnostics.
 */
export function detectClientSideModule(sourceText) {
    const signals = [];
    // Imports we treat as definitive client-only markers.
    const clientImportPatterns = [
        [/from\s+['"]react-native['"]/g, "import 'react-native'"],
        [/from\s+['"]react-native\//g, "import 'react-native/*'"],
        [/from\s+['"]react-native-[\w-]+['"]/g, "import 'react-native-*' (community)"],
        [/from\s+['"]@react-native-async-storage\/async-storage['"]/g, "import '@react-native-async-storage/async-storage'"],
        [/from\s+['"]@react-native\//g, "import '@react-native/*'"],
        [/from\s+['"]@react-navigation\//g, "import '@react-navigation/*'"],
        [/from\s+['"]expo['"]/g, "import 'expo'"],
        [/from\s+['"]expo-[\w-]+['"]/g, "import 'expo-*'"],
    ];
    for (const [re, label] of clientImportPatterns) {
        if (re.test(sourceText))
            signals.push(label);
    }
    // Direct symbol references that imply a browser / RN runtime. Guard
    // against false positives by requiring the symbol AS an identifier
    // (not inside a string / property name).
    const symbolPatterns = [
        [/(?<![A-Za-z0-9_$.])AsyncStorage\.(getItem|setItem|removeItem|multiGet|multiSet|clear)\b/g, "uses AsyncStorage API"],
        [/(?<![A-Za-z0-9_$.])localStorage\.(getItem|setItem|removeItem|clear)\b/g, "uses browser localStorage"],
        [/(?<![A-Za-z0-9_$.])sessionStorage\.(getItem|setItem|removeItem|clear)\b/g, "uses browser sessionStorage"],
        [/(?<![A-Za-z0-9_$.])window\.(document|location|navigator|alert|prompt)\b/g, "uses browser window globals"],
        [/(?<![A-Za-z0-9_$.])document\.(getElementById|querySelector|createElement|body)\b/g, "uses browser document"],
    ];
    for (const [re, label] of symbolPatterns) {
        if (re.test(sourceText))
            signals.push(label);
    }
    return { client: signals.length > 0, signals };
}
/**
 * Derive a service class name from a file path.
 *
 *   "db/queries/smc-rounds.ts" → "SmcRoundsService"
 *   "lib/round-guards.ts"      → "RoundGuardsService"
 *   "providers/AuthProvider.tsx" → "AuthProviderService"
 *
 * The file basename (no extension) is split on `-`, `_`, `.`, `/` and
 * each segment is PascalCased. `Service` suffix is appended unless the
 * derived name already ends in `Service` / `Controller`.
 */
export function deriveServiceName(filePath) {
    const basename = filePath.split('/').pop() ?? filePath;
    const noExt = basename.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
    const pascal = noExt
        .split(/[-_./]+/)
        .filter((p) => p.length > 0)
        .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
        .join('');
    if (/Service$/.test(pascal) || /Controller$/.test(pascal))
        return pascal;
    return `${pascal}Service`;
}
// ── Brace / paren matching helpers ───────────────────────────────────
// Inlined (not imported from express-routes) to keep this adapter
// self-contained. Same logic, same edge cases.
function findMatchingParen(source, openIdx) {
    if (source[openIdx] !== '(')
        return -1;
    let depth = 0;
    let strChar = null;
    for (let i = openIdx; i < source.length; i++) {
        const c = source[i];
        if (strChar) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === strChar)
                strChar = null;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            strChar = c;
            continue;
        }
        if (c === '(')
            depth++;
        else if (c === ')') {
            depth--;
            if (depth === 0)
                return i;
        }
    }
    return -1;
}
function findMatchingBrace(source, openIdx) {
    if (source[openIdx] !== '{')
        return -1;
    let depth = 0;
    let strChar = null;
    let inLineComment = false;
    let inBlockComment = false;
    for (let i = openIdx; i < source.length; i++) {
        const c = source[i];
        const next = source[i + 1];
        if (inLineComment) {
            if (c === '\n')
                inLineComment = false;
            continue;
        }
        if (inBlockComment) {
            if (c === '*' && next === '/') {
                inBlockComment = false;
                i++;
            }
            continue;
        }
        if (strChar) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === strChar)
                strChar = null;
            continue;
        }
        if (c === '/' && next === '/') {
            inLineComment = true;
            i++;
            continue;
        }
        if (c === '/' && next === '*') {
            inBlockComment = true;
            i++;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            strChar = c;
            continue;
        }
        if (c === '{')
            depth++;
        else if (c === '}') {
            depth--;
            if (depth === 0)
                return i;
        }
    }
    return -1;
}
function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
//# sourceMappingURL=module-functions.js.map