// Cron expression parser, scheduler, and describer

export type FieldType = 'second' | 'minute' | 'hour' | 'dom' | 'month' | 'dow';

export interface CronFieldConfig {
  min: number;
  max: number;
  label: string;
  shortLabel: string;
  names?: readonly string[];
  aliases?: Record<string, number>;
}

export const FIELD_CONFIGS: Record<FieldType, CronFieldConfig> = {
  second: { min: 0, max: 59, label: 'Second', shortLabel: 'SEC' },
  minute: { min: 0, max: 59, label: 'Minute', shortLabel: 'MIN' },
  hour:   { min: 0, max: 23, label: 'Hour',   shortLabel: 'HOUR' },
  dom:    { min: 1, max: 31, label: 'Day of Month', shortLabel: 'DOM' },
  month: {
    min: 1, max: 12, label: 'Month', shortLabel: 'MON',
    names: ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'],
    aliases: { JAN:1,FEB:2,MAR:3,APR:4,MAY:5,JUN:6,JUL:7,AUG:8,SEP:9,OCT:10,NOV:11,DEC:12 },
  },
  dow: {
    min: 0, max: 6, label: 'Day of Week', shortLabel: 'DOW',
    names: ['SUN','MON','TUE','WED','THU','FRI','SAT'],
    aliases: { SUN:0,MON:1,TUE:2,WED:3,THU:4,FRI:5,SAT:6 },
  },
};

export const MONTH_FULL_NAMES = [
  'January','February','March','April','May','June',
  'July','August','September','October','November','December',
];
export const DOW_FULL_NAMES = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
export const MONTH_SHORT_NAMES = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
export const DOW_SHORT_NAMES  = ['SUN','MON','TUE','WED','THU','FRI','SAT'];

export interface ParsedCronField {
  raw: string;
  values: Set<number>;
  isWildcard: boolean;
}

export interface ParsedCron {
  hasSeconds: boolean;
  fields: {
    second?: ParsedCronField;
    minute: ParsedCronField;
    hour:   ParsedCronField;
    dom:    ParsedCronField;
    month:  ParsedCronField;
    dow:    ParsedCronField;
  };
}

export interface CronFields {
  second?: string;
  minute: string;
  hour:   string;
  dom:    string;
  month:  string;
  dow:    string;
  hasSeconds: boolean;
}

// ── Special expressions ───────────────────────────────────────────────────────

const SPECIAL_EXPRESSIONS: Record<string, string | null> = {
  '@yearly':   '0 0 1 1 *',
  '@annually': '0 0 1 1 *',
  '@monthly':  '0 0 1 * *',
  '@weekly':   '0 0 * * 0',
  '@daily':    '0 0 * * *',
  '@midnight': '0 0 * * *',
  '@hourly':   '0 * * * *',
  '@reboot':   null,
};

// ── Low-level field parser ────────────────────────────────────────────────────

function resolveAlias(val: string, config: CronFieldConfig): number {
  const n = parseInt(val, 10);
  if (!isNaN(n)) return n;
  if (config.aliases) {
    const upper = val.toUpperCase();
    if (upper in config.aliases) return config.aliases[upper];
  }
  return NaN;
}

function parsePart(part: string, config: CronFieldConfig, values: Set<number>): boolean {
  // Step: */5 or 0-30/5 or 5/15
  const stepMatch = part.match(/^(.+)\/(\d+)$/);
  if (stepMatch) {
    const step = parseInt(stepMatch[2], 10);
    if (isNaN(step) || step < 1) return false;
    let start: number, end: number;
    if (stepMatch[1] === '*') {
      start = config.min; end = config.max;
    } else {
      const rm = stepMatch[1].match(/^(.+)-(.+)$/);
      if (rm) {
        start = resolveAlias(rm[1], config);
        end   = resolveAlias(rm[2], config);
      } else {
        start = resolveAlias(stepMatch[1], config);
        end   = config.max;
      }
    }
    if (isNaN(start) || isNaN(end) || start < config.min || end > config.max) return false;
    for (let i = start; i <= end; i += step) values.add(i);
    return true;
  }

  // Range: 1-5
  const rangeMatch = part.match(/^(.+)-(.+)$/);
  if (rangeMatch) {
    const start = resolveAlias(rangeMatch[1], config);
    const end   = resolveAlias(rangeMatch[2], config);
    if (isNaN(start) || isNaN(end) || start > end || start < config.min || end > config.max) return false;
    for (let i = start; i <= end; i++) values.add(i);
    return true;
  }

  // Single
  const n = resolveAlias(part, config);
  if (isNaN(n)) return false;
  const adjusted = (config === FIELD_CONFIGS.dow && n === 7) ? 0 : n;
  if (adjusted < config.min || adjusted > config.max) return false;
  values.add(adjusted);
  return true;
}

export function parseField(expr: string, config: CronFieldConfig): ParsedCronField | null {
  if (!expr) return null;
  const values = new Set<number>();
  if (expr === '*') {
    for (let i = config.min; i <= config.max; i++) values.add(i);
    return { raw: expr, values, isWildcard: true };
  }
  for (const part of expr.split(',')) {
    if (!parsePart(part.trim(), config, values)) return null;
  }
  return { raw: expr, values, isWildcard: false };
}

// ── Expression parser ─────────────────────────────────────────────────────────

export function parseCronExpression(expr: string): ParsedCron | null {
  expr = expr.trim();
  const lower = expr.toLowerCase();
  if (lower.startsWith('@')) {
    const resolved = SPECIAL_EXPRESSIONS[lower];
    if (resolved === undefined) return null;
    if (resolved === null) return null;
    expr = resolved;
  }
  const parts = expr.split(/\s+/);
  if (parts.length < 5 || parts.length > 6) return null;
  const hasSeconds = parts.length === 6;
  const o = hasSeconds ? 1 : 0;
  let secondField: ParsedCronField | undefined;
  if (hasSeconds) {
    const f = parseField(parts[0], FIELD_CONFIGS.second);
    if (!f) return null;
    secondField = f;
  }
  const minute = parseField(parts[o + 0], FIELD_CONFIGS.minute);
  const hour   = parseField(parts[o + 1], FIELD_CONFIGS.hour);
  const dom    = parseField(parts[o + 2], FIELD_CONFIGS.dom);
  const month  = parseField(parts[o + 3], FIELD_CONFIGS.month);
  const dow    = parseField(parts[o + 4], FIELD_CONFIGS.dow);
  if (!minute || !hour || !dom || !month || !dow) return null;
  return { hasSeconds, fields: { second: secondField, minute, hour, dom, month, dow } };
}

// ── Field value reconstruction ────────────────────────────────────────────────

export function rebuildFieldFromValues(values: Set<number>, config: CronFieldConfig): string {
  const sorted = [...values].sort((a, b) => a - b);
  if (sorted.length === 0) return '*';
  if (sorted.length === config.max - config.min + 1) return '*';

  // Regular step from min → */N
  if (sorted.length > 1) {
    const step = sorted[1] - sorted[0];
    if (step > 0 && sorted.every((v, i) => v === sorted[0] + i * step)) {
      if (sorted[0] === config.min) return `*/${step}`;
      return `${sorted[0]}-${sorted[sorted.length - 1]}/${step}`;
    }
    // Consecutive range
    if (sorted.every((v, i) => i === 0 || v === sorted[i - 1] + 1)) {
      return `${sorted[0]}-${sorted[sorted.length - 1]}`;
    }
  }
  return sorted.join(',');
}

// ── Split / build ─────────────────────────────────────────────────────────────

export function splitCronFields(expr: string): CronFields | null {
  const lower = expr.trim().toLowerCase();
  const resolved = SPECIAL_EXPRESSIONS[lower];
  if (resolved !== undefined) {
    if (resolved === null) return null;
    expr = resolved;
  }
  const parts = expr.trim().split(/\s+/);
  if (parts.length === 5) {
    return { minute: parts[0], hour: parts[1], dom: parts[2], month: parts[3], dow: parts[4], hasSeconds: false };
  }
  if (parts.length === 6) {
    return { second: parts[0], minute: parts[1], hour: parts[2], dom: parts[3], month: parts[4], dow: parts[5], hasSeconds: true };
  }
  return null;
}

export function buildCronExpression(fields: CronFields): string {
  const base = `${fields.minute} ${fields.hour} ${fields.dom} ${fields.month} ${fields.dow}`;
  return fields.hasSeconds && fields.second ? `${fields.second} ${base}` : base;
}

// ── Day matching ──────────────────────────────────────────────────────────────

function checkDay(d: Date, parsed: ParsedCron): boolean {
  const domWild = parsed.fields.dom.isWildcard;
  const dowWild = parsed.fields.dow.isWildcard;
  if (domWild && dowWild) return true;
  if (domWild) return parsed.fields.dow.values.has(d.getDay());
  if (dowWild) return parsed.fields.dom.values.has(d.getDate());
  return parsed.fields.dom.values.has(d.getDate()) || parsed.fields.dow.values.has(d.getDay());
}

// ── Smart advance algorithm ───────────────────────────────────────────────────

function advanceToNext(date: Date, parsed: ParsedCron): Date | null {
  const d = new Date(date);
  const maxDate = new Date(date.getTime() + 5 * 366 * 24 * 60 * 60 * 1000);
  let guard = 0;

  while (d < maxDate && guard++ < 200_000) {
    // Month
    const m = d.getMonth() + 1;
    if (!parsed.fields.month.values.has(m)) {
      const sorted = [...parsed.fields.month.values].sort((a, b) => a - b);
      const next = sorted.find(v => v > m);
      if (next !== undefined) {
        d.setMonth(next - 1, 1);
      } else {
        d.setFullYear(d.getFullYear() + 1, sorted[0] - 1, 1);
      }
      d.setHours(0, 0, 0, 0);
      continue;
    }

    // Day
    if (!checkDay(d, parsed)) {
      d.setDate(d.getDate() + 1);
      d.setHours(0, 0, 0, 0);
      continue;
    }

    // Hour
    const h = d.getHours();
    const sortedH = [...parsed.fields.hour.values].sort((a, b) => a - b);
    if (!parsed.fields.hour.values.has(h)) {
      const next = sortedH.find(v => v > h);
      if (next !== undefined) {
        d.setHours(next, 0, 0, 0);
      } else {
        d.setDate(d.getDate() + 1);
        d.setHours(sortedH[0], 0, 0, 0);
      }
      continue;
    }

    // Minute
    const min = d.getMinutes();
    const sortedM = [...parsed.fields.minute.values].sort((a, b) => a - b);
    if (!parsed.fields.minute.values.has(min)) {
      const next = sortedM.find(v => v > min);
      if (next !== undefined) {
        d.setMinutes(next, 0, 0);
      } else {
        const nextH = sortedH.find(v => v > h);
        if (nextH !== undefined) {
          d.setHours(nextH, sortedM[0], 0, 0);
        } else {
          d.setDate(d.getDate() + 1);
          d.setHours(sortedH[0], sortedM[0], 0, 0);
        }
      }
      continue;
    }

    return new Date(d);
  }
  return null;
}

export function getNextOccurrences(
  expr: string,
  count: number = 8,
  from: Date = new Date(),
): Date[] {
  const parsed = parseCronExpression(expr);
  if (!parsed) return [];
  const results: Date[] = [];
  let current = new Date(from);
  current.setSeconds(0, 0);
  current.setTime(current.getTime() + 60_000); // start from next minute

  for (let i = 0; i < count; i++) {
    const next = advanceToNext(current, parsed);
    if (!next) break;
    results.push(next);
    current = new Date(next.getTime() + 60_000);
  }
  return results;
}

// ── Human-readable description ────────────────────────────────────────────────

function isStepRaw(raw: string): boolean {
  return /^(\*|\d+)\/\d+$/.test(raw);
}

function stepValue(raw: string): number | null {
  const m = raw.match(/\/(\d+)$/);
  return m ? parseInt(m[1], 10) : null;
}

function ordinal(n: number): string {
  const s = ['th', 'st', 'nd', 'rd'];
  const v = n % 100;
  return n + (s[(v - 20) % 10] || s[v] || s[0]);
}

function formatTime12(hour: number, minute: number): string {
  const ampm = hour < 12 ? 'AM' : 'PM';
  const h = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
  return `${h}:${String(minute).padStart(2, '0')} ${ampm}`;
}

function formatHour(h: number): string {
  const ampm = h < 12 ? 'AM' : 'PM';
  const d = h === 0 ? 12 : h > 12 ? h - 12 : h;
  return `${d}:00 ${ampm}`;
}

function formatDowList(vals: number[]): string {
  if (vals.length === 1) return DOW_FULL_NAMES[vals[0]];
  if (vals.length === 7) return 'every day';
  return vals.map(v => DOW_FULL_NAMES[v]).join(', ');
}

export function describeCronExpression(expr: string): string {
  const lower = expr.trim().toLowerCase();
  const specialDescs: Record<string, string> = {
    '@yearly':   'Every year on January 1st at midnight',
    '@annually': 'Every year on January 1st at midnight',
    '@monthly':  'Every month on the 1st at midnight',
    '@weekly':   'Every week on Sunday at midnight',
    '@daily':    'Every day at midnight',
    '@midnight': 'Every day at midnight',
    '@hourly':   'Every hour at :00',
    '@reboot':   'Once at system reboot',
  };
  if (lower in specialDescs) return specialDescs[lower];

  const parsed = parseCronExpression(expr);
  if (!parsed) return 'Invalid cron expression';
  const { fields } = parsed;

  const mVals   = [...fields.minute.values].sort((a, b) => a - b);
  const hVals   = [...fields.hour.values].sort((a, b) => a - b);
  const domVals = [...fields.dom.values].sort((a, b) => a - b);
  const monVals = [...fields.month.values].sort((a, b) => a - b);
  const dowVals = [...fields.dow.values].sort((a, b) => a - b);

  const mWild   = fields.minute.isWildcard;
  const hWild   = fields.hour.isWildcard;
  const domWild = fields.dom.isWildcard;
  const monWild = fields.month.isWildcard;
  const dowWild = fields.dow.isWildcard;

  // Time
  let when = '';
  if (mWild && hWild) {
    when = 'Every minute';
  } else if (hWild && isStepRaw(fields.minute.raw)) {
    const s = stepValue(fields.minute.raw);
    when = s === 1 ? 'Every minute' : `Every ${s} minutes`;
  } else if (mWild && isStepRaw(fields.hour.raw)) {
    const s = stepValue(fields.hour.raw);
    when = s === 1 ? 'Every hour' : `Every ${s} hours`;
  } else if (!mWild && hWild) {
    if (isStepRaw(fields.minute.raw)) {
      const s = stepValue(fields.minute.raw);
      when = `Every ${s} minutes`;
    } else if (mVals.length === 1 && mVals[0] === 0) {
      when = 'Every hour at :00';
    } else if (mVals.length === 1) {
      when = `Every hour at :${String(mVals[0]).padStart(2, '0')}`;
    } else {
      when = `Every hour at minutes ${mVals.join(', ')}`;
    }
  } else if (mWild && !hWild) {
    if (hVals.length === 1) when = `Every minute of ${formatHour(hVals[0])}`;
    else when = `Every minute of hours ${hVals.join(', ')}`;
  } else {
    if (hVals.length === 1 && mVals.length === 1) {
      when = `At ${formatTime12(hVals[0], mVals[0])}`;
    } else if (hVals.length === 1) {
      when = `${formatHour(hVals[0])}, at minutes ${mVals.join(', ')}`;
    } else if (mVals.length === 1 && mVals[0] === 0) {
      when = `At ${hVals.map(formatHour).join(' and ')}`;
    } else {
      when = `At hours ${hVals.join(', ')}, minutes ${mVals.join(', ')}`;
    }
  }

  // Day
  let day = '';
  if (!domWild || !dowWild) {
    if (!domWild && !dowWild) {
      day = `on day ${domVals.map(ordinal).join(', ')} or ${formatDowList(dowVals)}`;
    } else if (!domWild) {
      day = domVals.length === 1 ? `on the ${ordinal(domVals[0])}` : `on days ${domVals.map(ordinal).join(', ')}`;
    } else {
      const isWeekdays = dowVals.length === 5 && [1,2,3,4,5].every(v => dowVals.includes(v));
      const isWeekends = dowVals.length === 2 && dowVals.includes(0) && dowVals.includes(6);
      if (isWeekdays) day = 'on weekdays';
      else if (isWeekends) day = 'on weekends';
      else day = `on ${formatDowList(dowVals)}`;
    }
  }

  // Month
  let month = '';
  if (!monWild) {
    month = `in ${monVals.map(v => MONTH_FULL_NAMES[v - 1]).join(', ')}`;
  }

  let result = when;
  if (day) result += `, ${day}`;
  if (month) result += `, ${month}`;
  return result;
}

export function validateCronExpression(expr: string): { valid: boolean; error?: string } {
  const parsed = parseCronExpression(expr);
  if (!parsed) return { valid: false, error: 'Invalid cron expression' };
  return { valid: true };
}

export function validateCronField(value: string, type: FieldType): { valid: boolean; error?: string } {
  if (!value.trim()) return { valid: false, error: 'Required' };
  const field = parseField(value, FIELD_CONFIGS[type]);
  if (!field) return { valid: false, error: `Invalid ${type} expression` };
  return { valid: true };
}
