"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useForm, useWatch, type FieldErrors, type FieldValues } from "react-hook-form";
import type { FormConfig, FormDataPayload, SubmitPayload, StageSectionConfig } from "@/types/form";

function sectionForField(
  sections: ReadonlyArray<StageSectionConfig>,
  fieldName: string,
): StageSectionConfig | undefined {
  for (const s of sections) {
    if (s.fields.some((f) => f.name === fieldName)) return s;
    for (const m of s.matrixGroups ?? []) {
      for (const row of m.rows) if (row.fields.includes(fieldName)) return s;
    }
  }
  return undefined;
}
import { evaluate } from "@/lib/conditional";
import { STAGE_OPTION_GROUP_ID } from "@/config/form";
import { StageSection } from "./StageSection";
import { loadDraft, saveDraft, clearDraft } from "@/lib/draft";

interface EngagementFormProps {
  config: FormConfig;
  cid: string;
  cs: string;
}

type LoadState =
  | { kind: "loading" }
  | { kind: "error"; message: string }
  | { kind: "ready"; data: FormDataPayload };

type SubmitStatus =
  | { kind: "idle" }
  | { kind: "submitting" }
  | { kind: "success" }
  | { kind: "error"; message: string };

type PathwayState = "past" | "current" | "future";

const STAGE_RANK: Record<string, number> = {
  Inquiry: 0,
  Organizing: 1,
  Feasibility: 2,
  "Business feasibility": 3,
  "Store Implementation": 4,
  "Stabilize newly opened co-op": 5,
};

export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
  const [load, setLoad] = useState<LoadState>({ kind: "loading" });
  const [submitState, setSubmitState] = useState<SubmitStatus>({ kind: "idle" });
  const [draftSavedAt, setDraftSavedAt] = useState<string | null>(null);
  const [draftRestored, setDraftRestored] = useState(false);

  const {
    register,
    handleSubmit,
    reset,
    control,
    watch,
    setFocus,
    formState: { errors, isDirty },
  } = useForm({ mode: "onBlur" });

  // Subscribe ONLY to current_stage. That's the single field that affects
  // section visibility, so re-rendering the whole form on every keystroke
  // (which `watch()` with no args would do) is wasteful — particularly with
  // 120 fields and a 39-cell matrix in Stage 5.
  //
  // The auto-save effect uses watch's subscription API for side-effect-only
  // change tracking (no re-render).
  const currentStageValue =
    (useWatch({ control, name: "current_stage" }) as string | undefined) ?? "";

  // State map passed to the conditional engine and to FieldRenderer for
  // readonly display values. Only fields referenced by visibility rules or
  // by readonly displays need to be here — currently just current_stage,
  // which gates every Stage 1–5 visibleWhen rule and is shown as a readonly
  // field in the Stage 0 header.
  const evalState = useMemo(
    () => ({ current_stage: currentStageValue }),
    [currentStageValue],
  );

  // ── Initial load ───────────────────────────────────────────────────────
  useEffect(() => {
    let cancelled = false;
    async function go() {
      try {
        const url = new URL("/api/data", window.location.origin);
        url.searchParams.set("cid", cid);
        url.searchParams.set("cs", cs);
        const res = await fetch(url.toString(), { cache: "no-store" });
        if (!res.ok) {
          const text = await res.text();
          let message = `Could not load form (HTTP ${res.status}).`;
          try {
            const j = JSON.parse(text) as { error?: string };
            if (j.error) message = j.error;
          } catch { /* keep default */ }
          if (!cancelled) setLoad({ kind: "error", message });
          return;
        }
        const data: FormDataPayload = await res.json();
        if (cancelled) return;

        // Build defaults: server prefill + current stage. Then check if a
        // local draft exists newer than the server data; if so, layer it on top.
        const defaults: Record<string, unknown> = {
          [config.stageField]: data.currentStage,
          ...data.prefill,
        };
        const draft = loadDraft(cid);
        if (draft && Object.keys(draft.values).length > 0) {
          Object.assign(defaults, draft.values);
          // Re-set the stage from server (draft can never override the org's actual stage).
          defaults[config.stageField] = data.currentStage;
          setDraftRestored(true);
          setDraftSavedAt(draft.savedAt);
        }
        reset(defaults);
        setLoad({ kind: "ready", data });
      } catch (e) {
        if (!cancelled) {
          setLoad({
            kind: "error",
            message: e instanceof Error ? e.message : "Unexpected error loading form.",
          });
        }
      }
    }
    void go();
    return () => {
      cancelled = true;
    };
  }, [cid, cs, reset, config.stageField]);

  // ── Auto-save draft on idle ────────────────────────────────────────────
  // Debounce — save 1.5s after the user stops editing. Uses watch's
  // subscription API so we get notified on every change WITHOUT re-rendering
  // the form on each keystroke.
  const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    if (load.kind !== "ready") return;
    const sub = watch((values) => {
      if (saveTimer.current) clearTimeout(saveTimer.current);
      saveTimer.current = setTimeout(() => {
        saveDraft(cid, values as Record<string, unknown>);
        setDraftSavedAt(new Date().toISOString());
      }, 1500);
    });
    return () => {
      sub.unsubscribe();
      if (saveTimer.current) clearTimeout(saveTimer.current);
    };
  }, [load.kind, cid, watch]);

  const currentRank = currentStageValue ? STAGE_RANK[currentStageValue] ?? 0 : 0;

  // Section pathway state — past / current / future is determined entirely
  // by stage rank vs the org's current rank. Sections never hide; future
  // stages render in a locked treatment so users can preview what's ahead.
  //
  // Section-level visibleWhen is still consulted on submit to strip
  // locked-stage values from the payload (see onSubmit below), so a future
  // stage's data never gets accidentally written.
  const sectionsToRender = useMemo(
    () =>
      config.sections.map((s) => {
        const pathwayState: PathwayState =
          s.rank < currentRank ? "past" : s.rank === currentRank ? "current" : "future";
        return { section: s, pathwayState, locked: pathwayState === "future" };
      }),
    [config.sections, currentRank],
  );

  // Track which sections the user has manually opened/closed so we can
  // programmatically re-open them when validation surfaces an error within.
  // Used by the onInvalid handler below.
  const expandSection = useCallback((sectionId: string) => {
    document.dispatchEvent(
      new CustomEvent("coop-checkin:expand-section", { detail: { sectionId } }),
    );
  }, []);

  // ── Render ─────────────────────────────────────────────────────────────
  if (load.kind === "loading") return <LoadingState />;
  if (load.kind === "error") return <ErrorState message={load.message} />;

  // After a successful submit, replace the form entirely with a destination
  // screen. This prevents accidental duplicate submits (back-button, double-
  // click, browser autofill replay) and gives the user a clear endpoint.
  if (submitState.kind === "success") {
    return (
      <SuccessDestination
        orgName={load.data.orgName}
        onAnother={() => {
          setSubmitState({ kind: "idle" });
          window.scrollTo({ top: 0, behavior: "smooth" });
        }}
      />
    );
  }

  const onSubmit = async (values: Record<string, unknown>) => {
    setSubmitState({ kind: "submitting" });
    try {
      // Strip values for hidden fields — never write data the user couldn't see.
      const visibleFieldNames = new Set<string>();
      for (const s of config.sections) {
        if (!evaluate(s.visibleWhen, values)) continue;
        for (const f of s.fields) {
          if (evaluate(f.visibleWhen, values)) visibleFieldNames.add(f.name);
        }
        for (const m of s.matrixGroups ?? []) {
          for (const row of m.rows) {
            for (const fname of row.fields) visibleFieldNames.add(fname);
          }
        }
      }
      const filtered: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(values)) {
        if (visibleFieldNames.has(k)) filtered[k] = v;
      }
      const payload: SubmitPayload = { cid, cs, values: filtered };
      const res = await fetch("/api/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (!res.ok) {
        const text = await res.text();
        let message = `Submit failed (HTTP ${res.status}).`;
        try {
          const j = JSON.parse(text) as { error?: string };
          if (j.error) message = j.error;
        } catch { /* keep default */ }
        setSubmitState({ kind: "error", message });
        return;
      }
      // Successful submit — clear draft and show confirmation.
      clearDraft(cid);
      setDraftSavedAt(null);
      setSubmitState({ kind: "success" });
    } catch (e) {
      setSubmitState({
        kind: "error",
        message: e instanceof Error ? e.message : "Unexpected submit error.",
      });
    }
  };

  // ── Invalid-submit handling ────────────────────────────────────────────
  // When required-field validation blocks submit, RHF calls onInvalid with
  // the errors map. Find the first error's section, expand it, scroll into
  // view, focus the field. Saves users from chasing a silent failure.
  const onInvalid = (errs: FieldErrors<FieldValues>) => {
    const firstErrorField = Object.keys(errs)[0];
    if (!firstErrorField) return;
    const section = sectionForField(config.sections, firstErrorField);
    if (section) {
      expandSection(section.id);
      // Allow the section to expand before focusing.
      requestAnimationFrame(() => {
        try {
          setFocus(firstErrorField);
        } catch {
          // setFocus throws if the field isn't registered; safe to ignore.
        }
        const el = document.getElementById(`field-${firstErrorField}`);
        el?.scrollIntoView({ behavior: "smooth", block: "center" });
      });
    }
    setSubmitState({
      kind: "error",
      message: "Please fill the required fields highlighted below before submitting.",
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit, onInvalid)} className="space-y-5" noValidate>
      <SubmissionContextHeader
        orgName={load.data.orgName}
        currentStageLabel={resolveStageLabel(load.data.currentStage, load.data.options)}
        currentRank={currentRank}
      />

      {draftRestored && draftSavedAt && (
        <DraftRestoredNotice savedAt={draftSavedAt} onDiscard={() => {
          clearDraft(cid);
          setDraftRestored(false);
          setDraftSavedAt(null);
          // Force re-fetch by bumping a key — simplest is full page reload.
          window.location.reload();
        }} />
      )}

      <ol className="relative space-y-5 md:pl-12">
        {sectionsToRender.map((entry, i) => {
          const { section, pathwayState, locked } = entry;
          const nextState =
            i < sectionsToRender.length - 1
              ? sectionsToRender[i + 1].pathwayState
              : undefined;
          return (
            <li key={section.id} className="relative list-none">
              <MobileStem show={i > 0} state={pathwayState} />
              <RailMarker
                rank={section.rank}
                state={pathwayState}
                nextState={nextState}
              />
              <StageSection
                section={section}
                register={register}
                control={control}
                errors={errors}
                formValues={evalState}
                isCurrent={pathwayState === "current"}
                locked={locked}
                defaultOpen={pathwayState === "current" || section.rank === 0}
                options={load.data.options ?? {}}
              />
            </li>
          );
        })}
      </ol>

      <SubmitBar
        state={submitState}
        isDirty={isDirty}
        draftSavedAt={draftSavedAt}
      />
    </form>
  );
}

function SubmissionContextHeader({
  orgName,
  currentStageLabel,
  currentRank,
}: {
  orgName: string;
  currentStageLabel: string;
  currentRank: number;
}) {
  return (
    <header className="rounded-lg border border-rule bg-paper-2/40 px-6 py-5 sm:px-7 sm:py-6">
      <p className="text-[11px] uppercase tracking-[0.18em] text-ink-mute">Check-in for</p>
      <h1 className="mt-1 font-display text-3xl font-medium leading-tight tracking-tight text-ink sm:text-[34px]">
        {orgName}
      </h1>
      <div className="mt-4 flex items-center gap-3">
        <StageProgress currentRank={currentRank} />
        <span className="text-[10px] uppercase tracking-[0.16em] font-medium text-ink-mute">
          Current stage
        </span>
      </div>
      <p className="mt-1.5 font-display text-xl sm:text-2xl font-medium leading-tight tracking-tight text-leaf-800">
        {currentStageLabel || "—"}
      </p>
    </header>
  );
}

/**
 * Resolve a Stage option-value (e.g. "Organizing") to its Civi option label
 * (e.g. "Stage 1 — Convene & Prepare"). Falls back to the raw value if the
 * option group hasn't loaded or the value isn't in the group.
 */
function resolveStageLabel(
  value: string,
  options: Record<number, { value: string; label: string }[]> | undefined,
): string {
  if (!value) return "";
  const group = options?.[STAGE_OPTION_GROUP_ID];
  if (!group) return value;
  return group.find((o) => o.value === value)?.label ?? value;
}

/**
 * Six small dots representing the six stages, with the current rank filled
 * and earlier ranks marked as visited. Quietly conveys progression without
 * pretending to be a "100% complete" progress bar.
 */
function StageProgress({ currentRank }: { currentRank: number }) {
  return (
    <div className="flex items-center gap-1.5" role="img" aria-label={`Stage ${currentRank} of 5`}>
      {[0, 1, 2, 3, 4, 5].map((r) => (
        <span
          key={r}
          className={
            "h-1.5 rounded-full transition-all " +
            (r < currentRank
              ? "w-2.5 bg-leaf-300"
              : r === currentRank
                ? "w-6 bg-leaf-700"
                : "w-2.5 bg-rule-soft")
          }
        />
      ))}
    </div>
  );
}

function DraftRestoredNotice({
  savedAt,
  onDiscard,
}: {
  savedAt: string;
  onDiscard: () => void;
}) {
  const ago = formatRelative(savedAt);
  return (
    <div
      role="status"
      className="flex flex-col gap-2 rounded-lg border border-clay-200 bg-clay-100/40 px-5 py-3 text-sm text-clay-700 sm:flex-row sm:items-center sm:justify-between"
    >
      <p>
        <span className="font-medium">Draft restored</span> from {ago}. Your in-progress edits were
        saved locally and have been re-applied.
      </p>
      <button
        type="button"
        onClick={onDiscard}
        className="self-start rounded border border-clay-400/40 bg-paper px-3 py-1 text-xs font-medium text-clay-700 transition hover:bg-clay-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-clay-500/40 sm:self-auto"
      >
        Discard draft
      </button>
    </div>
  );
}

function SubmitBar({
  state,
  isDirty,
  draftSavedAt,
}: {
  state: SubmitStatus;
  isDirty: boolean;
  draftSavedAt: string | null;
}) {
  return (
    <div
      className="sticky bottom-3 z-20 flex flex-col gap-3 rounded-lg border border-rule bg-paper/95 px-5 py-4 shadow-[0_-4px_24px_-12px_rgba(60,80,40,0.2)] backdrop-blur sm:flex-row sm:items-center sm:justify-between sm:px-6"
      style={{ marginBottom: "env(safe-area-inset-bottom)" }}
    >
      <div className="flex flex-col gap-0.5">
        <SubmitFeedback state={state} />
        {state.kind === "idle" && (
          <p className="text-xs text-ink-mute">
            {isDirty
              ? draftSavedAt
                ? `Draft saved ${formatRelative(draftSavedAt)} (locally on this device)`
                : "Editing — your draft will save automatically"
              : "Submitting saves a new check-in record on your co-op."}
          </p>
        )}
      </div>
      <button
        type="submit"
        disabled={state.kind === "submitting"}
        className="inline-flex items-center justify-center gap-2 rounded-md bg-leaf-700 px-6 py-2.5 font-medium text-paper transition hover:bg-leaf-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-500/40 disabled:cursor-not-allowed disabled:bg-ink-mute"
      >
        {state.kind === "submitting" ? (
          <>
            <Spinner /> Saving check-in…
          </>
        ) : (
          <>Submit check-in</>
        )}
      </button>
    </div>
  );
}

function SubmitFeedback({ state }: { state: SubmitStatus }) {
  if (state.kind === "submitting")
    return (
      <p className="text-sm text-ink-soft" role="status">
        Saving your check-in…
      </p>
    );
  if (state.kind === "error")
    return (
      <p className="text-sm font-medium text-clay-700" role="alert">
        ✕ {state.message}
      </p>
    );
  return null;
}

function Spinner() {
  return (
    <svg
      aria-hidden
      viewBox="0 0 16 16"
      className="h-4 w-4 animate-spin"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
    >
      <path d="M14 8a6 6 0 11-6-6" strokeLinecap="round" />
    </svg>
  );
}

function LoadingState() {
  return (
    <div className="rounded-lg border border-rule bg-paper px-6 py-12 text-center">
      <div
        aria-hidden
        className="mx-auto mb-4 h-7 w-7 animate-spin rounded-full border-[1.5px] border-rule border-t-leaf-700"
      />
      <p className="font-display text-base text-ink-soft italic">Loading your check-in…</p>
    </div>
  );
}

function SuccessDestination({
  orgName,
  onAnother,
}: {
  orgName: string;
  onAnother: () => void;
}) {
  return (
    <section
      role="status"
      aria-live="polite"
      className="rounded-lg border-2 border-leaf-600 bg-leaf-50/40 px-6 py-10 text-center shadow-[0_1px_0_0_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(60,80,40,0.22)] sm:px-10 sm:py-12"
    >
      <div
        aria-hidden
        className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-leaf-700 text-paper shadow-sm"
      >
        <svg viewBox="0 0 24 24" className="h-7 w-7" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
          <path d="M5 12l5 5 9-11" />
        </svg>
      </div>
      <h2 className="mt-5 font-display text-2xl font-medium leading-tight tracking-tight text-ink sm:text-3xl">
        Check-in saved
      </h2>
      <p className="mx-auto mt-3 max-w-prose text-[15px] leading-relaxed text-ink-soft">
        Thank you. We&apos;ve recorded this check-in for{" "}
        <span className="font-medium text-ink">{orgName}</span>. Your engagement coordinator will see
        it on their next review.
      </p>
      <div className="mt-7 flex flex-col items-center justify-center gap-3 sm:flex-row">
        <button
          type="button"
          onClick={onAnother}
          className="inline-flex items-center justify-center rounded-md border border-rule bg-paper px-5 py-2 text-sm font-medium text-ink-soft transition hover:bg-paper-2/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-500/40"
        >
          Submit another check-in
        </button>
        <p className="text-xs text-ink-mute">It&apos;s safe to close this window.</p>
      </div>
    </section>
  );
}

function ErrorState({ message }: { message: string }) {
  return (
    <div
      role="alert"
      className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7"
    >
      <h2 className="font-display text-xl font-medium text-clay-700">
        We couldn&apos;t open your check-in.
      </h2>
      <p className="mt-3 text-sm leading-relaxed text-ink-soft">{message}</p>
      <p className="mt-4 text-sm text-ink-soft">
        If this keeps happening, please contact your engagement coordinator and ask for a fresh
        link.
      </p>
    </div>
  );
}

/**
 * Vertical journey rail marker, visible on md+ only. Sits in the OL's left
 * gutter, aligned to the card header. Includes a downward connecting line
 * toward the next marker; the line's style is determined by whether the
 * NEXT stage is past/current (solid leaf) or future (dashed muted) — that
 * way the transition from "traveled" to "ahead" happens right after the
 * current marker, which reads correctly as "you've come this far."
 */
function RailMarker({
  rank,
  state,
  nextState,
}: {
  rank: number;
  state: PathwayState;
  nextState?: PathwayState;
}) {
  return (
    <div
      aria-hidden
      className="pointer-events-none hidden md:block absolute -left-9 top-0 bottom-0 w-7"
    >
      {nextState && (
        <span
          className={
            "absolute left-1/2 -translate-x-1/2 top-11 -bottom-11 " +
            (nextState === "future"
              ? "w-0 border-l border-dashed border-rule"
              : "w-px bg-leaf-500/70")
          }
        />
      )}
      <MarkerCircle rank={rank} state={state} />
      {state === "current" && (
        <span className="absolute left-1/2 -translate-x-1/2 top-[52px] whitespace-nowrap rounded-full bg-leaf-700 px-1.5 py-[1px] text-[8px] font-medium uppercase tracking-[0.1em] text-paper shadow-sm">
          Now
        </span>
      )}
    </div>
  );
}

function MarkerCircle({ rank, state }: { rank: number; state: PathwayState }) {
  if (state === "past") {
    return (
      <span className="absolute left-1/2 -translate-x-1/2 top-6 inline-flex h-5 w-5 items-center justify-center rounded-full bg-leaf-700 text-paper shadow-sm">
        <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round" className="h-2.5 w-2.5">
          <path d="M3 6.5l2 2 4-5" />
        </svg>
        <span className="sr-only">Stage {rank} (completed)</span>
      </span>
    );
  }
  if (state === "current") {
    return (
      <span className="absolute left-1/2 -translate-x-1/2 top-6 inline-flex h-6 w-6 items-center justify-center rounded-full bg-leaf-700 text-paper text-[10px] font-semibold shadow-md ring-2 ring-leaf-100 motion-safe:animate-[rail-pulse_2.6s_ease-in-out_infinite]">
        {rank}
        <span className="sr-only">Stage {rank} (current)</span>
      </span>
    );
  }
  return (
    <span className="absolute left-1/2 -translate-x-1/2 top-6 inline-flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-rule bg-paper text-ink-mute">
      <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" className="h-2.5 w-2.5">
        <rect x="2.5" y="5.5" width="7" height="5" rx="0.75" />
        <path d="M4 5.5V4a2 2 0 0 1 4 0v1.5" />
      </svg>
      <span className="sr-only">Stage {rank} (upcoming)</span>
    </span>
  );
}

/**
 * Mobile-only inter-card stem. Sits centered in the gap between adjacent
 * stage cards. Solid leaf when arriving at a past-or-current stage; dashed
 * muted when arriving at a future (locked) stage.
 */
function MobileStem({ show, state }: { show: boolean; state: PathwayState }) {
  if (!show) return null;
  return (
    <div aria-hidden className="md:hidden -mt-4 mb-1.5 flex justify-center">
      <span
        className={
          "block h-4 " +
          (state === "future"
            ? "w-0 border-l border-dashed border-rule"
            : "w-px bg-leaf-500/70")
        }
      />
    </div>
  );
}

function formatRelative(iso: string): string {
  const then = new Date(iso).getTime();
  if (Number.isNaN(then)) return iso;
  const seconds = Math.round((Date.now() - then) / 1000);
  if (seconds < 60) return "just now";
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) return `${minutes} min ago`;
  const hours = Math.round(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.round(hours / 24);
  if (days < 30) return `${days} days ago`;
  if (days < 365) return `${Math.round(days / 30)} months ago`;
  return `${Math.round(days / 365)} years ago`;
}
