// =================================================================
// pw-screens-agent-access.jsx — AI agent access (agent payments, migration 0069).
//
//   Publisher · AI agent access   — enable, default action, price, per-agent rules, path prices.
//   Publisher · Payouts           — the Agent revenue card (AgentRevenueCard).
//   Operator  · Publisher Config  — the same editor for one publisher (AgentSettingsCard).
//
// An AI agent that announces itself (GPTBot, ClaudeBot, …) and reaches a publisher page behind the
// agent-gate middleware is asked to pay; the price is the publisher's, in their payout currency, and
// stored in minor units. Only agents that identify themselves are covered: a scraper posing as a
// browser is not stopped by any of this, and the screen says so.
//
// Also holds the small helpers the other agent-payment screens share (live/demo reads, minor units).
// =================================================================

const AP_GATE_README = "https://github.com/xpcovertech/dropcap/blob/main/packages/agent-gate/README.md";
const AP_ACTIONS = [
  { value: "charge", label: "Charge" },
  { value: "allow", label: "Allow free" },
  { value: "block", label: "Block" },
];
const AP_ACTION_TONE = { charge: "accent", allow: "good", block: "warn" };
const AP_TOKEN_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
const apNotify = (msg, tone) => (window.__toast ? window.__toast(msg, tone) : null);
const apLive = () => Boolean(window.__api.getConfig().live);

// Minor units per currency, from Intl (USD/INR 2, JPY 0). Prices are stored as integers of these.
const apDigits = (currency) => {
  try { return new Intl.NumberFormat("en-US", { style: "currency", currency }).resolvedOptions().maximumFractionDigits; } catch (e) { return 2; }
};
const apMoney = (minor, currency) => fmt.currency(Number(minor || 0) / 10 ** apDigits(currency || "USD"), currency || "USD");
const apToMajorString = (minor, currency) => (minor === null || minor === undefined ? "" : String(Number(minor) / 10 ** apDigits(currency)));
// "0.05" → 5 for USD. NaN when it is not a positive amount with at most the currency's decimals.
const apToMinor = (major, currency) => {
  if (major === "" || major === null || major === undefined) return NaN;
  const n = Number(major);
  const scaled = n * 10 ** apDigits(currency);
  if (!Number.isFinite(n) || n <= 0 || Math.abs(scaled - Math.round(scaled)) > 1e-6) return NaN;
  return Math.round(scaled);
};

// A read that goes to the API in live mode and to the fixture store in demo mode. `demo` is called on
// every render, so an optimistic write to the store shows at once; live reads re-run on `reload()`
// and when the data layer switches mode or environment.
function useApRead(name, opts, demo, { enabled = true } = {}) {
  const live = apLive();
  const [st, setSt] = React.useState({ value: null, loading: live, error: null });
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => window.__api.on("change", () => setTick((n) => n + 1)), []);
  const key = JSON.stringify(opts || {});
  React.useEffect(() => {
    if (!live || !enabled) return undefined;
    let off = false;
    setSt((s) => ({ ...s, loading: true }));
    window.__api.call(name, opts || {})
      .then((v) => { if (!off) setSt({ value: v, loading: false, error: null }); })
      .catch((e) => { if (!off) setSt({ value: null, loading: false, error: e }); });
    return () => { off = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [live, name, key, tick, enabled]);
  return {
    live,
    value: live ? st.value : (enabled ? demo() : null),
    loading: live && st.loading,
    error: live ? st.error : null,
    reload: () => setTick((n) => n + 1),
  };
}

// The catalogue the operator screens pick from. The publisher reads GET /v1/agent-catalog; operator
// screens use the bundled copy (paywall-data.js), which scripts/check-agent-fixtures.mts pins to
// KNOWN_AGENTS, because that endpoint is publisher-scoped.
const apCatalog = (data) => data.agentCatalog || [];

// ---- The editor, shared by the publisher screen and the operator card ----
const apDraftFrom = (s) => ({
  enabled: Boolean(s && s.enabled),
  default_action: (s && s.default_action) || "charge",
  price: s ? apToMajorString(s.price_minor, s.currency || "USD") : "",
  rules: ((s && s.rules) || []).map((r) => ({ ...r })),
  path_prices: ((s && s.path_prices) || []).map((p) => ({ prefix: p.prefix, price: apToMajorString(p.price_minor, (s && s.currency) || "USD") })),
});

// The draft as PublisherAgentSettingsUpdate, plus what is wrong with it (the contract's refinements).
const apBuildBody = (draft, currency) => {
  const problems = [];
  const price_minor = draft.price === "" ? null : apToMinor(draft.price, currency);
  if (draft.price !== "" && !Number.isFinite(price_minor)) problems.push(`The price must be a positive amount in ${currency}.`);
  const charging = draft.default_action === "charge" || draft.rules.some((r) => r.action === "charge");
  if (draft.enabled && charging && price_minor === null) problems.push("Set a price before charging agents.");
  const path_prices = draft.path_prices.map((p) => ({ prefix: p.prefix.trim(), price_minor: apToMinor(p.price, currency) }));
  path_prices.forEach((p, i) => {
    if (!/^\//.test(p.prefix) || p.prefix.length > 200) problems.push(`Path ${i + 1}: a prefix starts with / (for example /premium).`);
    if (!Number.isFinite(p.price_minor)) problems.push(`Path ${i + 1}: the price must be a positive amount in ${currency}.`);
  });
  if (new Set(path_prices.map((p) => p.prefix)).size !== path_prices.length) problems.push("One price per path prefix.");
  if (path_prices.length > 50) problems.push("At most 50 path prices.");
  if (draft.rules.length > 100) problems.push("At most 100 agent rules.");
  return {
    body: { enabled: draft.enabled, default_action: draft.default_action, price_minor: Number.isFinite(price_minor) ? price_minor : null, rules: draft.rules, path_prices },
    problems,
  };
};

const AgentRulesTable = ({ catalog, rules, defaultAction, onChange, readOnly }) => {
  const [q, setQ] = React.useState("");
  const [custom, setCustom] = React.useState("");
  const byAgent = Object.fromEntries(rules.map((r) => [r.agent, r.action]));
  const setRule = (agent, action) => {
    const rest = rules.filter((r) => r.agent !== agent);
    onChange(action === "" ? rest : [...rest, { agent, action }]);
  };
  const known = new Set(catalog.map((a) => a.token));
  const others = rules.filter((r) => !known.has(r.agent));
  const term = q.trim().toLowerCase();
  const rows = [
    ...catalog.filter((a) => !term || [a.label, a.token, a.operator].some((v) => String(v).toLowerCase().includes(term))),
    ...others.filter((r) => !term || r.agent.includes(term)).map((r) => ({ token: r.agent, label: r.agent, operator: "—", category: "custom" })),
  ];
  const defaultLabel = (a) => a.category === "search"
    ? "Default: allow (search engine)"
    : `Default: ${(AP_ACTIONS.find((x) => x.value === defaultAction) || {}).label || defaultAction}`;
  const customOk = AP_TOKEN_RE.test(custom.trim()) && !byAgent[custom.trim()];
  return (
    <div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 10 }}>
        <div style={{ flex: "1 1 220px" }}><Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search agent, token or operator"/></div>
        {!readOnly && (
          <div style={{ display: "flex", gap: 6, flex: "1 1 260px" }}>
            <Input value={custom} onChange={(e) => setCustom(e.target.value.toLowerCase())} placeholder="Another agent's token, e.g. mybot"/>
            <Btn kind="secondary" size="sm" disabled={!customOk} onClick={() => { setRule(custom.trim(), "block"); setCustom(""); }}>Add rule</Btn>
          </div>
        )}
      </div>
      <DataTable rows={rows} keyField="token" rowsPerPage={0} empty="No agent matches."
        columns={[
          { key: "label", label: "Agent", render: (a) => <span><strong style={{ color: "var(--ink)" }}>{a.label}</strong> <span style={{ fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--muted)" }}>{a.token}</span></span> },
          { key: "operator", label: "Operator" },
          { key: "category", label: "Kind", render: (a) => <Pill tone={a.category === "ai" ? "accent" : a.category === "search" ? "info" : "neutral"}>{a.category === "ai" ? "AI" : a.category === "search" ? "Search" : "Custom"}</Pill> },
          { key: "action", label: "Action", sortable: false, render: (a) => readOnly
            ? (byAgent[a.token] ? <Pill tone={AP_ACTION_TONE[byAgent[a.token]]}>{byAgent[a.token]}</Pill> : <span style={{ fontSize: 12.5, color: "var(--muted)" }}>{defaultLabel(a)}</span>)
            : <div style={{ minWidth: 210 }}><Select value={byAgent[a.token] || ""} onChange={(v) => setRule(a.token, v)}
                options={[{ label: defaultLabel(a), value: "" }, ...AP_ACTIONS]}/></div> },
        ]}/>
    </div>
  );
};

const AgentSettingsEditor = ({ settings, catalog, onSave, readOnly, installHint }) => {
  const currency = (settings && settings.currency) || "USD";
  const [draft, setDraft] = React.useState(() => apDraftFrom(settings));
  const [busy, setBusy] = React.useState(false);
  const stamp = settings ? `${settings.publisher_id}:${settings.updated_at}` : "";
  React.useEffect(() => { setDraft(apDraftFrom(settings)); /* eslint-disable-next-line */ }, [stamp]);
  const set = (p) => setDraft((d) => ({ ...d, ...p }));
  const { body, problems } = apBuildBody(draft, currency);
  const dirty = JSON.stringify(draft) !== JSON.stringify(apDraftFrom(settings));
  const save = async () => {
    setBusy(true);
    try { await onSave(body); } catch (e) { /* write() has already said why */ } finally { setBusy(false); }
  };
  const setPath = (i, p) => set({ path_prices: draft.path_prices.map((x, j) => (j === i ? { ...x, ...p } : x)) });

  return (
    <div style={{ display: "grid", gap: "var(--gap)" }}>
      <Card title="Access and price" subtitle={`Prices are in your payout currency (${currency}). An agent that pays gets 24 hours of access to that page.`}
        action={readOnly ? <Pill tone="neutral">read only</Pill> : <Btn kind="primary" size="sm" disabled={!dirty || problems.length > 0 || busy} onClick={save}>{busy ? "Saving…" : "Save"}</Btn>}>
        <FieldRow label="Agent access" hint="Off: the middleware lets every agent through as before, and nothing is charged.">
          <Toggle checked={draft.enabled} onChange={(v) => !readOnly && set({ enabled: v })} label="Agent access"/>
        </FieldRow>
        <FieldRow label="Default for AI agents" hint="What happens to an AI agent with no rule of its own. Search engines are allowed unless you add a rule.">
          {readOnly ? <Pill tone={AP_ACTION_TONE[draft.default_action]}>{draft.default_action}</Pill>
            : <SegmentedControl value={draft.default_action} onChange={(v) => set({ default_action: v })} options={AP_ACTIONS}/>}
        </FieldRow>
        <FieldRow label={`Price per article (${currency})`} hint={draft.price !== "" && Number.isFinite(body.price_minor) ? `Stored as ${body.price_minor} minor units.` : "Required when anything is charged."}>
          <Input type="number" step="0.01" min="0" value={draft.price} disabled={readOnly} placeholder="0.05" onChange={(e) => set({ price: e.target.value })}/>
        </FieldRow>
      </Card>

      <Card title="Per-agent rules" subtitle="Override the default for one agent. The first rule that names one of a paying operator's declared agents decides." padded>
        <AgentRulesTable catalog={catalog} rules={draft.rules} defaultAction={draft.default_action} readOnly={readOnly} onChange={(rules) => set({ rules })}/>
      </Card>

      <Card title="Prices by path" subtitle="A different price under a path prefix. The longest matching prefix wins; everything else pays the price above."
        action={readOnly ? null : <Btn kind="secondary" size="sm" disabled={draft.path_prices.length >= 50} onClick={() => set({ path_prices: [...draft.path_prices, { prefix: "/", price: "" }] })}>Add a path</Btn>}>
        {draft.path_prices.length === 0 ? <div style={{ fontSize: 13, color: "var(--muted)" }}>No path prices. Every page costs the price above.</div>
          : draft.path_prices.map((p, i) => (
            <div key={i} style={{ display: "grid", gridTemplateColumns: "2fr 1fr auto", gap: 8, alignItems: "center", marginBottom: 8 }}>
              <Input value={p.prefix} disabled={readOnly} placeholder="/premium" onChange={(e) => setPath(i, { prefix: e.target.value })}/>
              <Input type="number" step="0.01" min="0" value={p.price} disabled={readOnly} placeholder="0.25" suffix={currency} onChange={(e) => setPath(i, { price: e.target.value })}/>
              {readOnly ? <span/> : <Btn kind="ghost" size="sm" icon={<IconTrash size={14}/>} onClick={() => set({ path_prices: draft.path_prices.filter((_, j) => j !== i) })}>Remove</Btn>}
            </div>
          ))}
      </Card>

      {!readOnly && problems.length > 0 && (
        <Banner tone="warn" icon={<IconInfo size={15}/>}><div style={{ display: "grid", gap: 2 }}>{problems.map((p) => <span key={p}>{p}</span>)}</div></Banner>
      )}
      {installHint}
      {settings && settings.updated_at && (
        <div style={{ fontSize: 12, color: "var(--muted)" }}>Last saved {fmt.dateTime(settings.updated_at)}{settings.updated_by ? ` by ${settings.updated_by}` : ""}.</div>
      )}
    </div>
  );
};

const AgentHonestLimit = () => (
  <Banner tone="info" icon={<IconInfo size={15}/>}>
    Only agents that identify themselves pay. An agent is recognised by the name it announces; a scraper posing as a browser
    is not stopped by this. Search engines are allowed unless you add a rule for them.
  </Banner>
);

const AgentInstallHint = ({ siteKey }) => (
  <Card title="Install the middleware" subtitle="Settings do nothing until the agent-gate middleware runs in front of your site.">
    <div style={{ fontSize: 13, color: "var(--ink-2)", lineHeight: 1.6 }}>
      Deploy the Cloudflare Worker from <code>packages/agent-gate</code> in front of your pages with your site key.
      It answers a recognised agent with HTTP 402 and a price, and lets it through once it shows a paid access token.
      {" "}<a href={AP_GATE_README} target="_blank" rel="noopener noreferrer">Read the agent-gate README</a>.
    </div>
    <div style={{ marginTop: 10, fontSize: 13 }}>
      Site key: {siteKey ? <code style={{ wordBreak: "break-all" }}>{siteKey}</code> : <span style={{ color: "var(--muted)" }}>see Integration kits</span>}
    </div>
  </Card>
);

// Publisher · AI agent access
const AgentAccessScreen = ({ data }) => {
  const settings = useApRead("getAgentSettings", {}, () => ({ settings: data.agentSettings }));
  const catalog = useApRead("getAgentCatalog", {}, () => ({ agents: data.agentCatalog || [] }));
  React.useEffect(() => { if (settings.error) apNotify("Could not load your agent settings.", "bad"); }, [settings.error]);
  React.useEffect(() => { if (catalog.error) apNotify("Could not load the agent list.", "bad"); }, [catalog.error]);
  const s = settings.value && settings.value.settings;
  const siteKey = data.embedConfig && data.embedConfig.site_key;

  const save = async (body) => {
    const res = await window.__api.write("saveAgentSettings",
      (d) => ({ ...d, agentSettings: { ...(d.agentSettings || {}), ...body, updated_by: "you", updated_at: new Date().toISOString() } }),
      { body, ok: "Agent settings saved", err: "Couldn’t save agent settings" });
    if (settings.live) settings.reload();
    return res;
  };

  return (
    <Screen>
      <PageHead icon={IconCpu} title="AI agent access"
        subtitle="Charge AI agents that read your articles, allow the ones you want for free, and block the rest."/>
      <AgentHonestLimit/>
      {settings.loading && !s ? <Card><div style={{ padding: 24, textAlign: "center", color: "var(--muted)" }}>Loading…</div></Card>
        : !s ? <Card><EmptyState icon={<IconCpu size={26}/>} title="Agent settings unavailable" description="They could not be loaded. Try again shortly."/></Card>
        : <AgentSettingsEditor settings={s} catalog={(catalog.value && catalog.value.agents) || []} onSave={save}
            installHint={<AgentInstallHint siteKey={siteKey}/>}/>}
    </Screen>
  );
};

// Operator · Publisher Config: one publisher's agent settings. Any staff member reads; a write needs
// the publisher in the staff member's scope (the API decides).
const AgentSettingsCard = ({ data, publisherId }) => {
  const demoDefault = { publisher_id: publisherId, enabled: false, default_action: "charge", price_minor: null, currency: "USD", rules: [], path_prices: [], updated_by: null, updated_at: null };
  const r = useApRead("adminGetPublisherAgentSettings", { params: { publisher_id: publisherId } },
    () => ({ settings: (data.opAgentSettings || {})[publisherId] || demoDefault }), { enabled: Boolean(publisherId) });
  React.useEffect(() => { if (r.error) apNotify("Could not load this publisher's agent settings.", "bad"); }, [r.error]);
  const s = r.value && r.value.settings;
  const save = async (body) => {
    await window.__api.write("adminSavePublisherAgentSettings",
      (d) => ({ ...d, opAgentSettings: { ...(d.opAgentSettings || {}), [publisherId]: { ...(s || demoDefault), ...body, updated_by: "you", updated_at: new Date().toISOString() } } }),
      { params: { publisher_id: publisherId }, body, ok: "Agent settings saved", err: "Couldn’t save agent settings" });
    if (r.live) r.reload();
  };
  return (
    <div style={{ gridColumn: "1 / -1", display: "grid", gap: "var(--gap)" }}>
      <div style={{ fontSize: 15, fontWeight: 600, color: "var(--ink)" }}>AI agent access</div>
      <AgentHonestLimit/>
      {!s ? <Card><div style={{ padding: 16, color: "var(--muted)", fontSize: 13 }}>{r.loading ? "Loading…" : "No agent settings to show."}</div></Card>
        : <AgentSettingsEditor settings={s} catalog={apCatalog(data)} onSave={save}/>}
    </div>
  );
};

// Publisher · Payouts: what agents paid over the last 14 days.
const apDay = (d) => d.toISOString().slice(0, 10);
const AgentRevenueCard = ({ data }) => {
  const [range] = React.useState(() => { const to = new Date(); const from = new Date(to.getTime() - 13 * 86400000); return { from: apDay(from), to: apDay(to) }; });
  const r = useApRead("getAgentRevenue", { query: range }, () => data.agentRevenue || null);
  const rev = r.value;
  if (!rev || !rev.totals) return null;
  const cur = rev.currency;
  const days = [...(rev.days || [])].sort((a, b) => String(b.day).localeCompare(String(a.day)));
  return (
    <Card title="Agent revenue" subtitle={`What AI agents paid to read your articles, ${days.length ? `${days[days.length - 1].day} to ${days[0].day}` : "last 14 days"}. Your share is held and paid with your other earnings.`}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 10, fontSize: 13 }}>
        <div><div style={{ color: "var(--muted)", fontSize: 12 }}>Articles bought</div><strong>{fmt.number(rev.totals.purchases)}</strong></div>
        <div><div style={{ color: "var(--muted)", fontSize: 12 }}>Agents paid</div><strong>{apMoney(rev.totals.gross_minor, cur)}</strong></div>
        <div><div style={{ color: "var(--muted)", fontSize: 12 }}>Your share</div><strong>{apMoney(rev.totals.publisher_share_minor, cur)}</strong></div>
      </div>
      {days.length === 0 ? <div style={{ fontSize: 13, color: "var(--muted)", paddingTop: 12 }}>No agent has bought an article yet.</div> : (
        <div style={{ display: "grid", gridTemplateColumns: "minmax(0, 3fr) minmax(0, 2fr)", gap: 16, marginTop: 14 }} className="pw-agent-rev">
          <Table rows={days.map((d) => ({ ...d, id: d.day }))} columns={[
            { key: "day", label: "Day", mono: true },
            { key: "purchases", label: "Articles", render: (d) => fmt.number(d.purchases) },
            { key: "gross_minor", label: "Paid", render: (d) => apMoney(d.gross_minor, cur) },
            { key: "publisher_share_minor", label: "Your share", render: (d) => apMoney(d.publisher_share_minor, cur) },
          ]}/>
          <div>
            <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 6, textTransform: "uppercase", letterSpacing: ".05em", fontWeight: 600 }}>Top agent operators</div>
            {(rev.top_agents || []).length === 0 ? <div style={{ fontSize: 13, color: "var(--muted)" }}>None yet.</div>
              : rev.top_agents.map((a) => (
                <div key={a.operator_name} style={{ display: "flex", justifyContent: "space-between", gap: 10, fontSize: 13, padding: "6px 0", borderBottom: "1px solid var(--line)" }}>
                  <span>{a.operator_name} <span style={{ color: "var(--muted)", fontSize: 12 }}>· {fmt.number(a.purchases)}</span></span>
                  <strong>{apMoney(a.gross_minor, cur)}</strong>
                </div>
              ))}
          </div>
        </div>
      )}
      <style>{`@media (max-width: 900px) { .pw-agent-rev { grid-template-columns: 1fr !important; } }`}</style>
    </Card>
  );
};

Object.assign(window, {
  AgentAccessScreen, AgentSettingsCard, AgentRevenueCard, AgentSettingsEditor,
  useApRead, apLive, apMoney, apDigits, apToMinor, apCatalog, AP_TOKEN_RE,
});
