/**
 * Default Account Credential Manager
 *
 * File-based adapter implementing AccountCredentialManager port.
 * Reads/writes credentials from ~/.cw/accounts/<uuid>/.credentials.json
 * and emits events on credential state changes.
 */

import { existsSync } from 'node:fs';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import type { EventBus } from '../../events/index.js';
import type {
  AccountCredentialManager,
  OAuthCredentials,
  RefreshResult,
  CredentialValidationResult,
} from './types.js';
import type {
  AccountCredentialsRefreshedEvent,
  AccountCredentialsExpiredEvent,
  AccountCredentialsValidatedEvent,
} from '../../events/types.js';
import { createModuleLogger } from '../../logger/index.js';

const log = createModuleLogger('credential-manager');

/** Anthropic OAuth token refresh endpoint */
const TOKEN_REFRESH_URL = 'https://console.anthropic.com/v1/oauth/token';

/** OAuth client ID for Claude CLI */
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';

/** Buffer before expiry to trigger refresh (5 minutes) */
const TOKEN_REFRESH_BUFFER_MS = 300_000;

/**
 * DefaultAccountCredentialManager - File-based credential management with event emission.
 *
 * Implements the AccountCredentialManager port for managing OAuth credentials
 * stored in account config directories.
 */
export class DefaultAccountCredentialManager implements AccountCredentialManager {
  constructor(private eventBus?: EventBus) {}

  /**
   * Read credentials from a config directory.
   */
  async read(configDir: string): Promise<OAuthCredentials | null> {
    try {
      const credPath = join(configDir, '.credentials.json');
      if (!existsSync(credPath)) return null;

      const raw = await readFile(credPath, 'utf-8');
      const parsed = JSON.parse(raw);
      const oauth = parsed.claudeAiOauth;

      if (!oauth || !oauth.accessToken) return null;

      return {
        accessToken: oauth.accessToken,
        refreshToken: oauth.refreshToken ?? null,
        expiresAt: oauth.expiresAt ?? null,
        subscriptionType: oauth.subscriptionType ?? null,
        rateLimitTier: oauth.rateLimitTier ?? null,
      };
    } catch {
      return null;
    }
  }

  /**
   * Check if credentials are expired or about to expire.
   */
  isExpired(credentials: OAuthCredentials): boolean {
    if (!credentials.expiresAt) return false; // Setup tokens without expiry are treated as non-expired
    return credentials.expiresAt < Date.now() + TOKEN_REFRESH_BUFFER_MS;
  }

  /**
   * Refresh an access token using the refresh token.
   */
  async refresh(configDir: string, refreshToken: string): Promise<RefreshResult | null> {
    try {
      const response = await fetch(TOKEN_REFRESH_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: OAUTH_CLIENT_ID,
          scope: 'user:inference user:profile',
        }),
      });

      if (!response.ok) {
        log.warn({ configDir, status: response.status }, 'token refresh failed');
        return null;
      }

      const data = await response.json();
      return {
        accessToken: data.access_token,
        refreshToken: data.refresh_token,
        expiresIn: data.expires_in,
      };
    } catch (err) {
      log.error({ configDir, err: err instanceof Error ? err.message : String(err) }, 'token refresh error');
      return null;
    }
  }

  /**
   * Write updated credentials to the config directory.
   */
  async write(
    configDir: string,
    accessToken: string,
    refreshToken: string,
    expiresIn: number,
  ): Promise<void> {
    const credPath = join(configDir, '.credentials.json');

    // Read existing credentials to preserve other fields
    let existing: Record<string, unknown> = {};
    try {
      if (existsSync(credPath)) {
        existing = JSON.parse(await readFile(credPath, 'utf-8'));
      }
    } catch {
      // Start fresh if can't read
    }

    // Calculate expiry in milliseconds
    const nowMs = Date.now();
    const expiresAt = nowMs + expiresIn * 1000;

    // Update claudeAiOauth section
    const claudeAiOauth = (existing.claudeAiOauth as Record<string, unknown>) ?? {};
    claudeAiOauth.accessToken = accessToken;
    claudeAiOauth.refreshToken = refreshToken;
    claudeAiOauth.expiresAt = expiresAt;
    existing.claudeAiOauth = claudeAiOauth;

    // Ensure directory exists
    await mkdir(dirname(credPath), { recursive: true });

    // Write back (compact JSON for consistency)
    await writeFile(credPath, JSON.stringify(existing));
    log.debug({ configDir }, 'credentials written after token refresh');
  }

  /**
   * Ensure credentials are valid, refreshing if needed.
   */
  async ensureValid(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
    const credentials = await this.read(configDir);

    if (!credentials) {
      log.warn({ configDir, accountId }, 'no credentials found');
      this.emitExpired(accountId, 'credentials_missing', 'Credentials file not found');
      return {
        valid: false,
        credentials: null,
        error: 'Credentials file not found',
        refreshed: false,
      };
    }

    if (!this.isExpired(credentials)) {
      log.debug({ configDir, accountId }, 'credentials valid, no refresh needed');
      this.emitValidated(accountId, true, credentials.expiresAt, false);
      return {
        valid: true,
        credentials,
        error: null,
        refreshed: false,
      };
    }

    // Credentials expired — attempt refresh if we have a refresh token
    if (!credentials.refreshToken) {
      log.warn({ configDir, accountId }, 'setup token expired, no refresh token available');
      this.emitExpired(accountId, 'token_expired', 'Setup token expired, no refresh token available');
      return {
        valid: false,
        credentials: null,
        error: 'Setup token expired, no refresh token available',
        refreshed: false,
      };
    }

    log.info({ configDir, accountId }, 'credentials expired, refreshing');
    const previousExpiresAt = credentials.expiresAt;
    const refreshed = await this.refresh(configDir, credentials.refreshToken);

    if (!refreshed) {
      log.error({ configDir, accountId }, 'failed to refresh credentials');
      this.emitExpired(accountId, 'refresh_failed', 'Token refresh failed');
      return {
        valid: false,
        credentials: null,
        error: 'Token refresh failed',
        refreshed: false,
      };
    }

    // Write refreshed credentials
    const newRefreshToken = refreshed.refreshToken || credentials.refreshToken;
    await this.write(configDir, refreshed.accessToken, newRefreshToken, refreshed.expiresIn);

    const newExpiresAt = Date.now() + refreshed.expiresIn * 1000;
    log.info({ configDir, accountId, expiresIn: refreshed.expiresIn }, 'credentials refreshed');

    this.emitRefreshed(accountId, newExpiresAt, previousExpiresAt);
    this.emitValidated(accountId, true, newExpiresAt, true);

    // Read back updated credentials
    const updatedCredentials = await this.read(configDir);
    return {
      valid: true,
      credentials: updatedCredentials,
      error: null,
      refreshed: true,
    };
  }

  /**
   * Validate credentials without attempting refresh.
   */
  async validate(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
    const credentials = await this.read(configDir);

    if (!credentials) {
      this.emitValidated(accountId, false, null, false);
      return {
        valid: false,
        credentials: null,
        error: 'Credentials file not found',
        refreshed: false,
      };
    }

    const expired = this.isExpired(credentials);
    this.emitValidated(accountId, !expired, credentials.expiresAt, false);

    if (expired) {
      return {
        valid: false,
        credentials,
        error: 'Token expired',
        refreshed: false,
      };
    }

    return {
      valid: true,
      credentials,
      error: null,
      refreshed: false,
    };
  }

  /**
   * Emit credentials refreshed event.
   */
  private emitRefreshed(
    accountId: string | undefined,
    expiresAt: number,
    previousExpiresAt: number | null,
  ): void {
    if (!this.eventBus) return;

    const event: AccountCredentialsRefreshedEvent = {
      type: 'account:credentials_refreshed',
      timestamp: new Date(),
      payload: {
        accountId: accountId ?? null,
        expiresAt,
        previousExpiresAt,
      },
    };
    this.eventBus.emit(event);
  }

  /**
   * Emit credentials expired event.
   */
  private emitExpired(
    accountId: string | undefined,
    reason: 'token_expired' | 'refresh_failed' | 'credentials_missing',
    error: string | null,
  ): void {
    if (!this.eventBus) return;

    const event: AccountCredentialsExpiredEvent = {
      type: 'account:credentials_expired',
      timestamp: new Date(),
      payload: {
        accountId: accountId ?? null,
        reason,
        error,
      },
    };
    this.eventBus.emit(event);
  }

  /**
   * Emit credentials validated event.
   */
  private emitValidated(
    accountId: string | undefined,
    valid: boolean,
    expiresAt: number | null,
    wasRefreshed: boolean,
  ): void {
    if (!this.eventBus) return;

    const event: AccountCredentialsValidatedEvent = {
      type: 'account:credentials_validated',
      timestamp: new Date(),
      payload: {
        accountId: accountId ?? null,
        valid,
        expiresAt,
        wasRefreshed,
      },
    };
    this.eventBus.emit(event);
  }
}
