'use strict';

const QRCode = require('qrcode');
const authStore = require('./mobile-auth-store');
const { requestBrowserOrigin, requestClientIp, requestOrigin } = require('./transport-security');

const CLAIM_PUBLIC_ENDPOINTS = new Set([
  'POST /api/auth/claim/begin-passkey',
  'POST /api/auth/claim/finish',
  'POST /api/auth/logout-local',
]);

let simpleWebAuthnPromise = null;

function simpleWebAuthn() {
  if (!simpleWebAuthnPromise) simpleWebAuthnPromise = import('@simplewebauthn/server');
  return simpleWebAuthnPromise;
}

function isMobileAuthPublicEndpoint(method, pathname) {
  const key = `${String(method || '').toUpperCase()} ${pathname}`;
  if (CLAIM_PUBLIC_ENDPOINTS.has(key)) return true;
  if (key === 'POST /api/auth/pairing-requests') return true;
  return String(method || '').toUpperCase() === 'POST'
    && /^\/api\/auth\/pairing-requests\/[^/]+\/poll$/.test(String(pathname || ''));
}

function isSecureWebAuthnOrigin(origin) {
  try {
    const u = new URL(origin);
    if (u.protocol === 'https:') return true;
    if (u.protocol !== 'http:') return false;
    return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1';
  } catch {
    return false;
  }
}

function rpIdFromOrigin(origin) {
  const u = new URL(origin);
  return u.hostname;
}

function normalizedOrigin(origin) {
  try {
    return new URL(String(origin || '').trim()).origin.toLowerCase();
  } catch {
    return '';
  }
}

function isLoopbackWebAuthnOrigin(origin) {
  try {
    const host = new URL(origin).hostname.toLowerCase();
    return host === 'localhost' || host === '127.0.0.1' || host === '::1';
  } catch {
    return false;
  }
}

function isHostedWalleMobileOrigin(origin) {
  return normalizedOrigin(origin) === 'https://m.walle.sh';
}

// Cookie lifetime tracks the server-side ABSOLUTE cap (default 180d), not a flat
// year: the server hard-rejects the token at the cap, so a longer-lived cookie
// only guarantees a stale-cookie 401 on day cap+1.
function tokenCookie(token, secure, maxAgeSeconds = Math.floor(authStore.deviceTokenAbsoluteMaxMs() / 1000)) {
  return `ctm_token=${encodeURIComponent(token)}; HttpOnly${secure ? '; Secure' : ''}; SameSite=Lax; Path=/; Max-Age=${maxAgeSeconds}`;
}

function stepUpCookie(token, secure, maxAgeSeconds = 10 * 60) {
  return `ctm_step_up=${encodeURIComponent(token)}; HttpOnly${secure ? '; Secure' : ''}; SameSite=Lax; Path=/; Max-Age=${maxAgeSeconds}`;
}

function readJsonBody(req, limit = 1024 * 1024) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk;
      if (body.length > limit) {
        reject(new Error('body_too_large'));
        try { req.destroy(); } catch {}
      }
    });
    req.on('end', () => {
      try { resolve(body ? JSON.parse(body) : {}); }
      catch { reject(new Error('bad_json')); }
    });
    req.on('error', reject);
  });
}

function sendJson(res, status, payload, headers = {}) {
  res.writeHead(status, { 'Content-Type': 'application/json', ...headers });
  res.end(JSON.stringify(payload));
}

function errorJson(res, status, code, message) {
  sendJson(res, status, { ok: false, error: code, message: message || code });
}

function clearLocalSession(req, res) {
  sendJson(res, 200, { ok: true }, {
    'Set-Cookie': [
      tokenCookie('', requestIsHttps(req), 0),
      stepUpCookie('', requestIsHttps(req), 0),
    ],
  });
}

function requestIsHttps(req) {
  return !!req?.socket?.encrypted || String(req?.headers?.['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase() === 'https';
}

function requestWebAuthnOrigin(req) {
  const origin = String(req?.headers?.origin || '').trim();
  const referer = String(req?.headers?.referer || req?.headers?.referrer || '').trim();
  for (const candidate of [origin, referer]) {
    if (!candidate || candidate === 'null') continue;
    const normalized = requestBrowserOrigin(req, candidate);
    if (normalized) return normalized;
  }
  return requestOrigin(req);
}

function claimWebAuthnOrigin(req, claim) {
  const observed = normalizedOrigin(requestWebAuthnOrigin(req));
  const expected = normalizedOrigin(claim?.origin || '');
  if (expected && (!observed || isLoopbackWebAuthnOrigin(observed))) {
    return { ok: true, origin: expected };
  }
  if (expected && observed && expected !== observed) {
    return {
      ok: false,
      status: 409,
      code: 'claim_origin_mismatch',
      message: 'This pairing link was created for a different tunnel URL. Refresh the setup page and create a new QR from the active tunnel URL.',
    };
  }
  return { ok: true, origin: observed || expected };
}

function responseChallenge(response) {
  const encoded = response?.response?.clientDataJSON;
  if (!encoded) return '';
  try {
    const clientData = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
    return String(clientData.challenge || '');
  } catch {
    return '';
  }
}

function publicDevice(device) {
  if (!device) return null;
  return {
    id: device.id,
    label: device.label,
    scopes: device.scopes,
    authorization_status: device.authorization_status || (device.revoked_at ? 'revoked' : 'authorized'),
    connection_status: device.connection_status || (device.revoked_at ? 'revoked' : 'offline'),
    connected_now: !!device.connected_now,
    recently_used: !!device.recently_used,
    device_hint: device.device_hint || '',
    credential_count: Number(device.credential_count || 0),
    credential_origins: Array.isArray(device.credential_origins) ? device.credential_origins : [],
    duplicate_group_id: device.duplicate_group_id || '',
    duplicate_count: Number(device.duplicate_count || 1),
    duplicate_of: device.duplicate_of || null,
    superseded_by: device.superseded_by || null,
    created_at: device.created_at,
    claimed_at: device.claimed_at,
    passkey_bound_at: device.passkey_bound_at,
    last_used_at: device.last_used_at,
    revoked_at: device.revoked_at,
    expires_at: device.expires_at,
  };
}

function publicCredential(credential) {
  if (!credential) return null;
  return {
    id: credential.id,
    rp_id: credential.rp_id,
    origin: credential.origin,
    transports: credential.transports,
    created_at: credential.created_at,
    last_used_at: credential.last_used_at,
  };
}

function publicPairingRequest(request) {
  if (!request) return null;
  return {
    id: request.id,
    code: request.code,
    label: request.label,
    scopes: request.scopes,
    origin: request.origin,
    device_hint: request.device_hint || '',
    created_at: request.created_at,
    expires_at: request.expires_at,
    approved_at: request.approved_at || null,
    rejected_at: request.rejected_at || null,
    status: request.status,
  };
}

function requireDeviceAuth(auth) {
  if (!auth?.authenticated || auth.isLoopback || !auth.deviceId) throw new Error('device_token_required');
  return auth;
}

function requireFreshDeviceAuth(auth) {
  const resolved = requireDeviceAuth(auth);
  if (!resolved.stepUpFresh) throw new Error('step_up_required');
  return resolved;
}

async function beginClaimPasskey(req, res, db) {
  const body = await readJsonBody(req);
  const claim = authStore.verifyDeviceClaimSecret(db, body.claim, body.secret);
  const originDecision = claimWebAuthnOrigin(req, claim);
  if (!originDecision.ok) {
    errorJson(res, originDecision.status || 409, originDecision.code, originDecision.message);
    return;
  }
  const origin = originDecision.origin;
  if (!isSecureWebAuthnOrigin(origin)) {
    errorJson(res, 400, 'secure_context_required', 'Phone pairing requires HTTPS, except localhost development.');
    return;
  }
  const rpId = rpIdFromOrigin(origin);
  const recoveryCredentials = authStore.listClaimRecoveryCredentials(db, {
    label: claim.label,
    userAgent: req.headers?.['user-agent'] || '',
    rpId,
  });
  if (recoveryCredentials.length) {
    const { generateAuthenticationOptions } = await simpleWebAuthn();
    const options = await generateAuthenticationOptions({
      rpID: rpId,
      allowCredentials: recoveryCredentials.map(({ credential }) => ({
        id: credential.credential_id,
        transports: credential.transports,
      })),
      userVerification: 'required',
      timeout: 5 * 60 * 1000,
    });
    authStore.saveWebAuthnChallenge(db, {
      kind: 'claim_recovery',
      claimId: claim.id,
      challenge: options.challenge,
      rpId,
      origin,
      ttlMs: 5 * 60 * 1000,
    });
    sendJson(res, 200, {
      ok: true,
      mode: 'recover',
      options,
      recovery: {
        device_count: new Set(recoveryCredentials.map(({ device }) => device.id)).size,
      },
      claim: {
        id: claim.id,
        label: claim.label,
        scopes: claim.scopes,
        expires_at: claim.expires_at,
      },
    });
    return;
  }

  const samePhoneProfiles = authStore.listActiveDeviceProfiles(db, {
    label: claim.label,
    userAgent: req.headers?.['user-agent'] || '',
  });
  const samePhoneOtherOrigin = samePhoneProfiles.find((device) => {
    const credentials = authStore.listCredentialsForDevice(db, device.id);
    return credentials.length && !credentials.some((credential) => credential.rp_id === rpId);
  });
  if (samePhoneOtherOrigin) {
    errorJson(
      res,
      409,
      'phone_origin_rotation_required',
      'This phone already has an active CTM passkey for another origin. Open that stable phone URL, or revoke/replace the old phone pairing from CTM Settings before creating a new passkey.'
    );
    return;
  }

  const { generateRegistrationOptions } = await simpleWebAuthn();
  const options = await generateRegistrationOptions({
    rpName: 'CTM',
    rpID: rpId,
    userName: claim.label,
    userDisplayName: claim.label,
    userID: Buffer.from(claim.id),
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'required',
    },
    preferredAuthenticatorType: 'localDevice',
    timeout: 5 * 60 * 1000,
  });
  authStore.saveWebAuthnChallenge(db, {
    kind: 'registration',
    claimId: claim.id,
    challenge: options.challenge,
    rpId,
    origin,
    ttlMs: 5 * 60 * 1000,
  });
  sendJson(res, 200, {
    ok: true,
    mode: 'register',
    options,
    claim: {
      id: claim.id,
      label: claim.label,
      scopes: claim.scopes,
      expires_at: claim.expires_at,
    },
  });
}

async function finishClaimRecovery(req, res, db, body, claim, challengeRow, challenge) {
  const credentialId = String(body.testCredential?.id || body.response?.id || '');
  const entry = authStore.getClaimRecoveryCredential(db, {
    label: claim.label,
    userAgent: req.headers?.['user-agent'] || '',
    rpId: challengeRow.rp_id,
    credentialId,
  });
  if (!entry) {
    errorJson(res, 401, 'credential_not_found');
    return;
  }

  let newCounter = entry.credential.counter;
  if (process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential) {
    newCounter = Number(body.testCredential.counter || entry.credential.counter);
  } else {
    const { verifyAuthenticationResponse } = await simpleWebAuthn();
    const verification = await verifyAuthenticationResponse({
      response: body.response,
      expectedChallenge: challengeRow.challenge,
      expectedOrigin: challengeRow.origin,
      expectedRPID: challengeRow.rp_id,
      credential: {
        id: entry.credential.credential_id,
        publicKey: entry.credential.public_key,
        counter: entry.credential.counter,
        transports: entry.credential.transports,
      },
      requireUserVerification: true,
    });
    if (!verification.verified) {
      errorJson(res, 401, 'passkey_verification_failed');
      return;
    }
    newCounter = verification.authenticationInfo.newCounter;
  }

  authStore.consumeWebAuthnChallenge(db, {
    kind: 'claim_recovery',
    claimId: claim.id,
    challenge,
  });
  authStore.updateCredentialCounter(db, entry.credential.id, newCounter);
  const issued = authStore.recoverDeviceClaim(db, {
    claimId: claim.id,
    secret: body.secret,
    deviceId: entry.device.id,
    credentialId: entry.credential.credential_id,
    remoteIp: req.socket?.remoteAddress || '',
    userAgent: req.headers?.['user-agent'] || '',
  });
  sendJson(res, 200, {
    ok: true,
    mode: 'recover',
    device: publicDevice(issued.device),
    credential: publicCredential(entry.credential),
  }, {
    'Set-Cookie': tokenCookie(issued.token, requestIsHttps(req)),
  });
}

async function finishClaimPasskey(req, res, db) {
  const body = await readJsonBody(req, 2 * 1024 * 1024);
  const claim = authStore.verifyDeviceClaimSecret(db, body.claim, body.secret);
  const challenge = process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential
    ? String(body.challenge || '')
    : responseChallenge(body.response);
  if (!challenge) {
    errorJson(res, 400, 'challenge_missing');
    return;
  }
  const mode = String(body.mode || '').toLowerCase() === 'recover' ? 'recover' : 'register';
  const challengeRow = authStore.getWebAuthnChallenge(db, {
    kind: mode === 'recover' ? 'claim_recovery' : 'registration',
    claimId: claim.id,
    challenge,
  });
  if (!challengeRow) {
    errorJson(res, 400, 'challenge_not_found');
    return;
  }
  if (mode === 'recover') {
    await finishClaimRecovery(req, res, db, body, claim, challengeRow, challenge);
    return;
  }

  let credential;
  if (process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential) {
    credential = {
      id: String(body.testCredential.id || 'test-credential'),
      publicKey: Buffer.from(String(body.testCredential.publicKey || 'test-public-key')),
      counter: Number(body.testCredential.counter || 0),
      transports: Array.isArray(body.testCredential.transports) ? body.testCredential.transports : ['internal'],
    };
  } else {
    const { verifyRegistrationResponse } = await simpleWebAuthn();
    const verification = await verifyRegistrationResponse({
      response: body.response,
      expectedChallenge: challengeRow.challenge,
      expectedOrigin: challengeRow.origin,
      expectedRPID: challengeRow.rp_id,
      requireUserVerification: true,
    });
    if (!verification.verified || !verification.registrationInfo?.credential) {
      errorJson(res, 401, 'passkey_verification_failed');
      return;
    }
    credential = {
      id: verification.registrationInfo.credential.id,
      publicKey: Buffer.from(verification.registrationInfo.credential.publicKey),
      counter: verification.registrationInfo.credential.counter || 0,
      transports: body.response?.response?.transports || [],
    };
  }

  authStore.consumeWebAuthnChallenge(db, {
    kind: 'registration',
    claimId: claim.id,
    challenge,
  });
  const issued = authStore.finishDeviceClaim(db, {
    claimId: claim.id,
    secret: body.secret,
    rpId: challengeRow.rp_id,
    origin: challengeRow.origin,
    credential,
    remoteIp: req.socket?.remoteAddress || '',
    userAgent: req.headers?.['user-agent'] || '',
  });
  sendJson(res, 200, {
    ok: true,
    device: publicDevice(issued.device),
  }, {
    'Set-Cookie': tokenCookie(issued.token, requestIsHttps(req)),
  });
}

async function beginStepUp(req, res, db, auth) {
  requireDeviceAuth(auth);
  const origin = requestWebAuthnOrigin(req);
  if (!isSecureWebAuthnOrigin(origin)) {
    errorJson(res, 400, 'secure_context_required', 'Step-up requires HTTPS, except localhost development.');
    return;
  }
  const rpId = rpIdFromOrigin(origin);
  const credentials = authStore.listCredentialsForDevice(db, auth.deviceId)
    .filter((credential) => credential.rp_id === rpId);
  if (!credentials.length) {
    errorJson(res, 409, 'passkey_not_registered_for_origin');
    return;
  }
  const { generateAuthenticationOptions } = await simpleWebAuthn();
  const options = await generateAuthenticationOptions({
    rpID: rpId,
    allowCredentials: credentials.map((credential) => ({
      id: credential.credential_id,
      transports: credential.transports,
    })),
    userVerification: 'required',
    timeout: 5 * 60 * 1000,
  });
  authStore.saveWebAuthnChallenge(db, {
    kind: 'authentication',
    deviceTokenId: auth.deviceId,
    challenge: options.challenge,
    rpId,
    origin,
    ttlMs: 5 * 60 * 1000,
  });
  sendJson(res, 200, { ok: true, options });
}

async function finishStepUp(req, res, db, auth) {
  requireDeviceAuth(auth);
  const body = await readJsonBody(req, 2 * 1024 * 1024);
  const challenge = process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential
    ? String(body.challenge || '')
    : responseChallenge(body.response);
  if (!challenge) {
    errorJson(res, 400, 'challenge_missing');
    return;
  }
  const challengeRow = authStore.getWebAuthnChallenge(db, {
    kind: 'authentication',
    deviceTokenId: auth.deviceId,
    challenge,
  });
  if (!challengeRow) {
    errorJson(res, 400, 'challenge_not_found');
    return;
  }
  const credentialId = String(body.testCredential?.id || body.response?.id || '');
  const credential = authStore.getCredentialForDevice(db, auth.deviceId, credentialId);
  if (!credential) {
    errorJson(res, 401, 'credential_not_found');
    return;
  }

  let newCounter = credential.counter;
  if (process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential) {
    newCounter = Number(body.testCredential.counter || credential.counter);
  } else {
    const { verifyAuthenticationResponse } = await simpleWebAuthn();
    const verification = await verifyAuthenticationResponse({
      response: body.response,
      expectedChallenge: challengeRow.challenge,
      expectedOrigin: challengeRow.origin,
      expectedRPID: challengeRow.rp_id,
      credential: {
        id: credential.credential_id,
        publicKey: credential.public_key,
        counter: credential.counter,
        transports: credential.transports,
      },
      requireUserVerification: true,
    });
    if (!verification.verified) {
      errorJson(res, 401, 'passkey_verification_failed');
      return;
    }
    newCounter = verification.authenticationInfo.newCounter;
  }

  authStore.consumeWebAuthnChallenge(db, {
    kind: 'authentication',
    deviceTokenId: auth.deviceId,
    challenge,
  });
  authStore.updateCredentialCounter(db, credential.id, newCounter);
  const stepUp = authStore.createStepUpSession(db, auth.deviceId);
  authStore.insertAudit(db, {
    deviceTokenId: auth.deviceId,
    action: 'step_up',
    decision: 'allow',
    remoteIp: req.socket?.remoteAddress || '',
    userAgent: req.headers?.['user-agent'] || '',
    details: { rpId: challengeRow.rp_id, stepUpHashPrefix: stepUp.idHashPrefix },
  });
  sendJson(res, 200, {
    ok: true,
    step_up: {
      expires_at: stepUp.expiresAt,
      hash_prefix: stepUp.idHashPrefix,
    },
  }, {
    'Set-Cookie': stepUpCookie(stepUp.token, requestIsHttps(req)),
  });
}

async function registerPasskey(req, res, db, auth) {
  requireFreshDeviceAuth(auth);
  const body = await readJsonBody(req, 2 * 1024 * 1024);
  const origin = requestWebAuthnOrigin(req);
  if (!isSecureWebAuthnOrigin(origin)) {
    errorJson(res, 400, 'secure_context_required', 'Passkey registration requires HTTPS, except localhost development.');
    return;
  }
  const rpId = rpIdFromOrigin(origin);

  if (!body.response && !body.testCredential) {
    const credentials = authStore.listCredentialsForDevice(db, auth.deviceId);
    const { generateRegistrationOptions } = await simpleWebAuthn();
    const options = await generateRegistrationOptions({
      rpName: 'CTM',
      rpID: rpId,
      userName: auth.tokenLabel || auth.deviceId,
      userDisplayName: auth.tokenLabel || auth.deviceId,
      userID: Buffer.from(auth.deviceId),
      attestationType: 'none',
      excludeCredentials: credentials.map((credential) => ({
        id: credential.credential_id,
        transports: credential.transports,
      })),
      authenticatorSelection: {
        residentKey: 'preferred',
        userVerification: 'required',
      },
      preferredAuthenticatorType: 'localDevice',
      timeout: 5 * 60 * 1000,
    });
    authStore.saveWebAuthnChallenge(db, {
      kind: 'registration',
      deviceTokenId: auth.deviceId,
      challenge: options.challenge,
      rpId,
      origin,
      ttlMs: 5 * 60 * 1000,
    });
    sendJson(res, 200, { ok: true, options });
    return;
  }

  const challenge = process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential
    ? String(body.challenge || '')
    : responseChallenge(body.response);
  if (!challenge) {
    errorJson(res, 400, 'challenge_missing');
    return;
  }
  const challengeRow = authStore.getWebAuthnChallenge(db, {
    kind: 'registration',
    deviceTokenId: auth.deviceId,
    challenge,
  });
  if (!challengeRow) {
    errorJson(res, 400, 'challenge_not_found');
    return;
  }

  let credential;
  if (process.env.CTM_AUTH_TEST_WEBAUTHN === '1' && body.testCredential) {
    credential = {
      id: String(body.testCredential.id || 'test-credential'),
      publicKey: Buffer.from(String(body.testCredential.publicKey || 'test-public-key')),
      counter: Number(body.testCredential.counter || 0),
      transports: Array.isArray(body.testCredential.transports) ? body.testCredential.transports : ['internal'],
    };
  } else {
    const { verifyRegistrationResponse } = await simpleWebAuthn();
    const verification = await verifyRegistrationResponse({
      response: body.response,
      expectedChallenge: challengeRow.challenge,
      expectedOrigin: challengeRow.origin,
      expectedRPID: challengeRow.rp_id,
      requireUserVerification: true,
    });
    if (!verification.verified || !verification.registrationInfo?.credential) {
      errorJson(res, 401, 'passkey_verification_failed');
      return;
    }
    credential = {
      id: verification.registrationInfo.credential.id,
      publicKey: Buffer.from(verification.registrationInfo.credential.publicKey),
      counter: verification.registrationInfo.credential.counter || 0,
      transports: body.response?.response?.transports || [],
    };
  }

  authStore.consumeWebAuthnChallenge(db, {
    kind: 'registration',
    deviceTokenId: auth.deviceId,
    challenge,
  });
  const added = authStore.registerWebAuthnCredential(db, auth.deviceId, {
    rpId: challengeRow.rp_id,
    origin: challengeRow.origin,
    credential,
  });
  sendJson(res, 200, { ok: true, credential: publicCredential(added) });
}

async function createPairingRequest(req, res, db) {
  const body = await readJsonBody(req, 64 * 1024);
  const origin = requestWebAuthnOrigin(req);
  if (isHostedWalleMobileOrigin(origin)) {
    errorJson(
      res,
      400,
      'hosted_walle_origin_not_direct_pairing',
      'm.walle.sh is the Walle Remote app. Open the direct CTM tunnel URL before requesting phone pairing.'
    );
    return;
  }
  if (!isSecureWebAuthnOrigin(origin)) {
    errorJson(res, 400, 'secure_context_required', 'Phone pairing requires HTTPS, except localhost development.');
    return;
  }
  const created = authStore.createPairingRequest(db, {
    label: body.label || '',
    scopes: body.scopes || ['read', 'respond'],
    origin,
    remoteIp: requestClientIp(req),
    userAgent: req.headers?.['user-agent'] || '',
  });
  sendJson(res, 200, {
    ok: true,
    request: publicPairingRequest(created.request),
    secret: created.secret,
  });
}

async function pollPairingRequest(req, res, db, requestId) {
  const body = await readJsonBody(req, 64 * 1024);
  const result = authStore.ensurePairingRequestClaim(db, requestId, body.secret);
  const payload = {
    ok: true,
    status: result.status,
    request: publicPairingRequest(result.request),
  };
  if (result.status === 'approved' && result.claim) payload.claim = result.claim;
  sendJson(res, 200, payload);
}

async function approvePairingRequest(req, res, db, auth, requestId) {
  const body = await readJsonBody(req, 64 * 1024);
  const patch = { decisionBy: auth?.deviceId || 'loopback' };
  if (Object.prototype.hasOwnProperty.call(body, 'label')) patch.label = body.label;
  if (Object.prototype.hasOwnProperty.call(body, 'scopes')) patch.scopes = body.scopes;
  const request = authStore.approvePairingRequest(db, requestId, patch);
  sendJson(res, 200, { ok: true, request: publicPairingRequest(request) });
}

async function rejectPairingRequest(req, res, db, auth, requestId) {
  const request = authStore.rejectPairingRequest(db, requestId, {
    decisionBy: auth?.deviceId || 'loopback',
  });
  sendJson(res, 200, { ok: true, request: publicPairingRequest(request) });
}

async function handleMobileAuthApi(req, res, url, options = {}) {
  const db = options.db;
  if (!db) return false;
  authStore.ensureMobileAuthSchema(db);

  try {
    if (url.pathname === '/api/auth/claim/begin-passkey' && req.method === 'POST') {
      await beginClaimPasskey(req, res, db);
      return true;
    }
    if (url.pathname === '/api/auth/claim/finish' && req.method === 'POST') {
      await finishClaimPasskey(req, res, db);
      return true;
    }
    if (url.pathname === '/api/auth/logout-local' && req.method === 'POST') {
      clearLocalSession(req, res);
      return true;
    }
    if (url.pathname === '/api/auth/pairing-requests' && req.method === 'POST') {
      await createPairingRequest(req, res, db);
      return true;
    }

    const pairingPollMatch = url.pathname.match(/^\/api\/auth\/pairing-requests\/([^/]+)\/poll$/);
    if (pairingPollMatch && req.method === 'POST') {
      await pollPairingRequest(req, res, db, decodeURIComponent(pairingPollMatch[1]));
      return true;
    }

    if (url.pathname === '/api/auth/device-claims' && req.method === 'POST') {
      const body = await readJsonBody(req);
      const origin = body.origin || requestOrigin(req);
      if (isHostedWalleMobileOrigin(origin)) {
        errorJson(
          res,
          400,
          'hosted_walle_origin_not_direct_claim',
          'm.walle.sh is the Walle Remote app, not a direct CTM claim endpoint.'
        );
        return true;
      }
      const claim = authStore.createDeviceClaim(db, {
        label: body.label || 'Phone',
        scopes: body.scopes || ['read', 'respond'],
        origin,
        createdBy: options.auth?.deviceId || 'loopback',
      });
      const qrDataUrl = await QRCode.toDataURL(claim.claimUrl, { margin: 1, scale: 5 });
      sendJson(res, 200, { ok: true, claim: { ...claim, qr_data_url: qrDataUrl } });
      return true;
    }

    if (url.pathname === '/api/auth/pairing-requests' && req.method === 'GET') {
      const limit = Math.max(1, Math.min(50, Number(url.searchParams.get('limit') || 20)));
      const includeResolved = url.searchParams.get('include_resolved') === '1';
      sendJson(res, 200, {
        ok: true,
        requests: authStore.listPairingRequests(db, { limit, includeResolved }).map(publicPairingRequest),
      });
      return true;
    }

    const pairingDecisionMatch = url.pathname.match(/^\/api\/auth\/pairing-requests\/([^/]+)\/(approve|reject)$/);
    if (pairingDecisionMatch && req.method === 'POST') {
      const requestId = decodeURIComponent(pairingDecisionMatch[1]);
      if (pairingDecisionMatch[2] === 'approve') {
        await approvePairingRequest(req, res, db, options.auth, requestId);
      } else {
        await rejectPairingRequest(req, res, db, options.auth, requestId);
      }
      return true;
    }

    const claimMatch = url.pathname.match(/^\/api\/auth\/device-claims\/([^/]+)$/);
    if (claimMatch && req.method === 'PATCH') {
      const body = await readJsonBody(req);
      const claim = authStore.updateDeviceClaim(db, decodeURIComponent(claimMatch[1]), body);
      sendJson(res, 200, { ok: true, claim });
      return true;
    }

    if (claimMatch && req.method === 'DELETE') {
      authStore.cancelDeviceClaim(db, decodeURIComponent(claimMatch[1]));
      sendJson(res, 200, { ok: true });
      return true;
    }

    if (url.pathname === '/api/auth/devices' && req.method === 'GET') {
      authStore.cleanupMobileAuthArtifacts(db);
      const connectedDeviceIds = typeof options.connectedDeviceIds === 'function'
        ? options.connectedDeviceIds()
        : (options.connectedDeviceIds || []);
      const devices = authStore.listDeviceTokenDetails(db, { connectedDeviceIds }).map(publicDevice);
      sendJson(res, 200, {
        ok: true,
        devices,
        authorized_device_count: devices.filter((device) => device.authorization_status === 'authorized').length,
        connected_device_count: devices.filter((device) => device.connection_status === 'connected').length,
        revoked_device_count: devices.filter((device) => device.authorization_status === 'revoked').length,
      });
      return true;
    }

    if (url.pathname === '/api/auth/device-duplicates/revoke' && req.method === 'POST') {
      const body = await readJsonBody(req, 64 * 1024);
      const result = authStore.revokeDuplicateDeviceTokens(db, body.keep_device_id || body.device_id || '', {
        reason: 'manual_keep_newest',
      });
      if (typeof options.onDeviceRevoked === 'function') options.onDeviceRevoked(result.deviceIds);
      sendJson(res, 200, { ok: true, revoked: result.count, device_ids: result.deviceIds });
      return true;
    }

    if (url.pathname === '/api/auth/begin-step-up' && req.method === 'POST') {
      await beginStepUp(req, res, db, options.auth);
      return true;
    }

    if (url.pathname === '/api/auth/finish-step-up' && req.method === 'POST') {
      await finishStepUp(req, res, db, options.auth);
      return true;
    }

    if (url.pathname === '/api/auth/register-passkey' && req.method === 'POST') {
      await registerPasskey(req, res, db, options.auth);
      return true;
    }

    if (url.pathname === '/api/auth/revoke-self' && req.method === 'POST') {
      requireFreshDeviceAuth(options.auth);
      authStore.revokeDeviceToken(db, options.auth.deviceId);
      if (typeof options.onDeviceRevoked === 'function') options.onDeviceRevoked([options.auth.deviceId]);
      sendJson(res, 200, { ok: true }, {
        'Set-Cookie': [
          tokenCookie('', requestIsHttps(req), 0),
          stepUpCookie('', requestIsHttps(req), 0),
        ],
      });
      return true;
    }

    const deviceMatch = url.pathname.match(/^\/api\/auth\/devices\/([^/]+)$/);
    if (deviceMatch && req.method === 'PATCH') {
      const body = await readJsonBody(req);
      const device = authStore.updateDeviceToken(db, decodeURIComponent(deviceMatch[1]), body);
      sendJson(res, 200, { ok: true, device: publicDevice(device) });
      return true;
    }

    if (deviceMatch && req.method === 'DELETE') {
      const deviceId = decodeURIComponent(deviceMatch[1]);
      authStore.revokeDeviceToken(db, deviceId);
      if (typeof options.onDeviceRevoked === 'function') options.onDeviceRevoked([deviceId]);
      sendJson(res, 200, { ok: true });
      return true;
    }

    if (url.pathname === '/api/auth/revoke-all' && req.method === 'POST') {
      const result = authStore.revokeAllDeviceTokens(db);
      if (typeof options.onDeviceRevoked === 'function') options.onDeviceRevoked(result.deviceIds);
      sendJson(res, 200, { ok: true, revoked: result.count });
      return true;
    }

    if (url.pathname === '/api/auth/audit' && req.method === 'GET') {
      const days = Math.max(1, Math.min(30, Number(url.searchParams.get('days') || 30)));
      const limit = Math.max(1, Math.min(500, Number(url.searchParams.get('limit') || 100)));
      sendJson(res, 200, {
        ok: true,
        audit: authStore.listAuthAudit(db, { sinceMs: Date.now() - days * 24 * 60 * 60 * 1000, limit }),
      });
      return true;
    }
  } catch (err) {
    const code = err && err.message ? err.message : 'auth_error';
    const status = /not_found/.test(code) ? 404
      : /step_up_required|device_token_required/.test(code) ? 403
      : /passkey_not_registered/.test(code) ? 409
      : /rate_limited/.test(code) ? 429
      : /expired|canceled|already|bad_json|too_large|required|missing|revoked|rejected/.test(code) ? 400
      : 500;
    errorJson(res, status, code);
    return true;
  }

  return false;
}

module.exports = {
  claimWebAuthnOrigin,
  handleMobileAuthApi,
  isHostedWalleMobileOrigin,
  isMobileAuthPublicEndpoint,
  requestIsHttps,
  isSecureWebAuthnOrigin,
  readJsonBody,
  requestWebAuthnOrigin,
  rpIdFromOrigin,
  stepUpCookie,
  tokenCookie,
};
