// `/` — the collaborative document editor. Auto-mapped from src/pages/page.tsx
// by the framework's file-convention page discovery.
//
// THIS is local-first collaboration over the reactive loop. The api's
// `documents` table has a `crdtText()` `body`; this page binds ONE
// `useCrdtText` cell (`@voltro/local-first/react`) to it:
//   1. `useSubscription('app','documents.list')` — streams the shared document
//      (with its MERGED body) live over WebSocket; the streamed body is the
//      hook's `remote`, folded into the local CRDT on every change,
//   2. `useMutation('app','documents.setBody')` is the hook's `push` — every
//      keystroke goes through `setText`, which diffs to a minimal single-span
//      edit (the framework's `crdtTextEdit`) and queues the encoded update.
//
// The convergence is authoritative and SERVER-SIDE: the api merges each update
// into the stored state before broadcasting, so two tabs typing at once both
// survive — no last-write-wins loser. Open this page in two tabs to see it.
//
// This is the app-level binding the local-first docs describe — the two names
// nothing can derive: `push` = the setBody mutation, `remote` = the documents
// subscription. Everything else (sync client lifecycle, offline queue, edit
// encoding) lives in the hook.

import { useMutation, useSubscription } from '@voltro/client'
import { useMemo, type ReactNode } from 'react'
import type { PageMeta } from '@voltro/web'
import { T, useTFn } from '@voltro/i18n'
import type { CrdtState } from '@voltro/local-first'
import { useCrdtText } from '@voltro/local-first/react'
import { getCatalog } from '../locales'

export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
  title: getCatalog(locale)['meta.editor.title'],
})

// The dev AuthMiddleware resolves the tenant from the `x-tenant` header,
// defaulting to 'acme'; the `documents` table's `tenant()` mixin auto-scopes
// reads/writes to it.
const TENANT = 'acme'

interface DocRow {
  readonly id: string
  readonly title: string
  // The encoded CRDT state. The api declares `body` as
  // `Schema.Uint8ArrayFromBase64`, so the rpc client hands it back as a
  // `Uint8Array`; we still accept a raw base64 `string` defensively.
  readonly body: Uint8Array | string | null
  readonly tenantId: string
  readonly createdAt: Date
}

const base64ToBytes = (b64: string): Uint8Array =>
  Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0))

// One editor bound to ONE document. Remounted (via `key={doc.id}`) when the
// bound document changes, so the hook's sync-client lifecycle is scoped to it.
const DocumentEditor = ({
  doc,
  saving,
  onUpdate,
}: {
  readonly doc: DocRow
  readonly saving: boolean
  readonly onUpdate: (update: CrdtState) => Promise<unknown>
}): ReactNode => {
  const t = useTFn()
  // The authoritative state as the subscription streams it. `null` while the
  // body is absent — useCrdtText folds nothing then, NOT an empty document.
  const remote = useMemo(
    () =>
      doc.body instanceof Uint8Array
        ? doc.body
        : typeof doc.body === 'string'
          ? base64ToBytes(doc.body)
          : null,
    [doc.body],
  )
  const body = useCrdtText({
    cell: { table: 'documents', id: doc.id, column: 'body' },
    remote,
    // A rejected push re-drives the queued write; log it and keep the rejection.
    push: (write) =>
      onUpdate(write.update).catch((mutationError: unknown) => {
        console.error('documents.setBody failed:', mutationError)
        throw mutationError
      }),
  })

  return (
    <section>
      <p style={{ color: '#666', fontSize: 13, margin: '0 0 8px' }}>
        <T id="editor.docLabel" values={{ title: doc.title, strong: (c: ReactNode) => <strong>{c}</strong> }} />
        {saving ? <T id="editor.status.saving" /> : null}
      </p>
      <textarea
        value={body.text}
        onChange={(event) => body.setText(event.target.value)}
        placeholder={t('editor.placeholder')}
        rows={12}
        style={{ width: '100%', padding: '10px 12px', fontSize: 14, fontFamily: 'ui-monospace, monospace', resize: 'vertical', boxSizing: 'border-box' }}
      />
      <p style={{ color: '#999', fontSize: 12, marginTop: 8 }}><T id="editor.hint" /></p>
    </section>
  )
}

const IndexPage = (): ReactNode => {
  const { data, revision, error } = useSubscription<ReadonlyArray<DocRow>>('app', 'documents.list')
  const create = useMutation<{ tenantId: string; title: string }>('app', 'documents.create')
  const setBody = useMutation<{ id: string; update: CrdtState }>('app', 'documents.setBody')

  const docs = data ?? []
  const doc = docs[0]

  const handleCreate = async (): Promise<void> => {
    try {
      await create.mutate({ tenantId: TENANT, title: 'Shared document' })
    } catch (mutationError) {
      console.error('documents.create failed:', mutationError)
    }
  }

  // Returned promise = the hook's ack; a rejection triggers its bounded retry.
  const handleUpdate = (id: string) => (update: CrdtState): Promise<unknown> =>
    setBody.mutate({ id, update })

  return (
    <div>
      <h1 style={{ fontSize: 24, marginBottom: 4 }}><T id="editor.heading" /></h1>
      <p style={{ color: '#444', marginTop: 0, fontSize: 14 }}>
        <T
          id="editor.intro"
          values={{ strong: (c: ReactNode) => <strong>{c}</strong>, code: (c: ReactNode) => <code>{c}</code> }}
        />
      </p>
      <p style={{ color: '#666', marginTop: 0, fontSize: 13 }}>
        <T id="editor.status.tenant" values={{ tenant: TENANT, revision, code: (c: ReactNode) => <code>{c}</code> }} />
        {data === undefined && error === undefined && <T id="editor.status.loading" />}
      </p>

      {error !== undefined && (
        <p style={{ color: '#c00', fontSize: 13 }}>
          <T id="editor.error" values={{ error: String(error) }} />
        </p>
      )}

      {doc === undefined && data !== undefined ? (
        <section style={{ border: '1px dashed #ccc', borderRadius: 8, padding: 24, textAlign: 'center' }}>
          <h2 style={{ fontSize: 16, margin: '0 0 4px' }}><T id="editor.empty.heading" /></h2>
          <p style={{ color: '#777', fontSize: 13, marginTop: 0 }}><T id="editor.empty.body" /></p>
          <button type="button" onClick={handleCreate} disabled={create.pending}>
            {create.pending ? <T id="editor.creating" /> : <T id="editor.create" />}
          </button>
        </section>
      ) : null}

      {doc !== undefined ? (
        <DocumentEditor key={doc.id} doc={doc} saving={setBody.pending} onUpdate={handleUpdate(doc.id)} />
      ) : null}
    </div>
  )
}

export default IndexPage
