"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.apiClientHubTokenRoutes = apiClientHubTokenRoutes;
const errors_1 = require("@yinuo-ngm/errors");
const api_1 = require("@yinuo-ngm/api");
const node_stream_1 = require("node:stream");
function normalizeNonEmptyString(value, field) {
    if (typeof value !== "string" || value.trim() === "") {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, `${field} is required`);
    }
    return value.trim();
}
function readHubTokenConfigFromProject(project) {
    const env = (project?.env ?? {});
    const baseUrl = env.NGM_HUB_V2_BASE_URL ?? env.HUB_V2_BASE_URL;
    const token = env.NGM_HUB_V2_TOKEN ?? env.HUB_V2_TOKEN;
    const personalToken = env.NGM_HUB_V2_PERSONAL_TOKEN ?? env.HUB_V2_PERSONAL_TOKEN;
    const projectKey = env.NGM_HUB_V2_PROJECT_KEY ?? env.HUB_V2_PROJECT_KEY;
    return {
        baseUrl: baseUrl?.trim() || "",
        token: token?.trim() || "",
        personalToken: personalToken?.trim() || "",
        projectKey: projectKey?.trim() || undefined,
    };
}
async function resolveHubTokenConfig(app, body, tokenType) {
    const inlineBaseUrl = body.baseUrl?.trim();
    const inlineToken = tokenType === "personal"
        ? body.personalToken?.trim() || body.token?.trim()
        : body.token?.trim();
    if (inlineBaseUrl && inlineToken) {
        return { baseUrl: inlineBaseUrl, token: inlineToken };
    }
    if (!body.projectId) {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "projectId is required when baseUrl/token are not provided");
    }
    const project = await app.core.project.get(body.projectId);
    const config = readHubTokenConfigFromProject(project);
    const resolvedToken = tokenType === "personal" ? body.personalToken?.trim() || config.personalToken : config.token;
    if (!config.baseUrl || !resolvedToken) {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, tokenType === "personal"
            ? "project hub-v2 config missing (NGM_HUB_V2_BASE_URL/NGM_HUB_V2_PERSONAL_TOKEN)"
            : "project hub-v2 config missing (NGM_HUB_V2_BASE_URL/NGM_HUB_V2_TOKEN)");
    }
    return {
        baseUrl: config.baseUrl,
        token: resolvedToken,
        projectKey: config.projectKey,
    };
}
async function apiClientHubTokenRoutes(fastify) {
    fastify.get("/projects/:projectId/issues/:issueId/attachments/:attachmentId/raw", async (req, reply) => {
        const params = (req.params ?? {});
        const projectId = normalizeNonEmptyString(params.projectId, "projectId");
        const issueId = normalizeNonEmptyString(params.issueId, "issueId");
        const attachmentId = normalizeNonEmptyString(params.attachmentId, "attachmentId");
        const { baseUrl, token, projectKey } = await resolveHubTokenConfig(fastify, { projectId }, "project");
        const normalizedPath = normalizeHubTokenPath(`/issues/${issueId}/attachments/${attachmentId}/raw`, projectKey);
        const response = await requestHubApiRaw(baseUrl, "/api/token", token, "GET", normalizedPath);
        if (!response.ok) {
            const payload = await parseJson(response);
            throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, payload?.message || `hub-v2 request failed (${response.status})`, {
                status: response.status,
                response: payload,
            });
        }
        copyRawResponseHeaders(response, reply);
        const body = response.body;
        if (!body) {
            return reply.status(response.status).send();
        }
        return reply.status(response.status).send(node_stream_1.Readable.fromWeb(body));
    });
    fastify.get("/projects/:projectId/issues/:issueId/uploads/:uploadId/raw", async (req, reply) => {
        const params = (req.params ?? {});
        const projectId = normalizeNonEmptyString(params.projectId, "projectId");
        const issueId = normalizeNonEmptyString(params.issueId, "issueId");
        const uploadId = normalizeNonEmptyString(params.uploadId, "uploadId");
        const { baseUrl, token, projectKey } = await resolveHubTokenConfig(fastify, { projectId }, "project");
        const normalizedPath = normalizeHubTokenPath(`/issues/${issueId}/uploads/${uploadId}/raw`, projectKey);
        const response = await requestHubApiRaw(baseUrl, "/api/token", token, "GET", normalizedPath);
        if (!response.ok) {
            const payload = await parseJson(response);
            throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, payload?.message || `hub-v2 request failed (${response.status})`, {
                status: response.status,
                response: payload,
            });
        }
        copyRawResponseHeaders(response, reply);
        const body = response.body;
        if (!body) {
            return reply.status(response.status).send();
        }
        return reply.status(response.status).send(node_stream_1.Readable.fromWeb(body));
    });
    fastify.get("/projects/:projectId/rd-items/:itemId/uploads/:uploadId/raw", async (req, reply) => {
        const params = (req.params ?? {});
        const projectId = normalizeNonEmptyString(params.projectId, "projectId");
        const itemId = normalizeNonEmptyString(params.itemId, "itemId");
        const uploadId = normalizeNonEmptyString(params.uploadId, "uploadId");
        const { baseUrl, token, projectKey } = await resolveHubTokenConfig(fastify, { projectId }, "project");
        const normalizedPath = normalizeHubTokenPath(`/rd-items/${itemId}/uploads/${uploadId}/raw`, projectKey);
        const response = await requestHubApiRaw(baseUrl, "/api/token", token, "GET", normalizedPath);
        if (!response.ok) {
            const payload = await parseJson(response);
            throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, payload?.message || `hub-v2 request failed (${response.status})`, {
                status: response.status,
                response: payload,
            });
        }
        copyRawResponseHeaders(response, reply);
        const body = response.body;
        if (!body) {
            return reply.status(response.status).send();
        }
        return reply.status(response.status).send(node_stream_1.Readable.fromWeb(body));
    });
    fastify.post("/request", async (req) => {
        const body = (req.body ?? {});
        const path = normalizeNonEmptyString(body.path, "path");
        assertPathProjectSegmentNotLocalProjectId(path, body.projectId);
        const method = (body.method ?? "GET").toUpperCase();
        const tokenType = body.tokenType === "personal" ? "personal" : "project";
        const { baseUrl, token, projectKey } = await resolveHubTokenConfig(fastify, body, tokenType);
        const query = { ...(body.query ?? {}) };
        if (query.projectKey === undefined && projectKey) {
            query.projectKey = projectKey;
        }
        else if (typeof query.projectKey === "string") {
            query.projectKey = String(query.projectKey).trim();
        }
        const normalizedPath = tokenType === "personal"
            ? normalizeHubPersonalPath(path, projectKey)
            : normalizeHubTokenPath(path, projectKey);
        const data = tokenType === "project"
            ? await requestByProjectTokenClient(baseUrl, token, method, normalizedPath, query, body.body, body.headers)
            : await requestHubApi(baseUrl, "/api/personal", token, method, normalizedPath, query, body.body, body.headers);
        return data;
    });
    fastify.post("/resolve", async (req) => {
        const body = (req.body ?? {});
        const resolvedProject = await resolveHubTokenConfig(fastify, body, "project");
        let personalTokenConfigured = false;
        try {
            await resolveHubTokenConfig(fastify, body, "personal");
            personalTokenConfigured = true;
        }
        catch {
            personalTokenConfigured = false;
        }
        return {
            baseUrl: resolvedProject.baseUrl,
            tokenConfigured: !!resolvedProject.token,
            personalTokenConfigured,
            projectKey: resolvedProject.projectKey ?? null,
        };
    });
}
function copyRawResponseHeaders(response, reply) {
    const passthroughHeaders = [
        "content-type",
        "content-disposition",
        "content-length",
        "cache-control",
        "etag",
        "last-modified",
    ];
    for (const key of passthroughHeaders) {
        const value = response.headers.get(key);
        if (!value) {
            continue;
        }
        reply.header(key, value);
    }
}
async function requestByProjectTokenClient(baseUrl, token, method, path, query, body, headers) {
    const client = new api_1.ProjectTokenApiClient({ baseUrl, apiToken: token });
    return client.request({
        method,
        path,
        query,
        body,
        headers,
    });
}
async function requestHubApi(baseUrl, apiPrefix, token, method, path, query, body, headers) {
    const root = baseUrl.replace(/\/+$/, "");
    const url = new URL(`${root}${apiPrefix}${path}`);
    for (const [key, value] of Object.entries(query ?? {})) {
        if (value === undefined || value === null || value === "") {
            continue;
        }
        url.searchParams.set(key, String(value));
    }
    const requestHeaders = {
        authorization: `Bearer ${token}`,
        ...(headers ?? {}),
    };
    if (body !== undefined) {
        requestHeaders["content-type"] = "application/json";
    }
    const response = await fetch(url.toString(), {
        method,
        headers: requestHeaders,
        body: body === undefined ? undefined : JSON.stringify(body),
    });
    const payload = await parseJson(response);
    if (!response.ok) {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, payload?.message || `hub-v2 request failed (${response.status})`, {
            status: response.status,
            response: payload,
        });
    }
    if (payload && typeof payload === "object" && "code" in payload) {
        if (payload.code !== "OK") {
            throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, payload.message || "hub-v2 response error", {
                response: payload,
            });
        }
        return payload.data;
    }
    return payload;
}
async function requestHubApiRaw(baseUrl, apiPrefix, token, method, path, query, headers) {
    const root = baseUrl.replace(/\/+$/, "");
    const url = new URL(`${root}${apiPrefix}${path}`);
    for (const [key, value] of Object.entries(query ?? {})) {
        if (value === undefined || value === null || value === "") {
            continue;
        }
        url.searchParams.set(key, String(value));
    }
    const requestHeaders = {
        authorization: `Bearer ${token}`,
        ...(headers ?? {}),
    };
    return fetch(url.toString(), {
        method,
        headers: requestHeaders,
    });
}
async function parseJson(response) {
    try {
        return await response.json();
    }
    catch {
        return null;
    }
}
function assertPathProjectSegmentNotLocalProjectId(path, projectId) {
    const localProjectId = projectId?.trim();
    if (!localProjectId) {
        return;
    }
    const normalizedPath = path.startsWith("/") ? path : `/${path}`;
    const matched = normalizedPath.match(/^\/projects\/([^\/]+)(?:\/|$)/i);
    if (!matched) {
        return;
    }
    const projectSegment = (matched[1] ?? "").trim();
    if (projectSegment && projectSegment === localProjectId) {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "path must use projectKey (or business relative path), not local projectId");
    }
}
function normalizeHubTokenPath(path, projectKey) {
    const p = path.startsWith("/") ? path : `/${path}`;
    const m = p.match(/^\/projects\/([^\/]+)(\/.*)?$/i);
    if (m) {
        const key = (m[1] ?? "").trim();
        const rest = m[2] ?? "";
        return `/projects/${key}${rest}`;
    }
    if (!projectKey) {
        throw new errors_1.GlobalError(errors_1.GlobalErrorCodes.BAD_REQUEST, "projectKey is required when path does not include /projects/:projectKey");
    }
    return `/projects/${projectKey}${p}`;
}
function normalizeHubPersonalPath(path, projectKey) {
    const p = path.startsWith("/") ? path : `/${path}`;
    if (/^\/me(?:\/|$)/i.test(p)) {
        return p;
    }
    return normalizeHubTokenPath(p, projectKey);
}
