// Pure deadline/date detection over lecture segments. NO wall-clock reads:
// every timestamp is derived from `anchorMs` (the lecture date) plus explicit
// year/month/day/hour components found in the text. Constructing Date objects
// from those explicit components is fine; calling Date.now() / new Date() with
// no args is NOT.

import type { Segment } from '../types';
import type { CandidateEvent } from './types';

const MONTHS: Record<string, number> = {
  january: 0,
  february: 1,
  march: 2,
  april: 3,
  may: 4,
  june: 5,
  july: 6,
  august: 7,
  september: 8,
  october: 9,
  november: 10,
  december: 11,
  jan: 0,
  feb: 1,
  mar: 2,
  apr: 3,
  jun: 5,
  jul: 6,
  aug: 7,
  sep: 8,
  sept: 8,
  oct: 9,
  nov: 10,
  dec: 11,
};

const WEEKDAYS: Record<string, number> = {
  sunday: 0,
  monday: 1,
  tuesday: 2,
  wednesday: 3,
  thursday: 4,
  friday: 5,
  saturday: 6,
};

const DEADLINE_WORDS = [
  'exam',
  'deadline',
  'due',
  'assignment',
  'midterm',
  'final',
  'quiz',
  'submit',
  'submission',
  'project',
  'homework',
  'test',
];

const SIX_MONTHS_MS = 1000 * 60 * 60 * 24 * 30 * 6;

interface TimeOfDay {
  hour: number;
  minute: number;
}

/** A partial match produced by a detector before titling/dating. */
interface RawMatch {
  /** Absolute resolved instant in ms. */
  dateMs: number;
  /** Whether a clock time was found (=> not all-day). */
  hasTime: boolean;
  /** Matched substring (for `raw`). */
  matched: string;
  /** Index of the match within the segment text (for clause extraction). */
  index: number;
  /** Base confidence before deadline-keyword boosting. */
  baseConfidence: number;
}

/**
 * Detect candidate dates/deadlines across all segments.
 *
 * @param segments  lecture segments (text + timing).
 * @param anchorMs  the lecture date in ms; relative dates anchor here and
 *                  absolute dates borrow this year when none is written.
 */
export function detectEvents(segments: Segment[], anchorMs: number): CandidateEvent[] {
  if (!segments.length || !Number.isFinite(anchorMs)) return [];

  const out: CandidateEvent[] = [];
  const seen = new Set<string>();

  for (const seg of segments) {
    const text = seg.text ?? '';
    if (!text) continue;

    const matches = [
      ...detectIso(text, anchorMs),
      ...detectMonthName(text, anchorMs),
      ...detectOrdinalOnly(text, anchorMs),
      ...detectRelative(text, anchorMs),
    ];

    for (const m of matches) {
      const title = clauseAround(text, m.index, m.matched.length);
      const hasDeadlineWord = DEADLINE_WORDS.some((w) =>
        title.toLowerCase().includes(w),
      );
      let confidence = m.baseConfidence;
      if (hasDeadlineWord) confidence = Math.min(1, confidence + 0.1);
      else confidence = Math.max(0, confidence - 0.2);

      const key = `${m.dateMs}|${title.toLowerCase()}`;
      if (seen.has(key)) continue;
      seen.add(key);

      out.push({
        title,
        dateMs: m.dateMs,
        allDay: !m.hasTime,
        sourceSegmentId: seg.id,
        start: seg.start,
        raw: m.matched.trim(),
        confidence: Number(confidence.toFixed(2)),
      });
    }
  }

  return out;
}

// ---------------------------------------------------------------------------
// Absolute detectors
// ---------------------------------------------------------------------------

/** ISO `YYYY-MM-DD`, optionally followed by a clock time. */
function detectIso(text: string, anchorMs: number): RawMatch[] {
  const out: RawMatch[] = [];
  const re = /\b(\d{4})-(\d{2})-(\d{2})\b/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(text))) {
    const year = Number(m[1]);
    const month = Number(m[2]) - 1;
    const day = Number(m[3]);
    if (month < 0 || month > 11 || day < 1 || day > 31) continue;
    const time = findTimeNear(text, m.index + m[0].length);
    const dateMs = buildUtc(year, month, day, time);
    out.push({
      dateMs,
      hasTime: !!time,
      matched: m[0],
      index: m.index,
      baseConfidence: 0.9,
    });
  }
  return out;
}

/** `March 3`, `March 3, 2026`, `3 March`, `on March 3rd`. */
function detectMonthName(text: string, anchorMs: number): RawMatch[] {
  const out: RawMatch[] = [];
  const monthAlt = Object.keys(MONTHS).join('|');

  // Month-first: "March 3", "March 3rd, 2026"
  const reMD = new RegExp(
    `\\b(${monthAlt})\\.?\\s+(\\d{1,2})(?:st|nd|rd|th)?(?:,?\\s+(\\d{4}))?`,
    'gi',
  );
  let m: RegExpExecArray | null;
  while ((m = reMD.exec(text))) {
    const month = MONTHS[m[1]!.toLowerCase()];
    const day = Number(m[2]);
    if (month === undefined || day < 1 || day > 31) continue;
    const explicitYear = m[3] ? Number(m[3]) : undefined;
    pushResolved(out, text, m, month, day, explicitYear, anchorMs);
  }

  // Day-first: "3 March", "3rd March 2026"
  const reDM = new RegExp(
    `\\b(\\d{1,2})(?:st|nd|rd|th)?\\s+(${monthAlt})\\.?(?:,?\\s+(\\d{4}))?`,
    'gi',
  );
  while ((m = reDM.exec(text))) {
    const day = Number(m[1]);
    const month = MONTHS[m[2]!.toLowerCase()];
    if (month === undefined || day < 1 || day > 31) continue;
    const explicitYear = m[3] ? Number(m[3]) : undefined;
    pushResolved(out, text, m, month, day, explicitYear, anchorMs);
  }

  return out;
}

function pushResolved(
  out: RawMatch[],
  text: string,
  m: RegExpExecArray,
  month: number,
  day: number,
  explicitYear: number | undefined,
  anchorMs: number,
): void {
  const time = findTimeNear(text, m.index + m[0].length);
  let year = explicitYear ?? utcYear(anchorMs);
  let dateMs = buildUtc(year, month, day, time);
  if (explicitYear === undefined && dateMs < anchorMs - SIX_MONTHS_MS) {
    year += 1;
    dateMs = buildUtc(year, month, day, time);
  }
  out.push({
    dateMs,
    hasTime: !!time,
    matched: m[0],
    index: m.index,
    baseConfidence: 0.9,
  });
}

/** `on the 14th` — day-of-month only, month/year from the anchor. */
function detectOrdinalOnly(text: string, anchorMs: number): RawMatch[] {
  const out: RawMatch[] = [];
  const re = /\bon the (\d{1,2})(?:st|nd|rd|th)\b/gi;
  let m: RegExpExecArray | null;
  while ((m = re.exec(text))) {
    const day = Number(m[1]);
    if (day < 1 || day > 31) continue;
    const month = utcMonth(anchorMs);
    const year = utcYear(anchorMs);
    const time = findTimeNear(text, m.index + m[0].length);
    let dateMs = buildUtc(year, month, day, time);
    // "on the 14th" means the upcoming 14th; if already well past, roll a month.
    if (dateMs < anchorMs - SIX_MONTHS_MS) {
      dateMs = buildUtc(year + 1, month, day, time);
    }
    out.push({
      dateMs,
      hasTime: !!time,
      matched: m[0],
      index: m.index,
      baseConfidence: 0.85,
    });
  }
  return out;
}

// ---------------------------------------------------------------------------
// Relative detectors (anchored purely to anchorMs)
// ---------------------------------------------------------------------------

function detectRelative(text: string, anchorMs: number): RawMatch[] {
  const out: RawMatch[] = [];
  const dayMs = 1000 * 60 * 60 * 24;

  const add = (offsetDays: number, m: RegExpExecArray) => {
    const time = findTimeNear(text, m.index + m[0].length);
    const dateMs = applyTime(anchorMs + offsetDays * dayMs, time);
    out.push({
      dateMs,
      hasTime: !!time,
      matched: m[0],
      index: m.index,
      baseConfidence: 0.6,
    });
  };

  let m: RegExpExecArray | null;

  const reToday = /\btoday\b/gi;
  while ((m = reToday.exec(text))) add(0, m);

  const reTomorrow = /\btomorrow\b/gi;
  while ((m = reTomorrow.exec(text))) add(1, m);

  const reNextWeek = /\bnext week\b/gi;
  while ((m = reNextWeek.exec(text))) add(7, m);

  // "in N days" / "in N weeks"
  const reInN = /\bin (\d{1,3}) (day|days|week|weeks)\b/gi;
  while ((m = reInN.exec(text))) {
    const n = Number(m[1]);
    const unit = m[2]!.toLowerCase();
    const days = unit.startsWith('week') ? n * 7 : n;
    add(days, m);
  }

  // "next Monday".."next Friday" (and weekend days)
  const reNextDow = /\bnext (sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/gi;
  while ((m = reNextDow.exec(text))) {
    const target = WEEKDAYS[m[1]!.toLowerCase()]!;
    const anchorDow = utcDow(anchorMs);
    // Days until the target weekday in the FOLLOWING week (1..7, never 0).
    let delta = (target - anchorDow + 7) % 7;
    if (delta === 0) delta = 7;
    delta += 7; // "next" => skip the current week's occurrence.
    add(delta, m);
  }

  return out;
}

// ---------------------------------------------------------------------------
// Time-of-day extraction
// ---------------------------------------------------------------------------

/** Look for a clock time shortly after the date phrase (within ~16 chars). */
function findTimeNear(text: string, from: number): TimeOfDay | undefined {
  const window = text.slice(from, from + 18);
  return parseTime(window);
}

function parseTime(s: string): TimeOfDay | undefined {
  // "at 3pm", "at 3:30 pm", "3pm"
  const re12 = /\b(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i;
  const m12 = re12.exec(s);
  if (m12) {
    let hour = Number(m12[1]);
    const minute = m12[2] ? Number(m12[2]) : 0;
    const mer = m12[3]!.toLowerCase();
    if (hour === 12) hour = 0;
    if (mer === 'pm') hour += 12;
    if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
      return { hour, minute };
    }
  }
  // 24h "14:00", "at 09:30"
  const re24 = /\b(?:at\s+)?([01]?\d|2[0-3]):([0-5]\d)\b/;
  const m24 = re24.exec(s);
  if (m24) {
    return { hour: Number(m24[1]), minute: Number(m24[2]) };
  }
  return undefined;
}

// ---------------------------------------------------------------------------
// Pure UTC helpers (constructed only from explicit components / anchorMs)
// ---------------------------------------------------------------------------

function buildUtc(
  year: number,
  month: number,
  day: number,
  time: TimeOfDay | undefined,
): number {
  return Date.UTC(year, month, day, time?.hour ?? 0, time?.minute ?? 0, 0, 0);
}

/** Replace the time-of-day on a relative instant, keeping its UTC calendar day. */
function applyTime(ms: number, time: TimeOfDay | undefined): number {
  if (!time) return ms;
  const d = new Date(ms);
  return Date.UTC(
    d.getUTCFullYear(),
    d.getUTCMonth(),
    d.getUTCDate(),
    time.hour,
    time.minute,
    0,
    0,
  );
}

function utcYear(ms: number): number {
  return new Date(ms).getUTCFullYear();
}
function utcMonth(ms: number): number {
  return new Date(ms).getUTCMonth();
}
function utcDow(ms: number): number {
  return new Date(ms).getUTCDay();
}

// ---------------------------------------------------------------------------
// Clause extraction for titles
// ---------------------------------------------------------------------------

/** Extract a short clause around the matched date as the event title. */
function clauseAround(text: string, index: number, length: number): string {
  // Bound to the surrounding sentence by splitting on sentence punctuation.
  const before = text.slice(0, index);
  const after = text.slice(index + length);

  const startBreak = Math.max(
    before.lastIndexOf('. '),
    before.lastIndexOf('? '),
    before.lastIndexOf('! '),
    before.lastIndexOf('; '),
    before.lastIndexOf(', '),
  );
  const afterBreakRel = firstBreak(after);

  const start = startBreak >= 0 ? startBreak + 2 : 0;
  const end =
    index + length + (afterBreakRel >= 0 ? afterBreakRel : after.length);

  return text.slice(start, end).trim().replace(/\s+/g, ' ');
}

function firstBreak(s: string): number {
  const idxs = ['. ', '? ', '! ', '; ', ', ']
    .map((p) => s.indexOf(p))
    .filter((i) => i >= 0);
  if (!idxs.length) return -1;
  return Math.min(...idxs);
}
