// Parses and validates session timing strings (durations, time ranges, multi-day).
export function parseTimeStr(s) {
  const t = s.trim().toLowerCase()
  // HHMM or HHMMam/pm (no colon) — e.g. "0900", "0900pm"
  let m = t.match(/^(\d{2})(\d{2})\s*(am|pm)?$/)
  if (m) {
    let h = Number(m[1]), min = Number(m[2])
    if (m[3] === 'pm' && h !== 12) h += 12
    if (m[3] === 'am' && h === 12) h = 0
    return h * 60 + min
  }
  // H:MM or HH:MM with optional am/pm — e.g. "9:00", "13:30", "9:00am"
  m = t.match(/^(\d{1,2}):(\d{2})\s*(am|pm)?$/)
  if (m) {
    let h = Number(m[1]), min = Number(m[2])
    if (m[3] === 'pm' && h !== 12) h += 12
    if (m[3] === 'am' && h === 12) h = 0
    return h * 60 + min
  }
  // Ham or Hpm (no minutes) — e.g. "9am", "1pm"
  m = t.match(/^(\d{1,2})\s*(am|pm)$/)
  if (m) {
    let h = Number(m[1])
    if (m[2] === 'pm' && h !== 12) h += 12
    if (m[2] === 'am' && h === 12) h = 0
    return h * 60
  }
  throw new Error(`invalid time: "${s.trim()}"`)
}

export function minsToHHMM(mins) {
  const h = Math.floor(mins / 60) % 24
  const m = mins % 60
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}

export function parseStartTime(value) {
  if (value == null) return null
  return String(value).split(';').map(s => minsToHHMM(parseTimeStr(s.trim())))
}

export function parseTimings(str) {
  const isDurationToken = t => /^\d+\s*m(?:in)?$/.test(t)
  const isRangeToken = t => /^.+?\s*-\s*.+$/.test(t) && !isDurationToken(t)

  const days = str.split(';').map(dayStr => {
    const s = dayStr.trim()

    // shorthand: "4 x 90m"
    const uniform = s.match(/^(\d+)\s*x\s*(\d+)\s*m(?:in)?$/)
    if (uniform) {
      return { sessions: Array(Number(uniform[1])).fill(Number(uniform[2])), startTime: null, impliedBreaks: null }
    }

    const tokens = s.split(/[,\s]+/).filter(Boolean)
    if (tokens.length === 0) throw new Error(`invalid timing: "${s}"`)

    if (isRangeToken(tokens[0])) {
      if (!tokens.every(isRangeToken)) throw new Error(`cannot mix time ranges and durations in timings: "${s}"`)
      const ranges = tokens.map(t => {
        const rm = t.match(/^(.+?)\s*-\s*(.+)$/)
        const start = parseTimeStr(rm[1])
        const end = parseTimeStr(rm[2])
        if (end <= start) throw new Error(`invalid time range "${t}": end must be after start`)
        return { start, end }
      })
      for (let i = 1; i < ranges.length; i++) {
        if (ranges[i].start < ranges[i - 1].end) {
          throw new Error(
            `time ranges overlap: session ${i} ends at ${minsToHHMM(ranges[i - 1].end)} but session ${i + 1} starts at ${minsToHHMM(ranges[i].start)}`
          )
        }
      }
      return {
        sessions: ranges.map(r => r.end - r.start),
        startTime: minsToHHMM(ranges[0].start),
        impliedBreaks: ranges.slice(1).map((r, i) => r.start - ranges[i].end),
      }
    }

    return {
      sessions: tokens.map(t => {
        const m = t.match(/^(\d+)\s*m(?:in)?$/)
        if (!m) throw new Error(`invalid timing: "${t}"`)
        return Number(m[1])
      }),
      startTime: null,
      impliedBreaks: null,
    }
  })

  return {
    sessions: days.map(d => d.sessions),
    startTimes: days.map(d => d.startTime),
    impliedBreaks: days.map(d => d.impliedBreaks),
  }
}

export function validateTimings(sessionDurationsByDay, sessionCountsPerDay) {
  for (let i = 0; i < sessionDurationsByDay.length; i++) {
    const expected = sessionDurationsByDay[i].length
    const actual = sessionCountsPerDay[i] ?? 0
    if (expected !== actual) {
      throw new Error(
        `timings declares ${expected} sessions for day ${i + 1} but body has ${actual} sessions`,
      )
    }
  }
}
