/**
 * "Does this message describe something someone has to do?"
 *
 * A deliberately small, explainable heuristic rather than a model call: it
 * runs on every rendered message, has to be instant, and a false positive
 * only ever costs one ignorable chip. The shape follows what shipping
 * products converge on for action-item detection (Slack's action items,
 * Gmail's suggested tasks, Todoist/Things quick-add parsing):
 *
 *  1. Explicit markers win outright ("TODO:", "action item", "[ ]").
 *  2. Otherwise accumulate weak signals — a request phrase, an imperative
 *     opening verb, a deadline cue, a directed @mention — and fire on two.
 *     One signal alone is too loose ("thanks, please" / any verb-initial
 *     sentence), two is where precision gets usable.
 *  3. Veto anything that reads as already-done, or as chatter.
 *
 * Exported separately from the component so it can be unit-tested against a
 * corpus of real-looking messages (see task-detect.test.ts) — the thresholds
 * are the kind of thing that regress silently when edited by feel.
 */

export interface TaskIntent {
  isTask: boolean;
  /** Why it fired — surfaced in the chip's tooltip so the guess is legible. */
  reason: string;
  score: number;
  /** ISO instant at local midnight when the text names a date, else null. */
  dueAt: string | null;
}

const NOT_A_TASK: TaskIntent = { isTask: false, reason: "", score: 0, dueAt: null };

/** Unambiguous, conventional ways people mark work in chat. */
const EXPLICIT = [
  { re: /(^|\s)todo\b|\btodo:/i, reason: "marked TODO" },
  { re: /\baction item\b|\baction-item\b/i, reason: "marked as an action item" },
  { re: /^\s*(\[\s?\]|\[\s*x\s*\]|- \[ \])/i, reason: "written as a checklist item" },
  { re: /\btask:/i, reason: "marked as a task" },
  { re: /\bfollow ?up\b.*\b(on|with)\b/i, reason: "a follow-up" },
];

/** Someone is being asked to do something. */
const REQUEST = [
  /\b(can|could|would|will)\s+(you|someone|somebody|anyone|we)\b/i,
  /\bplease\b/i,
  /\b(we|i|you)\s+(need|needs|have)\s+to\b/i,
  /\bi need (you|someone|somebody)\b/i,
  /\bi want (this|that|it|you|someone)\b/i,
  /\bwant (this|that|it)\s+(done|finished|completed|ready|by)\b/i,
  /\b(get|have|need)\s+(this|that|it)\s+(done|finished|completed|ready|by)\b/i,
  /\b(do|finish|complete|handle)\s+(this|that|it)\b/i,
  /\blet'?s\b/i,
  /\bmake sure\b/i,
  /\b(don'?t forget|remember)\s+to\b/i,
  /\bcould use\b/i,
  /\bshould (we|you|i)\b/i,
  /\bwho can\b/i,
];

/** Verbs that, when a message opens with them, read as an instruction. */
const IMPERATIVE_VERBS = [
  "add","update","fix","review","send","write","draft","check","create","build","ship","deploy","merge",
  "test","investigate","refactor","rename","remove","delete","migrate","document","schedule","book",
  "call","email","ping","follow","prepare","set","setup","configure","implement","design","research",
  "confirm","verify","validate","clean","upgrade","bump","revert","rollback","publish","release",
  "handle","take","move","split","fold","wire","hook","land","file","open","close","triage",
  "finish","complete","submit","share",
];

const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] as const;

/** Time pressure — on its own weak, but a strong corroborator. */
const DEADLINE =
  /\b(by (eod|eow|cob|today|tomorrow|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next week|the end of)|before (eod|the|we|you|tomorrow|friday)|due\b|deadline|asap|by \d{1,2}(:\d{2})?\s?(am|pm)|this (afternoon|morning|week|sprint)|end of (day|week|sprint)|today|tonight|tomorrow|(on|this|next)\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday)|in (a |\d+ )?(day|days|week|weeks))\b/i;

function atLocalMidnight(date: Date): Date {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

function addDays(date: Date, days: number): Date {
  const next = atLocalMidnight(date);
  next.setDate(next.getDate() + days);
  return next;
}

/** Upcoming `weekday` (0 = Sunday). Same-day counts as today unless `skipToday`. */
function upcomingWeekday(now: Date, weekday: number, skipToday = false): Date {
  const start = atLocalMidnight(now);
  let delta = weekday - start.getDay();
  if (delta < 0 || (delta === 0 && skipToday)) delta += 7;
  return addDays(start, delta);
}

/**
 * Pull a due date out of chat-style deadline phrasing ("by tomorrow",
 * "do this by Sunday", "next Friday"). Returns an ISO instant at local
 * midnight so the task modal's date input shows the calendar day the
 * speaker meant, not the UTC day.
 */
export function parseTaskDueAt(text: string | null | undefined, now = new Date()): string | null {
  const raw = (text ?? "").toLowerCase();
  if (!raw) return null;

  const named = raw.match(/\bnext\s+(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/);
  if (named?.[1]) {
    return upcomingWeekday(now, WEEKDAYS.indexOf(named[1] as (typeof WEEKDAYS)[number]), true).toISOString();
  }

  const weekday = raw.match(
    /\b(?:by|before|until|due(?:\s+on)?|on|this)\s+(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/,
  );
  if (weekday?.[1]) {
    return upcomingWeekday(now, WEEKDAYS.indexOf(weekday[1] as (typeof WEEKDAYS)[number])).toISOString();
  }

  if (/\bday after tomorrow\b|\bin two days\b/.test(raw)) return addDays(now, 2).toISOString();
  if (/\btomorrow\b/.test(raw)) return addDays(now, 1).toISOString();
  if (/\b(today|tonight|eod|cob|end of (the )?day|this (afternoon|morning))\b/.test(raw)) {
    return atLocalMidnight(now).toISOString();
  }
  if (/\b(eow|end of (the )?week)\b/.test(raw)) return upcomingWeekday(now, 5).toISOString();
  const inDays = raw.match(/\bin (\d+) days?\b/);
  if (inDays?.[1]) return addDays(now, Number(inDays[1])).toISOString();
  if (/\bnext week\b|\bin a week\b/.test(raw)) return addDays(now, 7).toISOString();

  return null;
}

/** Already handled — a report, not a request. */
const DONE_OR_PAST =
  /\b(i|we|just)\s+(fixed|shipped|deployed|merged|sent|added|updated|removed|wrote|finished|closed|landed|pushed|released|done)\b|\b(is|has been|was|already)\s+(done|fixed|shipped|deployed|merged|handled|resolved|closed)\b|\b(fyi|heads up|nice work|good catch|lgtm|thanks|thank you|ty|congrats|welcome)\b/i;

/** Verb-initial idioms that are conversational, not instructions — without
 * these, "check out this article" and "take a look" score as imperatives. */
const SOFT_OPENERS = /^\s*(check (out|this)|take a (look|peek)|have a look|look at|see (this|the|above)|feel free|hope|thought|wondering)\b/i;

/** Pure chatter that a verb match would otherwise pick up. */
const CHATTER = /^\s*(hi|hey|hello|yo|ok|okay|k|sure|yes|no|nope|yep|lol|haha|ha|thanks|ty|thx|morning|gm|gn|👍|\+1)\b[\s!.?]*$/i;

const MENTION = /@[a-z0-9._-]+/i;

export function detectTaskIntent(text: string | null | undefined): TaskIntent {
  const raw = (text ?? "").trim();
  if (raw.length < 3) return NOT_A_TASK;
  if (CHATTER.test(raw)) return NOT_A_TASK;

  for (const { re, reason } of EXPLICIT) {
    if (re.test(raw)) return { isTask: true, reason, score: 3, dueAt: parseTaskDueAt(raw) };
  }

  // A past-tense/acknowledgement veto applies only to the weak-signal path —
  // an explicit "TODO" beats it, but "thanks, I fixed it" must not fire.
  if (DONE_OR_PAST.test(raw)) return NOT_A_TASK;

  const reasons: string[] = [];
  let score = 0;

  if (REQUEST.some((re) => re.test(raw))) {
    score += 1;
    reasons.push("reads as a request");
  }

  const soft = SOFT_OPENERS.test(raw);
  const firstWord = raw.replace(/^[^a-z]+/i, "").split(/\s+/)[0]?.toLowerCase() ?? "";
  const words = raw.toLowerCase().match(/[a-z']+/g) ?? [];

  if (!soft && IMPERATIVE_VERBS.includes(firstWord)) {
    // A bare imperative ("Fix the login bug") is the one signal strong enough
    // to carry a message on its own, which is why the soft-opener veto above
    // has to run first.
    score += 2;
    reasons.push(`an instruction ("${firstWord}…")`);
  } else if (!soft && words.some((w) => IMPERATIVE_VERBS.includes(w))) {
    score += 1;
    reasons.push("names an action");
  }

  if (DEADLINE.test(raw)) {
    score += 1;
    reasons.push("mentions a deadline");
  }

  // A mention only counts alongside something actionable — "@ana ↑" is not a
  // task, but "@ana can you review this" already scored on the request.
  if (MENTION.test(raw) && score > 0) {
    score += 1;
    reasons.push("directed at someone");
  }

  if (score >= 2) {
    return { isTask: true, reason: reasons.join(", "), score, dueAt: parseTaskDueAt(raw) };
  }
  // Score is carried out even on a miss — a near-miss is what you want to see
  // when tuning the thresholds.
  return { isTask: false, reason: "", score, dueAt: parseTaskDueAt(raw) };
}
