// lateness.jsx — shared "completed late" outcome system
// Implements: A (Late badge) + C (Outcome field) + E (Reason prompt on close)

// Outcomes — used as a separate analytical field
const OUTCOMES = [
  { id: "ontime",    label: "On time",         tone: "green",  icon: "✓",  desc: "Completed by due date" },
  { id: "late",      label: "Completed late",  tone: "amber",  icon: "⏱",  desc: "Closed after the due date" },
  { id: "cancelled", label: "Cancelled",       tone: "slate",  icon: "✕",  desc: "Decided not to do" },
  { id: "punted",    label: "Punted",          tone: "blue",   icon: "↪",  desc: "Pushed to a later sprint" },
];

// Reasons — collected via the close-modal (Option E)
const DELAY_REASONS = [
  { id: "blocked",     label: "Blocked by dependency", desc: "Waiting on another task, team, or vendor" },
  { id: "scope",       label: "Scope grew",            desc: "Discovered more work mid-flight" },
  { id: "reprio",      label: "Reprioritized",         desc: "Pulled away to handle something more urgent" },
  { id: "estimate",    label: "Estimate was off",      desc: "Took longer than we thought" },
  { id: "review",      label: "Stuck in review",       desc: "Code or design review took too long" },
  { id: "other",       label: "Other",                 desc: "Reason doesn't fit any category" },
  { id: "skip",        label: "Skip for now",          desc: "Don't log a reason" },
];

// Due-change reasons — collected via DueChangeModal whenever an
// existing due date changes (not first-set, not cleared). Same
// vocabulary as DELAY_REASONS where the meaning carries over plus
// four new ids for due-shift-specific cases. No "skip" — the user
// asked to require a reason every time the date moves so trends
// surface in the Owner dashboard. The "pulled" option only appears
// when the new date is earlier than the old one (date pulled
// forward, not pushed back). "other" requires the free-text note.
const DUE_CHANGE_REASONS = [
  { id: "blocked",      label: "Blocked by dependency", desc: "Waiting on another task, team, or vendor" },
  { id: "scope",        label: "Scope grew",            desc: "Discovered more work mid-flight" },
  { id: "reprio",       label: "Reprioritized",         desc: "Pulled away to something more urgent" },
  { id: "estimate",     label: "Estimate was off",      desc: "Original timeline didn't fit, no specific blocker" },
  { id: "review",       label: "Stuck in review",       desc: "Code, design, or stakeholder approval is dragging" },
  { id: "resource",     label: "Resource unavailable",  desc: "Owner is on leave, sick, or pulled to another team" },
  { id: "external",     label: "External delay",        desc: "Client / vendor / third-party hasn't responded" },
  { id: "requirements", label: "Requirements changed",  desc: "Direction shifted, the work itself changed" },
  // "Accidentally selected" — the previous date was a misclick and
  // the current change is just correcting it. Useful so the metrics
  // don't blame "scope grew" or "estimate was off" for what was
  // really a stray finger on the date picker.
  { id: "accidental",   label: "Accidentally selected", desc: "Previous date was a misclick — correcting it" },
  // "pulled" is conditionally shown by DueChangeModal only when the
  // new date is earlier than the old one.
  { id: "pulled",       label: "Pulled earlier",        desc: "Date moved forward (priority bump), not pushed" },
  { id: "other",        label: "Other",                 desc: "Reason doesn't fit any category (add a note)" },
];

// Friendly label lookup for the activity feed / dashboards. Server
// stores the id; drawer renders the label via this map.
const DUE_CHANGE_REASON_LABELS = Object.fromEntries(
  DUE_CHANGE_REASONS.map(r => [r.id, r.label])
);

// ── helpers ──────────────────────────────────────────────────────
// Parses our seed-data due strings ("Apr 24", "May 02", "Today", "Tomorrow", "—")
// into a 0-indexed (month, day) pair we can compare. Returns null if no due date.
function parseDue(due) {
  if (!due || due === "—") return null;
  if (due === "Today")    return { mo: 3, day: 24 };  // pretend today is Apr 24
  if (due === "Tomorrow") return { mo: 3, day: 25 };
  // Strip optional trailing " @ H:MM AM/PM" before matching the date head.
  const head = String(due).split(" @ ")[0].trim();
  const m = /^([A-Za-z]{3})\s+(\d{1,2})$/.exec(head);
  if (!m) return null;
  const moMap = { Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11 };
  return { mo: moMap[m[1]] ?? 0, day: parseInt(m[2], 10) };
}

// "Apr 24" - "Apr 22" = 2 days late
function daysLateBetween(due, completed) {
  const d = parseDue(due);
  const c = parseDue(completed);
  if (!d || !c) return 0;
  // Approximate days of year (good enough for prototype; assumes 31-day months)
  const dDays = d.mo * 31 + d.day;
  const cDays = c.mo * 31 + c.day;
  return cDays - dDays;
}

// Auto-derives outcome from due + completedAt, unless explicitly set
function getOutcome(task) {
  if (task.outcome) return task.outcome;
  if (task.status !== "done") return null;
  if (!task.completedAt || task.due === "—" || !task.due) return "ontime";
  return daysLateBetween(task.due, task.completedAt) > 0 ? "late" : "ontime";
}

function isCompletedLate(task) {
  return task.status === "done" && getOutcome(task) === "late";
}

function daysLate(task) {
  if (!isCompletedLate(task)) return 0;
  return daysLateBetween(task.due, task.completedAt);
}

// ── inline CSS ───────────────────────────────────────────────────
const LATENESS_CSS = `
.late-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  background: #fef4e6; color: #b8651b;
  border: 1px solid #fbe0b6;
  border-radius: 999px;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  line-height: 1.3;
}
.late-badge .late-clock { font-size: 10px; line-height: 1; }
.late-badge.late-tiny { padding: 1px 6px; font-size: 10px; }
.late-badge.late-large { padding: 4px 10px; font-size: 12px; }

/* Overdue (currently late, not yet done) — red, more urgent than the
   amber late-badge which signals "shipped late". The pulsing border
   draws attention without being too aggressive. */
.overdue-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  background: #ffe1e6; color: #9b1f3a;
  border: 1px solid #f4b6c2;
  border-radius: 999px;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  line-height: 1.3;
  animation: overdue-pulse 1.8s ease-in-out infinite;
}
.overdue-badge .overdue-icon { font-size: 11px; line-height: 1; }
.overdue-badge.overdue-tiny  { padding: 1px 6px; font-size: 10px; }
.overdue-badge.overdue-large { padding: 4px 10px; font-size: 12px; }
@keyframes overdue-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(226, 68, 92, 0.0); }
  50%      { box-shadow: 0 0 0 4px rgba(226, 68, 92, 0.15); }
}

.outcome-pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.outcome-green { background: #e0f6ec; color: #14774e; border: 1px solid #b9e6cf; }
.outcome-amber { background: #fef4e6; color: #b8651b; border: 1px solid #fbe0b6; }
.outcome-slate { background: #f0f1f4; color: #5f6473; border: 1px solid #d8dbe2; }
.outcome-blue  { background: #e6f0fe; color: #1660c8; border: 1px solid #c2d8f7; }

/* Complete-task modal */
.complete-task-modal {
  width: 520px; max-width: 95vw;
  background: white; border-radius: 12px;
  box-shadow: 0 24px 64px rgba(16,22,40,.20);
  overflow: hidden;
}
.ctm-head {
  padding: 20px 24px 16px;
  border-bottom: 1px solid var(--border);
  display: flex; align-items: flex-start; gap: 14px;
}
.ctm-head-icon {
  width: 40px; height: 40px; border-radius: 50%;
  background: #fef4e6; color: #b8651b;
  display: grid; place-items: center;
  flex-shrink: 0;
  font-size: 18px;
}
.ctm-head-text h3 {
  margin: 0 0 4px; font-size: 17px; font-weight: 700;
  color: var(--ink-strong); letter-spacing: -0.01em;
}
.ctm-head-text p {
  margin: 0; font-size: 13px; color: var(--ink-muted); line-height: 1.45;
}
.ctm-body { padding: 16px 24px 20px; }
.ctm-section-label {
  font-size: 11px; font-weight: 700; color: var(--ink-muted);
  letter-spacing: 0.08em; text-transform: uppercase;
  margin-bottom: 10px;
}
.ctm-reasons {
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
}
.ctm-reason {
  padding: 12px 14px; border: 1.5px solid var(--border);
  border-radius: 8px; cursor: pointer;
  background: white; text-align: left;
  font-family: inherit;
  transition: border-color .12s, background .12s;
  display: flex; flex-direction: column; gap: 2px;
}
.ctm-reason:hover { border-color: var(--border-strong); background: #fafbfd; }
.ctm-reason.is-active {
  border-color: var(--brand);
  background: #f7eefc;
  box-shadow: 0 0 0 3px rgba(162,93,220,0.10);
}
.ctm-reason-label { font-size: 13.5px; font-weight: 600; color: var(--ink-strong); }
.ctm-reason-desc  { font-size: 11.5px; color: var(--ink-muted); line-height: 1.35; }
.ctm-reason.is-skip .ctm-reason-label { color: var(--ink-muted); font-weight: 500; }

.ctm-note {
  margin-top: 14px;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 13px; color: var(--ink-body);
  resize: vertical; min-height: 60px;
  outline: none;
}
.ctm-note:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(162,93,220,0.12); }

.ctm-footer {
  padding: 14px 20px;
  background: var(--bg-subtle);
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px;
}
.ctm-footer .ctm-info {
  font-size: 12px; color: var(--ink-muted);
  flex: 1;
}
.ctm-footer .ctm-info b { color: var(--ink-strong); font-weight: 600; }
`;

if (typeof document !== "undefined" && !document.getElementById("lateness-css")) {
  const s = document.createElement("style");
  s.id = "lateness-css";
  s.textContent = LATENESS_CSS;
  document.head.appendChild(s);
}

// ── components ───────────────────────────────────────────────────
function LateBadge({ task, size = "default" }) {
  if (!isCompletedLate(task)) return null;
  const n = daysLate(task);
  const cls = size === "tiny"  ? "late-badge late-tiny"
           : size === "large" ? "late-badge late-large"
           : "late-badge";
  return (
    <span className={cls} title={`Due ${task.due} · Completed ${task.completedAt}`}>
      <span className="late-clock">⏱</span>
      {n} day{n === 1 ? "" : "s"} late
    </span>
  );
}

// OverdueBadge — surfaces the warning for tasks that are CURRENTLY
// overdue (due date in the past, not yet done). LateBadge is a
// separate concept — it only appears AFTER a task is completed late.
// Together: OverdueBadge says "this needs attention", LateBadge says
// "this came in late, already shipped".
function OverdueBadge({ task, size = "default" }) {
  if (!isOverdueNow(task)) return null;
  // Reuse daysLateBetween with today as the "completed" anchor to
  // figure out how many days past due the task is right now.
  const today = todayMonDay();
  const days = daysLateBetween(task.due, today);
  const cls = size === "tiny"  ? "overdue-badge overdue-tiny"
           : size === "large" ? "overdue-badge overdue-large"
           : "overdue-badge";
  const label = days <= 0
    ? "Due today"
    : days === 1
    ? "1 day overdue"
    : `${days} days overdue`;
  return (
    <span className={cls} title={`Due ${task.due} · today is ${today}`}>
      <span className="overdue-icon">⚠</span>
      {label}
    </span>
  );
}

function OutcomePill({ task }) {
  const id = getOutcome(task);
  if (!id) return null;
  const o = OUTCOMES.find(x => x.id === id);
  if (!o) return null;
  return (
    <span className={`outcome-pill outcome-${o.tone}`} title={o.desc}>
      <span>{o.icon}</span> {o.label}
    </span>
  );
}

// ── Complete-task modal ─────────────────────────────────────────
function CompleteTaskModal({ open, task, onClose, onConfirm, todayLabel = "Apr 24" }) {
  const [reason, setReason] = React.useState(null);
  const [note, setNote] = React.useState("");
  React.useEffect(() => { if (open) { setReason(null); setNote(""); } }, [open, task?.id]);

  if (!open || !task) return null;

  // Compute lateness preview
  const completedAt = todayLabel;
  const late = daysLateBetween(task.due, completedAt);
  const isLate = late > 0;

  function confirm(skip = false) {
    const outcome = isLate ? "late" : "ontime";
    onConfirm({
      status: "done",
      outcome,
      completedAt,
      delayReason: skip ? null : reason,
      delayNote: skip ? null : (note.trim() || null),
    });
  }

  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="complete-task-modal" onClick={(e) => e.stopPropagation()}>
        <div className="ctm-head">
          <div className="ctm-head-icon" style={!isLate ? { background: "#e0f6ec", color: "#14774e" } : null}>
            {isLate ? "⏱" : "✓"}
          </div>
          <div className="ctm-head-text">
            <h3>{isLate ? `Mark "${task.name}" as done` : `Mark as done`}</h3>
            <p>
              {isLate
                ? <>This task was due <b>{task.due}</b>, and today is <b>{completedAt}</b>. It will be marked <b>{late} day{late === 1 ? "" : "s"} late</b>. Want to log why?</>
                : <>Closing <b>{task.name}</b> — on time.</>}
            </p>
          </div>
        </div>

        {isLate && (
          <div className="ctm-body">
            <div className="ctm-section-label">Reason for delay (optional)</div>
            <div className="ctm-reasons">
              {DELAY_REASONS.filter(r => r.id !== "skip").map(r => (
                <button
                  key={r.id}
                  className={`ctm-reason ${reason === r.id ? "is-active" : ""}`}
                  onClick={() => setReason(r.id)}>
                  <span className="ctm-reason-label">{r.label}</span>
                  <span className="ctm-reason-desc">{r.desc}</span>
                </button>
              ))}
            </div>
            <textarea
              className="ctm-note"
              placeholder="Add a quick note (optional, helps with retros)…"
              value={note}
              onChange={(e) => setNote(e.target.value)}/>
          </div>
        )}

        <div className="ctm-footer">
          <div className="ctm-info">
            {isLate
              ? <>Logged on the task & rolled into <b>On-time delivery</b> in Dashboards.</>
              : <>Will be marked <b>On time</b>.</>}
          </div>
          <button className="btn" onClick={onClose}>Cancel</button>
          {isLate && (
            <button className="btn" onClick={() => confirm(true)}>Skip — just close</button>
          )}
          <button className="btn btn-primary" onClick={() => confirm(false)} disabled={isLate && !reason}>
            {isLate ? "Mark late & close" : "Mark done"}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ── DueChangeModal ──────────────────────────────────────────────
// Opens whenever an EXISTING due date changes to a different non-
// null value. Captures the reason (required — no skip option) and an
// optional note. On confirm, calls back into updateTask with the
// date PATCH plus dueChangeReason + dueChangeNote, which the server
// folds into the existing 'due' task_activity entry so the reason
// shows up in the drawer's Activity tab alongside the date change.
//
// Gating (enforced by the caller in app.jsx):
//   * Fires when:   oldDue !== newDue, newDue is non-null, oldDue
//                   was non-null.
//   * Skips when:   first-time set (oldDue null), clearing the date
//                   (newDue null), or no-op.
//   * "pulled":     hidden unless newDue < oldDue (we don't surface
//                   "pulled earlier" when the date is being pushed
//                   out — that combination doesn't make sense).
function DueChangeModal({ open, task, oldDue, newDue, onClose, onConfirm }) {
  const [reason, setReason] = React.useState(null);
  const [note, setNote] = React.useState("");
  React.useEffect(() => {
    if (open) { setReason(null); setNote(""); }
  }, [open, task && task.id, oldDue, newDue]);

  if (!open || !task) return null;

  // Earlier-than detection — drives whether "pulled" is offered.
  // Uses the same parseDue() helper the lateness math uses, which
  // accepts "May 14" short-form. For ISO "2026-05-14" we fall back
  // to lexical compare (ISO strings sort chronologically anyway).
  function ordinal(s) {
    if (!s) return null;
    const head = String(s).split(" @ ")[0].trim();
    const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(head);
    if (iso) return Number(iso[1]) * 10000 + Number(iso[2]) * 100 + Number(iso[3]);
    const p = parseDue(head);
    if (!p) return null;
    return new Date().getFullYear() * 10000 + (p.mo + 1) * 100 + p.day;
  }
  const oOrd = ordinal(oldDue);
  const nOrd = ordinal(newDue);
  const isEarlier = oOrd != null && nOrd != null && nOrd < oOrd;
  const isLater   = oOrd != null && nOrd != null && nOrd > oOrd;
  const dayDelta  = (oOrd != null && nOrd != null) ? (nOrd - oOrd) : null;

  // "pulled" only makes sense when the date moved forward.
  const reasons = (typeof DUE_CHANGE_REASONS !== "undefined" ? DUE_CHANGE_REASONS : [])
    .filter(r => r.id !== "pulled" || isEarlier);

  const needsNote = reason === "other";
  const canSubmit = !!reason && (!needsNote || note.trim().length > 0);

  function confirm() {
    if (!canSubmit) return;
    onConfirm({
      due: newDue,
      dueChangeReason: reason,
      dueChangeNote: note.trim() || null,
    });
  }

  // Display label for the date pair. Falls back to raw string when
  // parseDue can't read it (rare — covers ISO + short-form already).
  const shiftLabel = isEarlier
    ? `pulled earlier by ${Math.abs(dayDelta)} day${Math.abs(dayDelta) === 1 ? "" : "s"}`
    : isLater
    ? `pushed out by ${dayDelta} day${dayDelta === 1 ? "" : "s"}`
    : "same length, different date";

  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="complete-task-modal" onClick={(e) => e.stopPropagation()}>
        <div className="ctm-head">
          <div className="ctm-head-icon" style={{ background: "#fff4e0", color: "#a16207" }}>
            📅
          </div>
          <div className="ctm-head-text">
            <h3>Why is the due date changing?</h3>
            <p>
              <b>{task.name}</b> · <span style={{ textDecoration: "line-through", color: "var(--ink-muted)" }}>{oldDue}</span>
              {" → "}<b>{newDue}</b>
              {dayDelta != null && <span style={{ color: "var(--ink-muted)" }}> · {shiftLabel}</span>}
            </p>
          </div>
        </div>

        <div className="ctm-body">
          <div className="ctm-section-label">
            Reason for change <span style={{ color: "var(--prio-critical, #c93636)" }}>*</span>
          </div>
          <div className="ctm-reasons">
            {reasons.map(r => (
              <button
                key={r.id}
                className={`ctm-reason ${reason === r.id ? "is-active" : ""}`}
                onClick={() => setReason(r.id)}>
                <span className="ctm-reason-label">{r.label}</span>
                <span className="ctm-reason-desc">{r.desc}</span>
              </button>
            ))}
          </div>
          <textarea
            className="ctm-note"
            placeholder={needsNote
              ? "What's the reason? (required when picking Other)…"
              : "Add a quick note (optional, helps with retros)…"}
            value={note}
            onChange={(e) => setNote(e.target.value)}/>
        </div>

        <div className="ctm-footer">
          <div className="ctm-info">
            Logged on the task & rolled into <b>Due-date changes</b> in the activity feed.
          </div>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary"
                  onClick={confirm}
                  disabled={!canSubmit}>
            Save change
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ── seed: pre-mark a few tasks as already-completed-late so the badges
//   show up in the existing UI without the user clicking anything.
function seedCompletionData(tasks) {
  const seeded = {
    // Already in seed data with due Apr 12 — pretend completed Apr 13
    t1:  { completedAt: "Apr 13", outcome: "late",   delayReason: "review",   delayNote: "PR sat in review for a day."     },
    t10: { completedAt: "Apr 08", outcome: "ontime"                                                                       },
    t15: { completedAt: "Apr 14", outcome: "late",   delayReason: "scope",    delayNote: "EU regs added during build."     },
  };
  return tasks.map(t => t.status === "done" && seeded[t.id]
    ? { ...t, ...seeded[t.id] }
    : t);
}

// Return today formatted "Mon DD" — matches the shape of the seed and
// of every UI date string. Used as the default todayLabel for
// CompleteTaskModal so the modal's preview reflects the actual day.
function todayMonDay() {
  return new Date().toLocaleDateString("en-US", { month: "short", day: "2-digit" });
}

// Is this task currently overdue? "Overdue" means: it has a due date,
// it's not already done, and the due date is strictly before today.
// We compare just dates (not times), so a task due "Apr 28" is overdue
// only on Apr 29 onward.
function isOverdueNow(task) {
  if (!task) return false;
  if (task.status === "done") return false;
  if (!task.due || task.due === "—") return false;
  const due = parseDue(task.due);
  if (!due) return false;
  const t = new Date();
  // Compare via month-of-year-ish ordinal (mirrors daysLateBetween)
  const today = { mo: t.getMonth(), day: t.getDate() };
  return (today.mo * 31 + today.day) > (due.mo * 31 + due.day);
}

// Is this task due today OR overdue? Used by the tab-title badge so
// the user sees pending work even with the app in a background tab.
function isDueOrOverdue(task) {
  if (!task) return false;
  if (task.status === "done") return false;
  if (!task.due || task.due === "—") return false;
  const due = parseDue(task.due);
  if (!due) return false;
  const t = new Date();
  return (t.getMonth() * 31 + t.getDate()) >= (due.mo * 31 + due.day);
}

Object.assign(window, {
  OUTCOMES, DELAY_REASONS,
  DUE_CHANGE_REASONS, DUE_CHANGE_REASON_LABELS,
  parseDue, daysLateBetween, getOutcome, isCompletedLate, daysLate,
  isOverdueNow, isDueOrOverdue, todayMonDay,
  LateBadge, OverdueBadge, OutcomePill, CompleteTaskModal, DueChangeModal,
  seedCompletionData,
});
