import { DEFAULT_BACKEND_METRICS_BASE_URL } from "./backend-metrics-config"; export interface BackendMetricsRow { a3IncreaseCount: string; afterViewSearchCount: string; afterViewSearchRate: string; cpSearch: string; cpa3: string; newA3Rate: string; starId: string; } interface FetchResponseLike { json(): Promise; ok: boolean; } type FetchLike = ( input: string, init?: RequestInit ) => Promise; interface BackendMetricsClientOptions { baseUrl?: string; fetchImpl?: FetchLike; getAccessToken: () => Promise; } export function createBackendMetricsClient(options: BackendMetricsClientOptions) { const baseUrl = options.baseUrl ?? DEFAULT_BACKEND_METRICS_BASE_URL; const fetchImpl = options.fetchImpl ?? defaultFetch; return { async searchByStarIds(starIds: string[]): Promise { const response = await fetchImpl(buildBackendMetricsSearchUrl(baseUrl), { body: JSON.stringify(buildBackendMetricsSearchRequestBody(starIds)), headers: { Authorization: `Bearer ${await options.getAccessToken()}`, "Content-Type": "application/json" }, method: "POST" }); if (!response.ok) { throw new Error("backend metrics request failed"); } return mapBackendMetricsSearchResponse(await response.json()); } }; } export function buildBackendMetricsSearchUrl(baseUrl: string): string { return new URL("/api/v1/history/talents/search", baseUrl).toString(); } export function buildBackendMetricsSearchRequestBody(starIds: string[]) { return { page: 1, size: Math.max(20, starIds.length), type: "star_id", values: starIds }; } export function mapBackendMetricsSearchResponse(payload: unknown): BackendMetricsRow[] { const rows = readResponseRows(payload); if (!rows) { throw new Error("backend metrics response is invalid"); } return rows.flatMap((row) => { if (!isRecord(row) || typeof row.star_id !== "string") { return []; } return [ { a3IncreaseCount: formatDecimalValue( readAverageA3IncreaseCount(row) ), afterViewSearchCount: formatDecimalValue(row.avg_after_view_search_cnt), afterViewSearchRate: formatRateValue(row.avg_after_view_search_rate), cpSearch: formatDecimalValue(row.cp_search), cpa3: formatDecimalValue(readCpa3Value(row)), newA3Rate: formatRateValue(row.avg_new_a3_rate), starId: row.star_id } ]; }); } function readAverageA3IncreaseCount(row: Record): number | null { const directAverage = readFiniteNumber(row.avg_a3_increase_cnt); if (directAverage !== null) { return directAverage; } const totalNewA3 = readTotalNewA3Value(row); const videoCount = readFiniteNumber(row.video_count) ?? readNestedVideoCount(row.videos); if (totalNewA3 === null || videoCount === null || videoCount <= 0) { return null; } return totalNewA3 / videoCount; } function readCpa3Value(row: Record): number | null { const directCpa3 = readFiniteNumber(row.cpa3); if (directCpa3 !== null) { return directCpa3; } const totalCost = readFiniteNumber(row.total_estimated_video_cost); const totalNewA3 = readTotalNewA3Value(row); if (totalCost === null || totalNewA3 === null || totalNewA3 <= 0) { return null; } return totalCost / totalNewA3; } function readTotalNewA3Value(row: Record): number | null { const derivedFromTotals = deriveTotalNewA3FromTotals(row); if (derivedFromTotals !== null) { return derivedFromTotals; } return deriveTotalNewA3FromVideos(row.videos); } function deriveTotalNewA3FromTotals(row: Record): number | null { const totalPlayCount = readFiniteNumber(row.total_play_cnt); const averageNewA3Rate = readFiniteNumber(row.avg_new_a3_rate); if (totalPlayCount === null || averageNewA3Rate === null) { return null; } return totalPlayCount * averageNewA3Rate; } function deriveTotalNewA3FromVideos(value: unknown): number | null { if (!Array.isArray(value)) { return null; } let total = 0; let hasFiniteValue = false; value.forEach((video) => { if (!isRecord(video)) { return; } const newA3 = readFiniteNumber(video.new_a3); if (newA3 === null) { return; } hasFiniteValue = true; total += newA3; }); return hasFiniteValue ? total : null; } function readNestedVideoCount(value: unknown): number | null { return Array.isArray(value) ? value.length : null; } function readResponseRows(payload: unknown): unknown[] | null { if (!isRecord(payload) || payload.success !== true) { return null; } const topLevelData = isRecord(payload.data) ? payload.data : null; return Array.isArray(topLevelData?.data) ? topLevelData.data : null; } function formatRateValue(value: unknown): string { const number = typeof value === "number" ? value : Number(value); if (Number.isFinite(number)) { const percentage = number * 100; const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2, minimumFractionDigits: percentage % 1 === 0 ? 0 : 2 }).format(percentage); return `${formatted}%`; } return ""; } function formatDecimalValue(value: unknown): string { const number = typeof value === "number" ? value : Number(value); if (!Number.isFinite(number)) { return ""; } return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(number); } async function defaultFetch(input: string, init?: RequestInit) { return fetch(input, init); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function readFiniteNumber(value: unknown): number | null { const number = typeof value === "number" ? value : Number(value); return Number.isFinite(number) ? number : null; }