'use strict';

/**
 * Local OAuth → Anthropic API proxy for Wall-E (Strategy 3).
 *
 * Listens on 127.0.0.1:OAUTH_PROXY_PORT (default 3458) and accepts standard
 * Anthropic API requests. On each request:
 *   1. Reads the user's Claude.ai OAuth access token by trying, in order:
 *      a. ANTHROPIC_API_KEY env if it starts with "sk-ant-oat"
 *      b. ANTHROPIC_AUTH_TOKEN env if it starts with "sk-ant-oat"
 *      c. macOS Keychain entry "Claude Code-credentials" (where
 *         `claude auth login` stores it). The OS prompts the user for
 *         keychain access on first read; "Always Allow" suppresses
 *         future prompts. That OS dialog IS the user consenting to
 *         this proxy reading the credential.
 *   2. Forwards the request to api.anthropic.com with:
 *        Authorization: Bearer <oauth_access_token>
 *        anthropic-beta: oauth-2025-04-20
 *      The beta header tells the API to accept the user-scoped OAuth
 *      Bearer (instead of an x-api-key). Token usage bills against the
 *      user's claude.ai subscription. NO key minting — works for users
 *      regardless of org admin privileges.
 *
 * What this is NOT:
 *   - Not a public service. Bound to 127.0.0.1 only.
 *   - Not authenticated beyond loopback (anything on this host can use it —
 *     same threat model as a local dev API key file).
 *   - Not officially supported by Anthropic. Same gray-area space as
 *     Claude Code's own OAuth flow; may break on TOS changes.
 *
 * Run: `node oauth-proxy.js`  (env: OAUTH_PROXY_PORT)
 */

const http = require('node:http');
const { execFile } = require('node:child_process');

const PORT = parseInt(process.env.OAUTH_PROXY_PORT || '3458', 10);
const HOST = '127.0.0.1';
const API_BASE = 'https://api.anthropic.com';
const OAUTH_BETA_HEADER = 'oauth-2025-04-20';

function _readOauthFromEnv() {
  const candidates = [process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN];
  for (const tok of candidates) {
    if (typeof tok === 'string' && /^sk-ant-oat\w*-/.test(tok)) return tok;
  }
  return null;
}

function _readOauthFromKeychain() {
  // macOS only — `security find-generic-password -s "Claude Code-credentials" -w`
  // returns the JSON-stringified credentials blob. First call may prompt the
  // user with a keychain dialog ("Allow" / "Always Allow") — that's the
  // user consenting to the proxy. We don't bypass it.
  if (process.platform !== 'darwin') return Promise.resolve(null);
  return new Promise((resolve) => {
    execFile(
      'security',
      ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
      { encoding: 'utf-8', timeout: 10000 },
      (err, stdout) => {
        if (err) return resolve(null);
        const raw = (stdout || '').trim();
        if (!raw) return resolve(null);
        try {
          const blob = JSON.parse(raw);
          const tok = blob && blob.claudeAiOauth && blob.claudeAiOauth.accessToken;
          resolve(typeof tok === 'string' && tok ? tok : null);
        } catch {
          resolve(null);
        }
      },
    );
  });
}

async function _readOauthToken() {
  const fromEnv = _readOauthFromEnv();
  if (fromEnv) return { token: fromEnv, source: 'env' };
  const fromKc = await _readOauthFromKeychain();
  if (fromKc) return { token: fromKc, source: 'keychain' };
  return null;
}

async function getOauthToken() {
  const oauth = await _readOauthToken();
  if (!oauth) {
    throw new Error('OAuth token not found. Run `claude auth login` to sign in to claude.ai, OR set ANTHROPIC_API_KEY to a sk-ant-oat… token.');
  }
  return oauth;
}

function _buildUpstreamUrl(reqUrl) {
  // reqUrl is the path component, e.g. "/v1/messages" or "/v1/messages?…".
  // We forward verbatim under api.anthropic.com.
  if (typeof reqUrl !== 'string' || !reqUrl.startsWith('/')) return null;
  return API_BASE + reqUrl;
}

async function _readRequestBody(req) {
  const chunks = [];
  let total = 0;
  const MAX = 16 * 1024 * 1024; // 16MB cap
  for await (const chunk of req) {
    total += chunk.length;
    if (total > MAX) throw new Error('request body exceeds 16MB');
    chunks.push(chunk);
  }
  return Buffer.concat(chunks);
}

async function handleRequest(req, res) {
  // Simple status endpoint (loopback only — no auth needed beyond bind).
  if (req.url === '/_health' || req.url === '/_status') {
    const oauth = await _readOauthToken();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({
      ok: true,
      oauth_token_present: !!oauth,
      oauth_source: oauth?.source || null,
    }));
    return;
  }

  const upstreamUrl = _buildUpstreamUrl(req.url);
  if (!upstreamUrl) {
    res.writeHead(400, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'bad request URL' }));
    return;
  }

  let oauth;
  try {
    oauth = await getOauthToken();
  } catch (err) {
    res.writeHead(502, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: { type: 'oauth_proxy_error', message: err.message } }));
    return;
  }

  let body;
  try {
    body = await _readRequestBody(req);
  } catch (err) {
    res.writeHead(413, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: { type: 'oauth_proxy_error', message: err.message } }));
    return;
  }

  // Build forwarded headers: copy non-auth client headers, replace auth with
  // the OAuth Bearer + the beta header that enables Bearer auth on api.anthropic.com.
  const fwdHeaders = {};
  for (const [name, value] of Object.entries(req.headers)) {
    const k = name.toLowerCase();
    if (k === 'host' || k === 'authorization' || k === 'x-api-key' || k === 'connection' || k === 'content-length') continue;
    if (Array.isArray(value)) fwdHeaders[name] = value.join(', ');
    else if (value !== undefined) fwdHeaders[name] = String(value);
  }
  fwdHeaders['authorization'] = `Bearer ${oauth.token}`;
  if (!fwdHeaders['anthropic-version']) fwdHeaders['anthropic-version'] = '2023-06-01';
  // Merge any caller-provided anthropic-beta with our oauth-2025-04-20 flag.
  const existingBeta = fwdHeaders['anthropic-beta'];
  fwdHeaders['anthropic-beta'] = existingBeta
    ? (existingBeta.includes(OAUTH_BETA_HEADER) ? existingBeta : `${existingBeta},${OAUTH_BETA_HEADER}`)
    : OAUTH_BETA_HEADER;

  const fetchOpts = {
    method: req.method,
    headers: fwdHeaders,
  };
  if (body.length > 0) fetchOpts.body = body;

  let upstreamResp;
  try {
    upstreamResp = await fetch(upstreamUrl, fetchOpts);
  } catch (err) {
    res.writeHead(502, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: { type: 'oauth_proxy_error', message: 'upstream fetch failed: ' + err.message } }));
    return;
  }

  // Stream response back. Anthropic SSE responses use chunked transfer encoding.
  const respHeaders = {};
  for (const [name, value] of upstreamResp.headers.entries()) {
    const k = name.toLowerCase();
    if (k === 'content-encoding' || k === 'transfer-encoding' || k === 'content-length' || k === 'connection') continue;
    respHeaders[name] = value;
  }
  res.writeHead(upstreamResp.status, respHeaders);
  if (upstreamResp.body) {
    const reader = upstreamResp.body.getReader();
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        res.write(Buffer.from(value));
      }
    } catch (err) {
      // Client disconnected or upstream error mid-stream — close gracefully.
    }
  }
  res.end();
}

function start({ port = PORT } = {}) {
  const server = http.createServer((req, res) => {
    handleRequest(req, res).catch((err) => {
      try {
        if (!res.headersSent) {
          res.writeHead(500, { 'Content-Type': 'application/json' });
          res.end(JSON.stringify({ error: { type: 'oauth_proxy_error', message: err.message } }));
        }
      } catch {}
    });
  });
  server.on('error', (err) => {
    console.error('[oauth-proxy] server error:', err.message);
  });
  return new Promise((resolve, reject) => {
    server.once('error', reject);
    server.listen(port, HOST, () => {
      console.log(`[oauth-proxy] listening on http://${HOST}:${port}`);
      resolve(server);
    });
  });
}

module.exports = { start, getOauthToken, _readOauthToken, _readOauthFromEnv, _readOauthFromKeychain };

// CLI mode: `node oauth-proxy.js`
if (require.main === module) {
  start().catch((err) => {
    console.error('[oauth-proxy] failed to start:', err.message);
    process.exit(1);
  });
}
