/**
 * Claude OAuth PKCE flow for Anthropic subscription authentication.
 * Adapted from CodeDeck's ClaudeOAuthService for server-side Node.js.
 *
 * User signs in at claude.ai, receives a code, pastes it back.
 * Credentials stored in ~/.claude/.credentials.json + macOS Keychain.
 */

import crypto from 'crypto';
import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { log } from '../shared/logger.js';

const OAUTH_CONFIG = {
  AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
  TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
  REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
  CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
  SCOPES: 'org:create_api_key user:profile user:inference',
};

const CLAUDE_DIR = path.join(os.homedir(), '.claude');
const CREDENTIALS_FILE = path.join(CLAUDE_DIR, '.credentials.json');

let codeVerifier: string | null = null;

/* ── Public API ── */

export function startClaudeOAuth(): { success: boolean; authUrl?: string; error?: string } {
  // Generate PKCE
  codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');

  const params = new URLSearchParams({
    code: 'true',
    client_id: OAUTH_CONFIG.CLIENT_ID,
    response_type: 'code',
    redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
    scope: OAUTH_CONFIG.SCOPES,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: codeVerifier, // state = verifier (OpenClaw legacy flow)
  });

  const authUrl = `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
  log.ok('Claude OAuth flow started');
  return { success: true, authUrl };
}

export async function exchangeClaudeCode(codeInput: string): Promise<{ success: boolean; error?: string }> {
  if (!codeVerifier) {
    return { success: false, error: 'OAuth flow not started. Click "Authenticate" first.' };
  }

  // Parse code — might be "code#state" or just "code"
  const parts = codeInput.trim().split('#');
  const code = parts[0].trim();
  const state = parts[1]?.trim() || codeVerifier;

  if (!code) {
    return { success: false, error: 'Invalid code. Please copy the full code from the page.' };
  }

  try {
    // Token exchange uses JSON body (not form-urlencoded)
    const payload = {
      grant_type: 'authorization_code',
      client_id: OAUTH_CONFIG.CLIENT_ID,
      code,
      state,
      redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
      code_verifier: codeVerifier,
    };

    const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      return { success: false, error: `Authentication failed (${response.status}). Please try again.` };
    }

    const tokens = await response.json();
    storeCredentials(tokens);
    codeVerifier = null;
    return { success: true };
  } catch (err: any) {
    return { success: false, error: err.message };
  }
}

export async function getClaudeAuthStatus(): Promise<{ authenticated: boolean; error?: string }> {
  const oauth = readOAuthBlock();
  if (!oauth) return { authenticated: false };

  if (oauth.accessToken && oauth.expiresAt && Date.now() < oauth.expiresAt) {
    return { authenticated: true };
  }

  // Token expired or missing — try refresh
  if (oauth.refreshToken) {
    const refreshed = await refreshClaudeToken(oauth.refreshToken);
    if (refreshed) return { authenticated: true };
  }

  return { authenticated: false, error: 'Token expired' };
}

export function readClaudeAccessToken(): string | null {
  const oauth = readOAuthBlock();
  if (!oauth?.accessToken) return null;
  if (oauth.expiresAt && Date.now() >= oauth.expiresAt) return null;
  return oauth.accessToken;
}

/** Read a valid access token, refreshing if expired. Returns null if unavailable. */
export async function getClaudeAccessToken(): Promise<string | null> {
  const oauth = readOAuthBlock();
  if (!oauth?.accessToken) return null;

  // Token still valid
  if (oauth.expiresAt && Date.now() < oauth.expiresAt) {
    return oauth.accessToken;
  }

  // Try refresh
  if (oauth.refreshToken) {
    const refreshed = await refreshClaudeToken(oauth.refreshToken);
    if (refreshed) {
      // Re-read after refresh
      const fresh = readOAuthBlock();
      return fresh?.accessToken || null;
    }
  }

  return null;
}

/* ── Helpers ── */

/**
 * Read the OAuth block from the most reliable source.
 * On macOS: Keychain is the source of truth (Claude Code reads/writes there on refresh).
 *           If Keychain has no valid entry, stale files are not trusted.
 * On Linux/Windows: credentials file is the source of truth.
 * Handles both formats:
 *   - Claude Code format: { claudeAiOauth: { accessToken, refreshToken, expiresAt, ... } }
 *   - Legacy flat format:  { accessToken, refreshToken, expiresAt }
 */
function readOAuthBlock(): { accessToken?: string; refreshToken?: string; expiresAt?: number } | null {
  // macOS: Keychain is the source of truth
  if (process.platform === 'darwin') {
    try {
      const result = execFileSync('security', [
        'find-generic-password', '-s', 'Claude Code-credentials', '-w',
      ], { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
      const parsed = JSON.parse(result);
      const oauth = parsed.claudeAiOauth || parsed;
      if (oauth.accessToken) {
        log.ok('Read credentials from macOS Keychain');
        return oauth;
      }
    } catch {}
    // On macOS, if Keychain has no valid entry, don't trust stale files
    log.warn('No valid credentials in macOS Keychain');
    return null;
  }

  // Linux/Windows: credentials file
  try {
    if (fs.existsSync(CREDENTIALS_FILE)) {
      const creds = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
      const oauth = creds.claudeAiOauth || creds;
      if (oauth.accessToken) return oauth;
    }
  } catch {}

  return null;
}

/**
 * Refresh an expired token using the refresh_token grant.
 * Returns true if refresh succeeded and new credentials were stored.
 */
async function refreshClaudeToken(refreshToken: string): Promise<boolean> {
  try {
    log.ok('Attempting Claude token refresh...');
    const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: OAUTH_CONFIG.CLIENT_ID,
        refresh_token: refreshToken,
      }),
    });

    if (!response.ok) {
      log.warn(`Claude token refresh failed: ${response.status}`);
      return false;
    }

    const tokens = await response.json();
    storeCredentials(tokens);
    log.ok('Claude token refreshed successfully');
    return true;
  } catch (err: any) {
    log.warn(`Claude token refresh error: ${err.message}`);
    return false;
  }
}

function storeCredentials(tokens: any): void {
  if (!fs.existsSync(CLAUDE_DIR)) {
    fs.mkdirSync(CLAUDE_DIR, { recursive: true });
  }

  // Read existing file to preserve other fields
  let fileData: Record<string, any> = {};
  try {
    if (fs.existsSync(CREDENTIALS_FILE)) {
      fileData = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
    }
  } catch {}

  // Build oauth block — merge with existing claudeAiOauth to preserve scopes etc.
  const existing = fileData.claudeAiOauth || {};
  const oauth: Record<string, any> = { ...existing };
  oauth.accessToken = tokens.access_token;
  if (tokens.refresh_token) oauth.refreshToken = tokens.refresh_token;
  if (tokens.expires_in) {
    oauth.expiresAt = Date.now() + (tokens.expires_in - 300) * 1000;
  }

  // Write in Claude Code's format
  fileData.claudeAiOauth = oauth;
  // Remove legacy flat keys if they exist
  delete fileData.accessToken;
  delete fileData.refreshToken;
  delete fileData.expiresAt;

  fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(fileData, null, 2), 'utf-8');
  try { fs.chmodSync(CREDENTIALS_FILE, 0o600); } catch {}
  log.ok('Claude credentials stored');

  // macOS: also write to Keychain
  if (process.platform === 'darwin') {
    try {
      const keychainValue = JSON.stringify({ claudeAiOauth: oauth });
      try {
        execFileSync('security', ['delete-generic-password', '-s', 'Claude Code-credentials'], {
          stdio: ['pipe', 'pipe', 'pipe'],
        });
      } catch {} // OK if entry doesn't exist
      execFileSync('security', [
        'add-generic-password',
        '-s', 'Claude Code-credentials',
        '-a', os.userInfo().username,
        '-w', keychainValue,
      ], { stdio: ['pipe', 'pipe', 'pipe'] });
    } catch {}
  }

  // Legacy fallback (~/.claude.json)
  try {
    const legacyPath = path.join(os.homedir(), '.claude.json');
    let legacyConfig: Record<string, any> = {};
    try {
      if (fs.existsSync(legacyPath)) {
        legacyConfig = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
      }
    } catch {}
    legacyConfig.oauthAccessToken = tokens.access_token;
    legacyConfig.hasCompletedOnboarding = true;
    fs.writeFileSync(legacyPath, JSON.stringify(legacyConfig, null, 2), 'utf-8');
    try { fs.chmodSync(legacyPath, 0o600); } catch {}
  } catch {}
}
