/**
 * `MarciTransport` over marcidb-server's HTTP API (docs/HTTP-API.md in the marci-db repo) — the
 * transport the runner installs in every project worker (`setDbTransport(marciHttpTransport(url))`).
 * Host-side code (Bun/Node): uses the platform `fetch`, never the SDK bridge. Mirrors marcidb's own
 * `httpTransport` (crates/marcidb-ts/src/prefix.js) minus the binary read fast path.
 *
 * `baseUrl` = `http://host:port/<db>`; an optional bearer token (marcidb-server `MARCI_TOKEN`) is sent
 * on every request. Pass `fetch` explicitly when the global one might be monkey-patched by untrusted
 * code running in the same realm (the runner captures it before evaluating a project bundle).
 */

import type { MarciOp, MarciTransport } from "./marci/query"

/** A failed marcidb request: `status` is the HTTP status (0 = network failure), `message` its body. */
export class MarciHttpError extends Error {
  readonly status: number
  constructor(status: number, message: string) {
    super(message)
    this.name = "MarciHttpError"
    this.status = status
  }
}

export type MarciHttpOptions = {
  token?: string | null
  fetch?: typeof fetch
}

/** `update/:id` and `delete/:id` take the key in the path: a bare value, or `k=v&k2=v2` for composite keys
 *  (nested ids flatten with dots — `{ chat: { id: 1 } }` → `chat.id=1`). */
export const encodeMarciId = (id: unknown): string => {
  if (typeof id === "object" && id !== null) {
    const parts: string[] = []
    const walk = (obj: Record<string, unknown>, prefix: string) => {
      for (const [k, v] of Object.entries(obj)) {
        const key = prefix ? `${prefix}.${k}` : k
        if (typeof v === "object" && v !== null) walk(v as Record<string, unknown>, key)
        else parts.push(`${key}=${encodeURIComponent(String(v))}`)
      }
    }
    walk(id as Record<string, unknown>, "")
    return parts.join("&")
  }
  return encodeURIComponent(String(id))
}

/** Low-level request helper (also used by the runner's admin calls: `$sync`, `DELETE /:db`). */
export const marciRequest = async (
  url: string,
  init: { method?: string, body?: unknown, rawBody?: string, token?: string | null, fetch?: typeof fetch } = {},
): Promise<any> => {
  const f = init.fetch ?? fetch
  const headers: Record<string, string> = {}
  if (init.token) headers.authorization = `Bearer ${init.token}`
  let body: string | undefined
  if (init.rawBody !== undefined) { body = init.rawBody; headers["content-type"] = "text/plain" }
  else if (init.body !== undefined) { body = JSON.stringify(init.body); headers["content-type"] = "application/json" }
  let res: Response
  try {
    res = await f(url, { method: init.method ?? "POST", headers, body })
  } catch (e) {
    throw new MarciHttpError(0, `marcidb unreachable: ${(e as Error)?.message ?? e}`)
  }
  const text = await res.text()
  if (!res.ok) throw new MarciHttpError(res.status, text || `HTTP ${res.status}`)
  if (!text) return undefined
  try { return JSON.parse(text) } catch { return text }
}

export const marciHttpTransport = (baseUrl: string, options: MarciHttpOptions = {}): MarciTransport => {
  const url = baseUrl.replace(/\/+$/, "")
  const call = (path: string, body?: unknown) => marciRequest(`${url}/${path}`, { body, token: options.token, fetch: options.fetch })
  return {
    exec(op: MarciOp) {
      switch (op.action) {
        case "findMany":   return call(`${op.model}/findMany`, op.query ?? {})
        case "findFirst":  return call(`${op.model}/findFirst`, op.query ?? {})
        case "insert":     return call(`${op.model}/insert`, op.data)
        case "update":     return call(`${op.model}/update/${encodeMarciId(op.id)}`, op.data).then(() => null)
        // no id in the path — the filter and the payload travel together in the body
        case "updateMany": return call(`${op.model}/updateMany`, { ...(op.query ?? {}), data: op.data })
        case "delete":     return call(`${op.model}/delete/${encodeMarciId(op.id)}`).then(() => null)
        case "deleteMany": return call(`${op.model}/deleteMany`, op.query ?? {})
        case "count":      return call(`${op.model}/count`, op.query ?? {})
        case "aggregate":  return call(`${op.model}/aggregate`, op.query ?? {})
        default: return Promise.reject(new Error(`marcidb: unknown action '${op.action}'`))
      }
    },
    batch(ops: MarciOp[]) {
      return call("$transaction", ops)
    },
  }
}
