import { join } from 'node:path';
import { homedir } from 'node:os';
import { getState, updateState } from './state';

const PROXY_PORT = Number(process.env.ANTHROPIC_PROXY_PORT ?? '5051');
const ANTHROPIC_API_BASE = 'https://api.anthropic.com';
const CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';

// Buffer: refresh 5 minutes before expiry
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;

type OAuthCredentials = {
  accessToken?: string;
  refreshToken?: string;
  expiresAt?: number;
};

type CredentialsFile = {
  claudeAiOauth?: OAuthCredentials;
};

async function readCredentials(): Promise<CredentialsFile | null> {
  try {
    const file = Bun.file(CREDENTIALS_PATH);
    if (!(await file.exists())) return null;
    return (await file.json()) as CredentialsFile;
  } catch {
    return null;
  }
}

async function writeCredentials(creds: CredentialsFile): Promise<void> {
  try {
    await Bun.write(CREDENTIALS_PATH, JSON.stringify(creds, null, 2));
  } catch (err) {
    console.error('[claude:proxy] failed to write credentials:', err);
  }
}

async function refreshOAuthToken(refreshToken: string): Promise<OAuthCredentials | null> {
  try {
    console.log('[claude:proxy] refreshing OAuth token...');
    const res = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: CLIENT_ID,
      }),
    });

    if (!res.ok) {
      const text = await res.text();
      console.error(`[claude:proxy] token refresh failed (${res.status}): ${text}`);
      return null;
    }

    const data = (await res.json()) as { access_token?: string; refresh_token?: string; expires_in?: number };
    if (!data.access_token) {
      console.error('[claude:proxy] token refresh response missing access_token');
      return null;
    }

    const expiresAt = data.expires_in ? Date.now() + data.expires_in * 1000 : undefined;
    console.log(`[claude:proxy] token refreshed, expires in ${data.expires_in ?? '?'}s`);

    return {
      accessToken: data.access_token,
      refreshToken: data.refresh_token ?? refreshToken,
      expiresAt,
    };
  } catch (err) {
    console.error('[claude:proxy] token refresh error:', err);
    return null;
  }
}

async function getValidToken(): Promise<string | null> {
  const creds = await readCredentials();
  if (!creds?.claudeAiOauth) return null;

  const oauth = creds.claudeAiOauth;
  const now = Date.now();

  // Check if token is still valid (with buffer)
  if (oauth.accessToken && oauth.expiresAt && oauth.expiresAt - now > EXPIRY_BUFFER_MS) {
    return oauth.accessToken.trim();
  }

  // Token expired or about to expire — try refresh
  if (oauth.refreshToken) {
    const refreshed = await refreshOAuthToken(oauth.refreshToken);
    if (refreshed?.accessToken) {
      creds.claudeAiOauth = { ...oauth, ...refreshed };
      await writeCredentials(creds);
      return refreshed.accessToken.trim();
    }
  }

  // Fallback: return current token even if potentially expired (let upstream decide)
  return oauth.accessToken?.trim() || null;
}

async function forceRefresh(): Promise<string | null> {
  const creds = await readCredentials();
  const refreshToken = creds?.claudeAiOauth?.refreshToken;
  if (!refreshToken) return null;

  const refreshed = await refreshOAuthToken(refreshToken);
  if (refreshed?.accessToken && creds?.claudeAiOauth) {
    creds.claudeAiOauth = { ...creds.claudeAiOauth, ...refreshed };
    await writeCredentials(creds);
    return refreshed.accessToken;
  }
  return null;
}

export function getProxySecret(): string {
  return getState().proxySecret;
}

export function ensureProxySecret(): string {
  const state = getState();
  if (state.proxySecret) return state.proxySecret;

  // Generate once, persist forever
  const secret = `sk-ant-api03-${crypto.randomUUID()}`;
  updateState({ proxySecret: secret });
  return secret;
}

export function startAnthropicProxy() {
  const secret = ensureProxySecret();

  Bun.serve({
    port: PROXY_PORT,
    hostname: '127.0.0.1',
    idleTimeout: 0,

    async fetch(req) {
      const url = new URL(req.url);

      // Internal refresh endpoint (no auth needed — localhost only)
      if (url.pathname === '/refresh') {
        const token = await forceRefresh();
        if (token) {
          return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
        }
        return new Response(JSON.stringify({ error: 'Refresh failed' }), {
          status: 502,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Validate proxy secret
      const incomingKey = req.headers.get('x-api-key');
      if (incomingKey !== secret) {
        return new Response(JSON.stringify({ error: 'Unauthorized' }), {
          status: 401,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Get valid token (refreshes if needed)
      const token = await getValidToken();
      if (!token) {
        return new Response(JSON.stringify({ error: 'No OAuth token available' }), {
          status: 502,
          headers: { 'Content-Type': 'application/json' },
        });
      }

      // Build upstream URL
      const upstream = `${ANTHROPIC_API_BASE}${url.pathname}${url.search}`;

      // Clone headers, swap proxy secret for the OAuth bearer token. Anthropic
      // only accepts Claude Pro/Max OAuth tokens via Authorization + the oauth beta header.
      // Preserve any anthropic-beta values Claude Code sent (context-management, etc.)
      // and append oauth-2025-04-20 alongside them.
      const headers = new Headers(req.headers);
      headers.delete('x-api-key');
      headers.set('Authorization', `Bearer ${token}`);
      const existingBeta = headers.get('anthropic-beta');
      const betas = existingBeta ? existingBeta.split(',').map((s) => s.trim()).filter(Boolean) : [];
      if (!betas.includes('oauth-2025-04-20')) betas.push('oauth-2025-04-20');
      headers.set('anthropic-beta', betas.join(','));
      headers.delete('host');

      // Read request body fully before forwarding
      const body = req.body ? await req.arrayBuffer() : null;

      // Forward request
      const upstreamRes = await fetch(upstream, {
        method: req.method,
        headers,
        body,
      });

      // If upstream returns 401, try one refresh and retry
      if (upstreamRes.status === 401) {
        const creds = await readCredentials();
        const refreshToken = creds?.claudeAiOauth?.refreshToken;
        if (refreshToken) {
          const refreshed = await refreshOAuthToken(refreshToken);
          if (refreshed?.accessToken) {
            if (creds?.claudeAiOauth) {
              creds.claudeAiOauth = { ...creds.claudeAiOauth, ...refreshed };
              await writeCredentials(creds);
            }

            headers.set('Authorization', `Bearer ${refreshed.accessToken.trim()}`);
            const retryBody = body ? new Uint8Array(body) : null;
            const retryRes = await fetch(upstream, {
              method: req.method,
              headers,
              body: retryBody,
            });

            const retryHeaders = new Headers();
            for (const [key, value] of retryRes.headers) {
              const lower = key.toLowerCase();
              if (lower === 'content-encoding' || lower === 'content-length' || lower === 'transfer-encoding') continue;
              retryHeaders.set(key, value);
            }

            return new Response(retryRes.body, {
              status: retryRes.status,
              statusText: retryRes.statusText,
              headers: retryHeaders,
            });
          }
        }
      }

      // Build clean response headers
      const resHeaders = new Headers();
      for (const [key, value] of upstreamRes.headers) {
        const lower = key.toLowerCase();
        if (lower === 'content-encoding' || lower === 'content-length' || lower === 'transfer-encoding') continue;
        resHeaders.set(key, value);
      }

      return new Response(upstreamRes.body, {
        status: upstreamRes.status,
        statusText: upstreamRes.statusText,
        headers: resHeaders,
      });
    },
  });

  console.log(`[claude:proxy] listening on 127.0.0.1:${PROXY_PORT}`);
}
