// Helpers for the 12-hour time picker used by the executive-meetings
// editor. The picker keeps storage / wire format unchanged
// (canonical "HH:mm" 24h) — these helpers only translate between that
// canonical form and the 3-control 12-hour shape the user sees, plus
// combine an hour + minute + AM/PM toggle draft into a single
// canonical value.
//
// The bug this fixes: the prior native <input type="time"> let users
// type "1:15" intending 1:15 PM and silently stored 01:15 (1:15 AM),
// because the schedule cell display drops the AM/PM marker (per task
// #292's "compact 12h, no AM/PM" decision) the typo was invisible.
// `combineSplit` rejects any 1–12 hour without an explicit AM/PM
// choice, so the same misinput is now surfaced as a clear "pick AM
// or PM" parse error rather than silently saved as AM.
//
// the picker exposes THREE separate controls per
// field (hour input 1–12, minute input 00–59, AM/PM toggle). The
// hour input is intentionally permissive: it also accepts a full
// time string ("13:30", "9:30 PM", "0930", "٠٩:٣٠") so power users
// can paste/type the whole time into one field. When the hour
// field looks like a complete time on its own (contains a
// separator/marker, or is a 3–4 digit compact form), `combineSplit`
// parses it standalone and ignores the minute field — otherwise it
// joins hour + minute and runs the result through `combineTextWithPeriod`.

// Use the explicit `.ts` extension so this module is loadable both by
// the Vite bundler (via `allowImportingTsExtensions`) AND by Node's
// built-in test runner when the unit tests in src/__tests__ import
// this module — the test runner does not auto-resolve extensionless
// TypeScript imports.
import { parseTypedTime } from "./parse-time.ts";

export type Period = "am" | "pm" | null;

export type CombineResult =
  | { value: string | null; error: null }
  | { value: null; error: "invalid" | "ambiguous" };

// Mirror of parseTypedTime's marker regex. Kept local so we can detect
// the marker WITHOUT actually parsing — used to decide whether a typed
// draft is unambiguous on its own (marker present) or needs the
// toggle to disambiguate (marker absent).
const AM_PM_MARKER_RE = /(a\.?m\.?|p\.?m\.?|ص|م)\s*$/i;

// Detects characters that mean "the hour field already contains the
// minute too" — a separator (":", ".", "-", whitespace) OR an AM/PM
// marker letter. When any of these is present we parse the hour
// field standalone and ignore the minute field, matching the
// power-user shortcut where the whole time is typed into the
// hour input.
const HAS_TIME_GLUE_RE = /[:.\-\s]|a\.?m\.?|p\.?m\.?|ص|م/i;

// Detects a 3–4 digit compact form (ASCII OR Arabic-Indic / Persian
// digits) — the user typed "0930" / "٠٩٣٠" into the hour field as a
// shorthand for 09:30. We treat those as "hour-field-is-complete"
// so the minute field is ignored.
const COMPACT_DIGITS_RE = /^[\d\u0660-\u0669\u06F0-\u06F9]{3,4}$/;

/**
 * Derive the AM/PM toggle position that matches a stored canonical
 * value. Used to seed the toggle when the editor opens against an
 * existing time. Empty / unparseable values return `null` (no
 * implicit default — matches the spec's "no implicit default" rule
 * for the toggle's empty state).
 */
export function periodFromCanonical(v: string | null | undefined): Period {
  if (!v) return null;
  const m = /^(\d{2}):/.exec(v);
  if (!m) return null;
  const h = Number(m[1]);
  if (!Number.isFinite(h)) return null;
  // 00:xx is "12:xx AM" (h=0 → AM), 12:xx is "12:xx PM" (h=12 → PM).
  // Anything 13–23 is afternoon → PM; anything 1–11 is morning → AM.
  return h < 12 ? "am" : "pm";
}

/**
 * Turn a canonical "HH:mm" 24h value into the 12-hour shape that the
 * picker shows in its single text input ("9:30", "1:15", "12:00", …).
 * Empty input returns "" so the input renders blank. Drops the
 * leading zero on the hour to match the schedule cell's compact
 * display style.
 *
 * Used by the legacy single-text-input variant of the picker; the
 * 3-control variant uses `splitCanonicalForPicker` instead.
 */
export function formatCanonicalAs12h(v: string | null | undefined): string {
  if (!v) return "";
  const m = /^(\d{2}):(\d{2})/.exec(v);
  if (!m) return v;
  const h = Number(m[1]);
  const mm = m[2];
  if (!Number.isFinite(h)) return v;
  let h12: number;
  if (h === 0) h12 = 12;
  else if (h > 12) h12 = h - 12;
  else h12 = h;
  return `${h12}:${mm}`;
}

/**
 * Split a canonical "HH:mm" 24h value into the {hour, minute}
 * strings that seed the 3-control picker's separate hour and
 * minute inputs. Hour is the 12h shape (1..12) without leading
 * zero; minute is preserved as the 2-digit zero-padded form.
 * Empty / unparseable input returns blank fields.
 */
export function splitCanonicalForPicker(
  v: string | null | undefined,
): { hour: string; minute: string } {
  if (!v) return { hour: "", minute: "" };
  const m = /^(\d{2}):(\d{2})/.exec(v);
  if (!m) return { hour: "", minute: "" };
  const h = Number(m[1]);
  const mm = m[2];
  if (!Number.isFinite(h)) return { hour: "", minute: "" };
  let h12: number;
  if (h === 0) h12 = 12;
  else if (h > 12) h12 = h - 12;
  else h12 = h;
  return { hour: String(h12), minute: mm };
}

/**
 * Combine the picker's typed draft (any of the shapes parseTypedTime
 * accepts: "9:30", "0930", "13:15", "1:15 PM", Arabic-Indic digits, …)
 * with the AM/PM toggle into a single canonical "HH:mm" value.
 *
 * Returns `{ value: null, error: null }` for empty input — caller
 * decides whether that means "clear the time" (current inline-editor
 * behaviour) or "leave the field empty" (manage form).
 *
 * Returns `{ error: "ambiguous" }` when the typed text resolves to a
 * 1–12 hour AND the user has not picked AM or PM. This is the core
 * fix for the "1:15 silently saved as AM" bug — the picker forces the
 * user to choose rather than guessing for them.
 *
 * Returns `{ error: "invalid" }` when the typed text is not parseable
 * at all (e.g. "25:99", "abc"). The marker in the text always wins
 * over the toggle (so "9:30 PM" is always 21:30 even if the toggle
 * happens to point at AM), and a 24h-only hour like "13:30" or
 * "00:30" is accepted directly because it cannot be misread.
 */
export function combineTextWithPeriod(
  rawText: string | null | undefined,
  period: Period,
): CombineResult {
  const trimmed = (rawText ?? "").trim();
  if (!trimmed) return { value: null, error: null };

  // If the typed text already contains an AM/PM marker, it is the
  // unambiguous source of truth — the toggle is irrelevant. This lets
  // power users type "9:30 PM" and have it Just Work, and is also
  // what makes the "12h with PM" e2e shape pass without a toggle
  // click.
  if (AM_PM_MARKER_RE.test(trimmed)) {
    const parsed = parseTypedTime(trimmed);
    if (parsed === null) return { value: null, error: "invalid" };
    return { value: parsed, error: null };
  }

  const parsedRaw = parseTypedTime(trimmed);
  if (parsedRaw === null) return { value: null, error: "invalid" };

  // Hours 0 and 13–23 cannot be a 12h shape, so there is no
  // ambiguity to resolve — accept regardless of the toggle. Hour 0
  // can only come from a typed "00:xx" / "0:xx" 24h shape (12h
  // notation would write midnight as "12:xx AM"); hours 13–23 are
  // unambiguous PM. Lets a user who knows the 24h time of their
  // meeting type "13:30" or "00:30" and save without also tapping
  // the toggle.
  const h = Number(parsedRaw.slice(0, 2));
  if (h === 0 || h >= 13) return { value: parsedRaw, error: null };

  // Hours 1–12 with no marker: the toggle MUST disambiguate. This
  // is the "1:15 → 13:15 vs 01:15" decision point that the prior
  // <input type="time"> silently got wrong.
  if (period === null) return { value: null, error: "ambiguous" };

  // Re-parse with the toggle applied so the existing parser handles
  // the 12 AM → 00:xx and 12 PM → 12:xx edge cases (instead of
  // duplicating that logic here).
  const withMarker = `${trimmed} ${period === "pm" ? "PM" : "AM"}`;
  const parsed = parseTypedTime(withMarker);
  if (parsed === null) return { value: null, error: "invalid" };
  return { value: parsed, error: null };
}

/**
 * Combine the 3-control picker's hour input + minute input + AM/PM
 * toggle into a single canonical "HH:mm" value. This is the primary
 * combine path for the picker rebuilt against the spec
 * ("three controls per field: hour input, minute input, AM/PM
 * toggle").
 *
 * Behaviour:
 * - Both fields blank → `{ value: null, error: null }` (cleared).
 * - The hour field accepts a full time on its own as a power-user
 *   shortcut: a separator/marker (":", "PM", "ص", …) or a 3–4 digit
 *   compact form ("0930") in the hour field means "I typed the whole
 *   time here, ignore the minute field". The hour field is then
 *   parsed standalone via `combineTextWithPeriod`.
 * - Otherwise the function joins `${hour}:${minute}` and runs the
 *   joined value through `combineTextWithPeriod`, so the same
 *   ambiguity / 24h-acceptance rules apply: hours 0 and 13–23 are
 *   unambiguous, hours 1–12 require an explicit toggle pick, an
 *   embedded marker overrides the toggle.
 * - A bare hour with no minute and no separator (e.g. "9", "12") is
 *   rejected as `invalid` because parseTypedTime requires both H and
 *   M — surfacing this as a parse error keeps the user from saving
 *   half-typed times.
 */
export function combineSplit(
  rawHour: string | null | undefined,
  rawMinute: string | null | undefined,
  period: Period,
): CombineResult {
  const h = (rawHour ?? "").trim();
  const m = (rawMinute ?? "").trim();
  if (!h && !m) return { value: null, error: null };

  let combined: string;
  if (h && (HAS_TIME_GLUE_RE.test(h) || COMPACT_DIGITS_RE.test(h))) {
    // Hour field already holds a complete time — power-user
    // shortcut. Minute field is intentionally ignored; documented
    // in the JSDoc above.
    combined = h;
  } else if (h && m) {
    // Normal split-control path: "9" + "30" → "9:30".
    combined = `${h}:${m}`;
  } else if (m && !h) {
    // Minute typed but no hour — incomplete, surface as invalid
    // so the picker re-focuses the hour input rather than
    // saving a malformed time.
    combined = `:${m}`;
  } else {
    // Bare hour, no minute, no separator. Falls through to
    // combineTextWithPeriod → parseTypedTime → null → invalid,
    // which is the right error to surface (user typed an hour
    // but never filled the minute).
    combined = h;
  }

  return combineTextWithPeriod(combined, period);
}
