import { html } from '@webjsdev/core';

export const metadata = { title: 'Server Actions | webjs' };

export default function ServerActions() {
  return html`
    <h1>Server Actions</h1>
    <p>Server actions are <strong>async functions that run exclusively on the server</strong> but can be imported and called from client-side web components as if they were local functions. webjs rewrites the import at serve time so the browser receives a thin RPC stub instead of the real implementation. The result is full-stack type safety with zero manual API layer.</p>

    <h2>Defining a Server Action: the Two-Marker Convention</h2>
    <p>Server-side files use <strong>two complementary markers</strong>. The combination determines behaviour:</p>

    <table>
      <thead>
        <tr><th>File</th><th><code>'use server'</code>?</th><th>What it is</th></tr>
      </thead>
      <tbody>
        <tr><td><code>*.server.{js,ts}</code></td><td>yes</td><td><strong>Server action.</strong> Source-protected AND RPC-callable: client imports are rewritten to RPC stubs that POST to <code>/__webjs/action/&lt;hash&gt;/&lt;fn&gt;</code>.</td></tr>
        <tr><td><code>*.server.{js,ts}</code></td><td>no</td><td><strong>Server-only utility.</strong> Source-protected; browser imports get a throw-at-load stub. Use for the Prisma singleton, session helpers, password hashing.</td></tr>
        <tr><td>Plain <code>.ts</code></td><td>yes</td><td><strong>Lint violation</strong> (<code>use-server-needs-extension</code>). The directive alone is silently ignored, and the file serves to the browser as plain source. Rename to add the <code>.server.</code> infix.</td></tr>
        <tr><td>Plain <code>.ts</code></td><td>no</td><td>Browser-safe; standard behaviour.</td></tr>
      </tbody>
    </table>

    <p>Bottom line: the <code>.server.{js,ts}</code> infix is the path-level boundary (the HTTP layer refuses to serve the source to the browser, full stop). The <code>'use server'</code> directive is what registers the exported functions as RPC endpoints. You need both for a server action.</p>

    <h3>Server action (path boundary + RPC marker)</h3>
    <pre>// modules/posts/actions/create-post.server.ts
'use server';
import { prisma } from '../../../lib/prisma.server.ts';

export async function createPost(input: { title: string; body: string }) {
  const post = await prisma.post.create({ data: input });
  return post;
}

export async function deletePost(id: number) {
  await prisma.post.delete({ where: { id } });
  return { ok: true };
}</pre>

    <h3>Server-only utility (path boundary, no RPC)</h3>
    <p>Drop the <code>'use server'</code> directive when the file should be unreachable from the browser but is NOT called as a server action (Prisma singleton, password hashing, helpers used by other server files). Browser imports throw at load time.</p>

    <pre>// lib/prisma.server.ts
import { PrismaClient } from '@prisma/client';

declare global { var __prisma: PrismaClient | undefined; }
export const prisma = globalThis.__prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalThis.__prisma = prisma;</pre>

    <p><strong>Do not import a server-only utility directly into a page, layout, or component.</strong> The stub throws when the module <em>loads</em> (not when called), so it works during SSR but crashes when a component hydrates or a page/layout module loads in the browser. Use server-only utilities <em>inside</em> server actions, <code>route.{js,ts}</code> handlers, or <code>middleware</code> (which only ever run on the server). A page reaches server logic by importing a <code>'use server'</code> <strong>action</strong> instead: its RPC stub loads safely on the client and isn't even called there.</p>

    <h2>How the Import Rewrite Works</h2>
    <p>When the browser requests a server module's URL (e.g. <code>/actions/posts.server.ts</code>), the dev server intercepts the request. Instead of serving the real file (which contains database calls, secrets, etc.), it generates and serves a <strong>client stub</strong>:</p>

    <pre>// Generated by webjs (you never see this file)
import { stringify as __wjStringify, parse as __wjParse } from '@webjsdev/core';

function __csrf() {
  const m = document.cookie.match(/(?:^|;\s*)webjs_csrf=([^;]+)/);
  return m ? decodeURIComponent(m[1]) : '';
}

async function __rpc(fn, args) {
  const body = await __wjStringify(args);
  const res = await fetch('/__webjs/action/a1b2c3d4e5/' + fn, {
    method: 'POST',
    headers: {
      'content-type': 'application/vnd.webjs+json',
      'x-webjs-csrf': __csrf()
    },
    credentials: 'same-origin',
    body
  });
  const ct = res.headers.get('content-type') || '';
  const text = await res.text();
  const parsed = ct.includes('application/vnd.webjs+json')
    ? __wjParse(text)
    : (ct.includes('application/json') ? JSON.parse(text) : text);
  if (!res.ok) {
    const msg = (parsed &amp;&amp; parsed.error) || ('webjs action ' + fn + ' -> ' + res.status);
    throw new Error(msg);
  }
  return parsed;
}

export const createPost = (...args) => __rpc('createPost', args);
export const deletePost = (...args) => __rpc('deletePost', args);</pre>

    <p>The hash (<code>a1b2c3d4e5</code>) is a SHA-256 digest of the file's absolute path, computed when the action index is first built (lazily, on the first request, not at boot). The stub's function signatures match the original exports, so TypeScript sees the real types through the import.</p>

    <h2>Full-Stack Type Safety</h2>
    <p>Because the browser's import statement points at the real <code>.server.ts</code> file, TypeScript's language server resolves types from the original source. Your editor shows the correct parameter types, return types, and JSDoc comments. The rewrite happens only at runtime in the browser, so the type checker never sees the stub.</p>

    <pre>// components/post-form.ts
import { WebComponent, html, css } from '@webjsdev/core';
import { createPost } from '../actions/posts.server.ts';
//                        ^-- TS sees: (input: { title: string; body: string }) => Promise&lt;Post&gt;

export class PostForm extends WebComponent {
  static styles = css\`:host { display: block; }\`;

  async handleSubmit(e: Event) {
    e.preventDefault();
    const form = (e.target as HTMLFormElement);
    const title = (form.elements.namedItem('title') as HTMLInputElement).value;
    const body = (form.elements.namedItem('body') as HTMLTextAreaElement).value;
    const post = await createPost({ title, body });
    //    ^-- post is typed as Post, with real Date objects (not strings)
    this.dispatchEvent(new CustomEvent('created', { detail: post }));
  }

  render() {
    return html\`
      &lt;form @submit=\${(e: Event) =&gt; this.handleSubmit(e)}&gt;
        &lt;input name="title" placeholder="Title" required /&gt;
        &lt;textarea name="body" placeholder="Write something..." required&gt;&lt;/textarea&gt;
        &lt;button type="submit"&gt;Publish&lt;/button&gt;
      &lt;/form&gt;
    \`;
  }
}
PostForm.register('post-form');</pre>

    <h2>The Wire Format</h2>
    <p>Server actions use <strong>webjs's built-in serializer</strong> (a pure-ESM, dependency-free implementation in <code>@webjsdev/core</code>) for the RPC wire, not plain <code>JSON.stringify</code>. The content type is <code>application/vnd.webjs+json</code>. Rich JavaScript types survive the round trip:</p>
    <ul>
      <li><code>Date</code> objects: arrive as real <code>Date</code> instances, not ISO strings</li>
      <li><code>Map</code> and <code>Set</code>: preserved as their native types</li>
      <li><code>BigInt</code>: preserved (plain JSON throws on BigInt)</li>
      <li><code>undefined</code>: distinguished from <code>null</code></li>
      <li><code>NaN</code>, <code>Infinity</code>, <code>-0</code>: preserved (plain JSON drops these)</li>
      <li><code>Error</code>: name + message + stack preserved</li>
      <li><code>TypedArray</code> (Int8...Float64), <code>ArrayBuffer</code>, <code>DataView</code>: inlined as base64</li>
      <li><code>Blob</code>, <code>File</code>, <code>FormData</code>: including binary content (file uploads through actions just work)</li>
      <li><code>Symbol.for(...)</code> registered symbols: preserved (local symbols throw a clear error)</li>
      <li>Reference cycles + shared references: preserved by id</li>
      <li>Nested combinations of all the above</li>
    </ul>
    <p>The stub serialises function arguments with <code>await stringify(args)</code> and deserialises the response with <code>parse(text)</code>. The server does the inverse. This is invisible to the developer, so you work with native types on both sides. The serializer is async because Blob/File/FormData require an <code>await arrayBuffer()</code>. For payloads without binary the async cost is just one Promise tick.</p>

    <h2>CSRF Protection</h2>
    <p>Every server action RPC call is protected against Cross-Site Request Forgery using a <strong>double-submit cookie</strong> pattern:</p>
    <ol>
      <li>The SSR response sets a <code>webjs_csrf</code> cookie (<code>SameSite=Lax</code>, readable by JavaScript, 7-day expiry). If the cookie already exists, it is left alone.</li>
      <li>The generated client stub reads this cookie and sends its value in the <code>x-webjs-csrf</code> request header on every action call.</li>
      <li>The server compares the header to the cookie using constant-time string comparison. If they do not match, the request is rejected with a 403.</li>
    </ol>
    <p>This works because a cross-origin attacker cannot read the victim's cookies (SameSite + same-origin policy), so they cannot set the header to the matching value. No server-side session store is needed.</p>

    <h2>expose(): REST Endpoints from Server Actions</h2>
    <p>The <code>expose()</code> function makes a server action available as a first-class REST endpoint in addition to its internal RPC URL. The same function implementation backs both call paths.</p>

    <pre>// actions/posts.server.ts
'use server';
import { expose } from '@webjsdev/core';
import { prisma } from '../lib/prisma.server.ts';

export const listPosts = expose('GET /api/posts', async () =&gt; {
  return prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
});

export const getPost = expose('GET /api/posts/:slug', async ({ slug }: { slug: string }) =&gt; {
  const post = await prisma.post.findUnique({ where: { slug } });
  if (!post) return new Response('Not found', { status: 404 });
  return post;
});

export const createPost = expose('POST /api/posts', async ({ title, body }: { title: string; body: string }) =&gt; {
  return prisma.post.create({ data: { title, body } });
});</pre>

    <p>Now <code>createPost</code> is reachable two ways:</p>
    <ul>
      <li><strong>From a client component:</strong> <code>import { createPost } from '../actions/posts.server.ts'</code> is auto-rewritten to an RPC stub, CSRF-protected, encoded with the webjs rich serializer.</li>
      <li><strong>From curl or another service:</strong> <code>POST /api/posts</code> with a JSON body. The function receives a single merged object of URL params + query string + JSON body.</li>
    </ul>

    <p>The pattern string follows the format <code>"METHOD /path"</code>. Path params use <code>:name</code> syntax (or <code>[name]</code> bracket syntax for familiarity):</p>
    <pre>expose('GET /api/posts/:slug', fn)       // :slug style
expose('DELETE /api/posts/[slug]', fn)   // [slug] style: equivalent</pre>

    <h3>Validate Hook (both call paths)</h3>
    <p>The optional <code>validate</code> function runs server-side before the action body on BOTH the internal RPC path (a client component import) AND the HTTP path (issue #245). It receives the action's first argument. Besides the throw-to-reject form below, it can return a structured <code>{ success: false, fieldErrors }</code> envelope (a <code>422</code> over HTTP, a <code>result.fieldErrors</code> object over RPC) or a transformed input value. To attach a validator to a pure-RPC action with no HTTP route, use <code>validateInput(fn, validate)</code> instead of <code>expose()</code>.</p>

    <pre>import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().min(1).max(20000),
});

export const createPost = expose('POST /api/posts', async (input) =&gt; {
  return prisma.post.create({ data: input });
}, {
  validate: CreatePostSchema.parse,  // zod.parse throws on invalid input
});</pre>

    <p>When validation fails, the response is <code>400 { "error": "...", "issues": [...] }</code>. If the thrown error has an <code>issues</code> property (as zod and valibot errors do), it is passed through for structured client-side handling.</p>

    <h3>expose() CORS Support</h3>
    <p>If your exposed endpoint will be called from a different origin (e.g. a mobile app or a partner's frontend), enable CORS:</p>

    <pre>// Allow any origin
export const publicList = expose('GET /api/posts', listFn, {
  cors: true,  // Access-Control-Allow-Origin: *
});

// Allow a specific origin with credentials
export const authedAction = expose('POST /api/posts', createFn, {
  cors: 'https://partner.example.com',
  // Sets: Access-Control-Allow-Origin: https://partner.example.com
  //       Access-Control-Allow-Credentials: true
});

// Allow multiple origins
export const multiOrigin = expose('GET /api/data', dataFn, {
  cors: ['https://app.example.com', 'https://admin.example.com'],
});

// Full control
export const custom = expose('POST /api/webhook', hookFn, {
  cors: {
    origin: 'https://partner.example.com',
    credentials: true,
    maxAge: 3600,
    headers: ['Content-Type', 'Authorization', 'X-Custom-Header'],
  },
});</pre>

    <p>When CORS is enabled, webjs automatically handles <code>OPTIONS</code> preflight requests at the same path, responding with the appropriate <code>Access-Control-Allow-*</code> headers.</p>

    <p><strong>Important:</strong> exposed REST endpoints are <em>not</em> CSRF-protected by default. They are designed for external consumers who authenticate via bearer tokens, API keys, or signed requests. Add your own auth in a middleware or inside the function body.</p>

    <h2>ActionResult&lt;T&gt; Envelope Pattern</h2>
    <p>A recommended convention for server actions is to return a discriminated union instead of throwing errors:</p>

    <pre>type ActionResult&lt;T&gt; =
  | { success: true; data: T }
  | { success: false; error: string; status: number };

export async function createPost(input: unknown): Promise&lt;ActionResult&lt;Post&gt;&gt; {
  const user = await currentUser();
  if (!user) return { success: false, error: 'Not signed in', status: 401 };

  if (!input || typeof input !== 'object') {
    return { success: false, error: 'Invalid input', status: 400 };
  }
  const { title, body } = input as { title: string; body: string };
  if (!title) return { success: false, error: 'title is required', status: 400 };

  const post = await prisma.post.create({
    data: { title, body, authorId: user.id },
  });
  return { success: true, data: post };
}</pre>

    <p>This pattern makes error handling explicit on the client side without relying on try/catch. The caller checks <code>result.success</code> and branches accordingly. It also works naturally with the <code>expose()</code> REST path, where the caller receives the same JSON shape.</p>

    <h2>Error Sanitisation in Production</h2>
    <p>When a server action throws (rather than returning an error envelope), webjs catches the exception and returns a JSON error response:</p>
    <ul>
      <li><strong>Development:</strong> the response includes both the error message and the full stack trace, making debugging easy.</li>
      <li><strong>Production:</strong> only the error message is included. Stack traces are never sent to the client. If the thrown error has no message, a generic "Internal server error" is returned. The full error is always logged server-side.</li>
    </ul>
    <p>This prevents accidental leakage of file paths, database schemas, or other implementation details to end users.</p>

    <h2>Complete Example</h2>
    <p>Here is the full flow from defining a server action to calling it from a component and exposing it as REST:</p>

    <pre>// 1. Define the server action
// actions/todos.server.ts
'use server';
import { expose } from '@webjsdev/core';

interface Todo { id: number; text: string; done: boolean; createdAt: Date; }
const todos: Todo[] = [];
let nextId = 1;

export const addTodo = expose('POST /api/todos', async ({ text }: { text: string }) =&gt; {
  const todo: Todo = { id: nextId++, text, done: false, createdAt: new Date() };
  todos.push(todo);
  return todo;
});

export const listTodos = expose('GET /api/todos', async () =&gt; {
  return todos;
});

// 2. Call from a web component
// components/todo-app.ts
import { WebComponent, html, css } from '@webjsdev/core';
import { addTodo, listTodos } from '../actions/todos.server.ts';

export class TodoApp extends WebComponent {
  static properties = {};
  static styles = css\`
    :host { display: block; max-width: 400px; }
    ul { list-style: none; padding: 0; }
    li { padding: 8px 0; border-bottom: 1px solid #eee; }
    .date { color: #999; font-size: 12px; }
  \`;

  todos = signal&lt;{ id: number; text: string; createdAt: Date }[]&gt;([]);

  async connectedCallback() {
    super.connectedCallback();
    const todos = await listTodos();
    this.todos.set(todos);
    // todos[0].createdAt is a real Date (the rich serializer preserved it)
  }

  async handleAdd(e: Event) {
    e.preventDefault();
    const input = this.shadowRoot!.querySelector('input') as HTMLInputElement;
    const todo = await addTodo({ text: input.value });
    this.todos.set([...this.todos.get(), todo]);
    input.value = '';
  }

  render() {
    return html\`
      &lt;form @submit=\${(e: Event) =&gt; this.handleAdd(e)}&gt;
        &lt;input placeholder="What needs doing?" required /&gt;
        &lt;button&gt;Add&lt;/button&gt;
      &lt;/form&gt;
      &lt;ul&gt;
        \${this.todos.get().map(t =&gt; html\`
          &lt;li&gt;
            \${t.text}
            &lt;span class="date"&gt;\${t.createdAt.toLocaleDateString()}&lt;/span&gt;
          &lt;/li&gt;
        \`)}
      &lt;/ul&gt;
    \`;
  }
}
TodoApp.register('todo-app');

// 3. Call the REST endpoint from curl
// curl -X POST http://localhost:8080/api/todos \\
//   -H 'Content-Type: application/json' \\
//   -d '{"text":"Buy milk"}'
// => {"id":1,"text":"Buy milk","done":false,"createdAt":"2026-04-15T..."}</pre>

    <h2>Plain HTML forms as an alternative</h2>
    <p>Server actions called via JS RPC are the right tool when you need typed return values back in the component (the createPost example above returns a <code>Post</code> object). For the simpler "submit form → server processes → render new page" flow, plain HTML forms pointed at a <code>route.ts</code> handler are often a cleaner fit:</p>
    <pre>// app/posts/route.ts
import { redirect } from '@webjsdev/core';
import { createPost } from '../../modules/posts/actions/create-post.server.ts';

export async function POST(req: Request) {
  const form = await req.formData();
  const result = await createPost({
    title: String(form.get('title') ?? ''),
    body: String(form.get('body') ?? ''),
  });
  // Validation failure -> return HTML with errors visible.
  // The client router applies any HTML response in place regardless of
  // status, so the user sees errors without losing their typed input
  // and without a full page reload.
  if (!result.success) {
    return new Response(renderPostFormHTML(result.errors, form), {
      status: 422,
      headers: { 'content-type': 'text/html; charset=utf-8' },
    });
  }
  // Success -> PRG redirect; fetch auto-follows, history records /posts/&lt;id&gt;
  redirect(\`/posts/\${result.data.id}\`);
}</pre>
    <pre>&lt;!-- The form: standard HTML, no JS handler needed --&gt;
&lt;form action="/posts" method="post"&gt;
  &lt;input name="title" required /&gt;
  &lt;textarea name="body" required&gt;&lt;/textarea&gt;
  &lt;button&gt;Publish&lt;/button&gt;
&lt;/form&gt;</pre>
    <p>The router intercepts the submit, sends the POST, applies the response (2xx with redirect for success, 4xx HTML for validation errors). Works without JavaScript (just slower, with a full page reload), and ramps up to partial-swap when the client router is active. Both ends of the progressive-enhancement spectrum from one piece of code. See the <a href="/docs/client-router">client router</a> docs for the rendering behavior.</p>
  `;
}
