/**
 * Programmatic UI page enumeration — translates "which frontend files changed"
 * into "which URLs should the agent capture blueprints for."
 *
 * Replaces the prose-driven Task 0 strategy ladder in the testbot prompt with
 * deterministic code so the candidate page list lives in the stateFile and is
 * reusable across analyze_changes consumers (testbot, analyze_test_health,
 * future blueprint-grounded recommenders).
 *
 * Strategies, tried in order:
 *
 *   Strategy 1 — Framework route grep
 *     Translate file-system-routed component paths directly to URLs. Covers
 *     Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
 *     Gated on detection of a framework config file in the repo.
 *
 *   Strategy 2 — Source-grounded routes
 *     Walks router/App files with the TS Compiler API for code-defined
 *     route declarations like `<Route path="/cart" element={<Cart/>}>`.
 *     Matches each route's component file OR declaration file against the
 *     changed-file list (so router rewrites surface their routes even when
 *     the component itself is unchanged). Handles React Router string-literal
 *     paths, cross-file constant resolution, Vue Router object literals,
 *     and Directus `defineModule` aggregation. See `sourceRouteExtractor.ts`.
 *
 *   Strategy 3 — Root fallback
 *     Read the workspace's frontend baseUrl and treat it as the single
 *     candidate page. Always available when a frontend service is configured.
 */
import * as fs from "fs";
import * as path from "path";
import { extractSourceRoutes } from "./sourceRouteExtractor.js";
import { readWorkspaceConfigRaw } from "./workspaceAuth.js";
// ── Strategy 1: framework route grep ────────────────────────────────────────
/**
 * File-system → URL mapping for filesystem-routed frameworks.
 *
 * Returns the URL path (no host) when the file is a route entrypoint, or
 * null when the file isn't a route in any recognized convention.
 *
 * The `null` cases include component files used by routes (e.g. shared
 * widgets in `components/`), API handlers (e.g. Next.js `pages/api/*`),
 * and arbitrary `.ts`/`.js` modules under frontend directories. These
 * are picked up by strategy 2 (import-graph walk, future) or by strategy
 * 3 (root fallback).
 */
export function frameworkFileToUrlPath(filePath) {
    // Normalize to forward slashes; works on Windows too.
    const normalized = filePath.replace(/\\/g, "/");
    // Next.js App Router root: app/page.{tsx,jsx} → /
    if (/(?:^|\/)app\/page\.(tsx|jsx|ts|js)$/.test(normalized)) {
        return "/";
    }
    // Next.js App Router nested: app/foo/page.tsx → /foo
    // app/(group)/foo/page.tsx → /foo (route groups stripped in URL)
    // app/foo/[id]/page.tsx → /foo/:id (Next.js bracket → colon)
    const nextAppMatch = normalized.match(/(?:^|\/)app\/(.+?)\/page\.(tsx|jsx|ts|js)$/);
    if (nextAppMatch) {
        return "/" + stripRouteGroups(nextAppMatch[1]).replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
    }
    // Next.js Pages Router: pages/foo.tsx → /foo, pages/index.tsx → /
    // Excludes pages/api/* (those are API handlers, not UI pages).
    const nextPagesMatch = normalized.match(/(?:^|\/)pages\/(?!api\/)(.+?)\.(tsx|jsx|ts|js)$/);
    if (nextPagesMatch) {
        let urlPath = nextPagesMatch[1];
        // /index → /, /foo/index → /foo
        urlPath = urlPath.replace(/(?:^|\/)index$/, "");
        urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
        return "/" + urlPath;
    }
    // Nuxt / Vue Router file-based: pages/foo.vue → /foo
    // Vue _id.vue → :id (Nuxt 2) or [id].vue → :id (Nuxt 3)
    const nuxtMatch = normalized.match(/(?:^|\/)pages\/(.+?)\.vue$/);
    if (nuxtMatch) {
        let urlPath = nuxtMatch[1];
        urlPath = urlPath.replace(/(?:^|\/)index$/, "");
        urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
        urlPath = urlPath.replace(/_(\w+)/g, ":$1");
        return "/" + urlPath;
    }
    // SvelteKit: routes/foo/+page.svelte → /foo, routes/+page.svelte → /
    const svelteMatch = normalized.match(/(?:^|\/)routes\/(.*?)\+page\.svelte$/);
    if (svelteMatch) {
        let urlPath = svelteMatch[1].replace(/\/$/, "");
        urlPath = urlPath.replace(/\[(\.\.\.)?(\w+)\]/g, ":$2");
        return "/" + urlPath;
    }
    return null;
}
/**
 * Drop Next.js App Router route-group segments — `(marketing)/foo` → `foo`.
 * Route groups don't appear in the URL.
 */
function stripRouteGroups(routePath) {
    return routePath
        .split("/")
        .filter((seg) => !/^\(.+\)$/.test(seg))
        .join("/");
}
/**
 * Detect whether the repository is configured for filesystem-based routing.
 *
 * Strategy 1 maps file paths to URLs based on conventions (Next.js Pages
 * Router, Nuxt, SvelteKit). Without a positive signal that the framework
 * is actually configured, a repo that just *happens* to have a `pages/`
 * directory (e.g. React with conventional layout, Vue with route-config
 * code) produces false-positive URLs. This guard suppresses strategy 1
 * for those repos so they fall through to root-fallback.
 *
 * Looks for framework config files in the repo root, common subdirectories,
 * and the `frontendFiles[]`-derived candidate frontend roots.
 */
export function detectsFilesystemRouting(repositoryPath) {
    const FRAMEWORK_CONFIG_FILES = [
        // Next.js
        "next.config.js", "next.config.mjs", "next.config.ts",
        // Nuxt
        "nuxt.config.ts", "nuxt.config.js",
        // SvelteKit
        "svelte.config.js", "svelte.config.ts",
    ];
    // Search the repo root and one level deep into common app subdirectories.
    // Most monorepos place the frontend at apps/web, app/, frontend/, web/, etc.
    // Going more than one level adds I/O without meaningful coverage gain.
    const ROOTS_TO_CHECK = [".", "frontend", "web", "app", "client", "apps/web"];
    for (const root of ROOTS_TO_CHECK) {
        for (const cfg of FRAMEWORK_CONFIG_FILES) {
            const fullPath = path.join(repositoryPath, root, cfg);
            try {
                if (fs.existsSync(fullPath))
                    return true;
            }
            catch {
                // ignore — fs errors don't change the answer
            }
        }
    }
    return false;
}
/**
 * Strategy 1 implementation. Filters frontend files through the framework
 * route mapper; collects the URL paths that mapped successfully.
 *
 * Suppressed when the repo doesn't have a recognized filesystem-routing
 * framework config — see `detectsFilesystemRouting`. Without that guard,
 * repos with conventional `pages/` directories but code-defined routes
 * (e.g. React with React Router) produce false-positive URLs.
 */
export function findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath) {
    // When repositoryPath is provided, gate on framework detection. When
    // omitted (e.g. older callers, unit tests), preserve original behavior.
    if (repositoryPath !== undefined && !detectsFilesystemRouting(repositoryPath)) {
        return [];
    }
    const byUrl = new Map();
    for (const file of frontendFiles) {
        const urlPath = frameworkFileToUrlPath(file);
        if (urlPath === null)
            continue;
        const normalizedBase = baseUrl.replace(/\/$/, "");
        const fullUrl = normalizedBase + urlPath;
        const existing = byUrl.get(fullUrl);
        if (existing) {
            // Same URL surfaced by multiple files — track all sources.
            if (!existing.sourcedFrom.includes(file)) {
                existing.sourcedFrom.push(file);
            }
        }
        else {
            byUrl.set(fullUrl, {
                url: fullUrl,
                sourcedFrom: [file],
                strategy: "framework-route-grep",
            });
        }
    }
    return Array.from(byUrl.values());
}
// ── Strategy 2: source-grounded routes ──────────────────────────────────────
/**
 * Strategy 2 implementation. Walks router/App files with the TS Compiler API
 * to find route declarations (`<Route path="/cart" element={<Cart/>}>`),
 * matches each declaration's component file against the changed-file list,
 * and emits one CandidateUiPage per matching route.
 *
 * Slice 3a covers string-literal `path` attributes only — `path={CONST}` and
 * Vue Router object literals are deferred to slice 3b.
 *
 * Returns empty when no route declarations match the changed files (which is
 * also the right answer for SPAs without client-side routing — they fall
 * through to root-fallback).
 */
export function findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath) {
    const sourceRoutes = extractSourceRoutes(repositoryPath);
    if (sourceRoutes.length === 0)
        return [];
    // Normalize changed files to absolute paths once.
    const changedAbs = new Set(frontendFiles.map((f) => path.resolve(repositoryPath, f)));
    const normalizedBase = baseUrl.replace(/\/$/, "");
    const byUrl = new Map();
    for (const route of sourceRoutes) {
        // A route surfaces as a candidate when *either* its component file or its
        // declaring file (router.ts, modules/<x>/index.ts) appears in the diff.
        // Component-only matching misses route renames in the declaration file
        // (the declaration changed; the component is unchanged).
        const matchedBy = (route.componentFile && changedAbs.has(route.componentFile))
            ? route.componentFile
            : (route.declaredIn && changedAbs.has(route.declaredIn))
                ? route.declaredIn
                : null;
        if (matchedBy === null)
            continue;
        const fullUrl = normalizedBase + (route.path.startsWith("/") ? route.path : "/" + route.path);
        const sourceFile = path.relative(repositoryPath, matchedBy);
        const existing = byUrl.get(fullUrl);
        if (existing) {
            if (!existing.sourcedFrom.includes(sourceFile)) {
                existing.sourcedFrom.push(sourceFile);
            }
        }
        else {
            byUrl.set(fullUrl, {
                url: fullUrl,
                sourcedFrom: [sourceFile],
                strategy: "source-grounded-routes",
            });
        }
    }
    return Array.from(byUrl.values());
}
// ── Strategy 3: root fallback ───────────────────────────────────────────────
/**
 * Strategy 3: when strategies 1+2 yield nothing, fall back to the workspace's
 * frontend baseUrl as the single candidate page. The agent's downstream
 * explore-and-discover step can then surface gated UI from the root.
 *
 * Returns null when the workspace has no recognizable frontend service.
 */
export async function findRootFallbackPage(repositoryPath) {
    const baseUrl = await pickFrontendBaseUrl(repositoryPath);
    if (!baseUrl)
        return null;
    return {
        url: baseUrl,
        sourcedFrom: [],
        strategy: "root-fallback",
    };
}
/**
 * Resolves the frontend service's baseUrl from the workspace config.
 *
 * Heuristics, in order:
 *   1. Service whose `serviceName` ends with `-frontend` or contains `frontend`
 *   2. Service whose `language` is `typescript` / `javascript` AND whose
 *      `framework` is one of the common UI frameworks (react, vue, svelte,
 *      next, nuxt, etc.)
 *   3. First service with a defined `api.baseUrl`
 *
 * Returns the baseUrl string, or null when no service has a usable URL.
 *
 * Exported (vs private) so analyze_changes and future tools can reuse the
 * same selection without re-implementing it.
 */
export async function pickFrontendBaseUrl(repositoryPath) {
    const config = await readWorkspaceConfigRaw(repositoryPath);
    if (!config?.services || !Array.isArray(config.services))
        return null;
    const services = config.services;
    // Heuristic 1: serviceName signals frontend.
    const byName = services.find((s) => typeof s?.serviceName === "string" &&
        /(?:^|[-_])frontend(?:[-_]|$)|(?:^|[-_])ui(?:[-_]|$)|(?:^|[-_])app(?:[-_]|$)|(?:^|[-_])web(?:[-_]|$)/.test(s.serviceName.toLowerCase()) &&
        s?.api?.baseUrl);
    if (byName)
        return byName.api.baseUrl;
    // Heuristic 2: language + framework signal frontend.
    const FRONTEND_FRAMEWORKS = new Set([
        "react", "vue", "svelte", "next", "nextjs", "nuxt", "sveltekit",
        "angular", "solid", "qwik", "remix", "astro",
    ]);
    const byFramework = services.find((s) => {
        const lang = (s?.language ?? "").toLowerCase();
        const fw = (s?.framework ?? "").toLowerCase();
        return ((lang === "typescript" || lang === "javascript") &&
            FRONTEND_FRAMEWORKS.has(fw) &&
            s?.api?.baseUrl);
    });
    if (byFramework)
        return byFramework.api.baseUrl;
    // Heuristic 3: first service with a baseUrl.
    const anyBase = services.find((s) => s?.api?.baseUrl);
    return anyBase?.api?.baseUrl ?? null;
}
// ── Composer ────────────────────────────────────────────────────────────────
/**
 * Run the strategy ladder in order: framework route grep (1) → source-grounded
 * routes (2) → root fallback (3). Each strategy is consulted only if the
 * previous one produced no candidates. Returns whatever the ladder produces
 * (possibly empty if the workspace has no frontend baseUrl AND none of the
 * earlier strategies matched).
 */
export async function enumerateCandidateUiPages(repositoryPath, frontendFiles) {
    if (frontendFiles.length === 0)
        return [];
    const baseUrl = await pickFrontendBaseUrl(repositoryPath);
    // Strategy 1 needs a baseUrl to construct full URLs. Without one, the
    // fallback path in strategy 3 also won't fire — return empty and let the
    // caller decide what to do (typically: persist nothing, agent does its
    // own enumeration via the existing prompt fallback).
    if (!baseUrl)
        return [];
    const fromRoutes = findCandidatePagesByFrameworkRoute(frontendFiles, baseUrl, repositoryPath);
    if (fromRoutes.length > 0)
        return fromRoutes;
    const fromSource = findCandidatePagesBySourceRoute(frontendFiles, baseUrl, repositoryPath);
    if (fromSource.length > 0)
        return fromSource;
    const fallback = await findRootFallbackPage(repositoryPath);
    return fallback ? [fallback] : [];
}
