import * as fs from "fs";
import path from "path";
import yaml from "js-yaml";
import { WorkspaceConfigManager } from "@skyramp/skyramp";
import { logger } from "./logger.js";
/**
 * Reads `.skyramp/workspace.yml`.
 *
 * Tries the library's Zod-validated `WorkspaceConfigManager.read()` first so
 * well-formed workspaces produce a normalized object. When validation fails
 * (e.g. a hand-rolled fixture that omits `metadata.createdAt` /`updatedAt`,
 * or any new field added by a future schema), falls back to a raw YAML parse.
 *
 * Callers are expected to read only the fields they need (`services`,
 * `services[].api.baseUrl`, etc.) — they should not depend on the strict
 * shape across both paths.
 *
 * Returns null if the file does not exist OR cannot be parsed as YAML at all.
 */
export async function readWorkspaceConfigRaw(workspacePath) {
    const wsMgr = new WorkspaceConfigManager(workspacePath);
    if (!await wsMgr.exists())
        return null;
    try {
        return await wsMgr.read();
    }
    catch (err) {
        // Validation failed — try a raw YAML parse so callers that only need
        // unvalidated fields (services / baseUrl) still work on partial fixtures.
        const configPath = path.join(workspacePath, ".skyramp", "workspace.yml");
        try {
            const raw = await fs.promises.readFile(configPath, "utf-8");
            const parsed = yaml.load(raw);
            if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
                logger.debug("workspace.yml failed strict validation; using raw parse", {
                    workspacePath,
                    error: err instanceof Error ? err.message : String(err),
                });
                return parsed;
            }
            return null;
        }
        catch {
            return null;
        }
    }
}
/**
 * Returns scheme+host+port for the service that owns the given test file.
 *
 * Matching strategy (in order):
 * 1. Filter services whose `testDirectory` is a path prefix of `testFile`.
 * 2. If multiple services match (shared testDirectory), narrow by `language`.
 * 3. If still ambiguous, return all candidates so the LLM can resolve it —
 *    never guess when multiple services are plausible owners.
 *
 * e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
 *   → { baseUrl: "http://localhost:8000", candidates: [] }
 */
export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
    const noMatch = { baseUrl: undefined, dockerNetwork: undefined, candidates: [] };
    if (!testFile)
        return noMatch;
    try {
        const rawConfig = await readWorkspaceConfigRaw(repositoryPath);
        if (!rawConfig)
            return noMatch;
        const services = rawConfig.services ?? [];
        // Normalise testFile to an absolute path for reliable prefix matching
        const absTestFile = path.isAbsolute(testFile)
            ? testFile
            : path.resolve(repositoryPath, testFile);
        // Step 1: filter by testDirectory prefix
        let matches = services.filter((s) => {
            if (!s.testDirectory || !s.api?.baseUrl)
                return false;
            const absTestDirectory = path.resolve(repositoryPath, s.testDirectory);
            return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
        });
        // Step 2: narrow by language if still ambiguous
        if (matches.length > 1 && language) {
            const byLanguage = matches.filter((s) => s.language?.toLowerCase() === language.toLowerCase());
            if (byLanguage.length > 0)
                matches = byLanguage;
        }
        if (matches.length === 1) {
            const parsed = new URL(matches[0].api.baseUrl);
            return {
                baseUrl: `${parsed.protocol}//${parsed.host}`,
                dockerNetwork: matches[0].runtimeDetails?.dockerNetwork,
                candidates: [],
            };
        }
        // Still ambiguous — return candidates for the LLM to resolve
        if (matches.length > 1) {
            return {
                baseUrl: undefined,
                dockerNetwork: undefined,
                candidates: matches.map((s) => ({
                    serviceName: s.serviceName ?? s.testDirectory,
                    baseUrl: s.api.baseUrl,
                })),
            };
        }
    }
    catch {
        logger.warning("Could not read workspace config or api.baseUrl is not a valid URL — skipping SKYRAMP_TEST_BASE_URL injection");
    }
    return noMatch;
}
/**
 * Known auth-related HTTP header names (lowercase).
 * Single source of truth used for both:
 *  - trace redaction  (trace-parser.ts  — headers to scrub from recorded traces)
 *  - auth extraction  (TestGenerationService — headers to pull out as auth credentials)
 */
export const KNOWN_AUTH_HEADERS = new Set([
    // Standard
    "authorization",
    "cookie",
    "proxy-authorization",
    // Generic API key headers (x-prefixed)
    "x-api-key",
    "x-api-token",
    "x-auth",
    "x-auth-token",
    "x-access-token",
    "x-oauth-token",
    "x-oauth-key",
    "x-client-token",
    "x-client-id",
    "x-client-secret",
    "x-client-secret-token",
    "x-client-access-token",
    "x-client-authorization",
    "x-service-key",
    "x-admin-key",
    "x-admin-token",
    "x-internal-token",
    // Platform-specific
    "x-github-token",
    "x-gitlab-token",
    "private-token", // GitLab personal access tokens
    "x-firebase-appcheck",
    "x-amz-security-token", // AWS SigV4
    "x-hasura-admin-secret", // Hasura GraphQL
    "x-wp-nonce", // WordPress REST API
    "x-shopify-access-token", // Shopify
    // Non-x-prefixed API keys
    "api-key",
    "apikey",
    "fastly-key",
    "heroku-bearer",
]);
/**
 * Cross-framework auth middleware signal patterns used by both the testbot and
 * recommendation prompts to detect auth when workspace config says authType: none.
 *
 * Format: plain strings (e.g. "HTTPBearer", "@PreAuthorize")
 *
 * This is the single source of truth — prompts must import and use this list
 * instead of hardcoding pattern names inline.
 */
export const AUTH_MIDDLEWARE_PATTERNS = [
    // Python — FastAPI / Starlette
    "HTTPBearer", "OAuth2PasswordBearer", "security.HTTPAuthorizationCredentials",
    "Depends(get_current_user)", "Depends(oauth2_scheme)", "jwt_required", "login_required",
    // Python — Django / DRF
    "IsAuthenticated", "authentication_classes", "permission_classes", "TokenAuthentication",
    "SessionAuthentication", "@login_required", "@permission_required",
    // Node.js — Express / NestJS / general
    "passport.authenticate", "passport", "jwt.verify", "verifyToken", "authMiddleware",
    "req.headers.authorization", "@UseGuards", "@UseGuards(AuthGuard)",
    "EnsureSessionDep", "session middleware",
    // Java / Kotlin — Spring Security
    "@PreAuthorize", "@Secured", "@RolesAllowed", "JwtAuthenticationFilter",
    "OncePerRequestFilter", "UsernamePasswordAuthenticationToken", "SecurityConfig",
    // Go — Gin / Chi / Echo / Fiber / gorilla
    "ValidateToken", "ParseWithClaims", 'c.GetHeader("Authorization")',
    "gin.BasicAuth", "echo.JWT", "fiber.Ctx", "r.Use(", "chi.Middleware",
    // Ruby on Rails
    "before_action :authenticate_user!", "authenticate_or_request_with_http_token",
    "current_user", "devise",
    // PHP — Laravel / Symfony
    "Auth::user()", "middleware('auth')", "auth:sanctum", "auth:api", "auth()->user()",
    "#[IsGranted]", "access_control", "security.yaml",
    // Rust — Axum / Actix
    "FromRequestParts", "extract::TypedHeader", "Authorization<Bearer>",
    // C# — ASP.NET Core
    "[Authorize]", "RequireAuthorization()", "AddJwtBearer()", "services.AddAuthentication()",
    "ClaimsPrincipal", "IAuthorizationService", "AuthorizationPolicy",
];
/** Rendered as a comma-separated string for use in prompts. */
export const AUTH_MIDDLEWARE_PATTERNS_STR = AUTH_MIDDLEWARE_PATTERNS.join(", ");
export var WorkspaceAuthType;
(function (WorkspaceAuthType) {
    WorkspaceAuthType["Bearer"] = "bearer";
    WorkspaceAuthType["Basic"] = "basic";
    WorkspaceAuthType["OAuth"] = "oauth";
    /** Generic custom-header auth (X-API-Key, X-Admin-Key, etc.) */
    WorkspaceAuthType["ApiKey"] = "apiKey";
    /** Session cookie auth — header is typically Cookie: <name>=<value> */
    WorkspaceAuthType["Cookie"] = "cookie";
    /** Alias for Cookie; some workspace configs write authType: session */
    WorkspaceAuthType["Session"] = "session";
    /** Django REST Framework / token-based: Authorization: Token <value> */
    WorkspaceAuthType["Token"] = "token";
    WorkspaceAuthType["None"] = "none";
})(WorkspaceAuthType || (WorkspaceAuthType = {}));
/**
 * All valid authType values as a tuple for use in zod enums.
 * Single source of truth — tools and prompts should use this instead of hardcoding.
 */
export const ALL_AUTH_TYPES = Object.values(WorkspaceAuthType);
/**
 * Formatted list of authType values for use in prompt text.
 * Example: "`bearer`, `basic`, `oauth`, `token`, `cookie`, `session`, `apiKey`, or `none`"
 */
export const AUTH_TYPES_PROMPT_LIST = ALL_AUTH_TYPES.slice(0, -1)
    .map((t) => `\`${t}\``)
    .join(", ") + `, or \`${ALL_AUTH_TYPES[ALL_AUTH_TYPES.length - 1]}\``;
export function parseWorkspaceAuthType(value) {
    if (value === undefined)
        return undefined;
    const normalized = value.toLowerCase();
    const mapping = {
        // Canonical values
        bearer: WorkspaceAuthType.Bearer,
        basic: WorkspaceAuthType.Basic,
        oauth: WorkspaceAuthType.OAuth,
        apikey: WorkspaceAuthType.ApiKey, // matches "apiKey" and "apikey"
        cookie: WorkspaceAuthType.Cookie,
        session: WorkspaceAuthType.Session,
        token: WorkspaceAuthType.Token,
        none: WorkspaceAuthType.None,
        // Common aliases — workspace.yml authors may use any of these
        jwt: WorkspaceAuthType.Bearer, // generic JWT → Bearer scheme
        "jwt-bearer": WorkspaceAuthType.Bearer,
        oauth2: WorkspaceAuthType.OAuth,
        oidc: WorkspaceAuthType.OAuth, // OpenID Connect tokens are Bearer
        "api-key": WorkspaceAuthType.ApiKey, // hyphenated variant
        "api_key": WorkspaceAuthType.ApiKey, // underscore variant
        apitoken: WorkspaceAuthType.ApiKey,
        "x-api-key": WorkspaceAuthType.ApiKey,
        "session-cookie": WorkspaceAuthType.Cookie,
        nextauth: WorkspaceAuthType.Cookie, // NextAuth.js uses session cookies
        supabase: WorkspaceAuthType.Bearer, // Supabase JWTs use Bearer
        firebase: WorkspaceAuthType.Bearer, // Firebase ID tokens use Bearer
        auth0: WorkspaceAuthType.Bearer, // Auth0 access tokens use Bearer
        keycloak: WorkspaceAuthType.Bearer, // Keycloak OIDC tokens use Bearer
        cognito: WorkspaceAuthType.Bearer, // AWS Cognito JWTs use Bearer
        sanctum: WorkspaceAuthType.Bearer, // Laravel Sanctum API tokens use Bearer
        devise: WorkspaceAuthType.Bearer, // Devise-JWT uses Bearer
        passport: WorkspaceAuthType.Bearer, // Passport.js JwtStrategy uses Bearer
    };
    return mapping[normalized];
}
/**
 * Returns the conventional HTTP header name for an auth type when authHeader
 * is not explicitly configured in workspace.yml.
 *
 * This is the single source of truth for the authType → authHeader mapping.
 * All prompt builders and tools must use this instead of hardcoding header names.
 *
 * - cookie / session           → "Cookie"        (always goes in Cookie header)
 * - apiKey                     → ""              (custom header; caller must read source)
 * - none                       → ""
 * - "" / undefined             → ""              (do not infer a header when authType is missing)
 * - bearer / basic / oauth / token → "Authorization"
 */
export function getDefaultAuthHeader(authType) {
    switch (authType) {
        case WorkspaceAuthType.Cookie:
        case WorkspaceAuthType.Session:
            return "Cookie";
        case WorkspaceAuthType.ApiKey:
        case WorkspaceAuthType.None:
        case "":
        case undefined:
            return "";
        default:
            // bearer, basic, oauth, token — all use Authorization header by convention
            return "Authorization";
    }
}
/**
 * Returns true when the given header NAME string is the Authorization header.
 *
 * Use this instead of sprinkling /^authorization$/i regexes across prompts and tools.
 */
export function isAuthorizationHeaderName(headerName) {
    return /^authorization$/i.test(headerName);
}
/**
 * Maps a WorkspaceAuthType to the canonical HTTP Authorization scheme prefix
 * (the word that appears before the token in "Authorization: <scheme> <token>").
 *
 * If `explicitScheme` is provided (from workspace.yml api.authScheme), it takes
 * precedence over the inferred scheme. This allows custom schemes like "Ghost",
 * "Hasura", or any internal API without hardcoding them in the codebase.
 *
 * Returns undefined for auth types that do NOT use the Authorization header
 * (Cookie, ApiKey, None) — callers should not pass authScheme in those cases.
 */
export function getAuthScheme(authType, explicitScheme) {
    // authScheme only applies to auth types that send credentials via the Authorization header.
    // For Cookie, ApiKey, Session, and None the header carries the token directly — there is no
    // scheme prefix — so always return undefined regardless of any explicit override.
    if (getDefaultAuthHeader(authType) !== "Authorization")
        return undefined;
    // Explicit scheme from workspace.yml takes precedence, including "" for raw tokens with no prefix.
    if (explicitScheme !== undefined)
        return explicitScheme;
    switch (authType) {
        case WorkspaceAuthType.Bearer: return "Bearer";
        case WorkspaceAuthType.Basic: return "Basic";
        case WorkspaceAuthType.Token: return "Token"; // Django REST Framework
        case WorkspaceAuthType.OAuth: return "Bearer"; // OAuth 2.0 access tokens use Bearer
        default:
            return undefined;
    }
}
export async function getWorkspaceAuthConfig(repositoryPath) {
    const rawConfig = await readWorkspaceConfigRaw(repositoryPath);
    if (!rawConfig)
        return {};
    const svc = rawConfig.services?.find((s) => s.api?.authHeader !== undefined || s.api?.authType !== undefined || s.api?.authScheme !== undefined);
    const authHeader = svc?.api?.authHeader;
    const authType = parseWorkspaceAuthType(svc?.api?.authType);
    const authScheme = svc?.api?.authScheme;
    return { authHeader, authType, authScheme };
}
