// ---------------------------------------------------------------------------
// automation/AutomationTrigger.tsx — what STARTS an automation (contract C3).
//
// ONE implementation, two frames. The board renders it as the strip above the
// lanes; the numbered-steps view renders it inside Step 1's card. It used to be
// written inline in `AutomationSteps`, and the moment the board needed the same
// controls that was a second copy of the cron round-trip, the tick honesty note
// and the next-run sentence — three things that go subtly wrong and stay wrong
// invisibly. There is one of each.
//
// ⛔ THE VOCABULARY IS THE SERVER'S (C3, and this is the rule the wave was
// explicit about). The options come from `triggers: [{key, label, ready, needs}]`
// on `GET /automations`. There is NO client union over the keys: a union means
// the two lists can diverge, and they diverge SILENTLY — the server offers a
// trigger the client has no arm for, the client draws nothing, and the product
// quietly loses a feature it shipped (the wave-9 silent-drop class). The keys are
// `string` here on purpose, and an unrecognised one still renders.
//
// A NOT-READY TRIGGER IS OFFERED, NOT HIDDEN. `ready: false` plus `needs`
// ("connect_gmail") renders as an option you can pick with a next step attached.
// R9 set that precedent with the disabled "when a field changes" slot and the
// reasoning holds: the question "can it run when an email arrives?" has an
// answer, and an option nobody can use yet SAYS it — a missing option says
// nothing and gets asked again.
// ---------------------------------------------------------------------------
import type { OAuthStatus, TickState, TriggerOption } from "./automationApi";
import { WEEKDAYS, readCron, writeCron } from "./steps";

interface Props {
  /**
   * The server's trigger list. ABSENT until the engine ships C3 — and absent is
   * not "no triggers": the face falls back to the two shapes today's server can
   * actually express, marked below, and stops consulting that fallback the moment
   * the list rides.
   */
  triggers?: TriggerOption[];
  /** The trigger currently stored, as a key. */
  current: string;
  cron: string;
  enabled: boolean;
  /** `null` = the server did not report whether schedules fire here (R9's third state). */
  tick: TickState | null;
  nextRunAt?: string;
  cronPresets: { cron: string; label: string }[];
  busy: boolean;
  /** Picking a trigger PERSISTS — a trigger you have to remember to Save is not a switch. */
  onPick: (key: string) => void;
  onSchedule: (next: { cron: string; enabled: boolean }) => void;
  /** Free-text cron edits: local only, committed on blur. See the input's note. */
  onCronText: (cron: string) => void;
  /** Whether this USER has connected each provider (C5). `null` = not asked yet. */
  oauth: OAuthStatus | null;
  /** True when the raw cron face is open — a VIEW choice, not a schedule change. */
  showCron: boolean;
  onShowCron: (v: boolean) => void;
  /**
   * WAVE 23 — the BUILDER supplies its own "Trigger type" select (Properties → Trigger details),
   * so this face drops its picker and contributes only the schedule half. It is a prop rather
   * than a second component because the thing worth having exactly once is what sits BELOW the
   * picker: the cron round-trip, the preset list and the tick-honesty note, three things that go
   * wrong invisibly. Two pickers for one fact is the defect this file's header forbids.
   */
  hidePicker?: boolean;
  /**
   * WAVE 24, owner item 7 — DOES THE CRON DRIVE THIS TRIGGER? REQUIRED, and it is a fact the
   * CALLER establishes rather than one this component derives, because deriving it means naming
   * keys and this file's header forbids a client union over the trigger vocabulary.
   *
   * `schedule` is no longer the only answer: `ig_profile_match` watches nothing — it MAKES rows,
   * on the cron — so C-TRIG gives it the Scheduled-run controls "always".
   */
  schedules: boolean;
}

/*
 * ⛔ `FALLBACK_TRIGGERS` IS GONE (D-43, wave 23). It described the only two shapes a stored
 * definition could express before C3 shipped (manual / on a schedule), and its own note set
 * the deletion condition: "NEVER consulted once the list arrives; when C3 lands, deleting this
 * constant changes nothing on screen." C3 has been live for two releases and the vocabulary is
 * now nine keys plus two planned, so the fallback could only ever have been WRONG — a server
 * hiccup would have painted a two-option picker that looks authoritative.
 *
 * The absent case is a STATE now, not a substitute list: no `triggers` means the surface says
 * so in one line (below) rather than quietly offering a vocabulary of its own.
 */

/** The one-line next step a not-ready trigger carries. Server words where there are any. */
function needsLabel(needs: string): string {
  if (needs === "connect_gmail") return "Connect Gmail";
  // An unrecognised `needs` is still SAID, verbatim. A blank next step is how a
  // not-ready trigger becomes a dead control.
  return needs.replace(/_/g, " ");
}

/**
 * What an option says about itself. THREE states, and they are not the same fact:
 * ready (nothing), waiting on a connection the USER can make ("needs setting up"), and
 * declared-but-not-built ("coming soon", R2) which is waiting on us.
 */
function optionLabel(t: TriggerOption): string {
  if (t.planned) return `${t.label} — coming soon`;
  return t.ready === false ? `${t.label} — needs setting up` : t.label;
}

/**
 * What the deployment will actually do with a schedule.
 *
 * ⛔ THREE ANSWERS, AND ONE OF THEM IS "WE WERE NOT TOLD". A card that said
 * "schedules won't fire" because a field was missing would be inventing a
 * measurement, and it would be WRONG on this product's own production: the
 * external EventBridge tick fires schedules on a deployment whose in-process
 * scheduler is off ([[aws-automation-cron]]). So `external` says the door is open,
 * not that the caller is alive — the server can only prove the first.
 */
function tickNote(tick: TickState | null): { text: string; tone: string } {
  // ⚠ AN EXPLICIT BOOLEAN OR NOTHING. A payload carrying `"tick": {}` — a
  // half-shipped field, a serialiser that dropped a false, a version skew — has no
  // `enabled` at all, and reading `undefined` as "off" printed "schedules won't
  // fire" as though it had been measured.
  if (!tick || typeof tick.enabled !== "boolean") {
    return {
      text: "Whether this deployment runs schedules is not reported here yet — press Run now if you need it now.",
      tone: "is-unknown",
    };
  }
  if (!tick.enabled) {
    return {
      text: "Schedules won't fire on this deployment — ask your admin to switch the scheduler on. Run now still works.",
      tone: "is-off",
    };
  }
  if (tick.source === "external") {
    return {
      text: "Schedules fire when the external scheduler calls in. This deployment does not run them itself.",
      tone: "is-on",
    };
  }
  return { text: "This deployment runs schedules itself.", tone: "is-on" };
}

export default function AutomationTrigger({
  triggers,
  current,
  cron,
  enabled,
  tick,
  nextRunAt,
  cronPresets,
  busy,
  onPick,
  onSchedule,
  onCronText,
  oauth,
  showCron,
  onShowCron,
  hidePicker,
  schedules,
}: Props) {
  // ⛔ THE SERVER'S LIST OR NOTHING (D-43). An empty array is not a fallback trigger to
  // invent — it is the one honest thing to say, and it is said below.
  const options = triggers || [];
  const picked = options.find((t) => t.key === current) || null;
  /**
   * ⚠ RENAMED FROM `current === "schedule"` (item 7). The cron face belongs to any trigger the
   * cron DRIVES, which is now more than one key — and the caller is the only thing entitled to
   * say which, so this reads the prop rather than the key it used to test.
   */
  const isSchedule = schedules;
  /**
   * ⛔⛔ WAVE 32 · T44 (owner item 2) — THE ARM SWITCH IS DELETED, AND ITS OWN NOTE ARGUED FOR
   * DELETING IT.
   *
   * That note (kept below, because the reasoning is still correct and only its conclusion was
   * wrong) said a switch beside the `schedule` picker *"would be two controls for one fact, which
   * this component's siblings forbid"* — and then rendered exactly that for every cron-driven
   * trigger that is not `schedule`, because the rail's own per-automation switch was not counted
   * as one of the two. It is one: `AutomationSurface`'s `auto-row-switch` writes the SAME
   * `schedule.enabled`, on every automation, from the row that carries its name.
   *
   * Owner, item 2: exactly one on/off control per automation, the one under its name — and the
   * tell they reported is that the other *"keeps disappearing or appearing"*. That is this
   * control's render condition read back verbatim: `schedules && current !== "schedule"` shows and
   * hides it as the trigger changes, so the same automation has one switch or two depending on
   * what it is triggered by.
   *
   * ⛔ THE CONDITION IS NOT WHAT GOES — THE CONTROL IS. Keeping the condition and hiding the
   * switch leaves a control that exists in one state and not the other, which is the complaint.
   * Nothing loses the ability to arm: the `schedule` trigger arms by being PICKED (`pickTrigger`
   * writes `enabled: true`), and every other cron-driven trigger arms from the rail switch, which
   * is reachable from every surface and does not come and go.
   *
   * ⚠ THE ORIGINAL REASONING, PRESERVED because the next reader will ask why a cron trigger does
   * not arm itself: picking `ig_profile_match` deliberately stores `enabled: false`, because
   * arming a cron the moment somebody chooses a trigger would put a BILLED vendor run on a timer
   * nobody asked for (C's amendment A1: *"the seed cannot spend … runs only when a person presses
   * Run now or arms the schedule"*). That is still true. The arming is now the rail switch.
   */
  const shape = readCron(cron);
  const note = tickNote(tick);
  const every = showCron ? "custom" : shape.every;
  // The connector this trigger is waiting on, and the route to it, BOTH COMPOSED BY THE
  // SERVER (`TriggerOption.connect`). No `connect` = we cannot name a route, which is a state
  // the block below states in words instead of drawing a button that goes nowhere.
  const waiting = !!picked && picked.ready === false && !picked.planned && !!picked.needs;
  const provider = picked?.connect?.provider || "";
  const connected = !!(provider && oauth?.[provider]?.connected);
  const startUrl = picked?.connect?.startUrl || "";

  const pickEvery = (next: string) => {
    onShowCron(next === "custom");
    if (next !== "custom")
      onSchedule({ cron: writeCron(next, shape.time || "06:00", cron), enabled });
  };

  return (
    <>
      <div className="auto-field-row">
        {/* ⚠ CONDITIONALLY RENDERED, never `hidden` — `.auto-field` sets `display: flex`, which
            beats the `hidden` attribute's UA `display: none`, so the attribute would have left
            a second trigger picker on screen looking exactly as if it had been asked for. */}
        {hidePicker ? null : (
        <div className="auto-field">
          <label htmlFor="auto-trigger-kind">When it runs</label>
          <select
            id="auto-trigger-kind"
            className="auto-input"
            value={current}
            disabled={busy}
            onChange={(e) => onPick(e.target.value)}
          >
            {/*
              THE STORED VALUE IS ALWAYS AN OPTION. A <select> whose `value` matches
              no <option> renders the FIRST one, so the next write persists a trigger
              nobody chose — the defect this codebase has already paid for twice
              ([[cg-condition-builder-items]]).
            */}
            {current && !options.some((t) => t.key === current) ? (
              <option value={current}>{current} (not offered here)</option>
            ) : null}
            {options.map((t) => (
              /*
                Not-ready is stated ON the option rather than by disabling it: the option is
                pickable, and picking it shows what it is waiting for.
                ⚠ PLANNED IS THE ONE EXCEPTION, and it is not the same fact (R2). The server
                REFUSES a planned key at the door, so leaving it pickable would offer a
                control whose only possible outcome is a refusal. It still renders — the
                question "can it run when a button is clicked" has an answer — it just is not
                an offer.
              */
              <option key={t.key} value={t.key} disabled={!!t.planned}>
                {optionLabel(t)}
              </option>
            ))}
          </select>
        </div>
        )}

        {/* ⛔ WAVE 32 · T44 — the second on/off control stood here ("Run on this schedule") and is
            DELETED, not hidden. See the note on `isSchedule` above for why, and for where arming
            lives now. `enabled` is still read on this component: every cron write below carries it
            through unchanged, which is what stops editing a repeat interval from silently
            disarming an automation. */}

        {isSchedule ? (
          <>
            <div className="auto-field">
              <label htmlFor="auto-trigger-every">Repeat</label>
              <select
                id="auto-trigger-every"
                className="auto-input"
                value={every}
                disabled={busy}
                onChange={(e) => pickEvery(e.target.value)}
              >
                <option value="day">Every day</option>
                {WEEKDAYS.map((d) => (
                  <option key={d.value} value={d.value}>
                    {d.label}
                  </option>
                ))}
                {/*
                  THE ESCAPE HATCH, AND IT IS LOAD-BEARING. Daily and weekly do not cover
                  the schedules the engine accepts (hourly, every 15 minutes, monthly, a
                  weekday range), and a picker that cannot express a stored cron would
                  render its FIRST option instead — so the next Save would write "every
                  day at 06:00" over a schedule nobody changed.
                */}
                <option value="custom">Custom (cron)</option>
              </select>
            </div>
            {every === "custom" ? (
              <>
                <div className="auto-field">
                  <label htmlFor="auto-trigger-preset">Or a preset</label>
                  {/*
                    ⚠ THE PRESET LIST IS THE SERVER'S. Daily and weekly are not everything
                    the engine runs — hourly, quarter-hourly and monthly are `CRON_PRESETS`
                    — and the parser that ACCEPTS a cron lives in Python, so a
                    client-invented preset is a schedule the server can refuse while the UI
                    looks fine.
                  */}
                  <select
                    id="auto-trigger-preset"
                    className="auto-input"
                    value={cronPresets.some((p) => p.cron === cron) ? cron : ""}
                    disabled={busy}
                    onChange={(e) => e.target.value && onSchedule({ cron: e.target.value, enabled })}
                  >
                    <option value="">Typed below</option>
                    {cronPresets.map((p) => (
                      <option key={p.cron} value={p.cron}>
                        {p.label}
                      </option>
                    ))}
                  </select>
                </div>
                <div className="auto-field">
                  <label htmlFor="auto-trigger-custom">Cron (minute hour day month weekday)</label>
                  {/*
                    ⚠ TYPING DOES NOT PATCH. Every other control here persists on change
                    because each is one discrete decision; a cron STRING is free text, so
                    per-keystroke saving would PATCH a half-typed schedule five times and
                    the server would refuse most of them out loud. It edits locally and
                    commits on BLUR.
                  */}
                  <input
                    id="auto-trigger-custom"
                    className="auto-input is-mono"
                    value={cron}
                    disabled={busy}
                    onChange={(e) => onCronText(e.target.value)}
                    onBlur={() => onSchedule({ cron, enabled })}
                  />
                </div>
              </>
            ) : (
              <div className="auto-field">
                <label htmlFor="auto-trigger-time">At</label>
                <input
                  id="auto-trigger-time"
                  className="auto-input"
                  type="time"
                  value={shape.time || "06:00"}
                  disabled={busy}
                  onChange={(e) => onSchedule({ cron: writeCron(shape.every, e.target.value, cron), enabled })}
                />
              </div>
            )}
          </>
        ) : null}
      </div>

      {/*
        THE ABSENT LIST, SAID OUT LOUD (D-43). With the fallback deleted there is no
        client-side vocabulary left to fall back TO, and that is the point: a picker that
        quietly offers two options because a payload was short is a surface stating something
        nobody measured. One line, no paragraph (R13).
      */}
      {!options.length && !hidePicker ? (
        <p className="auto-note">This server did not offer a trigger list.</p>
      ) : null}

      {/* NOT CONFIGURED IS A STATE WITH A NEXT STEP, never a dead control (C3). */}
      {waiting ? (
        <div className="autob-needs">
          <span className="autob-needs-text">
            {connected
              ? "Connected — this trigger is still being switched on for this deployment."
              : "This trigger is not set up yet."}
          </span>
          {!connected && startUrl ? (
            <a className="auto-btn is-primary autob-connect" href={startUrl}>
              {needsLabel(picked?.needs || "")}
            </a>
          ) : null}
        </div>
      ) : null}
      {connected && provider && oauth?.[provider]?.email ? (
        <p className="auto-step-detail">Connected as {oauth[provider]?.email}.</p>
      ) : null}

      {isSchedule ? <p className={"auto-tick-note " + note.tone}>{note.text}</p> : null}
      <p className="auto-step-detail">
        {/*
          ⚠ "NEXT RUN 12:30" IS A PROMISE, AND IT IS ONLY TRUE IF SOMETHING TICKS. The
          server computes the next fire from the cron alone — it does not know whether a
          scheduler is running — so printing it flat beside a line that has just said
          "schedules won't fire" is the surface contradicting itself twice over.
        */}
        {isSchedule
          ? nextRunAt
            ? tick && tick.enabled === false
              ? `It would run next at ${nextRunAt}, once a scheduler is running.`
              : `Next run ${nextRunAt}.`
            : "Schedules start from the moment they are switched on — turning on a daily job after today's time does not fire it today."
          : current === "manual"
            ? "It runs when you press Run now, and nothing else starts it."
            : /*
                ⛔ NOTHING. This line printed `It runs when ${label.toLowerCase()}` and the
                server's label is "When an email arrives" — so the screen read "It runs when
                when an email arrives". Composing a sentence out of somebody else's label is
                how that happens, and the repair is not a better composition: the field above
                is LABELLED "When it runs" and the picker already says the answer, so a
                caption restating it was over-explaining even when it was grammatical (R13).
              */
              ""}
      </p>
    </>
  );
}
