import { Cron } from 'croner';

import { type AbsoluteTimeRange, type TimeRange, durationToMilliseconds, parseDuration } from '@grafana/data';

export type TimeRegionMode = null | 'cron';
export interface TimeRegionConfig {
  mode?: TimeRegionMode;

  from?: string;
  fromDayOfWeek?: number; // 1-7

  to?: string;
  toDayOfWeek?: number; // 1-7

  timezone?: string;

  cronExpr?: string; // 0 9 * * 1-5
  duration?: string; // 8h
}

const secsInDay = 24 * 3600;

function getDurationSecs(
  fromDay: number,
  fromHour: number,
  fromMin: number,
  toDay: number,
  toHour: number,
  toMin: number
) {
  let days = toDay - fromDay;

  // account for rollover
  if (days < 0) {
    days += 7;
  }

  let fromSecs = fromHour * 3600 + fromMin * 60;
  let toSecs = toHour * 3600 + toMin * 60;

  let durSecs = 0;

  // account for toTime < fromTime on same day
  if (days === 0 && toSecs < fromSecs) {
    durSecs = 7 * secsInDay - (fromSecs - toSecs);
  } else {
    let daysSecs = days * secsInDay;
    durSecs = daysSecs - fromSecs + toSecs;
  }

  return durSecs;
}

export function convertToCron(
  fromDay?: number | null,
  from?: string | null,
  toDay?: number | null,
  to?: string | null
) {
  // valid defs must have a "from"
  if (fromDay == null && from == null) {
    return undefined;
  }

  const cronCfg = {
    cronExpr: '',
    duration: 0,
  };

  const isEveryDay = fromDay == null && toDay == null;
  // if the def contains only days of week, then they become end-day-inclusive
  const toDayEnd = fromDay != null && from == null && to == null;

  from ??= '00:00';

  // 1. create cron (only requires froms)
  let [fromHour, fromMin] = from.split(':').map((v) => +v);

  cronCfg.cronExpr = `${fromMin} ${fromHour} * * ${fromDay ?? '*'}`;

  // 2. determine duration
  fromDay ??= 1;
  toDay ??= fromDay;

  // e.g. from Wed to Fri (implies inclusive Fri)
  if (toDayEnd) {
    to = '00:00';
    toDay += toDay === 7 ? -6 : 1;
  }

  to ??= from;

  let [toHour, toMin] = to.split(':').map((v) => +v);

  let fromSecs = fromHour * 3600 + fromMin * 60;
  let toSecs = toHour * 3600 + toMin * 60;

  // e.g. every day from 22:00 to 02:00 (implied next day)
  // NOTE: the odd wrap-around case of toSecs < fromSecs in same day is handled inside getDurationSecs()
  if (isEveryDay && toSecs < fromSecs) {
    toDay += toDay === 7 ? -6 : 1;
  }

  cronCfg.duration = getDurationSecs(fromDay, fromHour, fromMin, toDay, toHour, toMin);

  return cronCfg;
}

export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange): AbsoluteTimeRange[] {
  const ranges: AbsoluteTimeRange[] = [];

  let cronExpr = '';
  let durationMs = 0;

  let { fromDayOfWeek, from, toDayOfWeek, to, duration = '' } = cfg;

  if (cfg.mode === 'cron') {
    cronExpr = cfg.cronExpr ?? '';
    durationMs = durationToMilliseconds(parseDuration(duration));
  } else {
    // remove empty strings
    from = from === '' ? undefined : from;
    to = to === '' ? undefined : to;

    const cron = convertToCron(fromDayOfWeek, from, toDayOfWeek, to);

    if (cron != null) {
      cronExpr = cron.cronExpr;
      durationMs = cron.duration * 1e3;
    }
  }

  if (cronExpr === '') {
    return [];
  }

  try {
    let tz = cfg.timezone === 'browser' ? undefined : cfg.timezone === 'utc' ? 'Etc/UTC' : cfg.timezone;

    let job = new Cron(cronExpr, { timezone: tz });

    // get previous run that may overlap with start of timerange
    let fromDate: Date | null = new Date(tRange.from.valueOf() - durationMs);

    let toMs = tRange.to.valueOf();
    let nextDate = job.nextRun(fromDate);

    while (nextDate != null) {
      let nextMs = +nextDate;

      if (nextMs > toMs) {
        break;
      } else {
        ranges.push({
          from: nextMs,
          to: nextMs + durationMs,
        });
        nextDate = job.nextRun(nextDate);
      }
    }
  } catch (e) {
    // invalid expression
    console.error(e);
  }

  return ranges;
}
