/**
 * classifier.ts
 *
 * Classifies raw ObservedRequest records into meaningful categories:
 *   - Doc page visits (sanity.io/docs/*)
 *   - Search queries (search endpoints, query params)
 *   - Sanity API calls (api.sanity.io, apicdn.sanity.io)
 *   - External requests (everything else)
 *
 * Also extracts metadata like doc slugs, search query text, and page titles.
 */
import { z } from "zod";
// ---------------------------------------------------------------------------
// Zod schemas for parsed JSON bodies (search request payloads)
// ---------------------------------------------------------------------------
/** Single search request item (e.g., Algolia multi-index entry) */
const SearchRequestItemSchema = z.object({
    params: z.string().optional(),
    query: z.string().optional(),
});
/** Search body payload — covers Algolia single-query, params-based, and multi-index formats */
const SearchBodySchema = z.object({
    params: z.string().optional(),
    query: z.string().optional(),
    requests: z.array(SearchRequestItemSchema).optional(),
});
// ---------------------------------------------------------------------------
// URL pattern matchers
// ---------------------------------------------------------------------------
/** Matches sanity.io documentation pages */
const SANITY_DOCS_PATTERN = /^https?:\/\/(r\.jina\.ai\/https?:\/\/)?(www\.)?sanity\.io\/docs(\/[^?#]*)?/;
/** Matches Sanity API endpoints */
const SANITY_API_PATTERN = /^https?:\/\/(api|apicdn|cdn)\.sanity\.io/;
/** Matches sanity.io non-docs pages (blog, plugins, etc.) — including via proxies */
const SANITY_SITE_PATTERN = /^https?:\/\/(r\.jina\.ai\/https?:\/\/)?(www\.)?sanity\.io/;
/** Common search endpoint patterns */
const SEARCH_PATTERNS = [
    // Sanity docs search (Algolia, custom, etc.)
    /sanity\.io\/docs.*[?&](q|query|search)=/i,
    /sanity\.io\/api\/search/i,
    /sanity\.io\/search/i,
    // Algolia search (used by many doc sites)
    /algolia(net)?\.com.*\/quer(y|ies)/i,
    /algolia(net)?\.com.*\/search/i,
    // Google / Bing / DuckDuckGo
    /google\.com\/search/i,
    /bing\.com\/search/i,
    /duckduckgo\.com/i,
    // Jina Reader proxied search URLs
    /r\.jina\.ai\/https?:\/\/(www\.)?duckduckgo\.com/i,
    /r\.jina\.ai\/https?:\/\/(www\.)?google\.com\/search/i,
    /r\.jina\.ai\/https?:\/\/(www\.)?bing\.com\/search/i,
];
/**
 * Classifies an array of raw observed requests into categorized buckets.
 */
export function classifyRequests(requests) {
    const result = {
        apiCalls: [],
        docPageVisits: [],
        externalRequests: [],
        searchQueries: [],
    };
    for (const req of requests) {
        // Skip failed requests (no response)
        if (req.statusCode === 0)
            continue;
        // Status-only entries (W0132) carry no body, so we can't infer search
        // queries or doc-page metadata reliably. They still count as API calls
        // (Sanity API) or external requests (everything else) so the run shape
        // shows that the call happened, but we skip the body-dependent buckets.
        const isStatusOnly = req.capture === "status-only";
        // Order matters: API calls first (they may have ?query= params that look like searches),
        // then searches, then doc pages, then external
        if (isSanityApiRequest(req)) {
            result.apiCalls.push({
                endpoint: extractApiEndpoint(req.url),
                method: req.method,
                timestamp: req.timestamp,
                url: req.url,
            });
        }
        else if (!isStatusOnly && isSearchRequest(req)) {
            result.searchQueries.push({
                query: extractSearchQuery(req),
                timestamp: req.timestamp,
                url: req.url,
            });
        }
        else if (!isStatusOnly && isDocPageRequest(req)) {
            const slug = extractDocSlug(req.url);
            result.docPageVisits.push({
                contentSize: req.responseSize,
                slug,
                timestamp: req.timestamp,
                title: extractPageTitle(req.responsePreview),
                url: req.url,
            });
        }
        else if (!SANITY_SITE_PATTERN.test(req.url)) {
            // External request (not on sanity.io at all)
            result.externalRequests.push({
                domain: extractDomain(req.url),
                method: req.method,
                timestamp: req.timestamp,
                url: req.url,
            });
        }
        // else: sanity.io non-docs page — we track it in raw requests but don't classify it
    }
    return result;
}
/**
 * Extracts the API endpoint path from a Sanity API URL.
 *
 * Example:
 *   https://api.sanity.io/v2021-03-25/data/query/production → "/data/query/production"
 */
export function extractApiEndpoint(url) {
    const match = url.match(/sanity\.io\/(v[\d-]+)?(.*)/);
    if (!match)
        return new URL(url).pathname;
    return match[2] || "/";
}
/**
 * Extracts the doc slug from a sanity.io/docs URL.
 *
 * Examples:
 *   https://www.sanity.io/docs/groq-introduction → "groq-introduction"
 *   https://www.sanity.io/docs/getting-started/create-a-schema → "getting-started/create-a-schema"
 *   https://www.sanity.io/docs → "" (root docs page)
 */
export function extractDocSlug(url) {
    const match = url.match(/sanity\.io\/docs\/([^?#]+)/);
    if (!match)
        return "";
    // Remove trailing slash and .md extension
    return match[1].replace(/\/$/, "").replace(/\.md$/, "");
}
// ---------------------------------------------------------------------------
// Metadata extractors
// ---------------------------------------------------------------------------
/**
 * Extracts the domain from a URL.
 */
export function extractDomain(url) {
    try {
        return new URL(url).hostname;
    }
    catch {
        const match = url.match(/https?:\/\/([^/?#]+)/);
        return match ? match[1] : "unknown";
    }
}
/**
 * Attempts to extract a page title from an HTML response preview.
 */
export function extractPageTitle(responsePreview) {
    if (!responsePreview)
        return undefined;
    const match = responsePreview.match(/<title[^>]*>([^<]+)<\/title>/i);
    return match ? match[1].trim() : undefined;
}
/**
 * Extracts a search query string from a URL or request body.
 */
export function extractSearchQuery(req) {
    // Handle Jina-proxied URLs by extracting the inner URL's query params
    // e.g., https://r.jina.ai/https://duckduckgo.com/?q=sanity+schema
    let searchUrl = req.url;
    const jinaMatch = req.url.match(/r\.jina\.ai\/(https?:\/\/.+)/);
    if (jinaMatch) {
        searchUrl = jinaMatch[1];
    }
    // Try URL query parameters first
    try {
        const urlObj = new URL(searchUrl);
        for (const param of ["q", "query", "search", "s"]) {
            const value = urlObj.searchParams.get(param);
            if (value)
                return value;
        }
    }
    catch {
        // Invalid URL — try regex fallback
        const match = req.url.match(/[?&](q|query|search|s)=([^&]+)/);
        if (match)
            return decodeURIComponent(match[2]);
    }
    // Try request body (for POST search requests like Algolia)
    if (req.body) {
        try {
            const bodyObj = SearchBodySchema.parse(JSON.parse(req.body));
            // Algolia format
            if (bodyObj.query)
                return bodyObj.query;
            if (bodyObj.params) {
                const params = new URLSearchParams(bodyObj.params);
                const q = params.get("query");
                if (q)
                    return q;
            }
            // Array of requests (Algolia multi-index)
            if (bodyObj.requests) {
                const queries = bodyObj.requests
                    .map((r) => {
                    if (r.query)
                        return r.query;
                    if (r.params) {
                        const p = new URLSearchParams(r.params);
                        return p.get("query");
                    }
                    return null;
                })
                    .filter(Boolean);
                if (queries.length > 0)
                    return queries.join("; ");
            }
        }
        catch {
            // Not JSON — ignore
        }
    }
    return "";
}
/**
 * Determines if a request is to a Sanity documentation page.
 */
export function isDocPageRequest(req) {
    return (SANITY_DOCS_PATTERN.test(req.url) &&
        !isSearchRequest(req) &&
        req.method === "GET" &&
        // Filter out static assets
        !isStaticAsset(req.url));
}
/**
 * Determines if a request is to the Sanity API (not docs).
 */
export function isSanityApiRequest(req) {
    return SANITY_API_PATTERN.test(req.url);
}
// ---------------------------------------------------------------------------
// Full classification pipeline
// ---------------------------------------------------------------------------
/**
 * Determines if a request is a search operation.
 */
export function isSearchRequest(req) {
    // Exclude Sanity API calls — they may have ?query= params (GROQ)
    if (SANITY_API_PATTERN.test(req.url))
        return false;
    // Check URL against search patterns
    if (SEARCH_PATTERNS.some((p) => p.test(req.url)))
        return true;
    // Check POST body for search-like payloads (Algolia, etc.)
    if (req.method === "POST" && req.body) {
        try {
            const bodyObj = SearchBodySchema.safeParse(JSON.parse(req.body));
            if (bodyObj.success && (bodyObj.data.query || bodyObj.data.requests))
                return true;
        }
        catch {
            /* not JSON */
        }
    }
    return false;
}
/**
 * Determines if a URL points to a static asset (JS, CSS, images, fonts).
 */
function isStaticAsset(url) {
    const assetExtensions = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map)(\?|$)/i;
    return assetExtensions.test(url);
}
