import {
  getSoulseekFavorites,
  addSoulseekFavorite,
  removeSoulseekFavorite,
  getSoulseekBrowseSnapshots,
  getSoulseekBrowseSnapshot,
  getSoulseekBrowseLevel,
  searchSoulseekBrowseTree,
  getSoulseekBrowseDirFiles,
  deleteSoulseekBrowse,
} from 'officerdb';
import { startBrowse } from './browse';
import { enqueueFromCache, DownloadError } from './download';

// Officer-owned Soulseek routes — everything slskd itself has no concept of. These are served HERE, by
// the sidecar, not forwarded upstream: the platform API stays a pure auth-and-forward proxy forever, and
// slskd stays unforked. They live under the `/_officer/` prefix, mirroring `/_health`, so they can never
// collide with slskd's own surface (all of which is under `/api/v0/`).
//
// ─────────────────────────────────────────────────────────────────────────────────────────────────
// CONTRACT (reached from the browser as /api/slskd/_officer/…)
//   GET    /_officer/favorites                       → string[]  favourited peers, alphabetical
//   POST   /_officer/favorites { username }          → { ok: true }  add (idempotent)
//   DELETE /_officer/favorites?username=<u>          → { ok: true }  remove (no-op if absent)
//
//   GET    /_officer/browse                          → Snapshot[]  every cached share tree
//   GET    /_officer/browse/<u>                      → Snapshot | null  one peer's cache state
//   POST   /_officer/browse/<u>                      → 202 { started }  kick off a background fetch
//   DELETE /_officer/browse/<u>                      → { ok: true }  drop the cache
//   GET    /_officer/browse/<u>/tree?parent=&limit=&offset= → { nodes, total }  one level (roots if no parent)
//   GET    /_officer/browse/<u>/tree/search?q=&limit=  → { nodes, matched, total, truncated }
//   GET    /_officer/browse/<u>/dirs/<id>/files      → BrowsedFile[]  one folder's files
//   POST   /_officer/browse/<u>/download { path, file? } → { queued }  enqueue a folder, or one file
// ─────────────────────────────────────────────────────────────────────────────────────────────────

// The authenticated user id arrives in X-Officer-User, injected by the platform proxy after auth. We
// bind loopback only, so it's trusted — same arrangement as the music sidecar's per-user state routes.
const userIdOf = (req: Request): number | null => {
  const n = Number(req.headers.get('x-officer-user'));
  return Number.isInteger(n) && n > 0 ? n : null;
};

// Soulseek usernames are opaque to us and may contain spaces, so the only shapes we reject are ones
// no nick can have: empty, absurdly long, or containing control characters.
const USERNAME_MAX = 255;
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
const cleanUsername = (v: unknown): string | null => {
  if (typeof v !== 'string') return null;
  const u = v.trim();
  if (!u || u.length > USERNAME_MAX) return null;
  return CONTROL_CHARS.test(u) ? null : u;
};

// A level, not a page: once you're inside a folder there's nothing to leaf through, so the UI asks for
// the whole level when it's wide. The cap is set above the widest fan-out measured on a real peer (1,181).
const DIR_PAGE_MAX = 2000;
const DIR_PAGE_DEFAULT = 200;
// A match cap, not a page: every match drags its ancestors along, so the response is up to ~depth times
// this. Truncation is reported in the payload rather than passed off as the whole answer.
const SEARCH_MATCH_MAX = 1000;
const SEARCH_MATCH_DEFAULT = 300;
const clampInt = (raw: string | null, fallback: number, min: number, max: number): number => {
  // An absent param has to be caught before Number(): `Number(null)` is 0, not NaN, so it passed the
  // finite check and clamped to `min` — which ran the tree search with LIMIT 1 and returned a single
  // match, making its siblings look like non-matches in the UI.
  if (raw === null || raw.trim() === '') return fallback;
  const n = Number(raw);
  if (!Number.isFinite(n)) return fallback;
  return Math.min(max, Math.max(min, Math.trunc(n)));
};

const badRequest = (error: string) => Response.json({ error }, { status: 400 });
const notFound = () => new Response('not found', { status: 404 });
const methodNotAllowed = () => new Response('method not allowed', { status: 405 });

async function handleFavorites(req: Request, url: URL, userId: number): Promise<Response> {
  if (req.method === 'GET') return Response.json(await getSoulseekFavorites(userId));

  if (req.method === 'POST') {
    const body = (await req.json().catch(() => null)) as { username?: unknown } | null;
    const username = cleanUsername(body?.username);
    if (!username) return badRequest('username required');
    await addSoulseekFavorite(userId, username);
    return Response.json({ ok: true });
  }

  if (req.method === 'DELETE') {
    const username = cleanUsername(url.searchParams.get('username'));
    if (!username) return badRequest('username required');
    await removeSoulseekFavorite(userId, username);
    return Response.json({ ok: true });
  }

  return methodNotAllowed();
}

type BrowseRouteParams = { req: Request; url: URL; userId: number; segments: string[] };

async function handleBrowse({ req, url, userId, segments }: BrowseRouteParams): Promise<Response> {
  // /_officer/browse — every cached tree, for badging the favourites list in one request.
  if (segments.length === 1) {
    if (req.method !== 'GET') return methodNotAllowed();
    return Response.json(await getSoulseekBrowseSnapshots(userId));
  }

  const username = cleanUsername(decodeURIComponent(segments[1] ?? ''));
  if (!username) return badRequest('username required');

  // /_officer/browse/<username>
  if (segments.length === 2) {
    if (req.method === 'GET') return Response.json(await getSoulseekBrowseSnapshot(userId, username));
    if (req.method === 'POST') {
      // 202: the fetch takes minutes and outlives this request. Poll the GET for status.
      const started = await startBrowse(userId, username);
      return Response.json({ started }, { status: 202 });
    }
    if (req.method === 'DELETE') {
      await deleteSoulseekBrowse(userId, username);
      return Response.json({ ok: true });
    }
    return methodNotAllowed();
  }

  // /_officer/browse/<username>/tree — one level of folders, paged. No `parent` means the roots.
  if (segments[2] === 'tree' && segments.length === 3) {
    if (req.method !== 'GET') return methodNotAllowed();
    const level = await getSoulseekBrowseLevel({
      userId,
      username,
      parent: url.searchParams.get('parent') ?? undefined,
      limit: clampInt(url.searchParams.get('limit'), DIR_PAGE_DEFAULT, 1, DIR_PAGE_MAX),
      offset: clampInt(url.searchParams.get('offset'), 0, 0, Number.MAX_SAFE_INTEGER),
    });
    return Response.json(level);
  }

  // /_officer/browse/<username>/tree/search — matches plus their ancestors, as one small tree. Kept off
  // the level route because the shape differs: this one is a skeleton spanning depths, not a page.
  if (segments[2] === 'tree' && segments.length === 4 && segments[3] === 'search') {
    if (req.method !== 'GET') return methodNotAllowed();
    const result = await searchSoulseekBrowseTree({
      userId,
      username,
      q: url.searchParams.get('q') ?? '',
      limit: clampInt(url.searchParams.get('limit'), SEARCH_MATCH_DEFAULT, 1, SEARCH_MATCH_MAX),
    });
    return Response.json(result);
  }

  // /_officer/browse/<username>/download — a folder's whole subtree, or one file inside it.
  if (segments[2] === 'download' && segments.length === 3) {
    if (req.method !== 'POST') return methodNotAllowed();
    const body = (await req.json().catch(() => null)) as { path?: unknown; file?: unknown } | null;
    const path = typeof body?.path === 'string' ? body.path.trim() : '';
    if (!path) return badRequest('path required');
    const file = typeof body?.file === 'string' && body.file.trim() ? body.file : undefined;
    try {
      const queued = await enqueueFromCache({ userId, username, path, file });
      return Response.json({ queued });
    } catch (err) {
      if (err instanceof DownloadError) return Response.json({ error: err.message }, { status: err.status });
      throw err;
    }
  }

  if (segments[2] !== 'dirs') return notFound();

  // /_officer/browse/<username>/dirs/<id>/files
  if (segments.length === 5 && segments[4] === 'files') {
    if (req.method !== 'GET') return methodNotAllowed();
    const dirId = Number(segments[3]);
    if (!Number.isInteger(dirId) || dirId <= 0) return badRequest('invalid directory id');
    const files = await getSoulseekBrowseDirFiles({ userId, username, dirId });
    return files ? Response.json(files) : notFound();
  }

  return notFound();
}

/** Handles a `/_officer/*` request, or returns null if the path isn't one of ours. */
export async function handleOfficerRoute(req: Request, url: URL): Promise<Response | null> {
  const segments = url.pathname
    .replace(/^\/_officer\//, '')
    .split('/')
    .filter(Boolean);
  if (!segments.length) return null;

  const userId = userIdOf(req);
  if (userId === null) return Response.json({ error: 'missing X-Officer-User' }, { status: 401 });

  if (segments[0] === 'favorites' && segments.length === 1) return handleFavorites(req, url, userId);
  if (segments[0] === 'browse') return handleBrowse({ req, url, userId, segments });

  return null;
}
