userActivity.js

/**
 * @module UserActivity
 */

import { fetchUserPractices, fetchUserPracticeMeta, fetchRecentUserActivities } from './railcontent'
import { GET, POST, PUT, DELETE } from '../infrastructure/http/HttpClient.ts'
import { DataContext, UserActivityVersionKey } from './dataContext.js'
import { fetchByRailContentIds, fetchParentChildRelationshipsFor } from './sanity'
import { getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
import { globalConfig } from './config'
import { postProcessBadge, getFormattedType } from '../contentTypeConfig'
import dayjs from 'dayjs'
import { addContextToContent } from './contentAggregator.js'
import { db, Q } from './sync'
import { COLLECTION_TYPE } from './sync/models/ContentProgress'
import { streakCalculator } from './user/streakCalculator'
import { mapContentsThatWereLastProgressedFromMethod } from "./content-org/learning-paths.ts";

const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']

const streakMessages = {
  startStreak: 'Start your streak by taking any lesson!',
  restartStreak: 'Restart your streak by taking any lesson!',

  // Messages when last active day is today
  dailyStreak: (streak) =>
    `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak! Way to keep it going!`,
  dailyStreakShort: (streak) =>
    `Nice! You have ${getIndefiniteArticle(streak)} ${streak} day streak!`,
  weeklyStreak: (streak) =>
    `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep up the momentum!`,
  greatJobWeeklyStreak: (streak) =>
    `Great job! You have ${getIndefiniteArticle(streak)} ${streak} week streak! Way to keep it going!`,

  // Messages when last active day is NOT today
  dailyStreakReminder: (streak) =>
    `You have ${getIndefiniteArticle(streak)} ${streak} day streak! Keep it going with any lesson or song!`,
  weeklyStreakKeepUp: (streak) =>
    `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep up the momentum!`,
  weeklyStreakReminder: (streak) =>
    `You have ${getIndefiniteArticle(streak)} ${streak} week streak! Keep it going with any lesson or song!`,
}

function getIndefiniteArticle(streak) {
  return streak === 8 || (streak >= 80 && streak <= 89) || (streak >= 800 && streak <= 899)
    ? 'an'
    : 'a'
}

async function getUserPractices(userId) {
  if (userId === globalConfig.sessionConfig.userId) {
    return getOwnPractices()
  } else {
    return await fetchUserPractices(userId)
  }
}

async function getOwnPractices(...clauses) {
  const query = await db.practices.queryAll(...clauses)
  const data = query.data.reduce((acc, practice) => {
    acc[practice.date] = acc[practice.date] || []
    acc[practice.date].push({
      id: practice.id,
      duration_seconds: Math.round(practice.duration_seconds),
    })
    return acc
  }, {})

  return data
}

export let userActivityContext = new DataContext(UserActivityVersionKey, function () {})

/**
 * Retrieves user activity statistics for the current week, including daily activity and streak messages.
 *
 * @returns {Promise<Object>} - A promise that resolves to an object containing weekly user activity statistics.
 *
 * @example
 * // Retrieve user activity statistics for the current week
 * getUserWeeklyStats()
 *   .then(stats => console.log(stats))
 *   .catch(error => console.error(error));
 */
export async function getUserWeeklyStats() {
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
  const today = dayjs()
  const startOfWeek = getMonday(today, timeZone)
  const weekDays = Array.from({ length: 7 }, (_, i) =>
    startOfWeek.add(i, 'day').format('YYYY-MM-DD')
  )

  const weekPractices = await getOwnPractices(
    Q.where('date', Q.oneOf(weekDays)),
    Q.sortBy('date', 'desc')
  )
  const practiceDaysSet = new Set(Object.keys(weekPractices))
  let dailyStats = []
  for (let i = 0; i < 7; i++) {
    const day = startOfWeek.add(i, 'day')
    const dayStr = day.format('YYYY-MM-DD')
    let hasPractice = practiceDaysSet.has(dayStr)
    let isActive = isSameDate(today.format(), dayStr)
    let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
    dailyStats.push({
      key: i,
      label: DAYS[i],
      isActive,
      inStreak: hasPractice,
      type,
      day: dayStr,
    })
  }

  const streakData = await streakCalculator.getStreakData()

  return {
    data: {
      dailyActiveStats: dailyStats,
      streakMessage: streakData.streakMessage,
      practices: weekPractices,
    },
  }
}

/**
 * Retrieves user activity statistics for a specified month and user, including daily and weekly activity data.
 * If no parameters are provided, defaults to the current year, current month, and the logged-in user.
 *
 * @param {Object} [params={}] - Parameters for fetching user statistics.
 * @param {number} [params.year=new Date().getFullYear()] - The year for which to retrieve the statistics.
 * @param {number} [params.month=new Date().getMonth()] - The 0-based month index (0 = January).
 * @param {number} [params.day=1] - The starting day (not used for grid calc, but kept for flexibility).
 * @param {number} [params.userId=globalConfig.sessionConfig.userId] - The user ID for whom to retrieve stats.
 *
 * @returns {Promise<Object>} A promise that resolves to an object containing:
 * - `dailyActiveStats`: Array of daily activity data for the calendar grid.
 * - `weeklyActiveStats`: Array of weekly streak summaries.
 * - `practiceDuration`: Total number of seconds practiced in the month.
 * - `currentDailyStreak`: Count of consecutive active days.
 * - `currentWeeklyStreak`: Count of consecutive active weeks.
 * - `daysPracticed`: Total number of active days in the month.
 *
 * @example
 * // Get stats for current user and month
 * getUserMonthlyStats().then(console.log);
 *
 * @example
 * // Get stats for March 2024
 * getUserMonthlyStats({ year: 2024, month: 2 }).then(console.log);
 *
 * @example
 * // Get stats for another user
 * getUserMonthlyStats({ userId: 123 }).then(console.log);
 */
export async function getUserMonthlyStats(params = {}) {
  const now = dayjs()
  const {
    year = now.year(),
    month = now.month(), // 0-indexed
    userId = globalConfig.sessionConfig.userId,
  } = params
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
  const practices = await getUserPractices(userId)

  const firstDayOfMonth = dayjs.tz(`${year}-${month + 1}-01`, timeZone).startOf('day')
  const endOfMonth = firstDayOfMonth.endOf('month')
  const today = dayjs().tz(timeZone).startOf('day')

  let startOfGrid = getMonday(firstDayOfMonth, timeZone)

  // Previous week range
  const previousWeekStart = startOfGrid.subtract(7, 'day')
  const previousWeekEnd = startOfGrid.subtract(1, 'day')

  let hadStreakBeforeMonth = false
  for (let d = previousWeekStart.clone(); d.isSameOrBefore(previousWeekEnd); d = d.add(1, 'day')) {
    const key = d.format('YYYY-MM-DD')
    if (practices[key]) {
      hadStreakBeforeMonth = true
      break
    }
  }

  // let endOfMonth = new Date(year, month + 1, 0)
  let endOfGrid = endOfMonth.clone()
  while (endOfGrid.day() !== 0) {
    endOfGrid = endOfGrid.add(1, 'day')
  }
  const daysInMonth = endOfGrid.diff(startOfGrid, 'day') + 1
  let dailyStats = []
  let practiceDuration = 0
  let daysPracticed = 0
  let weeklyStats = {}

  for (let i = 0; i < daysInMonth; i++) {
    let day = startOfGrid.add(i, 'day')
    let key = day.format('YYYY-MM-DD')
    let activity = practices[key] ?? null
    let weekKey = getWeekNumber(day)

    if (!weeklyStats[weekKey]) {
      weeklyStats[weekKey] = { key: weekKey, inStreak: false }
    }

    if (activity && day.isBetween(firstDayOfMonth, endOfMonth, null, '[]')) {
      practiceDuration += activity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
      daysPracticed++
    }

    if (activity) {
      weeklyStats[weekKey].inStreak = true
    }

    const isActive = day.isSame(today, 'day')
    const type = activity ? 'tracked' : isActive ? 'active' : 'none'

    dailyStats.push({
      key: i,
      label: key,
      isActive,
      inStreak: !!activity,
      type,
    })
  }

  // Continue streak into month
  if (hadStreakBeforeMonth) {
    const firstWeekKey = getWeekNumber(startOfGrid)
    if (weeklyStats[firstWeekKey]) {
      weeklyStats[firstWeekKey].continueStreak = true
    }
  }

  // Filter past practices only
  let filteredPractices = Object.entries(practices)
    .filter(([date]) => dayjs(date).isSameOrBefore(endOfMonth))
    .reduce((acc, [date, val]) => {
      acc[date] = val
      return acc
    }, {})

  const streakData = await streakCalculator.getStreakData()
  const currentDailyStreak = streakData.currentDailyStreak
  const currentWeeklyStreak = streakData.currentWeeklyStreak

  return {
    data: {
      dailyActiveStats: dailyStats,
      weeklyActiveStats: Object.values(weeklyStats),
      practiceDuration,
      currentDailyStreak,
      currentWeeklyStreak,
      daysPracticed,
    },
  }
}

/**
 * Records a manual user practice data, updating the local database and syncing with remote.
 *
 * @param {Object} practiceDetails - The details of the practice session.
 * @param {number} practiceDetails.duration_seconds - The duration of the practice session in seconds.
 * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
 * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
 * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
 * @param {number} [practiceDetails.instrument_id] - The ID of the associated instrument (if available).
 * @returns {Promise<Object>} - A promise that resolves to the response from logging the user practice.
 *
 * @example
 * // Record a manual practice session with a title
 * recordUserPractice({ title: "Some title", duration_seconds: 300 })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 *
 */
export async function recordUserPractice(practiceDetails) {
  const day = new Date().toLocaleDateString('sv-SE') // YYYY-MM-DD wall clock date in user's timezone
  const durationSeconds = practiceDetails.duration_seconds

  const result = await db.practices.recordManualPractice(day, durationSeconds, {
    title: practiceDetails.title ?? null,
    category_id: practiceDetails.category_id ?? null,
    thumbnail_url: practiceDetails.thumbnail_url ?? null,
    instrument_id: practiceDetails.instrument_id ?? null,
  })

  streakCalculator.invalidate()
  return result
}

export async function trackUserPractice(contentId, incSeconds) {
  const day = new Date().toLocaleDateString('sv-SE') // YYYY-MM-DD wall clock date in user's timezone
  const result = await db.practices.trackAutoPractice(contentId, day, incSeconds, {
    skipPush: true,
  }) // NOTE - SKIPS PUSH

  streakCalculator.invalidate()
  return result
}

/**
 * Updates a user's practice session with new details and syncs the changes remotely.
 *
 * @param {string} id - The unique identifier of the practice session to update.
 * @param {Object} practiceDetails - The updated details of the practice session.
 * @param {number} [practiceDetails.duration_seconds] - The duration of the practice session in seconds.
 * @param {number} [practiceDetails.category_id] - The ID of the associated category (if available).
 * @param {string} [practiceDetails.title] - The title of the practice session (max 64 characters).
 * @param {string} [practiceDetails.thumbnail_url] - The URL of the session's thumbnail (max 255 characters).
 * @param {number} [practiceDetails.instrument_id] - The ID of the associated instrument (if available).
 * @returns {Promise<Object>} - A promise that resolves to the response from updating the user practice.
 *
 * @example
 * // Update a practice session's duration
 * updateUserPractice(123, { duration_seconds: 600 })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 *
 */
export async function updateUserPractice(id, practiceDetails) {
  const result = await db.practices.updateDetails(id, practiceDetails)
  streakCalculator.invalidate()
  return result
}

/**
 * Removes a user's practice session by ID, updating both the local and remote activity context.
 *
 * @param {number} id - The unique identifier of the practice session to be removed.
 * @returns {Promise<void>} - A promise that resolves once the practice session is removed.
 *
 * @example
 * // Remove a practice session with ID 123
 * removeUserPractice(123)
 *   .then(() => console.log("Practice session removed successfully"))
 *   .catch(error => console.error(error));
 */
export async function removeUserPractice(id) {
  const result = await db.practices.deleteOne(id)
  streakCalculator.invalidate()
  return result
}

/**
 * Restores a previously deleted user's practice session by ID
 *
 * @param {number} id - The unique identifier of the practice session to be restored.
 * @returns {Promise<Object>} - A promise that resolves to the response containing the restored practice session data.
 *
 * @example
 * // Restore a deleted practice session with ID 123
 * restoreUserPractice(123)
 *   .then(response => console.log("Practice session restored:", response))
 *   .catch(error => console.error(error));
 */
export async function restoreUserPractice(id) {
  return await db.practices.restoreOne(id)
}

/**
 * Deletes all practice sessions for a specific day.
 *
 * This function retrieves all user practice session IDs for a given day and sends a DELETE request
 * to remove them from the server. It also updates the local context to reflect the deletion.
 *
 * @async
 * @param {string} day - The day (in `YYYY-MM-DD` format) for which practice sessions should be deleted.
 * @returns {Promise<string[]>} - A promise that resolves once the practice session is removed.
 *
 *  * @example
 * // Delete practice sessions for April 10, 2025
 * deletePracticeSession("2025-04-10")
 *   .then(deletedIds => console.log("Deleted sessions:", response))
 *   .catch(error => console.error("Delete failed:", error));
 */
export async function deletePracticeSession(day) {
  const ids = await db.practices.queryAllIds(Q.where('date', day))
  const result = await db.practices.deleteSome(ids.data)
  streakCalculator.invalidate()
  return result
}

/**
 * Restores deleted practice sessions for a specific date.
 *
 * Sends a PUT request to restore any previously deleted practices for a given date.
 * If restored practices are returned, they are added back into the local context.
 *
 * @async
 * @param {string} date - The date (in `YYYY-MM-DD` format) for which deleted practice sessions should be restored.
 * @returns {Promise<Object>} - The response object from the API, containing practices for selected date.
 *
 * @example
 * // Restore practice sessions deleted on April 10, 2025
 * restorePracticeSession("2025-04-10")
 *   .then(response => console.log("Practice session restored:", response))
 *   .catch(error => console.error("Restore failed:", error));
 */
export async function restorePracticeSession(date) {
  const ids = await db.practices.queryAllDeletedIds(Q.where('date', date))
  const response = await db.practices.restoreSome(ids.data)

  const formattedMeta = await formatPracticeMeta(response.data)
  const practiceDuration = formattedMeta.reduce(
    (total, practice) => total + (practice.duration || 0),
    0
  )
  streakCalculator.invalidate()
  return { data: formattedMeta, practiceDuration }
}

/**
 * Retrieves and formats a user's practice sessions for a specific day.
 *
 * @param {Object} params - Parameters for fetching practice sessions.
 * @param {string} params.day - The date for which practice sessions should be retrieved (format: YYYY-MM-DD).
 * @param {number} [params.userId=globalConfig.sessionConfig.userId] - Optional user ID to retrieve sessions for a specific user. Defaults to the logged-in user.
 * @returns {Promise<Object>} - A promise that resolves to an object containing:
 *   - `practices`: An array of formatted practice session data.
 *   - `practiceDuration`: Total practice duration (in seconds) for the given day.
 *
 * @example
 * // Get practice sessions for the current user on a specific day
 * getPracticeSessions({ day: "2025-03-31" })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 *
 * @example
 * // Get practice sessions for another user
 * getPracticeSessions({ day: "2025-03-31", userId: 456 })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 */
export async function getPracticeSessions(params = {}, options = {}) {
  const { day, userId = globalConfig.sessionConfig.userId } = params

  let data

  if (userId === globalConfig.sessionConfig.userId) {
    // since this MCS method abstracts db, provide pull abstractions instead of making MPF/MA do it on their own
    if (options.pull) {
      await db.practices.pull()
    }
    // otherwise check for fresh data from server by default
    else {
      db.practices.pull()
    }

    const query = await db.practices.queryAll(Q.where('date', day), Q.sortBy('created_at', 'asc'))
    data = query.data
  } else {
    const query = await fetchUserPracticeMeta(day, userId)
    data = query.data
  }

  if (!data.length) return { data: { practices: [], practiceDuration: 0 } }

  const formattedMeta = await formatPracticeMeta(data)
  const practiceDuration = Math.round(formattedMeta.reduce(
    (total, practice) => total + (practice.duration || 0),
    0
  ))

  return { data: { practices: formattedMeta, practiceDuration } }
}

/**
 * Retrieves user practice notes for a specific day.
 *
 * @async
 * @param {string} day - The day (in `YYYY-MM-DD` format) to fetch practice notes for.
 * @returns {Promise<{ data: Object[] }>} - A promise that resolves to an object containing the practice notes.
 *
 * @example
 * // Get notes for April 10, 2025
 * getPracticeNotes("2025-04-10")
 *   .then(({ data }) => console.log("Practice notes:", data))
 *   .catch(error => console.error("Failed to get notes:", error));
 */
export async function getPracticeNotes(date) {
  return await db.practiceDayNotes.queryOne(Q.where('date', date))
}

/**
 * Retrieves the user's recent activity.
 *
 * Returns an object containing recent practice activity.
 *
 * @async
 * @returns {Promise<{ data: Object[] }>} - A promise that resolves to an object containing recent activity items.
 *
 * @example
 * // Fetch recent practice activity
 * getRecentActivity()
 *   .then(({ data }) => console.log("Recent activity:", data))
 *   .catch(error => console.error("Failed to get recent activity:", error));
 */
export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
  const recentActivityData = await fetchRecentUserActivities({ page, limit, tabName })

  const filteredData = recentActivityData.data.filter((id) => id !== null)
  const allContentIds = filteredData.map((p) => p.contentId)

  let contents = await addContextToContent(
    fetchByRailContentIds,
    allContentIds,
    'progress-tracker',
    undefined,
    true,
    { bypassPermissions: true },
    {
      addNavigateTo: true,
      addNextLesson: true,
    }
  )
  contents = postProcessBadge(contents)

  contents = await mapContentsThatWereLastProgressedFromMethod(contents)

  recentActivityData.data = recentActivityData.data.map((practice) => {
    const content = contents?.find((c) => c.id === practice.contentId) || {}
    return {
      ...practice,
      thumbnail: content.thumbnail || null,
      title: content.title || practice.title,
      parent_id: content.parent_id || null,
      navigateTo: content.navigateTo || null,
      sanityType: content.type || null,
      artist_name: content.artist_name || null,
    }
  })
  return recentActivityData
}

/**
 * Creates practice notes for a specific date.
 *
 * @param {Object} payload - The data required to create practice notes.
 * @param {string} payload.date - The date for which to create notes (format: YYYY-MM-DD).
 * @param {string} payload.notes - The notes content to be saved.
 * @returns {Promise<Object>} - A promise that resolves to the API response after creating the notes.
 *
 * @example
 * createPracticeNotes({ date: '2025-04-10', notes: 'Worked on scales and arpeggios' })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 */
export async function createPracticeNotes(payload) {
  return await db.practiceDayNotes.upsertOne(payload.date, (r) => {
    r.date = payload.date
    r.notes = payload.notes
  })
}

/**
 * Updates existing practice notes for a specific date.
 *
 * @param {Object} payload - The data required to update practice notes.
 * @param {string} payload.date - The date for which to update notes (format: YYYY-MM-DD).
 * @param {string} payload.notes - The updated notes content.
 * @returns {Promise<Object>} - A promise that resolves to the API response after updating the notes.
 *
 * @example
 * updatePracticeNotes({ date: '2025-04-10', notes: 'Updated: Focused on technique and timing' })
 *   .then(response => console.log(response))
 *   .catch(error => console.error(error));
 */
export async function updatePracticeNotes(payload) {
  return await db.practiceDayNotes.updateOneId(payload.date, (r) => {
    r.notes = payload.notes
  })
}

export function getStreaksAndMessage(practices) {
  let { currentDailyStreak, currentWeeklyStreak, streakMessage } = calculateStreaks(practices, true)

  return {
    currentDailyStreak,
    currentWeeklyStreak,
    streakMessage,
  }
}

function buildQueryString(ids, paramName = 'practice_ids') {
  if (!ids.length) return ''
  return '?' + ids.map((id) => `${paramName}[]=${id}`).join('&')
}

// Helper: Calculate streaks
function calculateStreaks(practices, includeStreakMessage = false) {
  let currentDailyStreak = 0
  let currentWeeklyStreak = 0
  let lastActiveDay = null
  let streakMessage = ''

  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
  let sortedPracticeDays = Object.keys(practices)
    .map((dateStr) => {
      const [year, month, day] = dateStr.split('-').map(Number)
      const newDate = new Date()
      newDate.setFullYear(year, month - 1, day)
      return newDate
    })
    .sort((a, b) => a - b)
  if (sortedPracticeDays.length === 0) {
    return {
      currentDailyStreak: 0,
      currentWeeklyStreak: 0,
      streakMessage: streakMessages.startStreak,
    }
  }
  lastActiveDay = sortedPracticeDays[sortedPracticeDays.length - 1]

  let dailyStreak = 0
  let prevDay = null
  sortedPracticeDays.forEach((currentDay) => {
    if (prevDay === null || isNextDay(prevDay, currentDay)) {
      dailyStreak++
    } else {
      dailyStreak = 1
    }
    prevDay = currentDay
  })
  currentDailyStreak = dailyStreak

  // Weekly streak calculation - using Monday dates to handle year boundaries
  let weekStartMap = new Map()
  sortedPracticeDays.forEach((date) => {
    const mondayDate = getMonday(date).format('YYYY-MM-DD')
    if (!weekStartMap.has(mondayDate)) {
      weekStartMap.set(mondayDate, getMonday(date).valueOf()) // Store as timestamp
    }
  })
  let sortedWeekTimestamps = [...weekStartMap.values()].sort((a, b) => b - a) // Descending
  let weeklyStreak = 0
  let prevWeekTimestamp = null
  for (const currentWeekTimestamp of sortedWeekTimestamps) {
    if (prevWeekTimestamp === null) {
      weeklyStreak = 1
    } else {
      const daysDiff = (prevWeekTimestamp - currentWeekTimestamp) / (1000 * 60 * 60 * 24)
      const weeksDiff = Math.round(daysDiff / 7)

      if (weeksDiff === 1) {
        weeklyStreak++
      } else {
        break // Properly break on non-consecutive week
      }
    }
    prevWeekTimestamp = currentWeekTimestamp
  }
  currentWeeklyStreak = weeklyStreak

  // Calculate streak message only if includeStreakMessage is true
  if (includeStreakMessage) {
    let today = new Date()
    let yesterday = new Date(today)
    yesterday.setDate(today.getDate() - 1)

    let currentWeekStart = getMonday(today, timeZone)
    let lastWeekStart = currentWeekStart.subtract(7, 'days')

    let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
    let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
    let hasCurrentWeekPreviousPractice = sortedPracticeDays.some(
      (date) => date >= currentWeekStart && date < today
    )
    let hasLastWeekPractice = sortedPracticeDays.some(
      (date) => date >= lastWeekStart && date < currentWeekStart
    )
    let hasOlderPractice = sortedPracticeDays.some((date) => date < lastWeekStart)

    if (isSameDate(lastActiveDay, today)) {
      if (hasYesterdayPractice) {
        streakMessage = streakMessages.dailyStreak(currentDailyStreak)
      } else if (hasCurrentWeekPreviousPractice) {
        streakMessage = streakMessages.weeklyStreak(currentWeeklyStreak)
      } else if (hasLastWeekPractice) {
        streakMessage = streakMessages.greatJobWeeklyStreak(currentWeeklyStreak)
      } else {
        streakMessage = streakMessages.dailyStreakShort(currentDailyStreak)
      }
    } else {
      if (
        (hasYesterdayPractice && currentDailyStreak >= 2) ||
        (hasYesterdayPractice && sortedPracticeDays.length == 1) ||
        (hasYesterdayPractice && !hasLastWeekPractice && hasOlderPractice)
      ) {
        streakMessage = streakMessages.dailyStreakReminder(currentDailyStreak)
      } else if (hasCurrentWeekPractice) {
        streakMessage = streakMessages.weeklyStreakKeepUp(currentWeeklyStreak)
      } else if (hasLastWeekPractice) {
        streakMessage = streakMessages.weeklyStreakReminder(currentWeeklyStreak)
      } else {
        streakMessage = streakMessages.restartStreak
      }
    }
  }

  return { currentDailyStreak, currentWeeklyStreak, streakMessage }
}

/**
 * Calculates the longest daily, weekly streaks and totalPracticeSeconds from user practice dates.
 * @returns {{ longestDailyStreak: number, longestWeeklyStreak: number, totalPracticeSeconds:number }}
 */
export async function calculateLongestStreaks(userId = globalConfig.sessionConfig.userId) {
  let practices = await getUserPractices(userId)
  let totalPracticeSeconds = 0
  // Calculate total practice duration
  for (const date in practices) {
    for (const entry of practices[date]) {
      totalPracticeSeconds += entry.duration_seconds
    }
  }

  let practiceDates = Object.keys(practices)
    .map((dateStr) => {
      const [y, m, d] = dateStr.split('-').map(Number)
      const newDate = new Date()
      newDate.setFullYear(y, m - 1, d)
      return newDate
    })
    .sort((a, b) => a - b)

  if (!practiceDates || practiceDates.length === 0) {
    return { longestDailyStreak: 0, longestWeeklyStreak: 0, totalPracticeSeconds: 0 }
  }

  // Normalize to Date objects
  const normalizedDates = [
    ...new Set(
      practiceDates.map((d) => {
        const date = new Date(d)
        date.setHours(0, 0, 0, 0)
        return date.getTime()
      })
    ),
  ].sort((a, b) => a - b)

  // ----- Daily Streak -----
  let longestDailyStreak = 1
  let currentDailyStreak = 1
  for (let i = 1; i < normalizedDates.length; i++) {
    const diffInDays = (normalizedDates[i] - normalizedDates[i - 1]) / (1000 * 60 * 60 * 24)
    if (diffInDays === 1) {
      currentDailyStreak++
      longestDailyStreak = Math.max(longestDailyStreak, currentDailyStreak)
    } else {
      currentDailyStreak = 1
    }
  }

  // ----- Weekly Streak -----
  const weekStartDates = [
    ...new Set(
      normalizedDates.map((ts) => {
        const d = new Date(ts)
        const day = d.getDay()
        const diff = d.getDate() - day + (day === 0 ? -6 : 1) // adjust to Monday
        d.setDate(diff)
        return d.getTime() // timestamp for Monday
      })
    ),
  ].sort((a, b) => a - b)

  let longestWeeklyStreak = 1
  let currentWeeklyStreak = 1

  for (let i = 1; i < weekStartDates.length; i++) {
    const diffInWeeks = (weekStartDates[i] - weekStartDates[i - 1]) / (1000 * 60 * 60 * 24 * 7)
    if (diffInWeeks === 1) {
      currentWeeklyStreak++
      longestWeeklyStreak = Math.max(longestWeeklyStreak, currentWeeklyStreak)
    } else {
      currentWeeklyStreak = 1
    }
  }

  return {
    longestDailyStreak,
    longestWeeklyStreak,
    totalPracticeSeconds,
  }
}

async function formatPracticeMeta(practices = []) {
  const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
  let contents = await addContextToContent(
    fetchByRailContentIds,
    contentIds,
    'progress-tracker',
    undefined,
    true,
    { bypassPermissions: true },
    {
      addNavigateTo: true,
      addNextLesson: true,
    }
  )
  contents = postProcessBadge(contents)

  contents = await mapContentsThatWereLastProgressedFromMethod(contents)

  return practices.map((practice) => {
    const content =
      contents && contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}

    return {
      id: practice.id,
      auto: practice.auto,
      thumbnail: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
      thumbnail_url: practice.content_id ? content?.thumbnail : practice.thumbnail_url || '',
      duration: practice.duration_seconds || 0,
      duration_seconds: practice.duration_seconds || 0,
      content_url: content?.url || null,
      title: practice.content_id ? content?.title : practice?.title  || practice.content_id,
      category_id: practice.category_id,
      instrument_id: practice.instrument_id,
      content_type: getFormattedType(content?.type || '', content?.brand || null),
      content_id: practice.content_id || null,
      content_brand: content?.brand || null,
      created_at: dayjs(practice.created_at),
      sanity_type: content?.type || null,
      content_slug: content?.slug || null,
      parent_id: content?.parent_id || null,
      navigateTo: content?.navigateTo || null,
      artist_name: content?.artist_name || null,
    }
  })
}

/**
 * Records a new user activity in the system.
 *
 * @param {Object} payload - The data representing the user activity.
 * @param {number} payload.user_id - The ID of the user.
 * @param {string} payload.action - The type of action (e.g., 'start', 'complete', 'comment', etc.).
 * @param {string} payload.brand - The brand associated with the activity.
 * @param {string} payload.type - The content type (e.g., 'lesson', 'song', etc.).
 * @param {number} payload.content_id - The ID of the related content.
 * @param {string} payload.date - The date of the activity (ISO format).
 * @returns {Promise<Object>} - A promise that resolves to the API response after recording the activity.
 *
 * @example
 * recordUserActivity({
 *   user_id: 123,
 *   action: 'start',
 *   brand: 'pianote',
 *   type: 'lesson',
 *   content_id: 4561,
 *   date: '2025-05-15'
 * }).then(response => console.log(response))
 *   .catch(error => console.error(error));
 */
export async function recordUserActivity(payload) {
  const url = `/api/user-management-system/v1/activities`
  return await POST(url, payload)
}

/**
 * Deletes a specific user activity by its ID.
 *
 * @param {number|string} id - The ID of the user activity to delete.
 * @returns {Promise<Object>} - A promise that resolves to the API response after deletion.
 *
 * @example
 * deleteUserActivity(789)
 *   .then(response => console.log('Deleted:', response))
 *   .catch(error => console.error(error));
 */
export async function deleteUserActivity(id) {
  const url = `/api/user-management-system/v1/activities/${id}`
  return await DELETE(url)
}

/**
 * Restores a specific user activity by its ID.
 *
 * @param {number|string} id - The ID of the user activity to restore.
 * @returns {Promise<Object>} - A promise that resolves to the API response after restoration.
 *
 * @example
 * restoreUserActivity(789)
 *   .then(response => console.log('Restored:', response))
 *   .catch(error => console.error(error));
 */
export async function restoreUserActivity(id) {
  const url = `/api/user-management-system/v1/activities/${id}`
  return await POST(url, null)
}

export function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
  const isMap = progressOnItems instanceof Map
  const ids = isMap ? Array.from(progressOnItems.keys()) : Object.keys(progressOnItems).map(Number)
  const getProgress = (id) => isMap ? progressOnItems.get(id) : progressOnItems[id]

  if (contentType === 'guided-course' || contentType === COLLECTION_TYPE.LEARNING_PATH) {
    return ids.find((id) => getProgress(id) !== 'completed') || ids.at(0)
  }

  const currentIndex = ids.indexOf(Number(currentContentId))
  if (currentIndex === -1) return null

  for (let i = currentIndex + 1; i < ids.length; i++) {
    const id = ids[i]
    if (getProgress(id) !== 'completed') {
      return id
    }
  }

  return ids[0]
}

export async function fetchRecentActivitiesActiveTabs() {
  const url = `/api/user-management-system/v1/activities/tabs`
  const tabs = await GET(url)
  const activitiesTabs = []

  tabs.forEach((tab) => {
    activitiesTabs.push({ name: tab.label, short_name: tab.label })
  })

  return activitiesTabs
}