<script lang="ts">
  import { onMount } from "svelte";
  import { push } from "svelte-spa-router";
  import {
    signClaim,
    identityFromSeed,
    toPrettyJson,
    toMarkdown,
    toCompact,
    type SignedClaimEnvelope,
  } from "../lib/kez.js";
  import { addClaim, setSigchainSync } from "../lib/claims-store.js";
  import {
    appendSubjectEvent,
    resolveChainService,
  } from "../lib/sigchain-service.js";
  import { session } from "../lib/store.svelte.js";
  import {
    hasNip07,
    getNostrNpub,
    publishKezClaimToNostr,
    type PublishResult,
  } from "../lib/nip07.js";

  type ChannelKey = "github" | "dns" | "web" | "nostr" | "bluesky" | "ap";

  type Format = "compact" | "markdown" | "json";

  interface ChannelDef {
    key: ChannelKey;
    label: string;
    identifierPlaceholder: string;
    identifierLabel: string;
    /** Build the canonical KEZ identifier from the user's input. */
    toSubject: (raw: string) => string;
    /** Free-text instructions shown next to the publish artifact. */
    instructions: (subject: string) => string;
    /** Which encoding to show by default. */
    preferredFormat: Format;
  }

  const CHANNELS: ChannelDef[] = [
    {
      key: "github",
      label: "GitHub",
      identifierLabel: "GitHub username",
      identifierPlaceholder: "tudisco",
      toSubject: (s) => `github:${s.trim()}`,
      preferredFormat: "markdown",
      instructions: (subject) =>
        `1. Create a new PUBLIC gist on github.com.\n` +
        `2. Filename: \`kez.md\`\n` +
        `3. Paste the markdown block on the right as the file content.\n` +
        `4. (Alternative) Add the same markdown block to your <user>/<user> profile README.\n\n` +
        `A verifier resolving ${subject} will find the gist via the public gist listing API.`,
    },
    {
      key: "dns",
      label: "DNS",
      identifierLabel: "Domain",
      identifierPlaceholder: "tudisco.com",
      toSubject: (s) => `dns:${s.trim().toLowerCase()}`,
      preferredFormat: "compact",
      instructions: (subject) => {
        const domain = subject.slice(4);
        return (
          `1. Open your DNS registrar / nameserver console for ${domain}.\n` +
          `2. Add a TXT record:\n` +
          `       Name:  _kez.${domain}\n` +
          `       Value: (the kez:z1:… string on the right)\n` +
          `3. Save. Propagation can take a few minutes.\n\n` +
          `Notes:\n` +
          `- DNS TXT records have a 255-byte segment limit. Most registrars (Cloudflare, Route 53, Gandi, etc.) handle long values by splitting into multiple segments automatically — just paste the whole string. If your registrar refuses, see the FAQ.\n` +
          `- Verifiers resolve _kez.${domain} via DNS TXT lookups and decode the kez:z1: form to find your signed claim.`
        );
      },
    },
    {
      key: "web",
      label: "Your website",
      identifierLabel: "HTTPS URL",
      identifierPlaceholder: "https://tudisco.com",
      toSubject: (s) => `web:${s.trim().replace(/\/$/, "")}`,
      preferredFormat: "json",
      instructions: (subject) => {
        const base = subject.slice(4);
        return (
          `1. Save the JSON envelope on the right to a file named \`kez.json\`.\n` +
          `2. Upload it to your server at: ${base}/.well-known/kez.json\n` +
          `3. Make sure it's publicly fetchable (no auth, no robots.txt block).\n\n` +
          `Verifiers fetch ${base}/.well-known/kez.json directly.`
        );
      },
    },
    {
      key: "nostr",
      label: "Nostr",
      identifierLabel: "Your npub",
      identifierPlaceholder: "npub1...",
      toSubject: (s) => {
        const raw = s.trim();
        return raw.startsWith("nostr:") ? raw : `nostr:${raw}`;
      },
      preferredFormat: "markdown",
      instructions: () =>
        `Pick whichever is easiest — verifiers check all three:\n\n` +
        `A. Add the markdown block on the right to your nostr profile bio\n` +
        `   (the "About" field on Damus, primal.net, etc.). One-time edit,\n` +
        `   anyone fetching your profile sees the proof.\n\n` +
        `B. Post the markdown block as a normal nostr note (kind 1).\n` +
        `   Easiest if you're already active — just paste and publish.\n\n` +
        `C. (Advanced) Publish a NIP-78 kind-30078 event with d='kez' and\n` +
        `   the JSON envelope as the event 'content'. Cleanest for tooling\n` +
        `   but needs a client that exposes kind-30078 creation.`,
    },
    {
      key: "bluesky",
      label: "Bluesky",
      identifierLabel: "Bluesky handle",
      identifierPlaceholder: "tudisco.bsky.social",
      toSubject: (s) => `bluesky:${s.trim()}`,
      preferredFormat: "markdown",
      instructions: (subject) =>
        `1. Open Bluesky and start a new post on your account (${subject.slice(8)}).\n` +
        `2. Paste the markdown block on the right as the post body.\n` +
        `3. Publish the post (publicly).\n\n` +
        `Verifiers scan your public posts looking for the kez fence.`,
    },
    {
      key: "ap",
      label: "ActivityPub (Mastodon, Pleroma, …)",
      identifierLabel: "ActivityPub handle",
      identifierPlaceholder: "tudisco@mastodon.social",
      toSubject: (s) => {
        const raw = s.trim().replace(/^@/, "");
        return `ap:@${raw}`;
      },
      preferredFormat: "markdown",
      instructions: (subject) =>
        `1. Go to your profile settings on your instance.\n` +
        `2. Either paste the markdown block on the right into your "Profile fields" / metadata,\n` +
        `   or post it as a pinned post.\n\n` +
        `Verifiers resolve ${subject} via WebFinger then fetch the actor JSON.`,
    },
  ];

  let step = $state<"pick" | "identifier" | "sign" | "publish" | "done">("pick");
  let selected = $state<ChannelDef | null>(null);
  let identifierInput = $state("");
  let envelope = $state<SignedClaimEnvelope | null>(null);
  let format = $state<Format>("compact");
  let copied = $state(false);
  /** Result of one-click "Publish via Nostr extension" — shown on the publish step. */
  let nostrPublish = $state<
    | { status: "idle" }
    | { status: "pending" }
    | { status: "ok"; result: PublishResult }
    | { status: "error"; message: string }
  >({ status: "idle" });

  /** Outcome of mirroring this claim into the user's sigchain on the chain service. */
  let sigchainSync = $state<
    | { status: "idle" }
    | { status: "pending" }
    | { status: "ok"; seq: number; noop: boolean }
    | { status: "error"; message: string }
  >({ status: "idle" });

  /** Re-evaluated each render; cheap (just a typeof check on window.nostr). */
  const nip07Available = $derived(hasNip07());

  /**
   * Render the current envelope in the chosen format.
   * Async because the compact form initializes the zstd WASM module on
   * first use (subsequent calls are sync). Returns "" if no envelope.
   */
  async function renderArtifact(
    env: SignedClaimEnvelope,
    fmt: Format,
  ): Promise<string> {
    if (fmt === "markdown") return toMarkdown(env);
    if (fmt === "json") return toPrettyJson(env);
    return await toCompact(env);
  }

  onMount(() => {
    if (!session.unlocked) push("/unlock");
  });

  function pickChannel(c: ChannelDef) {
    selected = c;
    format = c.preferredFormat; // DNS → compact, github → markdown, web → json
    step = "identifier";
  }

  async function fillFromNostrExtension() {
    try {
      identifierInput = await getNostrNpub();
    } catch (e) {
      alert(`Couldn't read pubkey from nostr extension: ${(e as Error).message}`);
    }
  }

  async function publishViaNostrExtension() {
    if (!envelope) return;
    nostrPublish = { status: "pending" };
    try {
      const markdown = toMarkdown(envelope);
      const result = await publishKezClaimToNostr(markdown);
      nostrPublish = { status: "ok", result };
    } catch (e) {
      nostrPublish = { status: "error", message: (e as Error).message };
    }
  }

  function buildAndSign() {
    if (!selected || !session.unlocked) return;
    const subject = selected.toSubject(identifierInput);
    if (!subject.includes(":") || subject.endsWith(":")) {
      alert("Identifier looks empty — please fill in the field.");
      return;
    }
    // UnlockedIdentity has {handle, server, primary, seed} — not the
    // Ed25519Identity {seed, publicKey, identity} shape signClaim wants.
    // Reconstruct from the seed so signer.identity is defined and
    // ends up in payload.primary + signature.key.
    const signer = identityFromSeed(session.unlocked.seed);
    envelope = signClaim(signer, subject);
    step = "publish";
  }

  async function copyArtifact() {
    if (!envelope) return;
    const text = await renderArtifact(envelope, format);
    await navigator.clipboard.writeText(text);
    copied = true;
    setTimeout(() => (copied = false), 1500);
  }

  async function saveAndDone() {
    if (!envelope || !selected || !session.unlocked) return;
    const id = crypto.randomUUID();
    const subject = envelope.payload.subject;
    try {
      // $state wraps `envelope` in a deep Proxy; structuredClone (used
      // by idb-keyval) can't clone proxies and throws DataCloneError.
      // $state.snapshot returns a plain, cloneable object.
      await addClaim({
        id,
        envelope: $state.snapshot(envelope) as SignedClaimEnvelope,
        channel: selected.key,
      });
      step = "done";
    } catch (e) {
      console.error("saveAndDone failed", e);
      alert(`Failed to save claim: ${(e as Error).message}`);
      return;
    }
    // Mirror the claim into the user's sigchain on the chain service: append
    // a signed `add` event for this subject. Best-effort — the claim is
    // already saved locally; if the chain service is unreachable we record
    // the error and the user can retry from the Claims page later.
    sigchainSync = { status: "pending" };
    try {
      const chainService = await resolveChainService(session.unlocked.handle);
      const proofUrl =
        nostrPublish.status === "ok" ? nostrPublish.result.evidence_url : undefined;
      const result = await appendSubjectEvent({
        seed: session.unlocked.seed,
        chainService,
        subject,
        op: "add",
        proofUrl,
      });
      await setSigchainSync(id, {
        chain_service: result.chainService,
        sigchain_seq: result.seq,
        sigchain_status: "synced",
      });
      sigchainSync = { status: "ok", seq: result.seq, noop: result.noop };
    } catch (e) {
      await setSigchainSync(id, {
        sigchain_status: "error",
        sigchain_error: (e as Error).message,
      });
      sigchainSync = { status: "error", message: (e as Error).message };
    }
  }
</script>

<div class="space-y-6">
  <div class="flex items-center justify-between">
    <h1 class="text-2xl font-bold text-text">Add a claim</h1>
    <button
      class="text-sm text-text-muted hover:text-text"
      onclick={() => push("/claims")}
    >
      ← Back to claims
    </button>
  </div>

  <!-- Stepper -->
  <ol class="flex gap-2 text-xs text-text-muted">
    <li class={step === "pick" ? "font-semibold text-text" : ""}>1. Channel</li>
    <li>→</li>
    <li class={step === "identifier" ? "font-semibold text-text" : ""}>2. Identifier</li>
    <li>→</li>
    <li class={step === "publish" ? "font-semibold text-text" : ""}>3. Publish</li>
    <li>→</li>
    <li class={step === "done" ? "font-semibold text-text" : ""}>4. Done</li>
  </ol>

  {#if step === "pick"}
    <div class="grid sm:grid-cols-2 gap-3">
      {#each CHANNELS as c}
        <button
          class="text-left border border-border rounded-lg p-4 bg-surface hover:border-accent-dim transition"
          onclick={() => pickChannel(c)}
        >
          <p class="font-semibold text-text">{c.label}</p>
          <p class="text-xs text-text-muted mt-1 font-mono">{c.key}:&lt;…&gt;</p>
        </button>
      {/each}
    </div>
  {/if}

  {#if step === "identifier" && selected}
    <form
      class="space-y-4"
      onsubmit={(e) => { e.preventDefault(); buildAndSign(); }}
    >
      <div>
        <label class="block text-sm font-medium text-text-secondary" for="ident">
          {selected.identifierLabel}
        </label>
        <input
          id="ident"
          type="text"
          bind:value={identifierInput}
          placeholder={selected.identifierPlaceholder}
          class="mt-1 w-full px-3 py-2 border border-border rounded-md font-mono"
          autocomplete="off"
        />
        {#if selected.key === "nostr" && nip07Available}
          <button
            type="button"
            class="mt-2 inline-flex items-center gap-1 px-3 py-1.5 text-xs border border-accent/40 bg-accent/10 text-accent rounded-md hover:bg-accent/20"
            onclick={fillFromNostrExtension}
          >
            ⚡ Use my nostr extension
          </button>
          <span class="ml-2 text-xs text-text-muted">Reads your pubkey via NIP-07 — no copy/paste.</span>
        {/if}
        {#if identifierInput.trim()}
          <p class="mt-1 text-xs text-text-muted">
            Subject will be: <code class="bg-elevated px-1 rounded">{selected.toSubject(identifierInput)}</code>
          </p>
        {/if}
      </div>

      <div class="flex gap-2">
        <button
          type="button"
          class="px-4 py-2 border border-border rounded-md text-text-secondary hover:bg-elevated"
          onclick={() => { step = "pick"; selected = null; }}
        >
          Back
        </button>
        <button
          type="submit"
          class="px-4 py-2 bg-accent text-accent-contrast rounded-md hover:bg-accent-dim disabled:opacity-50"
          disabled={identifierInput.trim().length === 0}
        >
          Sign claim
        </button>
      </div>
    </form>
  {/if}

  {#if step === "publish" && envelope && selected}
    <div class="grid lg:grid-cols-2 gap-6">
      <section class="space-y-3">
        <h2 class="text-sm font-semibold text-text-secondary uppercase tracking-wide">
          1. Publish on {selected.label}
        </h2>
        <pre class="whitespace-pre-wrap text-sm bg-elevated border border-border rounded p-4 leading-relaxed">{selected.instructions(envelope.payload.subject)}</pre>
      </section>

      <section class="space-y-3">
        <div class="flex items-center gap-2 flex-wrap">
          <h2 class="text-sm font-semibold text-text-secondary uppercase tracking-wide flex-1">
            2. Copy this:
          </h2>
          <div class="flex border border-border rounded overflow-hidden text-xs">
            <button
              class={`px-2 py-1 ${format === "compact" ? "bg-accent text-accent-contrast" : "bg-surface text-text-secondary hover:bg-elevated"}`}
              onclick={() => (format = "compact")}
              title="kez:z1: — zstd + base64url. Fits in tight places like DNS TXT records, QR codes, chat messages."
            >compact</button>
            <button
              class={`px-2 py-1 border-l border-border ${format === "markdown" ? "bg-accent text-accent-contrast" : "bg-surface text-text-secondary hover:bg-elevated"}`}
              onclick={() => (format = "markdown")}
              title="Human-readable; embeds the JSON inside a ```kez fence. Best for gists and README files."
            >markdown</button>
            <button
              class={`px-2 py-1 border-l border-border ${format === "json" ? "bg-accent text-accent-contrast" : "bg-surface text-text-secondary hover:bg-elevated"}`}
              onclick={() => (format = "json")}
              title="Raw envelope JSON. Best for .well-known/kez.json and developer tooling."
            >JSON</button>
          </div>
        </div>

        {#await renderArtifact(envelope, format)}
          <pre class="text-xs bg-accent text-text rounded p-4 font-mono leading-relaxed max-h-96 min-h-32 flex items-center justify-center text-text-muted">Computing…</pre>
        {:then text}
          <pre class="text-xs bg-accent text-text rounded p-4 overflow-x-auto font-mono leading-relaxed max-h-96 overflow-y-auto whitespace-pre-wrap break-all">{text}</pre>
          {#if format === "compact"}
            <p class="text-xs text-text-muted">
              {text.length} chars · zstd-compressed signed envelope, base64url-encoded.
            </p>
          {/if}
        {:catch e}
          <pre class="text-xs bg-danger/10 border border-danger/40 text-danger rounded p-4">Error: {e.message}</pre>
        {/await}

        <button
          class="px-3 py-2 text-sm border border-border rounded-md text-text-secondary hover:bg-elevated"
          onclick={copyArtifact}
        >
          {copied ? "✓ Copied" : "Copy to clipboard"}
        </button>
      </section>
    </div>

    {#if selected.key === "nostr" && nip07Available}
      <div class="border border-accent/40 bg-accent/10 rounded-lg p-4">
        <p class="text-sm font-semibold text-accent">⚡ One-click publish via your nostr extension</p>
        <p class="mt-1 text-xs text-accent">
          Wraps the markdown block in a normal nostr post (kind 1), asks
          your extension to sign it, and broadcasts to the relay pool.
          Verifiers (web + Rust CLI) will pick it up automatically.
        </p>
        <div class="mt-3 flex items-center gap-3">
          <button
            class="px-3 py-1.5 text-sm bg-accent text-accent-contrast rounded-md hover:bg-accent-dim disabled:opacity-50"
            onclick={publishViaNostrExtension}
            disabled={nostrPublish.status === "pending"}
          >
            {nostrPublish.status === "pending" ? "Publishing…" : "Publish to nostr"}
          </button>
          {#if nostrPublish.status === "ok"}
            <span class="text-xs text-verified">
              ✓ Posted to {nostrPublish.result.ok.length} relay(s).
              <a
                href={nostrPublish.result.evidence_url}
                target="_blank"
                rel="noopener noreferrer"
                class="underline"
              >view on njump.me</a>
            </span>
          {:else if nostrPublish.status === "error"}
            <span class="text-xs text-danger">✗ {nostrPublish.message}</span>
          {/if}
        </div>
        {#if nostrPublish.status === "ok" && nostrPublish.result.failed.length > 0}
          <p class="mt-2 text-xs text-warning">
            {nostrPublish.result.failed.length} relay(s) didn't ack:
            {nostrPublish.result.failed.map((f) => f.relay).join(", ")}
          </p>
        {/if}
      </div>
    {/if}

    <div class="flex gap-2 pt-4 border-t border-border">
      <button
        class="px-4 py-2 border border-border rounded-md text-text-secondary hover:bg-elevated"
        onclick={() => { step = "identifier"; }}
      >
        Back
      </button>
      <button
        class="px-4 py-2 bg-accent text-accent-contrast rounded-md hover:bg-accent-dim"
        onclick={saveAndDone}
      >
        Save claim
      </button>
    </div>
  {/if}

  {#if step === "done" && envelope}
    <div class="border border-verified/40 bg-verified/10 rounded-lg p-6">
      <p class="text-lg font-semibold text-verified">✓ Claim saved</p>
      <p class="mt-2 text-sm text-verified">
        You signed a claim for
        <code class="font-mono">{envelope.payload.subject}</code>.
        Once you've published the proof on that channel, come back to the
        Claims page and mark it published.
      </p>

      <!-- Chain-service (sigchain) mirror status -->
      <div class="mt-3 text-sm">
        {#if sigchainSync.status === "pending"}
          <p class="text-text-secondary">⏳ Updating your sigchain on the chain service…</p>
        {:else if sigchainSync.status === "ok"}
          <p class="text-verified">
            {#if sigchainSync.noop}
              ⛓ Already on your sigchain (seq {sigchainSync.seq}) — nothing to add.
            {:else}
              ⛓ Sigchain updated — added at seq {sigchainSync.seq} on the chain service.
            {/if}
          </p>
        {:else if sigchainSync.status === "error"}
          <p class="text-warning">
            ⚠ Couldn't update your sigchain on the chain service:
            {sigchainSync.message}. The claim is saved locally — retry the
            sigchain sync from the Claims page.
          </p>
        {/if}
      </div>

      <div class="mt-4 flex gap-2">
        <a
          href="#/claims"
          class="px-4 py-2 bg-accent text-accent-contrast rounded-md hover:bg-accent-dim no-underline"
        >
          Back to claims
        </a>
        <button
          class="px-4 py-2 border border-verified/40 bg-surface rounded-md text-verified hover:bg-elevated"
          onclick={() => {
            step = "pick";
            selected = null;
            identifierInput = "";
            envelope = null;
            nostrPublish = { status: "idle" };
            sigchainSync = { status: "idle" };
          }}
        >
          Add another
        </button>
      </div>
    </div>
  {/if}
</div>
