import { mkdirSync, chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'fs';
import { resolve, join, relative, isAbsolute } from 'path';

const USER_AGENTS_MAX_BYTES = 64 * 1024;

export const USER_SUBDIRS = ['scripts', 'browser-macros', 'templates', 'recordings', 'trash', 'memory', 'pets', 'notes'] as const;
export type UserSubdir = typeof USER_SUBDIRS[number];

export function userRoot(rootDir: string, ownerId: string): string {
  if (!ownerId || ownerId.includes('/') || ownerId.includes('\\') || ownerId.includes('\0') || isAbsolute(ownerId)) {
    throw new Error(`invalid ownerId: ${JSON.stringify(ownerId)}`);
  }
  return resolve(rootDir, ownerId);
}

export function ensureUserFolder(rootDir: string, ownerId: string): string {
  const root = userRoot(rootDir, ownerId);
  if (!existsSync(root)) {
    mkdirSync(root, { recursive: true, mode: 0o700 });
    chmodSync(root, 0o700);
  }
  for (const sub of USER_SUBDIRS) {
    const p = join(root, sub);
    if (!existsSync(p)) {
      mkdirSync(p, { recursive: true, mode: 0o700 });
      chmodSync(p, 0o700);
    }
  }
  return root;
}

export function resolveUserSubdir(
  rootDir: string,
  ownerId: string,
  subdir: UserSubdir,
  relPath: string,
): string {
  if (!relPath) throw new Error('relPath must not be empty');
  if (isAbsolute(relPath)) throw new Error('relative path required');
  const base = join(userRoot(rootDir, ownerId), subdir);
  const full = resolve(base, relPath);
  const rel = relative(base, full);
  if (rel.startsWith('..') || isAbsolute(rel)) {
    throw new Error('path traversal: target outside owner folder');
  }
  return full;
}

/**
 * Returns the absolute path to a user's memory directory:
 *   {rootDir}/{ownerId}/memory
 * Does not create the directory — callers that need it to exist should call
 * ensureUserFolder first.
 */
export function userMemoryDir(rootDir: string, ownerId: string): string {
  return join(userRoot(rootDir, ownerId), 'memory');
}

/**
 * Returns the absolute path to a user's custom pieces directory:
 *   {rootDir}/{ownerId}/pieces
 * Does not create the directory.
 */
export function userPiecesDir(rootDir: string, ownerId: string): string {
  return join(userRoot(rootDir, ownerId), 'pieces');
}

export function assertOwnerAccess(ctx: { userId?: string }, ownerId: string): void {
  if (!ctx.userId) throw new Error('unauthenticated: ctx.userId missing');
  if (ctx.userId !== ownerId) throw new Error('forbidden: cross-user access');
}

export function writeUserAgentsMd(rootDir: string, ownerId: string, content: string): void {
  if (Buffer.byteLength(content, 'utf-8') > USER_AGENTS_MAX_BYTES) {
    throw new Error(`AGENTS.md exceeds ${USER_AGENTS_MAX_BYTES} bytes`);
  }
  const userDir = ensureUserFolder(rootDir, ownerId);
  const path = join(userDir, 'AGENTS.md');
  writeFileSync(path, content, 'utf-8');
}

export function deleteUserAgentsMd(rootDir: string, ownerId: string): boolean {
  let path: string;
  try {
    path = join(userRoot(rootDir, ownerId), 'AGENTS.md');
  } catch {
    return false;
  }
  if (!existsSync(path)) return false;
  unlinkSync(path);
  return true;
}

export function readUserAgentsMd(rootDir: string, ownerId: string): string | null {
  // Validates ownerId via userRoot's existing guard.
  let path: string;
  try {
    path = join(userRoot(rootDir, ownerId), 'AGENTS.md');
  } catch {
    return null; // bad ownerId — same as missing file
  }
  let stat;
  try {
    stat = statSync(path);
  } catch {
    return null;
  }
  if (!stat.isFile() || stat.size === 0) return null;
  if (stat.size <= USER_AGENTS_MAX_BYTES) {
    return readFileSync(path, 'utf-8');
  }
  const buf = Buffer.alloc(USER_AGENTS_MAX_BYTES);
  const fd = openSync(path, 'r');
  let bytesRead: number;
  try {
    bytesRead = readSync(fd, buf, 0, USER_AGENTS_MAX_BYTES, 0);
  } finally {
    closeSync(fd);
  }
  // Walk back to a UTF-8 codepoint boundary so we don't slice mid-character
  // (avoids U+FFFD replacement char for CJK / emoji content).
  let safe = bytesRead;
  while (safe > 0 && (buf[safe - 1]! & 0xc0) === 0x80) safe--;
  return buf.subarray(0, safe).toString('utf-8') + `\n\n[truncated: original was ${stat.size} bytes]`;
}
