/*
  Workout Tracker (React + TypeScript, no build step)
  Loaded via Babel standalone with the TypeScript preset.

  State is persisted on a Cloudflare Worker keyed by a random 128-bit token
  carried in location.hash. The hash form keeps the token out of Referer
  headers and server access logs of unrelated origins.
*/

type Workout = {
  id: string;
  name: string;
  intervalDays: number;
  createdAt: number;
  lastDone: number | null;
};

type SaveStatus = "idle" | "saving" | "saved" | "error";

const DAY_MS = 24 * 60 * 60 * 1000;
const TOKEN_RE = /^[a-f0-9]{32}$/;
const SAVE_DEBOUNCE_MS = 600;

const { useState, useEffect, useMemo, useRef, useCallback } = React;

function generateToken(): string {
  const bytes = new Uint8Array(16);
  crypto.getRandomValues(bytes);
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

function readTokenFromHash(): string | null {
  const params = new URLSearchParams(location.hash.replace(/^#/, ""));
  const t = params.get("token");
  return t && TOKEN_RE.test(t) ? t : null;
}

function writeTokenToHash(token: string): void {
  history.replaceState(null, "", `#token=${token}`);
}

function parseTokenInput(input: string): string | null {
  const trimmed = input.trim();
  if (!trimmed) return null;
  try {
    const url = new URL(trimmed);
    const params = new URLSearchParams(url.hash.replace(/^#/, ""));
    const t = params.get("token");
    if (t && TOKEN_RE.test(t)) return t;
  } catch {
    /* not a URL */
  }
  if (trimmed.startsWith("#")) {
    const params = new URLSearchParams(trimmed.slice(1));
    const t = params.get("token");
    if (t && TOKEN_RE.test(t)) return t;
  }
  if (TOKEN_RE.test(trimmed)) return trimmed;
  return null;
}

function sanitizeWorkouts(input: unknown): Workout[] {
  if (!Array.isArray(input)) return [];
  return input.filter(
    (w): w is Workout =>
      w &&
      typeof w.id === "string" &&
      typeof w.name === "string" &&
      typeof w.intervalDays === "number" &&
      typeof w.createdAt === "number" &&
      (w.lastDone === null || typeof w.lastDone === "number"),
  );
}

async function loadFromServer(token: string): Promise<Workout[]> {
  const res = await fetch(`/api/state/${token}`, { method: "GET" });
  if (!res.ok) throw new Error(`Load failed (${res.status})`);
  const data = await res.json();
  return sanitizeWorkouts(data?.workouts);
}

async function saveToServer(token: string, workouts: Workout[]): Promise<void> {
  const res = await fetch(`/api/state/${token}`, {
    method: "PUT",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ workouts }),
  });
  if (!res.ok) throw new Error(`Save failed (${res.status})`);
}

function newId(): string {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
}

function formatSince(lastDone: number, now: number): string {
  const ms = Math.max(0, now - lastDone);
  const totalMinutes = Math.floor(ms / 60000);
  if (totalMinutes < 1) return "just now";
  if (totalMinutes < 60) return `${totalMinutes} min ago`;
  const totalHours = Math.floor(totalMinutes / 60);
  if (totalHours < 24) return `${totalHours} h ago`;
  const days = Math.floor(ms / DAY_MS);
  return days === 1 ? "1 day ago" : `${days} days ago`;
}

type NewWorkoutFormProps = {
  onAdd: (name: string, intervalDays: number) => void;
};

function NewWorkoutForm({ onAdd }: NewWorkoutFormProps) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState("");
  const [interval, setInterval] = useState("7");

  function submit(e: React.FormEvent) {
    e.preventDefault();
    const trimmed = name.trim();
    const parsedInterval = Number(interval);
    if (!trimmed) return;
    if (!Number.isFinite(parsedInterval) || parsedInterval <= 0) return;
    onAdd(trimmed, Math.round(parsedInterval));
    setName("");
    setInterval("7");
    setOpen(false);
  }

  if (!open) {
    return (
      <button className="primary add-btn" onClick={() => setOpen(true)}>
        + New workout
      </button>
    );
  }

  return (
    <form className="new-form" onSubmit={submit}>
      <label>
        <span>Name</span>
        <input
          autoFocus
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="e.g. Push-ups"
        />
      </label>
      <label>
        <span>Max days between</span>
        <input
          type="number"
          min="1"
          step="1"
          value={interval}
          onChange={(e) => setInterval(e.target.value)}
        />
      </label>
      <div className="form-actions">
        <button type="button" onClick={() => setOpen(false)}>
          Cancel
        </button>
        <button type="submit" className="primary">
          Add
        </button>
      </div>
    </form>
  );
}

type WorkoutRowProps = {
  workout: Workout;
  now: number;
  onDidIt: (id: string) => void;
  onDelete: (id: string) => void;
};

function WorkoutRow({ workout, now, onDidIt, onDelete }: WorkoutRowProps) {
  const { lastDone, intervalDays } = workout;
  const overdue =
    lastDone !== null && now - lastDone > intervalDays * DAY_MS;

  let statusText: string;
  let statusClass: string;
  if (lastDone === null) {
    statusText = "Not started";
    statusClass = "status pending";
  } else if (overdue) {
    statusText = `${formatSince(lastDone, now)} · overdue`;
    statusClass = "status overdue";
  } else {
    statusText = formatSince(lastDone, now);
    statusClass = "status ok";
  }

  return (
    <li className={`workout${overdue ? " is-overdue" : ""}`}>
      <div className="info">
        <div className="name">{workout.name}</div>
        <div className="meta">
          every {intervalDays} {intervalDays === 1 ? "day" : "days"}
        </div>
        <div className={statusClass}>{statusText}</div>
      </div>
      <div className="actions">
        <button className="primary" onClick={() => onDidIt(workout.id)}>
          Did it!
        </button>
        <button
          className="link"
          aria-label={`Delete ${workout.name}`}
          onClick={() => onDelete(workout.id)}
        >
          Delete
        </button>
      </div>
    </li>
  );
}

type SaveLinkProps = {
  token: string;
  status: SaveStatus;
};

function SaveLink({ token, status }: SaveLinkProps) {
  const [copied, setCopied] = useState(false);
  const url = `${location.origin}${location.pathname}#token=${token}`;

  async function copy() {
    try {
      await navigator.clipboard.writeText(url);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1500);
    } catch {
      // Clipboard may be blocked; the input is selectable as a fallback.
    }
  }

  const statusLabel =
    status === "saving"
      ? "Saving…"
      : status === "error"
        ? "Save failed"
        : status === "saved"
          ? "Saved"
          : "";

  return (
    <section className="save-link">
      <div className="save-link-header">
        <span className="save-link-title">Your save URL</span>
        <span className={`save-status save-status-${status}`}>{statusLabel}</span>
      </div>
      <p className="save-link-help">
        Bookmark this URL — open it on any device to load your workouts.
      </p>
      <div className="save-link-row">
        <input
          readOnly
          value={url}
          onFocus={(e) => e.currentTarget.select()}
          aria-label="Save URL"
        />
        <button type="button" onClick={copy}>
          {copied ? "Copied" : "Copy"}
        </button>
      </div>
    </section>
  );
}

type LoadExistingTokenProps = {
  currentToken: string;
  onSwitch: (token: string) => void;
};

function LoadExistingToken({ currentToken, onSwitch }: LoadExistingTokenProps) {
  const [open, setOpen] = useState(false);
  const [input, setInput] = useState("");
  const [error, setError] = useState<string | null>(null);

  function reset() {
    setOpen(false);
    setInput("");
    setError(null);
  }

  function submit(e: React.FormEvent) {
    e.preventDefault();
    const parsed = parseTokenInput(input);
    if (!parsed) {
      setError("That doesn't look like a save URL or token.");
      return;
    }
    if (parsed === currentToken) {
      setError("You're already using this save URL.");
      return;
    }
    onSwitch(parsed);
    reset();
  }

  if (!open) {
    return (
      <button
        type="button"
        className="link load-token-toggle"
        onClick={() => setOpen(true)}
      >
        Load a different save URL
      </button>
    );
  }

  return (
    <form className="load-token-form" onSubmit={submit}>
      <label>
        <span>Paste your save URL or token</span>
        <input
          autoFocus
          type="text"
          value={input}
          onChange={(e) => {
            setInput(e.target.value);
            setError(null);
          }}
          placeholder="https://ai-ticker.com/workout/#token=…"
          spellCheck={false}
        />
      </label>
      {error && <div className="load-token-error">{error}</div>}
      <div className="form-actions">
        <button type="button" onClick={reset}>
          Cancel
        </button>
        <button type="submit" className="primary">
          Switch
        </button>
      </div>
    </form>
  );
}

function App() {
  const [token, setToken] = useState<string>(() => {
    const existing = readTokenFromHash();
    if (existing) return existing;
    const created = generateToken();
    writeTokenToHash(created);
    return created;
  });
  const [workouts, setWorkouts] = useState<Workout[]>([]);
  const [now, setNow] = useState<number>(() => Date.now());
  const [loaded, setLoaded] = useState(false);
  const [loadError, setLoadError] = useState<string | null>(null);
  const [saveStatus, setSaveStatus] = useState<SaveStatus>("idle");

  const saveTimer = useRef<number | null>(null);
  const skipNextSave = useRef(true);

  useEffect(() => {
    let cancelled = false;
    loadFromServer(token)
      .then((list) => {
        if (cancelled) return;
        setWorkouts(list);
        setLoaded(true);
      })
      .catch((err) => {
        if (cancelled) return;
        setLoadError(err instanceof Error ? err.message : "Load failed");
        setLoaded(true);
      });
    return () => {
      cancelled = true;
    };
  }, [token]);

  useEffect(() => {
    if (!loaded) return;
    if (skipNextSave.current) {
      skipNextSave.current = false;
      return;
    }
    setSaveStatus("saving");
    if (saveTimer.current !== null) {
      window.clearTimeout(saveTimer.current);
    }
    saveTimer.current = window.setTimeout(() => {
      saveToServer(token, workouts)
        .then(() => setSaveStatus("saved"))
        .catch(() => setSaveStatus("error"));
    }, SAVE_DEBOUNCE_MS);
    return () => {
      if (saveTimer.current !== null) {
        window.clearTimeout(saveTimer.current);
      }
    };
  }, [workouts, token, loaded]);

  useEffect(() => {
    const id = window.setInterval(() => setNow(Date.now()), 60000);
    return () => window.clearInterval(id);
  }, []);

  const addWorkout = useCallback((name: string, intervalDays: number) => {
    const workout: Workout = {
      id: newId(),
      name,
      intervalDays,
      createdAt: Date.now(),
      lastDone: null,
    };
    setWorkouts((prev) => [...prev, workout]);
  }, []);

  const markDone = useCallback((id: string) => {
    const ts = Date.now();
    setWorkouts((prev) =>
      prev.map((w) => (w.id === id ? { ...w, lastDone: ts } : w)),
    );
    setNow(ts);
  }, []);

  const deleteWorkout = useCallback((id: string) => {
    setWorkouts((prev) => prev.filter((w) => w.id !== id));
  }, []);

  const switchToken = useCallback(
    (newToken: string) => {
      if (newToken === token) return;
      writeTokenToHash(newToken);
      skipNextSave.current = true;
      if (saveTimer.current !== null) {
        window.clearTimeout(saveTimer.current);
        saveTimer.current = null;
      }
      setLoaded(false);
      setLoadError(null);
      setSaveStatus("idle");
      setWorkouts([]);
      setToken(newToken);
    },
    [token],
  );

  const sorted = useMemo(
    () => [...workouts].sort((a, b) => a.createdAt - b.createdAt),
    [workouts],
  );

  return (
    <div className="app">
      <header>
        <h1>Workout Tracker</h1>
      </header>
      <main>
        {!loaded ? (
          <p className="empty">Loading…</p>
        ) : loadError ? (
          <p className="empty error">Couldn't load saved data: {loadError}</p>
        ) : sorted.length === 0 ? (
          <p className="empty">No workouts yet. Add one to get started.</p>
        ) : (
          <ul className="workouts">
            {sorted.map((w) => (
              <WorkoutRow
                key={w.id}
                workout={w}
                now={now}
                onDidIt={markDone}
                onDelete={deleteWorkout}
              />
            ))}
          </ul>
        )}
        {loaded && !loadError && <NewWorkoutForm onAdd={addWorkout} />}
        <SaveLink token={token} status={saveStatus} />
        <LoadExistingToken currentToken={token} onSwitch={switchToken} />
      </main>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(<App />);
