• Flattens a match result into a single-level structure suitable for CSV export. Selects best match data and normalizes dates/genres for tabular output.

    Parameters

    Returns FlattenedMatchResult

    Flattened result with all fields in a single-level object.

    export function flattenMatchResult(
    match: MangaMatchResult | FlattenableMatchResult,
    ): FlattenedMatchResult {
    const kenmei = match.kenmeiManga;

    // Find the highest confidence match from anilistMatches
    const highestConfidenceMatch =
    match.anilistMatches && match.anilistMatches.length > 0
    ? match.anilistMatches.reduce((prev, current) => {
    const prevConf = prev.confidence ?? 0;
    const currConf = current.confidence ?? 0;
    return currConf > prevConf ? current : prev;
    }, match.anilistMatches[0])
    : null;

    // Use selectedMatch or fall back to highest confidence match
    const matchForData = match.selectedMatch ?? highestConfidenceMatch;

    // Extract confidence from MangaMatch or default to 0
    const confidence =
    matchForData && "confidence" in matchForData
    ? (matchForData.confidence ?? 0)
    : 0;

    // Extract AniListManga data safely - handles shapes without id/title fields
    const anilistData = extractAniListData(matchForData);

    // Fallback to selectedMatch format/genres when anilistData is unavailable
    // Use these fields only when AniList data is not present (no id/title from AniList)
    const selectedMatchFormat =
    anilistData === undefined ? match.selectedMatch?.format : undefined;
    const selectedMatchGenres =
    anilistData === undefined ? match.selectedMatch?.genres : undefined;

    // Normalize genres array to semicolon-separated string
    let genresString = "";
    if (Array.isArray(anilistData?.genres)) {
    genresString = anilistData.genres.join("; ");
    } else if (Array.isArray(selectedMatchGenres)) {
    genresString = selectedMatchGenres.join("; ");
    }

    const kenmeiRecord = kenmei as unknown as Record<string, unknown>;
    const getKenmeiNumber = (camel: string, snake: string): number => {
    const camelVal = kenmeiRecord[camel];
    if (typeof camelVal === "number") return camelVal;
    const snakeVal = kenmeiRecord[snake];
    if (typeof snakeVal === "number") return snakeVal;
    return 0;
    };
    const getKenmeiString = (camel: string, snake: string): string => {
    const camelVal = kenmeiRecord[camel];
    if (typeof camelVal === "string") return camelVal;
    const snakeVal = kenmeiRecord[snake];
    if (typeof snakeVal === "string") return snakeVal;
    return "";
    };

    return {
    // Kenmei data
    kenmeiId: Number(kenmei.id),
    kenmeiTitle: kenmei.title,
    kenmeiStatus: kenmei.status || "",
    kenmeiScore: kenmei.score ?? null,
    chaptersRead: getKenmeiNumber("chaptersRead", "chapters_read"),
    volumesRead: getKenmeiNumber("volumesRead", "volumes_read"),
    author: "author" in kenmei ? (kenmei.author ?? "") : "",
    notes: kenmei.notes || "",
    createdAt: getKenmeiString("createdAt", "created_at"),
    updatedAt: getKenmeiString("updatedAt", "updated_at"),
    lastReadAt: getKenmeiString("lastReadAt", "last_read_at"),

    // Match data
    matchStatus: match.status,
    matchDate: normalizeMatchDate(match.matchDate),
    confidence,

    // AniList data - explicitly set to empty strings when no AniList data available
    // This ensures selectedMatch shapes without id/title don't incorrectly populate these fields
    anilistId: anilistData?.id ?? null,
    anilistTitleRomaji:
    typeof anilistData?.title === "object"
    ? (anilistData.title.romaji ?? "")
    : (anilistData?.title ?? ""),
    anilistTitleEnglish:
    typeof anilistData?.title === "object"
    ? (anilistData.title.english ?? "")
    : (anilistData?.title ?? ""),
    anilistTitleNative:
    typeof anilistData?.title === "object"
    ? (anilistData.title.native ?? "")
    : (anilistData?.title ?? ""),
    // Prefer AniList data format, fall back to selectedMatch format when no AniList data
    format: anilistData?.format ?? selectedMatchFormat ?? "",
    totalChapters: anilistData?.chapters ?? null,
    totalVolumes: anilistData?.volumes ?? null,
    // Prefer AniList data genres, fall back to selectedMatch genres when no AniList data
    genres: genresString,
    };
    }