/**
 * User-friendly schedule ↔ cron
 * Standard: 5 Felder (minute hour dom month dow), DOW 0=So … 6=Sa
 * Intervall Sekunden: 6 Felder (sec min hour dom month dow)
 */

export type ScheduleMode = 'daily' | 'weekdays' | 'weekly' | 'calendar' | 'interval';

export type CalendarPeriod = 'monthly' | 'yearly';

/** sek, min, h, T (Tage), a (Jahre) — Cron nur näherungsweise für T/a */
export type IntervalUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'years';

export interface ScheduleSpec {
  mode: ScheduleMode;
  hour: number;
  minute: number;
  /** 0–6, cron DOW; nur bei mode === 'weekly' */
  weekdays: number[];
  /** Monatlich: Tag 1–31; Jährlich: Tag im gewählten Monat */
  monthDay: number;
  /** 1–12, nur bei calendar + yearly */
  monthIndex: number;
  calendarPeriod: CalendarPeriod;
  intervalValue: number;
  intervalUnit: IntervalUnit;
}

export const WEEKDAY_LABELS_DE = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] as const;

/** Anzeige Mo–So (cronDow wie oben) */
export const WEEKDAYS_MO_SO: readonly { cronDow: number; label: string }[] = [
  { cronDow: 1, label: 'Mo' },
  { cronDow: 2, label: 'Di' },
  { cronDow: 3, label: 'Mi' },
  { cronDow: 4, label: 'Do' },
  { cronDow: 5, label: 'Fr' },
  { cronDow: 6, label: 'Sa' },
  { cronDow: 0, label: 'So' },
];

export function defaultScheduleSpec(): ScheduleSpec {
  return {
    mode: 'daily',
    hour: 8,
    minute: 0,
    weekdays: [1, 2, 3, 4, 5],
    monthDay: 1,
    monthIndex: 1,
    calendarPeriod: 'monthly',
    intervalValue: 15,
    intervalUnit: 'minutes',
  };
}

function clamp(n: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, n));
}

/** Erzeugt einen Cron-String aus der benutzerfreundlichen Spezifikation */
export function buildCronFromSpec(spec: ScheduleSpec): string {
  const m = clamp(Math.floor(spec.minute), 0, 59);
  const h = clamp(Math.floor(spec.hour), 0, 23);

  switch (spec.mode) {
    case 'daily':
      return `${m} ${h} * * *`;
    case 'weekdays':
      return `${m} ${h} * * 1-5`;
    case 'weekly': {
      const days = [...new Set(spec.weekdays)]
        .filter((d) => d >= 0 && d <= 6)
        .sort((a, b) => {
          const order = (x: number) => (x === 0 ? 7 : x);
          return order(a) - order(b);
        });
      if (days.length === 0) return `${m} ${h} * * 1`;
      return `${m} ${h} * * ${days.join(',')}`;
    }
    case 'calendar': {
      const dom = clamp(Math.floor(spec.monthDay), 1, 31);
      if (spec.calendarPeriod === 'monthly') {
        return `${m} ${h} ${dom} * *`;
      }
      const month = clamp(Math.floor(spec.monthIndex), 1, 12);
      return `${m} ${h} ${dom} ${month} *`;
    }
    case 'interval': {
      const v = Math.max(1, Math.floor(spec.intervalValue));
      switch (spec.intervalUnit) {
        case 'seconds': {
          const s = clamp(v, 1, 59);
          return `*/${s} * * * * *`;
        }
        case 'minutes': {
          const mm = clamp(v, 1, 59);
          return `*/${mm} * * * *`;
        }
        case 'hours': {
          const hh = clamp(v, 1, 23);
          return `0 */${hh} * * *`;
        }
        case 'days': {
          if (v <= 1) return `0 0 * * *`;
          const d = clamp(v, 2, 31);
          return `0 0 */${d} * *`;
        }
        case 'years':
        default:
          // Standard-5-Feld-Cron hat kein Jahres-Intervall; 1. Jan. Mitternacht als Näherung
          return `0 0 1 1 *`;
      }
    }
    default:
      return `${m} ${h} * * *`;
  }
}

/** Best-effort Rückübersetzung für gespeicherte Cron-Zeilen */
export function parseCronToSpec(cron: string | undefined): ScheduleSpec | null {
  if (!cron || typeof cron !== 'string') return null;
  const p = cron.trim().split(/\s+/);

  if (p.length === 6) {
    const [secS, minS, hourS, domS, monthS, dowS] = p;
    if (
      secS.startsWith('*/') &&
      minS === '*' &&
      hourS === '*' &&
      domS === '*' &&
      monthS === '*' &&
      (dowS === '*' || dowS === '?')
    ) {
      const iv = parseInt(secS.slice(2), 10);
      if (!Number.isNaN(iv)) {
        return {
          ...defaultScheduleSpec(),
          mode: 'interval',
          intervalValue: iv,
          intervalUnit: 'seconds',
          minute: 0,
          hour: 0,
        };
      }
    }
    return null;
  }

  if (p.length < 5) return null;
  const [minS, hourS, domS, monthS, dowS] = p;
  const minute = parseInt(minS, 10);
  const hour = parseInt(hourS, 10);
  if (Number.isNaN(minute) || Number.isNaN(hour)) return null;

  if (minS.startsWith('*/') && p[1] === '*' && domS === '*') {
    const iv = parseInt(minS.slice(2), 10);
    if (!Number.isNaN(iv)) {
      return {
        ...defaultScheduleSpec(),
        mode: 'interval',
        intervalValue: iv,
        intervalUnit: 'minutes',
        minute: 0,
        hour: 0,
      };
    }
  }

  if (minS === '0' && hourS.startsWith('*/') && domS === '*') {
    const iv = parseInt(hourS.slice(2), 10);
    if (!Number.isNaN(iv)) {
      return {
        ...defaultScheduleSpec(),
        mode: 'interval',
        intervalValue: iv,
        intervalUnit: 'hours',
        minute: 0,
        hour: 0,
      };
    }
  }

  if (minS === '0' && hourS === '0' && domS.startsWith('*/') && monthS === '*' && (dowS === '*' || dowS === '?')) {
    const iv = parseInt(domS.slice(2), 10);
    if (!Number.isNaN(iv)) {
      return {
        ...defaultScheduleSpec(),
        mode: 'interval',
        intervalValue: iv,
        intervalUnit: 'days',
        minute: 0,
        hour: 0,
      };
    }
  }

  if (domS === '*' && dowS === '*') {
    return { ...defaultScheduleSpec(), mode: 'daily', hour, minute };
  }

  if (domS === '*' && dowS === '1-5') {
    return { ...defaultScheduleSpec(), mode: 'weekdays', hour, minute };
  }

  if (domS === '*' && dowS && dowS !== '*' && !dowS.includes('/')) {
    const parts = dowS.split(',').map((x) => parseInt(x.trim(), 10));
    const days = parts.filter((x) => !Number.isNaN(x) && x >= 0 && x <= 7);
    if (days.length > 0) {
      const norm = days.map((d) => (d === 7 ? 0 : d));
      return {
        ...defaultScheduleSpec(),
        mode: 'weekly',
        hour,
        minute,
        weekdays: norm,
      };
    }
  }

  const dom = parseInt(domS, 10);
  const month = monthS === '*' ? NaN : parseInt(monthS, 10);

  if (!Number.isNaN(dom) && dom >= 1 && dom <= 31 && monthS === '*' && (dowS === '*' || dowS === '?')) {
    return {
      ...defaultScheduleSpec(),
      mode: 'calendar',
      calendarPeriod: 'monthly',
      hour,
      minute,
      monthDay: dom,
    };
  }

  if (
    !Number.isNaN(dom) &&
    dom >= 1 &&
    dom <= 31 &&
    !Number.isNaN(month) &&
    month >= 1 &&
    month <= 12 &&
    (dowS === '*' || dowS === '?')
  ) {
    return {
      ...defaultScheduleSpec(),
      mode: 'calendar',
      calendarPeriod: 'yearly',
      hour,
      minute,
      monthDay: dom,
      monthIndex: month,
    };
  }

  return null;
}

const VALID_MODES: ScheduleMode[] = ['daily', 'weekdays', 'weekly', 'calendar', 'interval'];

function normalizeIntervalUnit(u: unknown): IntervalUnit {
  if (u === 'seconds' || u === 'minutes' || u === 'hours' || u === 'days' || u === 'years') return u;
  return 'minutes';
}

/** Liest Spec aus Node-Parametern (schedule-Objekt bevorzugt, sonst Cron parsen) */
export function scheduleSpecFromParams(params: Record<string, unknown>): ScheduleSpec {
  const raw = params.schedule;
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
    const o = raw as Record<string, unknown>;
    let mode = o.mode as string;
    if (mode === 'monthly') {
      mode = 'calendar';
    }
    if (VALID_MODES.includes(mode as ScheduleMode)) {
      const base = defaultScheduleSpec();
      let calendarPeriod: CalendarPeriod = base.calendarPeriod;
      if (mode === 'calendar') {
        calendarPeriod = o.calendarPeriod === 'yearly' ? 'yearly' : 'monthly';
      }
      return {
        mode: mode as ScheduleMode,
        hour: clamp(Number(o.hour) || base.hour, 0, 23),
        minute: clamp(Number(o.minute) || base.minute, 0, 59),
        weekdays: Array.isArray(o.weekdays)
          ? (o.weekdays as unknown[]).map((x) => clamp(Number(x), 0, 6)).filter((x) => !Number.isNaN(x))
          : base.weekdays,
        monthDay: clamp(Number(o.monthDay) || base.monthDay, 1, 31),
        monthIndex: clamp(Number(o.monthIndex) || base.monthIndex, 1, 12),
        calendarPeriod,
        intervalValue: Math.max(1, Number(o.intervalValue) || base.intervalValue),
        intervalUnit: normalizeIntervalUnit(o.intervalUnit),
      };
    }
  }
  const cron = typeof params.cron === 'string' ? params.cron : '';
  return parseCronToSpec(cron) ?? defaultScheduleSpec();
}
