// Minimal reference WRITE support for the @pcbjam/shared lib protocol (0004).
//
// User libraries are created + written by the editor. The closed registry server
// stores them per-user in Postgres+R2; this open reference server keeps them as
// plain files under USER_LIBS_DIR, owner-namespaced, in the same per-lib layout
// the read side uses:
//
//   <USER_LIBS_DIR>/<owner>/<libId>/index.json           { items: [...], type, name }
//   <USER_LIBS_DIR>/<owner>/<libId>/<Symbol>.kicad_sym    a saved symbol body
//   <USER_LIBS_DIR>/<owner>/<libId>/<Footprint>.kicad_mod a saved footprint body
//
// No parsing: the editor sends fork-native bytes, stored as-is (decision:
// user-saved bodies round-trip without a version shim). A user lib is a
// kind-agnostic container (symbols + footprints can coexist). Owner comes from
// OWNER_HEADER; absent ⇒ "default".

import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { Lib, LibItem } from "@pcbjam/shared";
import { extForKind } from "./libs.js";

export const DEFAULT_OWNER = "default";

// Write quotas: bound per-owner lib count and per-lib item count so an
// unauthenticated caller can't create unlimited libs/items; per-body size is
// capped by the server's bodyLimit (413). Overridable.
const MAX_LIBS_PER_OWNER = Number(process.env.MAX_LIBS_PER_OWNER ?? 100);
const MAX_ITEMS_PER_LIB = Number(process.env.MAX_ITEMS_PER_LIB ?? 1000);

export interface UserLibsConfig {
  /** Root for owner-namespaced writable user libs; null ⇒ writes disabled. */
  dir: string | null;
}

export function userLibsConfig(): UserLibsConfig {
  const dir = process.env.USER_LIBS_DIR ?? ".user-libs";
  return { dir: path.resolve(process.cwd(), dir) };
}

/** A safe path segment (owner slug, lib id, item name component). */
const SAFE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;

function safeSeg(s: string): string | null {
  return SAFE.test(s) ? s : null;
}

/** Derive a stable, filesystem-safe lib id from a display name. */
export function slugifyLibName(name: string): string {
  const slug = name
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9._+-]+/g, "-")
    .replace(/^-+|-+$/g, "");
  return slug || "lib";
}

interface IndexFile {
  type?: string;
  name?: string;
  description?: string | null;
  items?: {
    kind: string;
    name: string;
    description?: string | null;
    keywords?: string | null;
  }[];
}

function libDir(cfg: UserLibsConfig, owner: string, lib: string): string | null {
  if (!cfg.dir) return null;
  const o = safeSeg(owner);
  const l = safeSeg(lib);
  if (!o || !l) return null;
  return path.join(cfg.dir, o, l);
}

async function readIndex(dir: string): Promise<IndexFile | null> {
  try {
    return JSON.parse(await fs.readFile(path.join(dir, "index.json"), "utf8"));
  } catch {
    return null;
  }
}

async function writeIndex(dir: string, idx: IndexFile): Promise<void> {
  await fs.writeFile(path.join(dir, "index.json"), JSON.stringify(idx, null, 2));
}

export class UserLibError extends Error {
  constructor(
    public status: 400 | 404 | 409 | 429,
    message: string,
  ) {
    super(message);
  }
}

/** Count an owner's existing user libs (dirs with a readable index.json). */
async function ownerLibCount(cfg: UserLibsConfig, owner: string): Promise<number> {
  return (await listUserLibs(cfg, owner)).length;
}

/** Create a user lib for an owner. Idempotent-ish: 409 if the id already exists. */
export async function createUserLib(
  cfg: UserLibsConfig,
  owner: string,
  name: string,
): Promise<Lib> {
  const id = slugifyLibName(name);
  const dir = libDir(cfg, owner, id);
  if (!dir) throw new UserLibError(400, "user libs not configured or bad name");
  if (await readIndex(dir)) {
    throw new UserLibError(409, `library "${id}" already exists`);
  }
  if ((await ownerLibCount(cfg, owner)) >= MAX_LIBS_PER_OWNER) {
    throw new UserLibError(
      400,
      `library limit reached (max ${MAX_LIBS_PER_OWNER} per owner)`,
    );
  }
  await fs.mkdir(dir, { recursive: true });
  const idx: IndexFile = { type: "user", name, description: null, items: [] };
  await writeIndex(dir, idx);
  return { id, name, type: "user", description: null, itemCount: 0 };
}

/** List an owner's user libs. */
export async function listUserLibs(
  cfg: UserLibsConfig,
  owner: string,
): Promise<Lib[]> {
  const o = cfg.dir && safeSeg(owner);
  if (!cfg.dir || !o) return [];
  const root = path.join(cfg.dir, o);
  let entries: import("node:fs").Dirent[];
  try {
    entries = await fs.readdir(root, { withFileTypes: true });
  } catch {
    return [];
  }
  const out: Lib[] = [];
  for (const e of entries) {
    if (!e.isDirectory() || e.name.startsWith(".")) continue;
    const idx = await readIndex(path.join(root, e.name));
    if (!idx) continue;
    out.push({
      id: e.name,
      name: idx.name ?? e.name,
      type: "user",
      description: idx.description ?? null,
      itemCount: idx.items?.length ?? 0,
    });
  }
  return out;
}

/** List a user lib's items, or null if the lib isn't a user lib for this owner. */
export async function listUserItems(
  cfg: UserLibsConfig,
  owner: string,
  lib: string,
): Promise<LibItem[] | null> {
  const dir = libDir(cfg, owner, lib);
  if (!dir) return null;
  const idx = await readIndex(dir);
  if (!idx) return null;
  return (idx.items ?? []).map((i) => ({
    kind: i.kind,
    name: i.name,
    description: i.description ?? null,
    keywords: i.keywords ?? null,
  }));
}

/** Resolve the on-disk body path for a user item (read), or null. */
export function userItemBodyPath(
  cfg: UserLibsConfig,
  owner: string,
  lib: string,
  kind: string,
  name: string,
): string | null {
  const dir = libDir(cfg, owner, lib);
  const ext = extForKind(kind);
  if (!dir || !ext || !safeSeg(name)) return null;
  return path.join(dir, `${name}${ext}`);
}

/** Write one item body (symbol or footprint) into a user lib + index it. */
export async function writeUserItem(
  cfg: UserLibsConfig,
  owner: string,
  lib: string,
  kind: string,
  name: string,
  body: string,
): Promise<LibItem> {
  const ext = extForKind(kind);
  if (!ext) throw new UserLibError(400, `unsupported kind "${kind}"`);
  const dir = libDir(cfg, owner, lib);
  if (!dir || !safeSeg(name)) throw new UserLibError(400, "bad lib or item name");
  const idx = await readIndex(dir);
  if (!idx) throw new UserLibError(404, `user library "${lib}" not found`);

  const items = idx.items ?? [];
  const existing = items.find((i) => i.kind === kind && i.name === name);
  // Only NEW items grow the lib — overwrites of an existing item are always
  // allowed (the editor re-saves), so being at the cap never blocks edits.
  if (!existing && items.length >= MAX_ITEMS_PER_LIB) {
    throw new UserLibError(
      429,
      `item limit reached (max ${MAX_ITEMS_PER_LIB} per library)`,
    );
  }

  await fs.writeFile(path.join(dir, `${name}${ext}`), body, "utf8");

  if (!existing) items.push({ kind, name });
  idx.items = items;
  await writeIndex(dir, idx);

  return { kind, name, description: null, keywords: null };
}
