// =================================================================
// pw-screens-conversions.jsx — CPA conversion tracking (payments phase 5, 0059).
//
//   BrandConversionsScreen      brand portal: conversion keys (issue / rotate / revoke), the
//                               conversions reported against its CPA clicks, and disputes (D19)
//   BrandConversionGuideScreen  brand portal: how to install the pixel and the server postback
//   OpConversionsScreen         operator: the hold queue (approve / reject), disputes, and any
//                               assigned brand's keys
//
// Nothing here moves money. A postback is billed by the API on arrival, a pixel conversion only when
// an operator approves it, and a dispute is reversed by the API — each through the one billing point.
// A postback secret is shown once, in the response that issued it, and never again.
//
// Live mode: brand routes go through advCall (advertiser token, pw-screens-recommendations.jsx);
// operator routes through window.__api.call (staff session). Demo mode renders the `conversionKeys`
// and `conversions` fixtures in paywall-data.js and never pretends a key or a charge is real.
// =================================================================

const CV_STATUS_TONE = { held: "warn", billed: "good", not_billed: "neutral", rejected: "neutral", disputed: "info", pending: "info" };
const CV_STATUS_LABEL = { held: "Held for review", billed: "Billed", not_billed: "Not billed (wallet empty)", rejected: "Rejected", disputed: "Disputed · refunded", pending: "Pending" };
const cvMoney = (minor, currency) => {
  if (!currency) return "—";
  const n = Number(minor || 0) / 100;
  try { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(n); }
  catch (e) { return `${n.toFixed(2)} ${currency}`; }
};
const cvWhen = (s) => (s ? new Date(s).toLocaleString() : "—");
const cvApiBase = () => (window.__api?.getConfig?.().baseUrl || "https://api-dropcap.krafts.ai").replace(/\/+$/, "");
const cvKeyState = (k) => {
  if (k.status === "revoked") return { tone: "neutral", label: "Revoked" };
  if (k.expires_at && Date.parse(k.expires_at) <= Date.now()) return { tone: "neutral", label: "Expired" };
  if (k.expires_at) return { tone: "warn", label: `Rotated · works until ${cvWhen(k.expires_at)}` };
  return { tone: "good", label: "Active" };
};
const cvDisputable = (c) => c.status === "billed" && c.dispute_window_ends_at && Date.parse(c.dispute_window_ends_at) > Date.now();
const cvDemoKeyId = (kind) => `${kind === "postback" ? "ck" : "pk"}_demo${Math.random().toString(16).slice(2, 14).padEnd(12, "0")}${Date.now().toString(16)}`;

// ---- The one-time secret, shown after a postback key is issued or rotated ----
const SecretBanner = ({ issued, onDismiss }) => {
  if (!issued) return null;
  return <Banner tone="warn" icon={<IconInfo size={15}/>}>
    <div style={{ display: "grid", gap: 6 }}>
      <strong>Copy this {issued.key.kind} key{issued.secret ? " and its secret" : ""} now{issued.secret ? " — the secret is not shown again" : ""}.</strong>
      <div>Key: <code>{issued.key.key_id}</code></div>
      {issued.secret && <div>Secret: <code style={{ wordBreak: "break-all" }}>{issued.secret}</code></div>}
      {issued.replaced && <div>The key it replaces keeps working until {cvWhen(issued.replaced.expires_at)} so you can deploy this one.</div>}
      <div><Btn size="sm" onClick={onDismiss}>I have copied it</Btn></div>
    </div>
  </Banner>;
};

// ---- Keys table, shared by the brand portal and the operator view ----
const ConversionKeysCard = ({ keys, busy, onCreate, onRotate, onRevoke, subtitle }) => (
  <Card title="Conversion keys" subtitle={subtitle} padded={false}
    action={<div style={{ display: "flex", gap: 8 }}>
      <Btn size="sm" kind="primary" disabled={busy} onClick={() => onCreate("postback")}>New postback key</Btn>
      <Btn size="sm" disabled={busy} onClick={() => onCreate("pixel")}>New pixel key</Btn>
    </div>}>
    {keys.length === 0
      ? <EmptyState icon={<IconKey size={26}/>} title="No keys yet" description="Issue a postback key for your server, or a pixel key for your pages."/>
      : <Table rows={keys.map(k => ({ ...k, id: k.key_id }))} columns={[
          { key: "kind", label: "Kind", render: k => k.kind === "postback" ? "Server postback" : "Browser pixel" },
          { key: "key_id", label: "Key", mono: true },
          { key: "status", label: "State", render: k => { const s = cvKeyState(k); return <Pill tone={s.tone}>{s.label}</Pill>; } },
          { key: "last_used_at", label: "Last used", render: k => cvWhen(k.last_used_at) },
          { key: "created_at", label: "Issued", render: k => cvWhen(k.created_at) },
          { key: "_a", label: "", sortable: false, align: "right", render: k => k.status === "active" ? <div style={{ display: "flex", gap: 6, justifyContent: "flex-end" }}>
              {!k.rotated_to && <Btn size="sm" disabled={busy} onClick={() => onRotate(k)}>Rotate</Btn>}
              <Btn size="sm" kind="ghost" disabled={busy} onClick={() => onRevoke(k)}>Revoke</Btn>
            </div> : null },
        ]}/>}
  </Card>
);

// ---- Conversions table ----
const ConversionsTable = ({ rows, busy, onDispute, onReview }) => (
  rows.length === 0
    ? <EmptyState icon={<IconChart size={26}/>} title="No conversions" description="Conversions reported against your CPA clicks appear here."/>
    : <Table rows={rows.map(c => ({ ...c, id: c.conversion_id }))} columns={[
        { key: "received_at", label: "Received", render: c => cvWhen(c.received_at) },
        { key: "conversion_type", label: "Type" },
        { key: "order_id", label: "Order", render: c => c.order_id || "—" },
        { key: "source", label: "Via", render: c => c.source === "pixel" ? "Pixel" : "Postback" },
        { key: "status", label: "Status", render: c => <Pill tone={CV_STATUS_TONE[c.status] || "neutral"}>{CV_STATUS_LABEL[c.status] || c.status}</Pill> },
        { key: "charged_minor", label: "Charged", align: "right", render: c => c.charged_minor ? cvMoney(c.charged_minor, c.currency) : "—" },
        { key: "dispute_window_ends_at", label: "Disputable until", render: c => c.status === "billed" ? cvWhen(c.dispute_window_ends_at) : "—" },
        { key: "_a", label: "", sortable: false, align: "right", render: c => <div style={{ display: "flex", gap: 6, justifyContent: "flex-end" }}>
            {onReview && c.status === "held" && <>
              <Btn size="sm" kind="primary" disabled={busy} onClick={() => onReview(c, "approve")}>Approve</Btn>
              <Btn size="sm" disabled={busy} onClick={() => onReview(c, "reject")}>Reject</Btn>
            </>}
            {onDispute && cvDisputable(c) && <Btn size="sm" kind="ghost" disabled={busy} onClick={() => onDispute(c)}>Dispute</Btn>}
          </div> },
      ]}/>
);

// Reason prompt for disputes and rejections. A plain prompt keeps this screen dependency-free.
const cvAskReason = (what) => {
  const r = window.prompt(`${what} — reason (shown on the record):`);
  return r && r.trim().length >= 3 ? r.trim() : null;
};

// =================================================================
// Brand portal
// =================================================================
const BrandConversionsScreen = ({ data }) => {
  const live = advLive();
  const token = loadAdvToken();
  const [keys, setKeys] = React.useState(live ? [] : (data.conversionKeys || []));
  const [conversions, setConversions] = React.useState(live ? [] : (data.conversions || []));
  const [issued, setIssued] = React.useState(null);
  const [status, setStatus] = React.useState("");
  const [busy, setBusy] = React.useState(false);

  const load = React.useCallback(async () => {
    if (!live) return;
    if (!token) { setStatus("Sign in on Login & Profile to manage conversion tracking."); return; }
    try {
      const [k, c] = await Promise.all([
        advCall("/advertiser/v1/conversion-keys", { method: "GET", token }),
        advCall("/advertiser/v1/conversions", { method: "GET", token }),
      ]);
      setKeys(k.keys || []); setConversions(c.conversions || []); setStatus("");
    } catch (e) { setStatus(e.message || "Could not load conversion tracking."); }
  }, [live, token]);
  React.useEffect(() => { load(); }, [load]);

  const act = async (fn) => { setBusy(true); try { await fn(); } catch (e) { setStatus(e.message || "That did not work."); } finally { setBusy(false); } };

  const create = (kind) => act(async () => {
    if (!live) {
      const key = { key_id: cvDemoKeyId(kind), advertiser_id: "adv_demo", kind, status: "active", label: null, expires_at: null, rotated_to: null, created_at: new Date().toISOString(), revoked_at: null, last_used_at: null };
      setKeys(ks => [key, ...ks]);
      setIssued({ key, secret: kind === "postback" ? "dcs_demo-secret-not-valid-anywhere" : null, replaced: null });
      setStatus("Demo mode: this key is not real and will not verify anything.");
      return;
    }
    const out = await advCall("/advertiser/v1/conversion-keys", { token, body: { kind } });
    setIssued(out); await load();
  });
  const rotate = (k) => act(async () => {
    if (!live) { setStatus("Demo mode: rotation issues a new key and keeps the old one working for 24 hours."); return; }
    const out = await advCall(`/advertiser/v1/conversion-keys/${encodeURIComponent(k.key_id)}/rotate`, { token, body: {} });
    setIssued(out); await load();
  });
  const revoke = (k) => act(async () => {
    if (!window.confirm(`Revoke ${k.key_id}? Conversions signed or sent with it are refused from now on.`)) return;
    if (!live) { setKeys(ks => ks.map(x => x.key_id === k.key_id ? { ...x, status: "revoked", revoked_at: new Date().toISOString() } : x)); return; }
    await advCall(`/advertiser/v1/conversion-keys/${encodeURIComponent(k.key_id)}/revoke`, { token, body: {} });
    await load();
  });
  const dispute = (c) => act(async () => {
    const reason = cvAskReason("Dispute this conversion (refunds your wallet)");
    if (!reason) return;
    if (!live) { setConversions(cs => cs.map(x => x.conversion_id === c.conversion_id ? { ...x, status: "disputed", dispute_reason: reason } : x)); setStatus("Demo mode: nothing was refunded."); return; }
    await advCall(`/advertiser/v1/conversions/${encodeURIComponent(c.conversion_id)}/dispute`, { token, body: { reason } });
    setStatus("Disputed. Your wallet is refunded and the publisher's share reversed."); await load();
  });

  const billed = conversions.filter(c => c.status === "billed");
  const held = conversions.filter(c => c.status === "held");
  const currency = billed[0]?.currency || "USD";

  return <Screen>
    <PageHead icon={IconGauge} title="Conversions" subtitle="CPA campaigns are charged per conversion you report against a DropCap click. Server postbacks are billed on arrival; pixel conversions are held for review."
      actions={<Btn onClick={() => window.__nav("advertiser-conversion-guide")}>Install guide</Btn>}/>
    {!live && <Banner tone="info" icon={<IconInfo size={15}/>}>Demo mode — sample keys and conversions. Nothing is issued or charged.</Banner>}
    {status && <Banner tone="info" icon={<IconInfo size={15}/>}>{status}</Banner>}
    <SecretBanner issued={issued} onDismiss={() => setIssued(null)}/>
    <KpiGrid cols={3}>
      <StatCard label="Billed" value={String(billed.length)} sub={cvMoney(billed.reduce((s, c) => s + Number(c.charged_minor || 0), 0), currency)}/>
      <StatCard label="Held for review" value={String(held.length)} sub="Pixel conversions: not charged until approved"/>
      <StatCard label="Disputed" value={String(conversions.filter(c => c.status === "disputed").length)} sub="Refunded inside the 30-day window"/>
    </KpiGrid>
    <ConversionKeysCard keys={keys} busy={busy} onCreate={create} onRotate={rotate} onRevoke={revoke}
      subtitle="A postback key signs your server's reports (its secret is shown once). A pixel key is public and goes in your page."/>
    <Card title="Reported conversions" subtitle="One conversion per click per type. You may dispute a billed conversion for 30 days; the charge is refunded to your wallet." padded={false}>
      <ConversionsTable rows={conversions} busy={busy} onDispute={dispute}/>
    </Card>
  </Screen>;
};

// =================================================================
// Pixel install guide (brand portal)
// =================================================================
const BrandConversionGuideScreen = ({ data }) => {
  const live = advLive();
  const token = loadAdvToken();
  const [keys, setKeys] = React.useState(live ? [] : (data.conversionKeys || []));
  React.useEffect(() => {
    if (!live || !token) return;
    advCall("/advertiser/v1/conversion-keys", { method: "GET", token }).then(k => setKeys(k.keys || [])).catch(() => {});
  }, [live, token]);
  const active = (kind) => keys.find(k => k.kind === kind && k.status === "active" && !k.rotated_to);
  const pixelKey = active("pixel")?.key_id || "pk_YOUR_PIXEL_KEY";
  const postbackKey = active("postback")?.key_id || "ck_YOUR_POSTBACK_KEY";
  const api = cvApiBase();

  const landing = `<!-- 1. On every landing page: keep the DropCap click ID for 30 days -->
<script>
(function () {
  var m = location.search.match(/[?&]dcclid=([^&#]+)/) || location.hash.match(/dcclid=([^&]+)/);
  if (m) try { localStorage.setItem("dcclid", decodeURIComponent(m[1])); localStorage.setItem("dcclid_at", String(Date.now())); } catch (e) {}
})();
</script>`;
  const pixel = `<!-- 2. On the confirmation page, once per conversion -->
<script>
(function () {
  var clid; try { clid = localStorage.getItem("dcclid"); } catch (e) {}
  if (!clid) return;
  var q = "k=${pixelKey}&clid=" + encodeURIComponent(clid)
        + "&type=purchase&order=" + encodeURIComponent("ORDER_ID")   // your order / sign-up id
        + "&value=49.90&currency=INR";                                 // optional, informational
  new Image().src = "${api}/conversions/v1/pixel?" + q;
})();
</script>`;
  const postback = `# 3. Or, better: report it from your server (billed on arrival)
BODY='{"click_id":"<dcclid>","conversion_type":"purchase","order_id":"A-1001","value":49.9,"currency":"INR"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$DROPCAP_POSTBACK_SECRET" -hex | sed 's/^.* //')
curl -X POST ${api}/conversions/v1/postback \\
  -H "Content-Type: application/json" \\
  -H "X-DropCap-Key: ${postbackKey}" \\
  -H "X-DropCap-Timestamp: $TS" \\
  -H "X-DropCap-Signature: $SIG" \\
  --data "$BODY"`;

  return <Screen>
    <PageHead icon={IconList} title="Pixel install guide" subtitle="Tell DropCap when a reader who clicked your CPA ad converts."
      actions={<Btn onClick={() => window.__nav("advertiser-conversions")}>Keys & conversions</Btn>}/>
    <Card title="How it works">
      <ol style={{ margin: 0, paddingLeft: 18, display: "grid", gap: 6, fontSize: 13.5 }}>
        <li>A reader clicks your ad. DropCap sends them to your page with <code>dcclid</code> in the URL (and in the fragment).</li>
        <li>Keep that click ID. When they convert, report it with the click ID — from your page (pixel) or your server (postback).</li>
        <li>A conversion counts only within your campaign's attribution window (30 days by default), once per click per conversion type, and only for a click on one of your CPA campaigns.</li>
        <li>You are charged your CPA bid from your wallet. Server postbacks are billed on arrival. <strong>Pixel conversions are held for DropCap review and charged only if approved.</strong></li>
        <li>You can dispute a billed conversion for 30 days (a refund or cancelled order): your wallet is refunded.</li>
      </ol>
    </Card>
    <Card title="1 · Capture the click ID on your landing pages"><CodeBlock language="html">{landing}</CodeBlock></Card>
    <Card title="2 · Fire the pixel on your confirmation page" subtitle={active("pixel") ? `Using your pixel key ${pixelKey}.` : "Issue a pixel key on the Conversions page first; it replaces pk_YOUR_PIXEL_KEY."}>
      <CodeBlock language="html">{pixel}</CodeBlock>
      <div style={{ fontSize: 12.5, color: "var(--muted)", marginTop: 8 }}>
        Test it: open the pixel URL with a real click ID and read the <code>X-DropCap-Conversion</code> response header —
        <code> held</code> means received and awaiting review; <code>rejected_unknown_click</code> or <code>rejected_outside_window</code> mean the click ID is not one of yours or is too old.
        The pixel is rate limited per visitor.
      </div>
    </Card>
    <Card title="3 · Or report from your server (recommended)" subtitle="Signed with your postback secret: HMAC-SHA256 over timestamp + '.' + the exact request body. Timestamps more than 5 minutes off are refused, and a signed request is accepted once.">
      <CodeBlock language="bash">{postback}</CodeBlock>
      <div style={{ fontSize: 12.5, color: "var(--muted)", marginTop: 8 }}>
        Responses: <code>200</code> recorded (or <code>duplicate</code> — the same click and type was already reported; safe to retry with a new timestamp),
        <code> 401</code> bad key, signature or timestamp, <code>409</code> replayed request, <code>422</code> unknown click or outside the attribution window.
      </div>
    </Card>
  </Screen>;
};

// =================================================================
// Operator: hold queue, disputes, brand keys
// =================================================================
const OpConversionsScreen = ({ data }) => {
  const live = !!window.__api?.getConfig?.().live;
  const [rows, setRows] = React.useState(live ? [] : (data.conversions || []));
  const [brands, setBrands] = React.useState(live ? [] : (data.advertisers || []));
  const [brand, setBrand] = React.useState("");
  const [keys, setKeys] = React.useState([]);
  const [issued, setIssued] = React.useState(null);
  const [filter, setFilter] = React.useState("held");
  const [busy, setBusy] = React.useState(false);
  const notify = (m, tone) => window.__toast && window.__toast(m, tone || "good");

  const load = React.useCallback(async () => {
    if (!live) return;
    try {
      const out = await window.__api.call("adminListConversions", { query: filter ? { status: filter } : {} });
      setRows(out.conversions || []);
    } catch (e) { notify(e.message || "Could not load conversions.", "warn"); }
  }, [live, filter]);
  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => {
    if (!live) return;
    window.__api.call("listAdvertisers", { query: { limit: 100 } }).then(p => setBrands(p.advertisers || [])).catch(() => {});
  }, [live]);
  const loadKeys = React.useCallback(async () => {
    if (!brand) { setKeys([]); return; }
    if (!live) { setKeys((data.conversionKeys || []).map(k => ({ ...k, advertiser_id: brand }))); return; }
    try { setKeys((await window.__api.call("adminListConversionKeys", { params: { advertiser_id: brand } })).keys || []); }
    catch (e) { notify(e.message || "Could not load keys.", "warn"); }
  }, [brand, live]);
  React.useEffect(() => { loadKeys(); }, [loadKeys]);

  const act = async (fn) => { setBusy(true); try { await fn(); } catch (e) { notify(e.message || "That did not work.", "warn"); } finally { setBusy(false); } };

  const review = (c, decision) => act(async () => {
    const reason = decision === "reject" ? cvAskReason("Reject this conversion (it will never be charged)") : null;
    if (decision === "reject" && !reason) return;
    if (!live) { setRows(rs => rs.map(x => x.conversion_id === c.conversion_id ? { ...x, status: decision === "approve" ? "billed" : "rejected" } : x)); notify("Demo mode: nothing was charged."); return; }
    const out = await window.__api.call("adminReviewConversion", { params: { conversion_id: c.conversion_id }, body: { decision, ...(reason ? { reason } : {}) } });
    notify(decision === "approve" ? `Approved — ${out.conversion.status === "billed" ? `charged ${cvMoney(out.conversion.charged_minor, out.conversion.currency)}` : "the wallet could not pay; not billed"}.` : "Rejected. Nothing was charged.");
    await load();
  });
  const dispute = (c) => act(async () => {
    const reason = cvAskReason("Dispute this conversion (refunds the brand, reverses the publisher share)");
    if (!reason) return;
    if (!live) { setRows(rs => rs.map(x => x.conversion_id === c.conversion_id ? { ...x, status: "disputed", dispute_reason: reason } : x)); notify("Demo mode: nothing was refunded."); return; }
    await window.__api.call("adminDisputeConversion", { params: { conversion_id: c.conversion_id }, body: { reason } });
    notify("Disputed. The brand is refunded and the publisher's held share reversed."); await load();
  });
  const create = (kind) => act(async () => {
    if (!brand) return;
    if (!live) { notify("Demo mode: no key issued."); return; }
    setIssued(await window.__api.call("adminCreateConversionKey", { params: { advertiser_id: brand }, body: { kind } })); await loadKeys();
  });
  const rotate = (k) => act(async () => {
    if (!live) { notify("Demo mode: no key rotated."); return; }
    setIssued(await window.__api.call("adminRotateConversionKey", { params: { advertiser_id: brand, key_id: k.key_id }, body: {} })); await loadKeys();
  });
  const revoke = (k) => act(async () => {
    if (!window.confirm(`Revoke ${k.key_id}? The brand's conversions sent with it are refused from now on.`)) return;
    if (!live) { setKeys(ks => ks.map(x => x.key_id === k.key_id ? { ...x, status: "revoked" } : x)); return; }
    await window.__api.call("adminRevokeConversionKey", { params: { advertiser_id: brand, key_id: k.key_id }, body: {} }); await loadKeys();
  });

  const shown = live ? rows : rows.filter(r => !filter || r.status === filter);
  const brandName = (id) => brands.find(b => b.advertiser_id === id)?.name || id;

  return <Screen>
    <PageHead icon={IconGauge} title="Conversions" subtitle="CPA conversions reported by brands. Pixel conversions wait here until approved; approval charges the brand once, rejection charges nothing."/>
    {!live && <Banner tone="info" icon={<IconInfo size={15}/>}>Demo mode — sample conversions. Nothing is charged, refunded or issued.</Banner>}
    <Card title="Queue" padded={false} action={<Select value={filter} onChange={setFilter} options={[
      { label: "Held for review", value: "held" }, { label: "Billed", value: "billed" }, { label: "Disputed", value: "disputed" },
      { label: "Rejected", value: "rejected" }, { label: "Not billed", value: "not_billed" }, { label: "All", value: "" },
    ]}/>}>
      <ConversionsTable rows={shown.map(c => ({ ...c, conversion_type: `${c.conversion_type} · ${brandName(c.advertiser_id)}` }))} busy={busy} onReview={review} onDispute={dispute}/>
    </Card>
    <Card title="Brand keys" subtitle="Issue, rotate or revoke a brand's conversion keys. Managers see only brands assigned to them.">
      <div style={{ maxWidth: 360 }}>
        <Select value={brand} onChange={setBrand} options={[{ label: "Choose a brand…", value: "" }, ...brands.map(b => ({ label: b.name, value: b.advertiser_id }))]}/>
      </div>
    </Card>
    <SecretBanner issued={issued} onDismiss={() => setIssued(null)}/>
    {brand && <ConversionKeysCard keys={keys} busy={busy} onCreate={create} onRotate={rotate} onRevoke={revoke}
      subtitle={`Keys for ${brandName(brand)}. A new postback secret is shown once — pass it to the brand securely.`}/>}
  </Screen>;
};
