• Calculates advanced artist-level insights beyond basic metrics

    Returns Promise<
        | null
        | {
            mostCompatibleArtists: { artistName: string; compatibility: number }[];
            leastCompatibleArtists: { artistName: string; compatibility: number }[];
            listeningTrends: Record<
                string,
                { trend: "increasing"
                | "decreasing"
                | "stable"; changeRate: number },
            >;
            genreAffinities: Record<string, number>;
            timeBasedPreferences: Record<
                string,
                { preferredHours: number[]; avoidedHours: number[] },
            >;
            recommendedExploration: { artistName: string; reason: string }[];
        },
    >

    Object with advanced artist insights and recommendations

    export async function calculateArtistInsights() {
    try {
    const statistics = await getStatistics();
    const artistMetrics = await aggregateArtistSkipMetrics();
    const timePatterns = await analyzeTimeBasedPatterns();

    // Initialize insights object
    const insights = {
    mostCompatibleArtists: [] as Array<{
    artistName: string;
    compatibility: number;
    }>,
    leastCompatibleArtists: [] as Array<{
    artistName: string;
    compatibility: number;
    }>,
    listeningTrends: {} as Record<
    string,
    { trend: "increasing" | "decreasing" | "stable"; changeRate: number }
    >,
    genreAffinities: {} as Record<string, number>,
    timeBasedPreferences: {} as Record<
    string,
    { preferredHours: number[]; avoidedHours: number[] }
    >,
    recommendedExploration: [] as Array<{
    artistName: string;
    reason: string;
    }>,
    };

    // Only proceed if we have enough data for meaningful insights
    if (
    !statistics ||
    !artistMetrics ||
    Object.keys(artistMetrics).length < 5
    ) {
    console.log("Not enough artist data for meaningful insights");
    return insights;
    }

    // Calculate artist compatibility based on skip rates and listening time
    const compatibilityScores = Object.entries(artistMetrics)
    .map(([, metrics]) => {
    // Only consider artists with enough data
    if (metrics.uniqueTracksSkipped.length < 3) return null;

    // Compatibility score based on inverse of skip ratio and listening time
    const skipFactor = 1 - (metrics.skipRatio || 0); // Higher is better
    const playFactor = metrics.uniqueTracksSkipped.length / 20; // Normalize, higher is better
    const compatibility = skipFactor * 0.7 + playFactor * 0.3; // Weighted score

    return {
    artistName: metrics.artistName,
    compatibility: Math.min(compatibility, 1), // Cap at 1.0
    };
    })
    .filter(Boolean) as Array<{ artistName: string; compatibility: number }>;

    // Sort and get most/least compatible artists
    compatibilityScores.sort((a, b) => b.compatibility - a.compatibility);
    insights.mostCompatibleArtists = compatibilityScores.slice(0, 5);
    insights.leastCompatibleArtists = [...compatibilityScores]
    .reverse()
    .slice(0, 5);

    // Analyze listening trends over time
    const artistListenHistory: Record<string, Record<string, number>> = {};

    // Group by month for trend analysis
    if (statistics.dailyMetrics) {
    Object.entries(statistics.dailyMetrics).forEach(([dateStr, metrics]) => {
    const month = dateStr.substring(0, 7); // YYYY-MM

    const artists = Array.isArray(metrics.uniqueArtists)
    ? metrics.uniqueArtists
    : Array.from(metrics.uniqueArtists as Set<string>);

    artists.forEach((artistId) => {
    const artistData = statistics.artistMetrics[artistId];
    if (!artistData) return;

    // Initialize artist history if needed
    if (!artistListenHistory[artistData.name]) {
    artistListenHistory[artistData.name] = {};
    }

    // Count listens by month
    artistListenHistory[artistData.name][month] =
    (artistListenHistory[artistData.name][month] || 0) + 1;
    });
    });
    }

    // Calculate trends based on month-to-month changes
    Object.entries(artistListenHistory).forEach(([artistName, monthData]) => {
    const months = Object.keys(monthData).sort();
    if (months.length < 2) return; // Need at least 2 months for a trend

    // Get last three months if available
    const recentMonths = months.slice(-3);
    const values = recentMonths.map((m) => monthData[m] || 0);

    // Simple trend detection
    if (values.length >= 2) {
    const latestValue = values[values.length - 1];
    const previousValue = values[values.length - 2];

    const changeRate =
    previousValue === 0
    ? 0
    : (latestValue - previousValue) / previousValue;

    let trend: "increasing" | "decreasing" | "stable";

    if (changeRate > 0.2) trend = "increasing";
    else if (changeRate < -0.2) trend = "decreasing";
    else trend = "stable";

    insights.listeningTrends[artistName] = { trend, changeRate };
    }
    });

    // Analyze time-based preferences for artists
    if (timePatterns && timePatterns.patternsByArtist) {
    Object.entries(timePatterns.patternsByArtist).forEach(
    ([artist, pattern]) => {
    const hourlyDistribution = pattern.hourlyDistribution;
    if (!hourlyDistribution || hourlyDistribution.length !== 24) return;

    // Calculate average hourly plays
    const avg =
    hourlyDistribution.reduce((sum, count) => sum + count, 0) / 24;

    // Find preferred and avoided hours
    const preferredHours = hourlyDistribution
    .map((count, hour) => ({ hour, count }))
    .filter((x) => x.count > avg * 1.5 && x.count >= 3)
    .map((x) => x.hour);

    const avoidedHours = hourlyDistribution
    .map((count, hour) => ({ hour, count }))
    .filter(
    (x) =>
    x.count < avg * 0.5 && artist.toLowerCase() in artistMetrics,
    )
    .map((x) => x.hour);

    insights.timeBasedPreferences[artist] = {
    preferredHours,
    avoidedHours,
    };
    },
    );
    }

    // Generate exploration recommendations based on listening patterns
    const topArtists = Object.entries(artistMetrics)
    .sort(
    (a, b) =>
    b[1].uniqueTracksSkipped.length - a[1].uniqueTracksSkipped.length,
    )
    .slice(0, 10)
    .map(([, data]) => data.artistName);

    // Create simple artist recommendations based on complementary patterns
    topArtists.forEach((artist) => {
    // Skip artists with high skip rates
    const artistData = Object.values(artistMetrics).find(
    (a) => a.artistName === artist,
    );
    if (!artistData || artistData.skipRatio > 0.7) return;

    // Check time patterns
    const timePreference = insights.timeBasedPreferences[artist];
    if (!timePreference) return;

    // Recommend artists with similar time preferences
    Object.entries(insights.timeBasedPreferences)
    .filter(([otherArtist]) => otherArtist !== artist)
    .forEach(([otherArtist, otherPattern]) => {
    // Check if time preferences overlap
    const hasOverlap = timePreference.preferredHours.some((hour) =>
    otherPattern.preferredHours.includes(hour),
    );

    if (hasOverlap) {
    insights.recommendedExploration.push({
    artistName: otherArtist,
    reason: `Similar listening time preferences to ${artist}`,
    });
    }
    });
    });

    // Deduplicate recommendations
    insights.recommendedExploration = [
    ...new Map(
    insights.recommendedExploration.map((item) => [item.artistName, item]),
    ).values(),
    ].slice(0, 5); // Limit to 5 recommendations

    // Save insights to file for later use
    const insightsFilePath = join(
    ensureStatisticsDir(),
    "artist_insights.json",
    );
    writeJsonSync(insightsFilePath, insights, { spaces: 2 });

    return insights;
    } catch (error) {
    console.error("Error calculating artist insights:", error);
    return null;
    }
    }