/**
 * Pure rule-based Korean due-date parser.
 *
 * Returns ISO YYYY-MM-DD when a high-confidence rule matches; iso=null with
 * confidence='medium' when an ambiguous token (월말, 주말, 퇴근 전, 시각) is
 * detected so the caller (AiWorker) can defer to AI; iso=null + confidence=null
 * when no token at all is found.
 *
 * `todayKst` is a Date representing the "today" reference. The parser does NOT
 * apply any timezone offset; the caller is expected to pass a Date already
 * aligned to KST midnight (e.g. via UTC math). All date arithmetic uses
 * getUTC* / setUTC* methods on the passed reference.
 */

export interface ParseResult {
  iso: string | null;
  confidence: 'high' | 'medium' | null;
  matchedToken: string | null;
}

const DAY_MS = 86_400_000;

/** 월=0, 화=1, 수=2, 목=3, 금=4, 토=5, 일=6 */
const WEEKDAY_OFFSET: Record<string, number> = {
  '월': 0,
  '화': 1,
  '수': 2,
  '목': 3,
  '금': 4,
  '토': 5,
  '일': 6
};

function toIso(d: Date): string {
  return d.toISOString().slice(0, 10);
}

function addDays(d: Date, n: number): Date {
  return new Date(d.getTime() + n * DAY_MS);
}

/** Validate a YYYY-MM-DD literal via Date roundtrip. */
function isValidYmd(y: number, m: number, day: number): boolean {
  const d = new Date(Date.UTC(y, m - 1, day));
  return (
    d.getUTCFullYear() === y &&
    d.getUTCMonth() === m - 1 &&
    d.getUTCDate() === day
  );
}

function makeUtcDate(y: number, m: number, day: number): Date {
  return new Date(Date.UTC(y, m - 1, day));
}

/** Calendar-arithmetic add of N months. */
function addMonths(d: Date, n: number): Date {
  const out = new Date(d.getTime());
  const targetMonth = out.getUTCMonth() + n;
  out.setUTCMonth(targetMonth);
  return out;
}

/**
 * Monday of the current week (Mon-Sun convention, Sunday = end of week).
 * Sun => -6 days; Mon => 0; Tue => -1; ...; Sat => -5.
 */
function thisMonday(today: Date): Date {
  const dow = today.getUTCDay(); // 0=Sun..6=Sat
  const daysSinceMonday = dow === 0 ? 6 : dow - 1;
  return addDays(today, -daysSinceMonday);
}

/**
 * Monday of "next week" — the Monday following the upcoming Sunday.
 * When today is Sunday, the upcoming Sunday is treated as 7 days away,
 * so "다음 주 월요일" lands on today + 8.
 */
function nextWeekMonday(today: Date): Date {
  const dow = today.getUTCDay(); // 0=Sun..6=Sat
  const daysUntilNextSunday = dow === 0 ? 7 : 7 - dow;
  return addDays(today, daysUntilNextSunday + 1);
}

export function parseDueDate(text: string, todayKst: Date): ParseResult {
  // Normalize to KST midnight (the caller already does this; we just trust the input).
  const today = new Date(
    Date.UTC(
      todayKst.getUTCFullYear(),
      todayKst.getUTCMonth(),
      todayKst.getUTCDate()
    )
  );

  // 1. YYYY-MM-DD literal
  {
    const re = /\b(\d{4})-(\d{2})-(\d{2})\b/;
    const m = re.exec(text);
    if (m) {
      const y = Number(m[1]);
      const mo = Number(m[2]);
      const d = Number(m[3]);
      if (isValidYmd(y, mo, d)) {
        return {
          iso: toIso(makeUtcDate(y, mo, d)),
          confidence: 'high',
          matchedToken: m[0]
        };
      }
      // invalid literal → fall through (parser does NOT silently coerce)
    }
  }

  // 2. N월 N일 (current year if month/day >= today, else next year)
  {
    const re = /(\d{1,2})\s*월\s*(\d{1,2})\s*일/;
    const m = re.exec(text);
    if (m) {
      const mo = Number(m[1]);
      const d = Number(m[2]);
      if (mo >= 1 && mo <= 12 && d >= 1 && d <= 31) {
        const ty = today.getUTCFullYear();
        const tm = today.getUTCMonth() + 1;
        const td = today.getUTCDate();
        let year = ty;
        if (mo < tm || (mo === tm && d < td)) year = ty + 1;
        if (isValidYmd(year, mo, d)) {
          return {
            iso: toIso(makeUtcDate(year, mo, d)),
            confidence: 'high',
            matchedToken: m[0]
          };
        }
      }
    }
  }

  // 3. MM/DD
  {
    const re = /(?<!\d)(\d{1,2})\/(\d{1,2})(?!\d)/;
    const m = re.exec(text);
    if (m) {
      const mo = Number(m[1]);
      const d = Number(m[2]);
      if (mo >= 1 && mo <= 12 && d >= 1 && d <= 31) {
        const ty = today.getUTCFullYear();
        const tm = today.getUTCMonth() + 1;
        const td = today.getUTCDate();
        let year = ty;
        if (mo < tm || (mo === tm && d < td)) year = ty + 1;
        if (isValidYmd(year, mo, d)) {
          return {
            iso: toIso(makeUtcDate(year, mo, d)),
            confidence: 'high',
            matchedToken: m[0]
          };
        }
      }
    }
  }

  // 4. N일 (뒤|후)
  {
    const re = /(\d{1,3})\s*일\s*(뒤|후)/;
    const m = re.exec(text);
    if (m) {
      const n = Number(m[1]);
      return {
        iso: toIso(addDays(today, n)),
        confidence: 'high',
        matchedToken: m[0]
      };
    }
  }

  // 5. N주 (뒤|후)
  {
    const re = /(\d{1,2})\s*주\s*(뒤|후)/;
    const m = re.exec(text);
    if (m) {
      const n = Number(m[1]);
      return {
        iso: toIso(addDays(today, n * 7)),
        confidence: 'high',
        matchedToken: m[0]
      };
    }
  }

  // 6. N개월 (뒤|후)
  {
    const re = /(\d{1,2})\s*개월\s*(뒤|후)/;
    const m = re.exec(text);
    if (m) {
      const n = Number(m[1]);
      return {
        iso: toIso(addMonths(today, n)),
        confidence: 'high',
        matchedToken: m[0]
      };
    }
  }

  // 7-9. 글피 / 모레 / 내일 — first by text position so multi-token text
  // (e.g., "내일도 모레도") resolves to the earliest mentioned token.
  {
    const candidates: Array<{ token: string; offset: number }> = [
      { token: '글피', offset: 3 },
      { token: '모레', offset: 2 },
      { token: '내일', offset: 1 }
    ];
    let best: { token: string; offset: number; idx: number } | null = null;
    for (const c of candidates) {
      const idx = text.indexOf(c.token);
      if (idx >= 0 && (best === null || idx < best.idx)) {
        best = { ...c, idx };
      }
    }
    if (best) {
      return {
        iso: toIso(addDays(today, best.offset)),
        confidence: 'high',
        matchedToken: best.token
      };
    }
  }

  // 10. 다음 주 X요일
  {
    const re = /다음\s*주\s*([월화수목금토일])요일/;
    const m = re.exec(text);
    if (m) {
      const wd = m[1]!;
      const offset = WEEKDAY_OFFSET[wd]!;
      const base = nextWeekMonday(today);
      return {
        iso: toIso(addDays(base, offset)),
        confidence: 'high',
        matchedToken: m[0]
      };
    }
  }

  // 11. 이번 주 X요일
  {
    const re = /이번\s*주\s*([월화수목금토일])요일/;
    const m = re.exec(text);
    if (m) {
      const wd = m[1]!;
      const offset = WEEKDAY_OFFSET[wd]!;
      const base = thisMonday(today);
      return {
        iso: toIso(addDays(base, offset)),
        confidence: 'high',
        matchedToken: m[0]
      };
    }
  }

  // 12. 다음 달 N일
  {
    const re = /다음\s*달\s*(\d{1,2})\s*일/;
    const m = re.exec(text);
    if (m) {
      const d = Number(m[1]);
      const next = addMonths(today, 1);
      const target = new Date(
        Date.UTC(next.getUTCFullYear(), next.getUTCMonth(), d)
      );
      if (
        target.getUTCFullYear() === next.getUTCFullYear() &&
        target.getUTCMonth() === next.getUTCMonth() &&
        d >= 1 &&
        d <= 31
      ) {
        return { iso: toIso(target), confidence: 'high', matchedToken: m[0] };
      }
    }
  }

  // 13. 다음 주 (alone) → next Monday
  {
    const re = /다음\s*주(?![가-힣])/;
    const m = re.exec(text);
    if (m) {
      const base = nextWeekMonday(today);
      return { iso: toIso(base), confidence: 'high', matchedToken: m[0] };
    }
  }

  // 14. 다음 달 (alone) → first day of next month
  {
    const re = /다음\s*달/;
    const m = re.exec(text);
    if (m) {
      const next = addMonths(today, 1);
      const first = new Date(
        Date.UTC(next.getUTCFullYear(), next.getUTCMonth(), 1)
      );
      return { iso: toIso(first), confidence: 'high', matchedToken: m[0] };
    }
  }

  // 15. 오늘
  if (text.includes('오늘')) {
    return { iso: toIso(today), confidence: 'high', matchedToken: '오늘' };
  }

  // ── Ambiguous tokens — caller defers to AI ──
  // 월말 / 주말 / 퇴근 전 / 오후 \d시 / 오전 \d시 / \d시
  const ambiguousPatterns: Array<RegExp> = [
    /월말/,
    /주말/,
    /퇴근\s*전/,
    /오후\s*\d{1,2}\s*시/,
    /오전\s*\d{1,2}\s*시/,
    /\d{1,2}\s*시/
  ];
  for (const re of ambiguousPatterns) {
    const m = re.exec(text);
    if (m) {
      return { iso: null, confidence: 'medium', matchedToken: m[0] };
    }
  }

  return { iso: null, confidence: null, matchedToken: null };
}

/**
 * Returns ALL high-confidence and medium-confidence candidate matches in the
 * given text, sorted by text-position (ascending). Used by F7 AI-primary flow
 * to inject candidates into the prompt as hints — the AI picks (or rejects).
 *
 * Internal `matchPosition` field is stripped before return; consumers see only
 * `iso`, `confidence`, `matchedToken`. The original `parseDueDate` (first-
 * match-wins) is preserved for backward compatibility.
 */
export function parseAllCandidates(text: string, todayKst: Date): ParseResult[] {
  const today = new Date(
    Date.UTC(
      todayKst.getUTCFullYear(),
      todayKst.getUTCMonth(),
      todayKst.getUTCDate()
    )
  );

  interface Candidate extends ParseResult {
    matchPosition: number;
    matchLength: number;
  }
  const out: Candidate[] = [];

  function pushAllMatches(re: RegExp, build: (m: RegExpExecArray) => { iso: string | null; matchedToken: string } | null): void {
    const flagged = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
    let m: RegExpExecArray | null;
    while ((m = flagged.exec(text)) !== null) {
      const built = build(m);
      if (built !== null) {
        out.push({
          iso: built.iso,
          confidence: 'high',
          matchedToken: built.matchedToken,
          matchPosition: m.index,
          matchLength: m[0].length
        });
      }
      // Avoid infinite loop on zero-length matches
      if (m.index === flagged.lastIndex) flagged.lastIndex++;
    }
  }

  // 1. YYYY-MM-DD literal
  pushAllMatches(/\b(\d{4})-(\d{2})-(\d{2})\b/, (m) => {
    const y = Number(m[1]);
    const mo = Number(m[2]);
    const d = Number(m[3]);
    if (!isValidYmd(y, mo, d)) return null;
    return { iso: toIso(makeUtcDate(y, mo, d)), matchedToken: m[0] };
  });

  // 2. N월 N일
  pushAllMatches(/(\d{1,2})\s*월\s*(\d{1,2})\s*일/, (m) => {
    const mo = Number(m[1]);
    const d = Number(m[2]);
    if (!(mo >= 1 && mo <= 12 && d >= 1 && d <= 31)) return null;
    const ty = today.getUTCFullYear();
    const tm = today.getUTCMonth() + 1;
    const td = today.getUTCDate();
    let year = ty;
    if (mo < tm || (mo === tm && d < td)) year = ty + 1;
    if (!isValidYmd(year, mo, d)) return null;
    return { iso: toIso(makeUtcDate(year, mo, d)), matchedToken: m[0] };
  });

  // 3. MM/DD
  pushAllMatches(/(?<!\d)(\d{1,2})\/(\d{1,2})(?!\d)/, (m) => {
    const mo = Number(m[1]);
    const d = Number(m[2]);
    if (!(mo >= 1 && mo <= 12 && d >= 1 && d <= 31)) return null;
    const ty = today.getUTCFullYear();
    const tm = today.getUTCMonth() + 1;
    const td = today.getUTCDate();
    let year = ty;
    if (mo < tm || (mo === tm && d < td)) year = ty + 1;
    if (!isValidYmd(year, mo, d)) return null;
    return { iso: toIso(makeUtcDate(year, mo, d)), matchedToken: m[0] };
  });

  // 4. N일 (뒤|후)
  pushAllMatches(/(\d{1,3})\s*일\s*(뒤|후)/, (m) => {
    const n = Number(m[1]);
    return { iso: toIso(addDays(today, n)), matchedToken: m[0] };
  });

  // 5. N주 (뒤|후)
  pushAllMatches(/(\d{1,2})\s*주\s*(뒤|후)/, (m) => {
    const n = Number(m[1]);
    return { iso: toIso(addDays(today, n * 7)), matchedToken: m[0] };
  });

  // 6. N개월 (뒤|후)
  pushAllMatches(/(\d{1,2})\s*개월\s*(뒤|후)/, (m) => {
    const n = Number(m[1]);
    return { iso: toIso(addMonths(today, n)), matchedToken: m[0] };
  });

  // 7-9. 글피 / 모레 / 내일 — collect ALL occurrences as separate entries
  for (const { token, offset } of [
    { token: '글피', offset: 3 },
    { token: '모레', offset: 2 },
    { token: '내일', offset: 1 }
  ]) {
    let from = 0;
    let idx: number;
    while ((idx = text.indexOf(token, from)) >= 0) {
      out.push({
        iso: toIso(addDays(today, offset)),
        confidence: 'high',
        matchedToken: token,
        matchPosition: idx,
        matchLength: token.length
      });
      from = idx + token.length;
    }
  }

  // 10. 다음 주 X요일
  pushAllMatches(/다음\s*주\s*([월화수목금토일])요일/, (m) => {
    const wd = m[1]!;
    const offset = WEEKDAY_OFFSET[wd]!;
    const base = nextWeekMonday(today);
    return { iso: toIso(addDays(base, offset)), matchedToken: m[0] };
  });

  // 11. 이번 주 X요일
  pushAllMatches(/이번\s*주\s*([월화수목금토일])요일/, (m) => {
    const wd = m[1]!;
    const offset = WEEKDAY_OFFSET[wd]!;
    const base = thisMonday(today);
    return { iso: toIso(addDays(base, offset)), matchedToken: m[0] };
  });

  // 12. 다음 달 N일
  pushAllMatches(/다음\s*달\s*(\d{1,2})\s*일/, (m) => {
    const d = Number(m[1]);
    const next = addMonths(today, 1);
    const target = new Date(
      Date.UTC(next.getUTCFullYear(), next.getUTCMonth(), d)
    );
    if (
      target.getUTCFullYear() !== next.getUTCFullYear() ||
      target.getUTCMonth() !== next.getUTCMonth() ||
      d < 1 ||
      d > 31
    ) {
      return null;
    }
    return { iso: toIso(target), matchedToken: m[0] };
  });

  // 13. 다음 주 (alone) → next Monday
  pushAllMatches(/다음\s*주(?![가-힣])/, (m) => {
    const base = nextWeekMonday(today);
    return { iso: toIso(base), matchedToken: m[0] };
  });

  // 14. 다음 달 (alone) → first day of next month
  pushAllMatches(/다음\s*달/, (m) => {
    const next = addMonths(today, 1);
    const first = new Date(
      Date.UTC(next.getUTCFullYear(), next.getUTCMonth(), 1)
    );
    return { iso: toIso(first), matchedToken: m[0] };
  });

  // 15. 오늘 — collect all occurrences
  {
    const token = '오늘';
    let from = 0;
    let idx: number;
    while ((idx = text.indexOf(token, from)) >= 0) {
      out.push({
        iso: toIso(today),
        confidence: 'high',
        matchedToken: token,
        matchPosition: idx,
        matchLength: token.length
      });
      from = idx + token.length;
    }
  }

  // ── Medium-confidence ambiguous tokens ──
  const ambiguousPatterns: Array<RegExp> = [
    /월말/,
    /주말/,
    /퇴근\s*전/,
    /오후\s*\d{1,2}\s*시/,
    /오전\s*\d{1,2}\s*시/,
    /\d{1,2}\s*시/
  ];
  for (const re of ambiguousPatterns) {
    const flagged = new RegExp(re.source, 'g');
    let m: RegExpExecArray | null;
    while ((m = flagged.exec(text)) !== null) {
      out.push({
        iso: null,
        confidence: 'medium',
        matchedToken: m[0],
        matchPosition: m.index,
        matchLength: m[0].length
      });
      if (m.index === flagged.lastIndex) flagged.lastIndex++;
    }
  }

  // Suppress candidates whose span is fully contained in another's span,
  // EXCEPT when the candidate produces a distinct iso (different semantic
  // value). This handles "다음 주 월요일" matching both rule 10 (full
  // weekday) and rule 13 (다음 주 alone) — drop rule 13's contained match.
  const filtered = out.filter((c, i) => {
    for (let j = 0; j < out.length; j++) {
      if (i === j) continue;
      const other = out[j]!;
      if (other.matchLength <= c.matchLength) continue;
      const cEnd = c.matchPosition + c.matchLength;
      const oEnd = other.matchPosition + other.matchLength;
      const contained = other.matchPosition <= c.matchPosition && oEnd >= cEnd;
      if (contained) return false;
    }
    return true;
  });

  // Sort by text position ascending, stable on insertion order tie-break.
  filtered.sort((a, b) => a.matchPosition - b.matchPosition);

  // Strip internal matchPosition / matchLength before returning.
  return filtered.map(({ iso, confidence, matchedToken }) => ({
    iso,
    confidence,
    matchedToken
  }));
}
