import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
import { createWriteStream, mkdirSync } from "node:fs";
import { open, readFile, rename, stat, writeFile } from "node:fs/promises";
import { basename, dirname, relative, resolve, sep } from "node:path";
import pathPosix from "node:path/posix";
import postgres from "postgres";
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";

type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
type JsonRecord = Record<string, JsonValue>;

type TransferDirection = "upload" | "download";
type TransferStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";

interface RuntimeConfig {
  host: string;
  port: number;
  databaseUrl: string;
  logFile: string;
  databasePoolMax: number;
  clientId: string;
  clientSecret: string;
  tokenKey: string;
  appRoot: string;
  stagingDir: string;
  partSizeBytes: number;
  userAgent: string;
}

interface AuthSessionRow {
  id: string;
  user_code: string;
  verification_url: string;
  qrcode_url: string;
  expires_at: string | Date;
  poll_interval_seconds: number;
  status: string;
  error: string | null;
  last_poll_at: string | Date | null;
  poll_count: number;
  token_account_id: string | null;
  created_at: string | Date;
  updated_at: string | Date;
}

interface TokenRow {
  account_id: string;
  access_token_ciphertext: string;
  refresh_token_ciphertext: string;
  expires_at: string | Date;
  scope: string;
  generation: number;
}

interface TransferJobRow {
  id: string;
  account_id: string | null;
  direction: TransferDirection;
  status: TransferStatus;
  local_path: string;
  remote_path: string;
  fs_id: string | null;
  size_bytes: string | number | null;
  bytes_done: string | number | null;
  part_size: number | null;
  block_list_json: JsonValue;
  uploadid: string | null;
  retry_count: number;
  error: string | null;
  result: JsonValue;
  created_at: string | Date;
  updated_at: string | Date;
}

class HttpError extends Error {
  readonly status: number;
  readonly detail: JsonRecord;

  constructor(status: number, message: string, detail: JsonRecord = {}) {
    super(message);
    this.name = "HttpError";
    this.status = status;
    this.detail = detail;
  }
}

class BaiduApiError extends Error {
  readonly status: number;
  readonly body: JsonValue;

  constructor(message: string, status: number, body: JsonValue) {
    super(message);
    this.name = "BaiduApiError";
    this.status = status;
    this.body = body;
  }
}

const serviceStartedAt = new Date().toISOString();
const recentLogs: JsonRecord[] = [];

function deployMetadata(): JsonRecord {
  return {
    serviceId: process.env.UNIDESK_DEPLOY_SERVICE_ID || "baidu-netdisk",
    ref: process.env.UNIDESK_DEPLOY_REF || "",
    repo: process.env.UNIDESK_DEPLOY_REPO || "",
    commit: process.env.UNIDESK_DEPLOY_COMMIT || "",
    requestedCommit: process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT || "",
  };
}

function normalizedAppRoot(value: string): string {
  const raw = String(value || "/").trim() || "/";
  const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : `/${raw}`);
  return normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || "/";
}

function configFromEnv(): RuntimeConfig {
  const databaseUrl = process.env.DATABASE_URL || "";
  if (!databaseUrl) throw new Error("DATABASE_URL is required");
  const partSizeBytes = Number(process.env.BAIDU_NETDISK_PART_SIZE_BYTES || 4 * 1024 * 1024);
  return {
    host: process.env.HOST || "0.0.0.0",
    port: Number(process.env.PORT || 4244),
    databaseUrl,
    logFile: process.env.LOG_FILE || "",
    databasePoolMax: Math.max(1, Math.min(8, Number(process.env.DATABASE_POOL_MAX || 2) || 2)),
    clientId: process.env.BAIDU_NETDISK_CLIENT_ID || process.env.BAIDU_NETDISK_APP_KEY || "",
    clientSecret: process.env.BAIDU_NETDISK_CLIENT_SECRET || process.env.BAIDU_NETDISK_SECRET_KEY || "",
    tokenKey: process.env.BAIDU_NETDISK_TOKEN_KEY || "",
    appRoot: normalizedAppRoot(process.env.BAIDU_NETDISK_APP_ROOT || "/"),
    stagingDir: resolve(process.env.BAIDU_NETDISK_STAGING_DIR || "/data/staging"),
    partSizeBytes: Number.isFinite(partSizeBytes) && partSizeBytes > 0 ? partSizeBytes : 4 * 1024 * 1024,
    userAgent: process.env.BAIDU_NETDISK_USER_AGENT || "pan.baidu.com",
  };
}

const config = configFromEnv();
const sql = postgres(config.databaseUrl, {
  max: config.databasePoolMax,
  idle_timeout: 20,
  connect_timeout: 10,
  connection: { application_name: "unidesk-baidu-netdisk" },
});
const logWriter = config.logFile
  ? createHourlyJsonlWriter({
      baseLogFile: config.logFile,
      service: "baidu-netdisk",
      maxBytes: logRetentionBytesForService("baidu-netdisk"),
    })
  : null;
logWriter?.prune();
let schemaReady = false;
let appRootEnsuredAt = 0;
let appRootEnsurePromise: Promise<void> | null = null;

function jsonResponse(body: JsonValue, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json; charset=utf-8" },
  });
}

function redactString(value: string): string {
  if (value.length <= 10) return "[redacted]";
  return `${value.slice(0, 4)}...[redacted]...${value.slice(-4)}`;
}

function redactJson(value: unknown): JsonValue {
  if (value === null || value === undefined) return null;
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
  if (Array.isArray(value)) return value.slice(0, 50).map((item) => redactJson(item));
  if (typeof value !== "object") return String(value);
  const output: JsonRecord = {};
  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
    const normalized = key.toLowerCase();
    if (normalized.includes("token") || normalized.includes("secret") || normalized === "dlink" || normalized.includes("ciphertext")) {
      output[key] = typeof item === "string" ? redactString(item) : "[redacted]";
    } else {
      output[key] = redactJson(item);
    }
  }
  return output;
}

function errorToJson(error: unknown): JsonRecord {
  if (error instanceof BaiduApiError) return { name: error.name, message: error.message, status: error.status, body: redactJson(error.body) };
  if (error instanceof HttpError) return { name: error.name, message: error.message, status: error.status, detail: error.detail };
  if (error instanceof Error) return { name: error.name, message: error.message };
  return { message: String(error) };
}

function errorResponse(error: unknown): Response {
  const status = error instanceof HttpError ? error.status : 500;
  log("request_error", { status, error: errorToJson(error) });
  if (error instanceof HttpError) return jsonResponse({ ok: false, error: error.message, ...error.detail }, status);
  return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, status);
}

function log(event: string, detail: JsonRecord = {}): void {
  const record: JsonRecord = { at: new Date().toISOString(), event, ...redactJson(detail) as JsonRecord };
  recentLogs.push(record);
  if (recentLogs.length > 300) recentLogs.shift();
  if (logWriter !== null) {
    try {
      logWriter.appendJson(record, new Date(String(record.at)));
    } catch {
      // Logging must not break request handling.
    }
  }
}

function iso(value: string | Date | null | undefined): string | null {
  if (value === null || value === undefined) return null;
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
}

function asNumber(value: unknown, fallback = 0): number {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function asRecord(value: unknown): JsonRecord {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {};
}

async function readJsonBody(req: Request): Promise<JsonRecord> {
  const text = await req.text();
  if (!text.trim()) return {};
  try {
    const parsed = JSON.parse(text) as unknown;
    if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed as JsonRecord;
    throw new Error("JSON body must be an object");
  } catch (error) {
    throw new HttpError(400, "invalid JSON body", { detail: error instanceof Error ? error.message : String(error) });
  }
}

function requireBaiduConfigured(): void {
  const missing = [];
  if (!config.clientId) missing.push("BAIDU_NETDISK_CLIENT_ID");
  if (!config.clientSecret) missing.push("BAIDU_NETDISK_CLIENT_SECRET");
  if (!config.tokenKey) missing.push("BAIDU_NETDISK_TOKEN_KEY");
  if (missing.length > 0) {
    throw new HttpError(400, "Baidu Netdisk OAuth is not configured", { missing: missing as JsonValue });
  }
}

function encryptionKey(): Buffer {
  if (!config.tokenKey) throw new HttpError(400, "BAIDU_NETDISK_TOKEN_KEY is required");
  return createHash("sha256").update(config.tokenKey).digest();
}

function encryptSecret(value: string): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv);
  const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `v1:${iv.toString("base64url")}:${tag.toString("base64url")}:${ciphertext.toString("base64url")}`;
}

function decryptSecret(value: string): string {
  const [version, ivText, tagText, ciphertextText] = value.split(":");
  if (version !== "v1" || !ivText || !tagText || !ciphertextText) throw new Error("unsupported encrypted secret format");
  const decipher = createDecipheriv("aes-256-gcm", encryptionKey(), Buffer.from(ivText, "base64url"));
  decipher.setAuthTag(Buffer.from(tagText, "base64url"));
  return Buffer.concat([decipher.update(Buffer.from(ciphertextText, "base64url")), decipher.final()]).toString("utf8");
}

async function ensureSchema(): Promise<void> {
  await sql`
    CREATE TABLE IF NOT EXISTS baidu_netdisk_accounts (
      id TEXT PRIMARY KEY,
      baidu_uid TEXT NOT NULL DEFAULT '',
      username TEXT NOT NULL DEFAULT '',
      avatar_url TEXT NOT NULL DEFAULT '',
      vip_type INTEGER,
      root_path TEXT NOT NULL DEFAULT '',
      quota_json JSONB NOT NULL DEFAULT '{}'::jsonb,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS baidu_netdisk_tokens (
      account_id TEXT PRIMARY KEY REFERENCES baidu_netdisk_accounts(id) ON DELETE CASCADE,
      access_token_ciphertext TEXT NOT NULL,
      refresh_token_ciphertext TEXT NOT NULL,
      expires_at TIMESTAMPTZ NOT NULL,
      scope TEXT NOT NULL DEFAULT '',
      generation INTEGER NOT NULL DEFAULT 0,
      last_refresh_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS baidu_netdisk_auth_sessions (
      id TEXT PRIMARY KEY,
      device_code_ciphertext TEXT NOT NULL,
      user_code TEXT NOT NULL,
      verification_url TEXT NOT NULL DEFAULT '',
      qrcode_url TEXT NOT NULL DEFAULT '',
      expires_at TIMESTAMPTZ NOT NULL,
      poll_interval_seconds INTEGER NOT NULL DEFAULT 5,
      status TEXT NOT NULL DEFAULT 'pending',
      error TEXT,
      poll_count INTEGER NOT NULL DEFAULT 0,
      last_poll_at TIMESTAMPTZ,
      token_account_id TEXT,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS baidu_netdisk_transfer_jobs (
      id TEXT PRIMARY KEY,
      account_id TEXT,
      direction TEXT NOT NULL,
      status TEXT NOT NULL,
      local_path TEXT NOT NULL DEFAULT '',
      remote_path TEXT NOT NULL DEFAULT '',
      fs_id TEXT,
      size_bytes BIGINT,
      bytes_done BIGINT NOT NULL DEFAULT 0,
      part_size INTEGER,
      block_list_json JSONB NOT NULL DEFAULT '[]'::jsonb,
      uploadid TEXT,
      retry_count INTEGER NOT NULL DEFAULT 0,
      error TEXT,
      result JSONB NOT NULL DEFAULT '{}'::jsonb,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
    )
  `;
  await sql`
    CREATE TABLE IF NOT EXISTS baidu_netdisk_transfer_events (
      id BIGSERIAL PRIMARY KEY,
      job_id TEXT NOT NULL REFERENCES baidu_netdisk_transfer_jobs(id) ON DELETE CASCADE,
      level TEXT NOT NULL,
      message TEXT NOT NULL,
      data_json JSONB NOT NULL DEFAULT '{}'::jsonb,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    )
  `;
  await sql`CREATE INDEX IF NOT EXISTS idx_baidu_netdisk_auth_sessions_status ON baidu_netdisk_auth_sessions(status, updated_at DESC)`;
  await sql`CREATE INDEX IF NOT EXISTS idx_baidu_netdisk_transfer_jobs_status ON baidu_netdisk_transfer_jobs(status, updated_at DESC)`;
}

async function waitForSchema(): Promise<void> {
  let lastError: unknown = null;
  for (let attempt = 1; attempt <= 30; attempt += 1) {
    try {
      await ensureSchema();
      schemaReady = true;
      log("schema_ready", { attempt });
      return;
    } catch (error) {
      lastError = error;
      log("schema_wait", { attempt, error: errorToJson(error) });
      await new Promise((resolveWait) => setTimeout(resolveWait, 1000));
    }
  }
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
}

function buildUrl(base: string, params: Record<string, string | number | boolean | undefined>): URL {
  const url = new URL(base);
  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined) url.searchParams.set(key, String(value));
  }
  return url;
}

function baiduHeaders(extra: HeadersInit = {}): Headers {
  const headers = new Headers(extra);
  headers.set("user-agent", config.userAgent);
  if (!headers.has("accept")) headers.set("accept", "application/json");
  return headers;
}

function baiduErrorMessage(body: unknown): string {
  const record = asRecord(body);
  for (const key of ["error_description", "error", "errmsg", "msg"]) {
    if (typeof record[key] === "string" && String(record[key]).length > 0) return String(record[key]);
  }
  if (record.errno !== undefined && Number(record.errno) !== 0) return `Baidu API errno ${String(record.errno)}`;
  return "Baidu API request failed";
}

async function fetchJsonRaw(url: URL, init: RequestInit = {}): Promise<{ ok: boolean; status: number; body: JsonRecord }> {
  const response = await fetch(url, { ...init, headers: baiduHeaders(init.headers) });
  const text = await response.text();
  let body: unknown;
  try {
    body = text ? JSON.parse(text) as unknown : {};
  } catch {
    body = { text };
  }
  const record = asRecord(body);
  return { ok: response.ok, status: response.status, body: record };
}

async function fetchJson(url: URL, init: RequestInit = {}, checkErrno = true): Promise<JsonRecord> {
  const { ok, status, body: record } = await fetchJsonRaw(url, init);
  if (!ok) throw new BaiduApiError(baiduErrorMessage(record), status, redactJson(record));
  if (checkErrno && record.errno !== undefined && Number(record.errno) !== 0) {
    throw new BaiduApiError(baiduErrorMessage(record), status, redactJson(record));
  }
  return record;
}

async function oauthJson(url: URL): Promise<JsonRecord> {
  return fetchJson(url, {}, false);
}

async function oauthJsonAllowHttpError(url: URL): Promise<JsonRecord> {
  return (await fetchJsonRaw(url)).body;
}

function formBody(fields: Record<string, string | number | boolean | JsonValue | undefined>): URLSearchParams {
  const body = new URLSearchParams();
  for (const [key, value] of Object.entries(fields)) {
    if (value === undefined) continue;
    if (typeof value === "object") body.set(key, JSON.stringify(value));
    else body.set(key, String(value));
  }
  return body;
}

async function baiduFormJson(url: URL, fields: Record<string, string | number | boolean | JsonValue | undefined>, checkErrno = true): Promise<JsonRecord> {
  return fetchJson(url, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: formBody(fields),
  }, checkErrno);
}

function baiduErrno(body: JsonRecord): number | null {
  if (body.errno === undefined || body.errno === null) return null;
  const value = Number(body.errno);
  return Number.isFinite(value) ? value : null;
}

function baiduApiErrorFromBody(body: JsonRecord, status = 200): BaiduApiError {
  return new BaiduApiError(baiduErrorMessage(body), status, redactJson(body));
}

function errorHasBaiduErrno(error: unknown, errno: number): boolean {
  if (!(error instanceof BaiduApiError)) return false;
  return baiduErrno(asRecord(error.body)) === errno;
}

async function createRemoteFolder(accessToken: string, path: string): Promise<JsonRecord> {
  const result = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "create", access_token: accessToken }), {
    path,
    size: 0,
    isdir: 1,
    rtype: 0,
  }, false);
  const errno = baiduErrno(result);
  if (errno !== null && errno !== 0 && errno !== -8) throw baiduApiErrorFromBody(result);
  return result;
}

async function ensureAppRoot(accessToken: string, force = false): Promise<void> {
  if (config.appRoot === "/") {
    appRootEnsuredAt = Date.now();
    if (force) log("remote_root_ready", { path: config.appRoot });
    return;
  }
  if (!force && appRootEnsuredAt > 0 && Date.now() - appRootEnsuredAt < 10 * 60 * 1000) return;
  if (appRootEnsurePromise) return appRootEnsurePromise;
  appRootEnsurePromise = (async () => {
    const result = await createRemoteFolder(accessToken, config.appRoot);
    appRootEnsuredAt = Date.now();
    const errno = baiduErrno(result);
    log(errno === -8 ? "app_root_exists" : "app_root_ready", { path: config.appRoot, result });
  })();
  try {
    await appRootEnsurePromise;
  } finally {
    appRootEnsurePromise = null;
  }
}

function remotePathInsideRoot(input: string | undefined, fallback = config.appRoot): string {
  const raw = String(input || fallback).trim() || fallback;
  if (raw.includes("\0")) throw new HttpError(400, "remote path must not contain null bytes");
  const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : pathPosix.join(config.appRoot, raw));
  const clean = normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || config.appRoot;
  if (config.appRoot !== "/" && clean !== config.appRoot && !clean.startsWith(`${config.appRoot}/`)) {
    throw new HttpError(400, "remote path must stay inside configured root", { rootPath: config.appRoot, path: clean });
  }
  return clean;
}

function remoteFilePath(input: string | undefined, localName = "upload.bin"): string {
  const path = remotePathInsideRoot(input || pathPosix.join(config.appRoot, localName));
  if (path.endsWith("/")) return remotePathInsideRoot(pathPosix.join(path, localName));
  return path;
}

function resolveStagingPath(input: string | undefined, fallbackName = "file"): string {
  const raw = String(input || fallbackName).trim() || fallbackName;
  const candidate = raw.startsWith("/") ? resolve(raw) : resolve(config.stagingDir, raw);
  const rel = relative(config.stagingDir, candidate);
  if (rel === "" || (!rel.startsWith("..") && !rel.includes(`..${sep}`) && !resolve(candidate).includes("\0"))) return candidate;
  throw new HttpError(400, "local path must stay inside staging directory", { stagingDir: config.stagingDir });
}

function authSessionFromRow(row: AuthSessionRow): JsonRecord {
  const expiresAt = iso(row.expires_at);
  const secondsRemaining = expiresAt ? Math.max(0, Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000)) : 0;
  return {
    id: row.id,
    userCode: row.user_code,
    verificationUrl: row.verification_url,
    qrcodeUrl: row.qrcode_url,
    expiresAt,
    secondsRemaining,
    pollIntervalSeconds: row.poll_interval_seconds,
    status: row.status,
    error: row.error || "",
    pollCount: row.poll_count,
    lastPollAt: iso(row.last_poll_at),
    tokenAccountId: row.token_account_id || "",
    createdAt: iso(row.created_at),
    updatedAt: iso(row.updated_at),
  };
}

async function startDeviceAuth(): Promise<JsonRecord> {
  requireBaiduConfigured();
  const url = buildUrl("https://openapi.baidu.com/oauth/2.0/device/code", {
    response_type: "device_code",
    client_id: config.clientId,
    scope: "basic,netdisk",
  });
  const body = await oauthJson(url);
  const deviceCode = String(body.device_code || "");
  const userCode = String(body.user_code || "");
  if (!deviceCode || !userCode) throw new BaiduApiError("Baidu device code response is missing device_code/user_code", 502, redactJson(body));
  const expiresIn = Math.max(60, asNumber(body.expires_in, 1800));
  const interval = Math.max(5, asNumber(body.interval, 5));
  const sessionId = `bd_${randomUUID()}`;
  const verificationUrl = String(body.verification_url || "https://openapi.baidu.com/device");
  const qrcodeUrl = String(body.qrcode_url || `https://openapi.baidu.com/device?display=mobile&code=${encodeURIComponent(userCode)}`);
  const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
  await sql`
    INSERT INTO baidu_netdisk_auth_sessions (id, device_code_ciphertext, user_code, verification_url, qrcode_url, expires_at, poll_interval_seconds, status)
    VALUES (${sessionId}, ${encryptSecret(deviceCode)}, ${userCode}, ${verificationUrl}, ${qrcodeUrl}, ${expiresAt}, ${interval}, 'pending')
  `;
  log("device_auth_started", { sessionId, userCode, expiresAt, interval });
  const rows = await sql<AuthSessionRow[]>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`;
  return { ok: true, session: authSessionFromRow(rows[0]) };
}

async function fetchBaiduAccount(accessToken: string): Promise<{ uinfo: JsonRecord; quota: JsonRecord }> {
  const [uinfo, quota] = await Promise.all([
    fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/nas", { method: "uinfo", access_token: accessToken, vip_version: "v2" })),
    fetchJson(buildUrl("https://pan.baidu.com/api/quota", { access_token: accessToken, checkfree: 1, checkexpire: 1 })),
  ]);
  return { uinfo, quota };
}

async function persistTokenFromOAuth(tokenBody: JsonRecord): Promise<JsonRecord> {
  const accessToken = String(tokenBody.access_token || "");
  const refreshToken = String(tokenBody.refresh_token || "");
  if (!accessToken || !refreshToken) throw new BaiduApiError("Baidu token response is missing tokens", 502, redactJson(tokenBody));
  const { uinfo, quota } = await fetchBaiduAccount(accessToken);
  const uid = String(uinfo.uk || uinfo.baidu_uid || "default");
  const accountId = `baidu_${uid.replace(/[^A-Za-z0-9_-]/g, "_")}`;
  const username = String(uinfo.netdisk_name || uinfo.baidu_name || "");
  const expiresIn = Math.max(60, asNumber(tokenBody.expires_in, 2592000));
  const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
  const scope = String(tokenBody.scope || "");
  await sql`
    INSERT INTO baidu_netdisk_accounts (id, baidu_uid, username, avatar_url, vip_type, root_path, quota_json, updated_at)
    VALUES (${accountId}, ${uid}, ${username}, ${String(uinfo.avatar_url || "")}, ${uinfo.vip_type === undefined ? null : Number(uinfo.vip_type)}, ${config.appRoot}, ${sql.json(quota)}, now())
    ON CONFLICT (id) DO UPDATE SET
      baidu_uid = EXCLUDED.baidu_uid,
      username = EXCLUDED.username,
      avatar_url = EXCLUDED.avatar_url,
      vip_type = EXCLUDED.vip_type,
      root_path = EXCLUDED.root_path,
      quota_json = EXCLUDED.quota_json,
      updated_at = now()
  `;
  await sql`
    INSERT INTO baidu_netdisk_tokens (account_id, access_token_ciphertext, refresh_token_ciphertext, expires_at, scope, generation, last_refresh_at, updated_at)
    VALUES (${accountId}, ${encryptSecret(accessToken)}, ${encryptSecret(refreshToken)}, ${expiresAt}, ${scope}, 0, now(), now())
    ON CONFLICT (account_id) DO UPDATE SET
      access_token_ciphertext = EXCLUDED.access_token_ciphertext,
      refresh_token_ciphertext = EXCLUDED.refresh_token_ciphertext,
      expires_at = EXCLUDED.expires_at,
      scope = EXCLUDED.scope,
      generation = baidu_netdisk_tokens.generation + 1,
      last_refresh_at = now(),
      updated_at = now()
  `;
  await ensureAppRoot(accessToken, true);
  log("token_persisted", { accountId, uid, username, expiresAt, scope });
  return { accountId, uid, username, avatarUrl: String(uinfo.avatar_url || ""), vipType: Number(uinfo.vip_type ?? 0), rootPath: config.appRoot, quota, expiresAt, scope };
}

function oauthPendingError(error: string): boolean {
  return ["authorization_pending", "authorization_declined", "slow_down", "expired_token", "access_denied"].includes(error);
}

async function pollDeviceAuth(sessionId: string): Promise<JsonRecord> {
  requireBaiduConfigured();
  const rows = await sql<Array<AuthSessionRow & { device_code_ciphertext: string }>>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId} LIMIT 1`;
  const row = rows[0];
  if (!row) throw new HttpError(404, "auth session not found", { sessionId });
  const expiresAtMs = new Date(row.expires_at).getTime();
  if (row.status === "succeeded" || row.status === "failed" || row.status === "rejected" || row.status === "expired") {
    return { ok: true, session: authSessionFromRow(row) };
  }
  if (Number.isFinite(expiresAtMs) && expiresAtMs <= Date.now()) {
    await sql`UPDATE baidu_netdisk_auth_sessions SET status = 'expired', error = 'device code expired', updated_at = now() WHERE id = ${sessionId}`;
    const latest = await sql<AuthSessionRow[]>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`;
    return { ok: true, session: authSessionFromRow(latest[0]) };
  }
  const lastPollMs = row.last_poll_at ? new Date(row.last_poll_at).getTime() : 0;
  const minIntervalMs = Math.max(5, row.poll_interval_seconds) * 1000;
  if (Number.isFinite(lastPollMs) && Date.now() - lastPollMs < minIntervalMs) {
    return { ok: true, session: authSessionFromRow(row), nextPollAfterMs: Math.max(0, minIntervalMs - (Date.now() - lastPollMs)) };
  }
  const tokenUrl = buildUrl("https://openapi.baidu.com/oauth/2.0/token", {
    grant_type: "device_token",
    code: decryptSecret(row.device_code_ciphertext),
    client_id: config.clientId,
    client_secret: config.clientSecret,
  });
  const tokenBody = await oauthJsonAllowHttpError(tokenUrl);
  const oauthError = typeof tokenBody.error === "string" ? tokenBody.error : "";
  if (oauthError) {
    const description = String(tokenBody.error_description || oauthError);
    let status = "pending";
    let pollInterval = row.poll_interval_seconds;
    if (oauthError === "slow_down") pollInterval = Math.max(5, pollInterval + 5);
    if (oauthError === "expired_token") status = "expired";
    if (oauthError === "authorization_declined" || oauthError === "access_denied") status = "rejected";
    if (!oauthPendingError(oauthError)) status = "failed";
    const sessionError = status === "pending" ? "" : description;
    await sql`
      UPDATE baidu_netdisk_auth_sessions
      SET status = ${status}, error = ${sessionError}, poll_interval_seconds = ${pollInterval}, poll_count = poll_count + 1, last_poll_at = now(), updated_at = now()
      WHERE id = ${sessionId}
    `;
    log("device_auth_poll", { sessionId, status, oauthError, pollInterval });
    const latest = await sql<AuthSessionRow[]>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`;
    return { ok: true, session: authSessionFromRow(latest[0]) };
  }
  const account = await persistTokenFromOAuth(tokenBody);
  await sql`
    UPDATE baidu_netdisk_auth_sessions
    SET status = 'succeeded', error = NULL, token_account_id = ${String(account.accountId)}, poll_count = poll_count + 1, last_poll_at = now(), updated_at = now()
    WHERE id = ${sessionId}
  `;
  const latest = await sql<AuthSessionRow[]>`SELECT * FROM baidu_netdisk_auth_sessions WHERE id = ${sessionId}`;
  return { ok: true, session: authSessionFromRow(latest[0]), account };
}

async function activeTokenRow(): Promise<TokenRow | null> {
  const rows = await sql<TokenRow[]>`
    SELECT * FROM baidu_netdisk_tokens
    ORDER BY updated_at DESC
    LIMIT 1
  `;
  return rows[0] ?? null;
}

async function refreshToken(accountId: string): Promise<string> {
  requireBaiduConfigured();
  return sql.begin(async (tx) => {
    const rows = await tx<TokenRow[]>`SELECT * FROM baidu_netdisk_tokens WHERE account_id = ${accountId} FOR UPDATE`;
    const row = rows[0];
    if (!row) throw new HttpError(401, "Baidu account is not logged in");
    const expiresAt = new Date(row.expires_at).getTime();
    if (Number.isFinite(expiresAt) && expiresAt - Date.now() > 5 * 60 * 1000) return decryptSecret(row.access_token_ciphertext);
    const tokenUrl = buildUrl("https://openapi.baidu.com/oauth/2.0/token", {
      grant_type: "refresh_token",
      refresh_token: decryptSecret(row.refresh_token_ciphertext),
      client_id: config.clientId,
      client_secret: config.clientSecret,
    });
    const tokenBody = await oauthJson(tokenUrl);
    if (typeof tokenBody.error === "string" && tokenBody.error) throw new BaiduApiError(baiduErrorMessage(tokenBody), 502, redactJson(tokenBody));
    const accessToken = String(tokenBody.access_token || "");
    const refreshTokenValue = String(tokenBody.refresh_token || "");
    if (!accessToken || !refreshTokenValue) throw new BaiduApiError("Baidu refresh response is missing tokens", 502, redactJson(tokenBody));
    const expiresIn = Math.max(60, asNumber(tokenBody.expires_in, 2592000));
    const expiresAtText = new Date(Date.now() + expiresIn * 1000).toISOString();
    await tx`
      UPDATE baidu_netdisk_tokens
      SET access_token_ciphertext = ${encryptSecret(accessToken)},
          refresh_token_ciphertext = ${encryptSecret(refreshTokenValue)},
          expires_at = ${expiresAtText},
          scope = ${String(tokenBody.scope || row.scope || "")},
          generation = generation + 1,
          last_refresh_at = now(),
          updated_at = now()
      WHERE account_id = ${accountId}
    `;
    log("token_refreshed", { accountId, expiresAt: expiresAtText });
    return accessToken;
  });
}

async function getAccessToken(): Promise<{ accountId: string; accessToken: string }> {
  const row = await activeTokenRow();
  if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in");
  const expiresAt = new Date(row.expires_at).getTime();
  if (Number.isFinite(expiresAt) && expiresAt - Date.now() > 5 * 60 * 1000) {
    return { accountId: row.account_id, accessToken: decryptSecret(row.access_token_ciphertext) };
  }
  return { accountId: row.account_id, accessToken: await refreshToken(row.account_id) };
}

function accountView(account: JsonRecord, token: JsonRecord | null): JsonRecord {
  const quota = asRecord(account.quota_json);
  const total = asNumber(quota.total, 0);
  const used = asNumber(quota.used, 0);
  return {
    id: String(account.id || ""),
    baiduUid: String(account.baidu_uid || ""),
    username: String(account.username || ""),
    avatarUrl: String(account.avatar_url || ""),
    vipType: account.vip_type === null || account.vip_type === undefined ? null : Number(account.vip_type),
    rootPath: String(account.root_path || config.appRoot),
    quota: {
      ...quota,
      total,
      used,
      freeBytes: Math.max(0, total - used),
      usedPercent: total > 0 ? Math.round((used / total) * 1000) / 10 : 0,
    },
    token: token ? {
      expiresAt: iso(String(token.expires_at || "")),
      scope: String(token.scope || ""),
      generation: Number(token.generation || 0),
      lastRefreshAt: iso(String(token.last_refresh_at || "")),
    } : null,
    updatedAt: iso(String(account.updated_at || "")),
  };
}

async function accountSummary(refreshRemote: boolean): Promise<JsonRecord> {
  const row = await activeTokenRow();
  if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in");
  const { accountId, accessToken } = await getAccessToken();
  await ensureAppRoot(accessToken);
  if (refreshRemote) {
    const { uinfo, quota } = await fetchBaiduAccount(accessToken);
    await sql`
      UPDATE baidu_netdisk_accounts
      SET baidu_uid = ${String(uinfo.uk || "")},
          username = ${String(uinfo.netdisk_name || uinfo.baidu_name || "")},
          avatar_url = ${String(uinfo.avatar_url || "")},
          vip_type = ${uinfo.vip_type === undefined ? null : Number(uinfo.vip_type)},
          root_path = ${config.appRoot},
          quota_json = ${sql.json(quota)},
          updated_at = now()
      WHERE id = ${accountId}
    `;
  }
  const rows = await sql<JsonRecord[]>`
    SELECT a.*, t.expires_at, t.scope, t.generation, t.last_refresh_at
    FROM baidu_netdisk_accounts a
    LEFT JOIN baidu_netdisk_tokens t ON t.account_id = a.id
    WHERE a.id = ${accountId}
    LIMIT 1
  `;
  const account = rows[0];
  if (!account) throw new HttpError(401, "Baidu account row is missing");
  return { ok: true, account: accountView(account, account) };
}

async function authSummary(): Promise<JsonRecord> {
  const [accountRows, sessionRows] = await Promise.all([
    sql<JsonRecord[]>`SELECT a.*, t.expires_at, t.scope, t.generation, t.last_refresh_at FROM baidu_netdisk_accounts a LEFT JOIN baidu_netdisk_tokens t ON t.account_id = a.id ORDER BY a.updated_at DESC LIMIT 1`,
    sql<AuthSessionRow[]>`SELECT * FROM baidu_netdisk_auth_sessions ORDER BY updated_at DESC LIMIT 1`,
  ]);
  return {
    configured: Boolean(config.clientId && config.clientSecret && config.tokenKey),
    clientIdConfigured: Boolean(config.clientId),
    clientSecretConfigured: Boolean(config.clientSecret),
    tokenKeyConfigured: Boolean(config.tokenKey),
    loggedIn: accountRows.length > 0,
    account: accountRows[0] ? accountView(accountRows[0], accountRows[0]) : null,
    latestSession: sessionRows[0] ? authSessionFromRow(sessionRows[0]) : null,
  };
}

async function health(): Promise<JsonRecord> {
  return {
    ok: schemaReady,
    service: "baidu-netdisk",
    startedAt: serviceStartedAt,
    deploy: deployMetadata(),
    storage: { postgres: schemaReady ? "ready" : "starting", stagingDir: config.stagingDir },
    auth: await authSummary(),
    baidu: { appRoot: config.appRoot, userAgent: config.userAgent, apiReachability: "not_checked" },
    queue: { worker: "in-process", transfers: await transferCounts() },
  };
}

async function forceRefreshAuth(): Promise<JsonRecord> {
  const row = await activeTokenRow();
  if (!row) throw new HttpError(401, "Baidu Netdisk is not logged in");
  const accessToken = await refreshToken(row.account_id);
  return { ok: true, accountId: row.account_id, accessToken: redactString(accessToken), account: (await accountSummary(true)).account };
}

async function logoutAuth(): Promise<JsonRecord> {
  await sql`UPDATE baidu_netdisk_transfer_jobs SET status = 'canceled', error = 'logout requested', updated_at = now() WHERE status IN ('queued', 'running')`;
  await sql`DELETE FROM baidu_netdisk_tokens`;
  await sql`DELETE FROM baidu_netdisk_accounts`;
  await sql`UPDATE baidu_netdisk_auth_sessions SET status = 'expired', error = 'logout requested', updated_at = now() WHERE status = 'pending'`;
  log("logout_completed");
  return { ok: true };
}

function fileEntry(raw: JsonRecord): JsonRecord {
  const rawPath = String(raw.path || "");
  return {
    fsId: String(raw.fs_id || raw.fsid || ""),
    path: rawPath,
    serverFilename: String(raw.server_filename || raw.filename || raw.name || (rawPath ? pathPosix.basename(rawPath) : "")),
    isDir: Number(raw.isdir || 0) === 1,
    size: asNumber(raw.size, 0),
    category: raw.category === undefined ? null : Number(raw.category),
    serverMtime: raw.server_mtime === undefined ? null : Number(raw.server_mtime),
    serverCtime: raw.server_ctime === undefined ? null : Number(raw.server_ctime),
    localMtime: raw.local_mtime === undefined ? null : Number(raw.local_mtime),
    md5: typeof raw.md5 === "string" ? raw.md5 : "",
    thumbs: asRecord(raw.thumbs),
  };
}

async function listFiles(url: URL): Promise<JsonRecord> {
  const { accessToken } = await getAccessToken();
  const dir = remotePathInsideRoot(url.searchParams.get("dir") || config.appRoot);
  const start = Math.max(0, asNumber(url.searchParams.get("start"), 0));
  const limit = Math.min(500, Math.max(1, asNumber(url.searchParams.get("limit"), 100)));
  const order = url.searchParams.get("order") || "name";
  const desc = url.searchParams.get("desc") || "0";
  await ensureAppRoot(accessToken);
  const requestList = () => fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", {
    method: "list",
    access_token: accessToken,
    dir,
    start,
    limit,
    order,
    desc,
    web: 1,
    folder: 0,
  }));
  let body: JsonRecord;
  try {
    body = await requestList();
  } catch (error) {
    if (dir !== config.appRoot || !errorHasBaiduErrno(error, -9)) throw error;
    await ensureAppRoot(accessToken, true);
    body = await requestList();
  }
  const list = Array.isArray(body.list) ? body.list.map((item) => fileEntry(asRecord(item))) : [];
  return { ok: true, rootPath: config.appRoot, dir, start, limit, hasMore: Number(body.has_more || 0) === 1, files: list };
}

function parseFsIds(value: string | null): string[] {
  if (!value) return [];
  const trimmed = value.trim();
  if (!trimmed) return [];
  if (trimmed.startsWith("[")) {
    try {
      const parsed = JSON.parse(trimmed) as unknown;
      if (Array.isArray(parsed)) return parsed.map((item) => String(item)).filter(Boolean);
    } catch {
      return [];
    }
  }
  return trimmed.split(",").map((item) => item.trim()).filter(Boolean);
}

async function fileMetas(fsIds: string[], dlink: boolean): Promise<JsonRecord> {
  if (fsIds.length === 0) throw new HttpError(400, "fsids is required");
  const { accessToken } = await getAccessToken();
  const body = await fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/multimedia", {
    method: "filemetas",
    access_token: accessToken,
    fsids: JSON.stringify(fsIds.map((id) => Number.isFinite(Number(id)) ? Number(id) : id)),
    dlink: dlink ? 1 : 0,
  }));
  const list = Array.isArray(body.list) ? body.list.map((item) => {
    const record = fileEntry(asRecord(item));
    if (dlink) record.dlinkRedacted = Boolean(asRecord(item).dlink);
    return record;
  }) : [];
  return { ok: true, files: list, dlinkRedacted: dlink };
}

async function createFolder(body: JsonRecord): Promise<JsonRecord> {
  const { accessToken } = await getAccessToken();
  const path = remotePathInsideRoot(String(body.path || ""));
  await ensureAppRoot(accessToken);
  const result = await createRemoteFolder(accessToken, path);
  log("folder_created", { path, result });
  return { ok: true, folder: fileEntry(result), result: redactJson(result) };
}

function sanitizeFileManagerList(opera: string, raw: unknown): JsonValue[] {
  if (!Array.isArray(raw) || raw.length === 0) throw new HttpError(400, "filelist must be a non-empty array");
  return raw.map((item) => {
    const record = asRecord(item);
    const output: JsonRecord = { path: remotePathInsideRoot(String(record.path || "")) };
    if (opera === "copy" || opera === "move") output.dest = remotePathInsideRoot(String(record.dest || config.appRoot));
    if (opera === "copy" || opera === "move" || opera === "rename") {
      const newname = String(record.newname || "").trim();
      if (!newname || newname.includes("/") || newname.includes("\\")) throw new HttpError(400, "newname is required and must not contain slashes");
      output.newname = newname;
    }
    return output;
  });
}

async function manageFiles(body: JsonRecord): Promise<JsonRecord> {
  const opera = String(body.opera || "").trim();
  if (!["copy", "move", "rename", "delete"].includes(opera)) throw new HttpError(400, "opera must be copy, move, rename, or delete");
  const filelist = sanitizeFileManagerList(opera, body.filelist);
  const { accessToken } = await getAccessToken();
  const result = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "filemanager", access_token: accessToken, opera }), {
    async: body.async === 0 ? 0 : 1,
    ondup: typeof body.ondup === "string" ? body.ondup : undefined,
    filelist,
  });
  log("files_managed", { opera, count: filelist.length, result });
  return { ok: true, opera, result: redactJson(result) };
}

function transferFromRow(row: TransferJobRow): JsonRecord {
  const sizeBytes = asNumber(row.size_bytes, 0);
  const bytesDone = asNumber(row.bytes_done, 0);
  return {
    id: row.id,
    accountId: row.account_id || "",
    direction: row.direction,
    status: row.status,
    localPath: row.local_path,
    remotePath: row.remote_path,
    fsId: row.fs_id || "",
    sizeBytes,
    bytesDone,
    progressPercent: sizeBytes > 0 ? Math.round((Math.min(bytesDone, sizeBytes) / sizeBytes) * 1000) / 10 : (row.status === "succeeded" ? 100 : 0),
    partSize: row.part_size || config.partSizeBytes,
    blockList: row.block_list_json,
    uploadId: row.uploadid || "",
    retryCount: row.retry_count,
    error: row.error || "",
    result: redactJson(row.result),
    createdAt: iso(row.created_at),
    updatedAt: iso(row.updated_at),
  };
}

async function transferCounts(): Promise<JsonRecord> {
  const rows = await sql<JsonRecord[]>`SELECT status, count(*)::int AS count FROM baidu_netdisk_transfer_jobs GROUP BY status`;
  const counts: JsonRecord = {};
  for (const row of rows) counts[String(row.status)] = Number(row.count || 0);
  return counts;
}

async function recordTransferEvent(jobId: string, level: "info" | "warn" | "error", message: string, data: JsonRecord = {}): Promise<void> {
  await sql`INSERT INTO baidu_netdisk_transfer_events (job_id, level, message, data_json) VALUES (${jobId}, ${level}, ${message}, ${sql.json(redactJson(data))})`;
  log("transfer_event", { jobId, level, message, data });
}

async function updateTransfer(jobId: string, patch: Partial<{ status: TransferStatus; accountId: string; localPath: string; remotePath: string; fsId: string; sizeBytes: number; bytesDone: number; partSize: number; blockList: JsonValue; uploadId: string; error: string; result: JsonValue }>): Promise<void> {
  const current = (await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`)[0];
  if (!current) return;
  await sql`
    UPDATE baidu_netdisk_transfer_jobs
    SET status = ${patch.status ?? current.status},
        account_id = ${patch.accountId ?? current.account_id},
        local_path = ${patch.localPath ?? current.local_path},
        remote_path = ${patch.remotePath ?? current.remote_path},
        fs_id = ${patch.fsId ?? current.fs_id},
        size_bytes = ${patch.sizeBytes ?? current.size_bytes},
        bytes_done = ${patch.bytesDone ?? current.bytes_done},
        part_size = ${patch.partSize ?? current.part_size},
        block_list_json = ${sql.json(patch.blockList ?? current.block_list_json)},
        uploadid = ${patch.uploadId ?? current.uploadid},
        error = ${patch.error ?? current.error},
        result = ${sql.json(patch.result ?? current.result)},
        updated_at = now()
    WHERE id = ${jobId}
  `;
}

async function transferIsCanceled(jobId: string): Promise<boolean> {
  const rows = await sql<Array<{ status: string }>>`SELECT status FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`;
  return rows[0]?.status === "canceled";
}

async function assertNotCanceled(jobId: string): Promise<void> {
  if (await transferIsCanceled(jobId)) throw new HttpError(409, "transfer was canceled", { jobId });
}

async function createTransferJob(direction: TransferDirection, body: JsonRecord): Promise<JsonRecord> {
  const { accountId } = await getAccessToken();
  const id = `bdt_${Date.now()}_${randomUUID().slice(0, 8)}`;
  const localPath = direction === "upload"
    ? resolveStagingPath(String(body.localPath || ""))
    : resolveStagingPath(String(body.localPath || body.localDir || "downloads"));
  const remotePath = direction === "upload"
    ? remoteFilePath(String(body.remotePath || ""), basename(localPath))
    : remotePathInsideRoot(String(body.remotePath || config.appRoot));
  const fsId = body.fsId === undefined || body.fsId === null ? null : String(body.fsId);
  await sql`
    INSERT INTO baidu_netdisk_transfer_jobs (id, account_id, direction, status, local_path, remote_path, fs_id, part_size)
    VALUES (${id}, ${accountId}, ${direction}, 'queued', ${localPath}, ${remotePath}, ${fsId}, ${config.partSizeBytes})
  `;
  await recordTransferEvent(id, "info", "transfer queued", { direction, localPath, remotePath, fsId: fsId || "" });
  setTimeout(() => {
    runTransferJob(id).catch((error) => log("transfer_worker_uncaught", { jobId: id, error: errorToJson(error) }));
  }, 0);
  const row = (await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${id}`)[0];
  return { ok: true, job: transferFromRow(row) };
}

function safeStagingFilename(value: string, fallback: string): string {
  const base = basename(value || fallback || "upload.txt").replace(/[^A-Za-z0-9._-]+/gu, "_").replace(/^_+|_+$/gu, "");
  return base || fallback || "upload.txt";
}

function contentUploadBuffer(body: JsonRecord): { buffer: Buffer; source: string } {
  if (typeof body.dataBase64 === "string" && body.dataBase64.length > 0) return { buffer: Buffer.from(body.dataBase64, "base64"), source: "dataBase64" };
  if (typeof body.content === "string") return { buffer: Buffer.from(body.content, "utf8"), source: "content" };
  throw new HttpError(400, "upload-content requires content or dataBase64");
}

async function createContentUpload(body: JsonRecord): Promise<JsonRecord> {
  const { buffer, source } = contentUploadBuffer(body);
  const maxBytes = Math.max(1, Math.min(10 * 1024 * 1024, asNumber(body.maxBytes, 10 * 1024 * 1024)));
  if (buffer.byteLength > maxBytes) throw new HttpError(413, "upload-content body is too large", { maxBytes, bytes: buffer.byteLength });
  const remotePath = remoteFilePath(String(body.remotePath || ""), safeStagingFilename(String(body.filename || ""), "upload.txt"));
  const filename = safeStagingFilename(String(body.filename || pathPosix.basename(remotePath)), pathPosix.basename(remotePath) || "upload.txt");
  const relativePath = pathPosix.join("content-uploads", `${Date.now()}-${randomUUID().slice(0, 8)}-${filename}`);
  const localPath = resolveStagingPath(relativePath);
  mkdirSync(dirname(localPath), { recursive: true });
  await writeFile(localPath, buffer);
  const sha256 = createHash("sha256").update(buffer).digest("hex");
  const created = await createTransferJob("upload", { localPath: relativePath, remotePath });
  return {
    ok: created.ok === true,
    source,
    remotePath,
    local: { relativePath, bytes: buffer.byteLength, sha256 },
    job: asRecord(created.job),
  };
}

async function computeMd5Blocks(filePath: string, jobId: string): Promise<{ size: number; fullMd5: string; blocks: string[] }> {
  const info = await stat(filePath);
  if (!info.isFile()) throw new HttpError(400, "localPath must be a file inside staging directory", { localPath: filePath });
  const fullHash = createHash("md5");
  const blocks: string[] = [];
  const handle = await open(filePath, "r");
  let offset = 0;
  try {
    while (offset < info.size) {
      await assertNotCanceled(jobId);
      const chunkSize = Math.min(config.partSizeBytes, info.size - offset);
      const buffer = Buffer.allocUnsafe(chunkSize);
      const { bytesRead } = await handle.read(buffer, 0, chunkSize, offset);
      if (bytesRead <= 0) break;
      const chunk = buffer.subarray(0, bytesRead);
      fullHash.update(chunk);
      blocks.push(createHash("md5").update(chunk).digest("hex"));
      offset += bytesRead;
      await updateTransfer(jobId, { sizeBytes: info.size, bytesDone: Math.min(offset, info.size), blockList: blocks });
    }
  } finally {
    await handle.close();
  }
  return { size: info.size, fullMd5: fullHash.digest("hex"), blocks };
}

function uploadServerUrl(locateResult: JsonRecord): string {
  const servers = Array.isArray(locateResult.servers) ? locateResult.servers : [];
  const first = servers.find((item) => typeof item === "string" || (typeof item === "object" && item !== null));
  const raw = typeof first === "string" ? first : String(asRecord(first).server || asRecord(first).host || "");
  if (!raw) return "https://d.pcs.baidu.com";
  return raw.startsWith("http://") || raw.startsWith("https://") ? raw.replace(/\/+$/u, "") : `https://${raw.replace(/^\/+|\/+$/gu, "")}`;
}

async function uploadPart(uploadBaseUrl: string, accessToken: string, filePath: string, remotePath: string, uploadId: string, partIndex: number): Promise<number> {
  const info = await stat(filePath);
  const start = partIndex * config.partSizeBytes;
  const chunkSize = Math.min(config.partSizeBytes, Math.max(0, info.size - start));
  const handle = await open(filePath, "r");
  try {
    const buffer = Buffer.allocUnsafe(chunkSize);
    const { bytesRead } = await handle.read(buffer, 0, chunkSize, start);
    const chunk = buffer.subarray(0, bytesRead);
    const form = new FormData();
    form.append("file", new Blob([chunk]), basename(remotePath));
    const url = buildUrl(`${uploadBaseUrl}/rest/2.0/pcs/superfile2`, {
      method: "upload",
      access_token: accessToken,
      type: "tmpfile",
      path: remotePath,
      uploadid: uploadId,
      partseq: partIndex,
    });
    await fetchJson(url, { method: "POST", body: form });
    return bytesRead;
  } finally {
    await handle.close();
  }
}

async function runUploadJob(job: TransferJobRow): Promise<JsonRecord> {
  const { accountId, accessToken } = await getAccessToken();
  await updateTransfer(job.id, { accountId });
  await ensureAppRoot(accessToken);
  await recordTransferEvent(job.id, "info", "computing md5 blocks");
  const hashes = await computeMd5Blocks(job.local_path, job.id);
  await assertNotCanceled(job.id);
  await recordTransferEvent(job.id, "info", "precreate upload", { size: hashes.size, blocks: hashes.blocks.length, fullMd5: hashes.fullMd5 });
  const precreate = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "precreate", access_token: accessToken }), {
    path: job.remote_path,
    size: hashes.size,
    isdir: 0,
    autoinit: 1,
    rtype: 1,
    block_list: hashes.blocks,
  });
  const uploadId = String(precreate.uploadid || "");
  await updateTransfer(job.id, { uploadId, blockList: hashes.blocks, bytesDone: 0, sizeBytes: hashes.size });
  const requiredParts = Array.isArray(precreate.block_list)
    ? precreate.block_list.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
    : hashes.blocks.map((_, index) => index);
  if (hashes.size > 0 && requiredParts.length > 0) {
    if (!uploadId) throw new BaiduApiError("precreate response is missing uploadid", 502, redactJson(precreate));
    const locate = await fetchJson(buildUrl("https://d.pcs.baidu.com/rest/2.0/pcs/file", {
      method: "locateupload",
      appid: 250528,
      access_token: accessToken,
      path: job.remote_path,
      uploadid: uploadId,
      upload_version: "2.0",
    }));
    const server = uploadServerUrl(locate);
    let bytesUploaded = 0;
    for (const partIndex of requiredParts) {
      await assertNotCanceled(job.id);
      const bytesRead = await uploadPart(server, accessToken, job.local_path, job.remote_path, uploadId, partIndex);
      bytesUploaded += bytesRead;
      await updateTransfer(job.id, { bytesDone: Math.min(hashes.size, bytesUploaded), uploadId });
      await recordTransferEvent(job.id, "info", "uploaded part", { partIndex, bytesRead });
    }
  }
  await assertNotCanceled(job.id);
  const created = await baiduFormJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/file", { method: "create", access_token: accessToken }), {
    path: job.remote_path,
    size: hashes.size,
    isdir: 0,
    rtype: 1,
    uploadid: uploadId,
    block_list: hashes.blocks,
  });
  return { remotePath: job.remote_path, sizeBytes: hashes.size, md5: hashes.fullMd5, baidu: redactJson(created) };
}

async function outputPathForDownload(job: TransferJobRow, filename: string): Promise<string> {
  const raw = job.local_path;
  if (raw.endsWith("/") || raw.endsWith(sep)) {
    const rel = relative(config.stagingDir, raw);
    return resolveStagingPath(rel ? `${rel}/${filename}` : filename);
  }
  try {
    const info = await stat(raw);
    if (info.isDirectory()) {
      const rel = relative(config.stagingDir, raw);
      return resolveStagingPath(rel ? `${rel}/${filename}` : filename);
    }
  } catch {
    // Missing path is treated as the final file path.
  }
  return raw;
}

async function runDownloadJob(job: TransferJobRow): Promise<JsonRecord> {
  if (!job.fs_id) throw new HttpError(400, "fsId is required for download jobs", { jobId: job.id });
  const { accountId, accessToken } = await getAccessToken();
  await updateTransfer(job.id, { accountId });
  const rawMeta = await fetchJson(buildUrl("https://pan.baidu.com/rest/2.0/xpan/multimedia", {
    method: "filemetas",
    access_token: accessToken,
    fsids: JSON.stringify([Number.isFinite(Number(job.fs_id)) ? Number(job.fs_id) : job.fs_id]),
    dlink: 1,
  }));
  const first = Array.isArray(rawMeta.list) ? asRecord(rawMeta.list[0]) : {};
  const dlink = String(first.dlink || "");
  if (!dlink) throw new BaiduApiError("Baidu file meta response is missing dlink", 502, redactJson(rawMeta));
  const filename = String(first.server_filename || first.filename || `${job.fs_id}.download`);
  const outPath = await outputPathForDownload(job, filename);
  mkdirSync(dirname(outPath), { recursive: true });
  const tempPath = `${outPath}.partial-${job.id}`;
  const url = new URL(dlink);
  url.searchParams.set("access_token", accessToken);
  const response = await fetch(url, { headers: baiduHeaders(), redirect: "follow" });
  if (!response.ok || !response.body) throw new BaiduApiError(`download failed with HTTP ${response.status}`, response.status, { statusText: response.statusText });
  const total = asNumber(response.headers.get("content-length"), asNumber(first.size, 0));
  await updateTransfer(job.id, { localPath: outPath, sizeBytes: total, bytesDone: 0, remotePath: String(first.path || job.remote_path || "") });
  const writer = createWriteStream(tempPath);
  const reader = response.body.getReader();
  let bytesDone = 0;
  let lastUpdate = 0;
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      await assertNotCanceled(job.id);
      if (value && value.byteLength > 0) {
        bytesDone += value.byteLength;
        if (!writer.write(Buffer.from(value))) await new Promise((resolveDrain) => writer.once("drain", resolveDrain));
      }
      if (Date.now() - lastUpdate > 800) {
        lastUpdate = Date.now();
        await updateTransfer(job.id, { bytesDone, sizeBytes: total });
      }
    }
  } catch (error) {
    writer.destroy();
    throw error;
  }
  await new Promise<void>((resolveEnd, rejectEnd) => {
    writer.once("error", rejectEnd);
    writer.end(resolveEnd);
  });
  await rename(tempPath, outPath);
  await updateTransfer(job.id, { bytesDone, sizeBytes: total, localPath: outPath });
  return { localPath: outPath, remotePath: String(first.path || job.remote_path || ""), fsId: job.fs_id, sizeBytes: bytesDone, filename };
}

async function runTransferJob(jobId: string): Promise<void> {
  const row = (await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`)[0];
  if (!row || row.status !== "queued") return;
  await updateTransfer(jobId, { status: "running", error: "" });
  await recordTransferEvent(jobId, "info", "transfer started", { direction: row.direction });
  try {
    const result = row.direction === "upload" ? await runUploadJob(row) : await runDownloadJob(row);
    await updateTransfer(jobId, { status: "succeeded", result, bytesDone: asNumber(result.sizeBytes, asNumber(row.size_bytes, 0)) });
    await recordTransferEvent(jobId, "info", "transfer succeeded", result);
  } catch (error) {
    const canceled = await transferIsCanceled(jobId);
    const message = error instanceof Error ? error.message : String(error);
    await updateTransfer(jobId, { status: canceled ? "canceled" : "failed", error: message });
    await recordTransferEvent(jobId, canceled ? "warn" : "error", canceled ? "transfer canceled" : "transfer failed", { error: errorToJson(error) });
  }
}

async function listTransfers(url: URL): Promise<JsonRecord> {
  const limit = Math.min(200, Math.max(1, asNumber(url.searchParams.get("limit"), 80)));
  const rows = await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs ORDER BY updated_at DESC LIMIT ${limit}`;
  return { ok: true, jobs: rows.map(transferFromRow), counts: await transferCounts() };
}

async function transferDetail(jobId: string): Promise<JsonRecord> {
  const rows = await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`;
  if (!rows[0]) throw new HttpError(404, "transfer job not found", { jobId });
  const events = await sql<JsonRecord[]>`SELECT id, level, message, data_json AS data, created_at FROM baidu_netdisk_transfer_events WHERE job_id = ${jobId} ORDER BY id DESC LIMIT 120`;
  return { ok: true, job: transferFromRow(rows[0]), events: events.map((event) => ({ ...event, created_at: iso(String(event.created_at || "")) })) as JsonValue };
}

async function cancelTransfer(jobId: string): Promise<JsonRecord> {
  const rows = await sql<TransferJobRow[]>`UPDATE baidu_netdisk_transfer_jobs SET status = 'canceled', error = 'cancel requested', updated_at = now() WHERE id = ${jobId} AND status IN ('queued', 'running') RETURNING *`;
  if (!rows[0]) return transferDetail(jobId);
  await recordTransferEvent(jobId, "warn", "cancel requested");
  return { ok: true, job: transferFromRow(rows[0]) };
}

async function retryTransfer(jobId: string): Promise<JsonRecord> {
  const rows = await sql<TransferJobRow[]>`
    UPDATE baidu_netdisk_transfer_jobs
    SET status = 'queued', retry_count = retry_count + 1, error = NULL, bytes_done = 0, updated_at = now()
    WHERE id = ${jobId} AND status IN ('failed', 'canceled')
    RETURNING *
  `;
  if (!rows[0]) return transferDetail(jobId);
  await recordTransferEvent(jobId, "info", "retry queued");
  setTimeout(() => {
    runTransferJob(jobId).catch((error) => log("transfer_retry_uncaught", { jobId, error: errorToJson(error) }));
  }, 0);
  return { ok: true, job: transferFromRow(rows[0]) };
}

async function waitForTransferTerminal(jobId: string, timeoutMs = 90_000): Promise<JsonRecord> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const rows = await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE id = ${jobId}`;
    if (!rows[0]) throw new HttpError(404, "transfer job not found", { jobId });
    const view = transferFromRow(rows[0]);
    if (["succeeded", "failed", "canceled"].includes(String(view.status))) return view;
    await new Promise((resolveWait) => setTimeout(resolveWait, 800));
  }
  const detail = await transferDetail(jobId);
  throw new HttpError(504, "transfer self-test timed out", { jobId, detail });
}

async function runSelfTest(body: JsonRecord): Promise<JsonRecord> {
  const timeoutMs = Math.min(300_000, Math.max(30_000, asNumber(body.timeoutMs, 90_000)));
  const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
  const filename = `unidesk-smoke-${stamp}-${randomUUID().slice(0, 8)}.txt`;
  const uploadLocalRel = `self-test/${filename}`;
  const downloadLocalRel = `self-test/downloaded-${filename}`;
  const uploadLocalPath = resolveStagingPath(uploadLocalRel);
  const remotePath = remoteFilePath(pathPosix.join(config.appRoot, filename), filename);
  const content = `UniDesk Baidu Netdisk smoke test\ncreatedAt=${new Date().toISOString()}\nremotePath=${remotePath}\n`;
  mkdirSync(dirname(uploadLocalPath), { recursive: true });
  await writeFile(uploadLocalPath, content, "utf8");
  const expectedMd5 = createHash("md5").update(content).digest("hex");

  const uploadCreated = await createTransferJob("upload", { localPath: uploadLocalRel, remotePath });
  const uploadJobId = String(asRecord(uploadCreated.job).id || "");
  const uploadJob = await waitForTransferTerminal(uploadJobId, timeoutMs);
  if (uploadJob.status !== "succeeded") throw new HttpError(502, "upload self-test failed", { job: uploadJob });

  const listUrl = new URL("http://baidu-netdisk.local/api/files");
  listUrl.searchParams.set("dir", config.appRoot);
  listUrl.searchParams.set("order", "time");
  listUrl.searchParams.set("desc", "1");
  listUrl.searchParams.set("limit", "100");
  const listed = await listFiles(listUrl);
  const uploadedEntry = (Array.isArray(listed.files) ? listed.files : [])
    .map((item) => asRecord(item))
    .find((item) => String(item.path || "") === remotePath);
  const uploadResult = asRecord(uploadJob.result);
  const baiduResult = asRecord(uploadResult.baidu);
  const fsId = String(uploadedEntry?.fsId || baiduResult.fs_id || "");
  if (!fsId) throw new HttpError(502, "upload self-test could not find uploaded fs_id", { remotePath, listed });

  const downloadCreated = await createTransferJob("download", { fsId, localPath: downloadLocalRel });
  const downloadJobId = String(asRecord(downloadCreated.job).id || "");
  const downloadJob = await waitForTransferTerminal(downloadJobId, timeoutMs);
  if (downloadJob.status !== "succeeded") throw new HttpError(502, "download self-test failed", { job: downloadJob });
  const downloadedPath = String(downloadJob.localPath || resolveStagingPath(downloadLocalRel));
  const downloaded = await readFile(downloadedPath);
  const downloadedMd5 = createHash("md5").update(downloaded).digest("hex");
  if (downloadedMd5 !== expectedMd5) {
    throw new HttpError(502, "downloaded content md5 does not match uploaded fixture", { expectedMd5, downloadedMd5, downloadedPath });
  }

  const result = {
    ok: true,
    remotePath,
    fsId,
    sizeBytes: Buffer.byteLength(content),
    expectedMd5,
    downloadedMd5,
    uploadJob,
    downloadJob,
    listed: Boolean(uploadedEntry),
    downloadedPath,
    stagingFixture: uploadLocalPath,
  };
  log("self_test_succeeded", result);
  return result;
}

async function logsResponse(url: URL): Promise<JsonRecord> {
  const limit = Math.min(300, Math.max(1, asNumber(url.searchParams.get("limit"), 120)));
  return { ok: true, logs: recentLogs.slice(-limit).reverse() };
}

async function route(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const path = url.pathname;
  if (path === "/health") return jsonResponse(await health());
  if (path === "/logs") return jsonResponse(await logsResponse(url));
  if (path === "/api/auth/device/start" && req.method === "POST") return jsonResponse(await startDeviceAuth());
  if (path === "/api/auth/device/status" && req.method === "GET") return jsonResponse(await pollDeviceAuth(String(url.searchParams.get("sessionId") || "")));
  if (path === "/api/auth/status" && req.method === "GET") return jsonResponse({ ok: true, auth: await authSummary() });
  if (path === "/api/auth/refresh" && req.method === "POST") return jsonResponse(await forceRefreshAuth());
  if (path === "/api/auth/logout" && req.method === "POST") return jsonResponse(await logoutAuth());
  if (path === "/api/account" && req.method === "GET") return jsonResponse(await accountSummary(url.searchParams.get("refresh") !== "0"));
  if (path === "/api/files" && req.method === "GET") return jsonResponse(await listFiles(url));
  if (path === "/api/files/meta" && req.method === "GET") return jsonResponse(await fileMetas(parseFsIds(url.searchParams.get("fsids")), url.searchParams.get("dlink") === "1"));
  if (path === "/api/folders" && req.method === "POST") return jsonResponse(await createFolder(await readJsonBody(req)));
  if (path === "/api/files/manage" && req.method === "POST") return jsonResponse(await manageFiles(await readJsonBody(req)));
  if (path === "/api/transfers/upload-from-path" && req.method === "POST") return jsonResponse(await createTransferJob("upload", await readJsonBody(req)));
  if (path === "/api/transfers/upload-content" && req.method === "POST") return jsonResponse(await createContentUpload(await readJsonBody(req)));
  if (path === "/api/transfers/download-to-path" && req.method === "POST") return jsonResponse(await createTransferJob("download", await readJsonBody(req)));
  if (path === "/api/self-test" && req.method === "POST") return jsonResponse(await runSelfTest(await readJsonBody(req)));
  if (path === "/api/transfers" && req.method === "GET") return jsonResponse(await listTransfers(url));
  const transferMatch = path.match(/^\/api\/transfers\/([^/]+)(?:\/(cancel|retry))?$/u);
  if (transferMatch && req.method === "GET" && !transferMatch[2]) return jsonResponse(await transferDetail(decodeURIComponent(transferMatch[1] || "")));
  if (transferMatch && req.method === "POST" && transferMatch[2] === "cancel") return jsonResponse(await cancelTransfer(decodeURIComponent(transferMatch[1] || "")));
  if (transferMatch && req.method === "POST" && transferMatch[2] === "retry") return jsonResponse(await retryTransfer(decodeURIComponent(transferMatch[1] || "")));
  return jsonResponse({ ok: false, error: "not found", path }, 404);
}

async function resumeQueuedTransfers(): Promise<void> {
  await sql`UPDATE baidu_netdisk_transfer_jobs SET status = 'failed', error = 'service restarted before transfer completed', updated_at = now() WHERE status = 'running'`;
  const rows = await sql<TransferJobRow[]>`SELECT * FROM baidu_netdisk_transfer_jobs WHERE status = 'queued' ORDER BY created_at ASC LIMIT 20`;
  for (const row of rows) {
    setTimeout(() => {
      runTransferJob(row.id).catch((error) => log("transfer_resume_uncaught", { jobId: row.id, error: errorToJson(error) }));
    }, 0);
  }
}

await waitForSchema();
mkdirSync(config.stagingDir, { recursive: true });
await resumeQueuedTransfers();

const server = Bun.serve({
  hostname: config.host,
  port: config.port,
  idleTimeout: 120,
  async fetch(req) {
    try {
      return await route(req);
    } catch (error) {
      return errorResponse(error);
    }
  },
});

log("server_listening", {
  url: `http://${config.host}:${server.port}`,
  appRoot: config.appRoot,
  stagingDir: config.stagingDir,
  clientIdConfigured: Boolean(config.clientId),
  tokenKeyConfigured: Boolean(config.tokenKey),
});
