{% if sub_clients %}
{# Namespaced client with sub-clients #}
{% if include_imports %}
{% for tag in tags %}
import { {{ tag.class_name }} } from "./{{ tag.slug }}";
{% endfor %}
import { HttpClientAdapter, FetchAdapter } from "./http";
import { APIError, NetworkError } from "./errors";
import { APILogger, type LoggerConfig } from "./logger";
import { withRetry, type RetryConfig } from "./retry";


{% endif %}
/**
 * Async API client for {{ info.title }}.
 *
 * Usage:
 * ```typescript
 * const client = new APIClient('https://api.example.com');
 * const users = await client.users.list();
 * const post = await client.posts.create(newPost);
 *
 * // Custom HTTP adapter (e.g., Axios)
 * const client = new APIClient('https://api.example.com', {
 *   httpClient: new AxiosAdapter()
 * });
 * ```
 */
export class APIClient {
  private baseUrl: string;
  private httpClient: HttpClientAdapter;
  private logger: APILogger | null = null;
  private retryConfig: RetryConfig | null = null;
  private tokenGetter: (() => string | null) | null = null;

  // Sub-clients
{% for tag in tags %}
  public {{ tag.property }}: {{ tag.class_name }};
{% endfor %}

  constructor(
    baseUrl: string,
    options?: {
      httpClient?: HttpClientAdapter;
      loggerConfig?: Partial<LoggerConfig>;
      retryConfig?: RetryConfig;
      tokenGetter?: () => string | null;
    }
  ) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.httpClient = options?.httpClient || new FetchAdapter();
    this.tokenGetter = options?.tokenGetter || null;

    // Initialize logger if config provided
    if (options?.loggerConfig !== undefined) {
      this.logger = new APILogger(options.loggerConfig);
    }

    // Store retry configuration
    if (options?.retryConfig !== undefined) {
      this.retryConfig = options.retryConfig;
    }

    // Initialize sub-clients
{% for tag in tags %}
    this.{{ tag.property }} = new {{ tag.class_name }}(this);
{% endfor %}
  }

  /**
   * Get CSRF token from cookies (for SessionAuthentication).
   *
   * Returns null if cookie doesn't exist (JWT-only auth).
   */
  getCsrfToken(): string | null {
    const name = 'csrftoken';
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) {
      return parts.pop()?.split(';').shift() || null;
    }
    return null;
  }

  /**
   * Get the base URL for building streaming/download URLs.
   */
  getBaseUrl(): string {
    return this.baseUrl;
  }

  /**
   * Get JWT token for URL authentication (used in streaming endpoints).
   * Returns null if no token getter is configured or no token is available.
   */
  getToken(): string | null {
    return this.tokenGetter ? this.tokenGetter() : null;
  }

  /**
   * Make HTTP request with Django CSRF and session handling.
   * Automatically retries on network errors and 5xx server errors.
   */
  async request<T>(
    method: string,
    path: string,
    options?: {
      params?: Record<string, any>;
      body?: any;
      formData?: FormData;
      binaryBody?: Blob | ArrayBuffer;
      headers?: Record<string, string>;
    }
  ): Promise<T> {
    // Wrap request in retry logic if configured
    if (this.retryConfig) {
      return withRetry(() => this._makeRequest<T>(method, path, options), {
        ...this.retryConfig,
        onFailedAttempt: (info) => {
          // Log retry attempts
          if (this.logger) {
            this.logger.warn(
              `Retry attempt ${info.attemptNumber}/${info.retriesLeft + info.attemptNumber} ` +
              `for ${method} ${path}: ${info.error.message}`
            );
          }
          // Call user's onFailedAttempt if provided
          this.retryConfig?.onFailedAttempt?.(info);
        },
      });
    }

    // No retry configured, make request directly
    return this._makeRequest<T>(method, path, options);
  }

  /**
   * Internal request method (without retry wrapper).
   * Used by request() method with optional retry logic.
   */
  private async _makeRequest<T>(
    method: string,
    path: string,
    options?: {
      params?: Record<string, any>;
      body?: any;
      formData?: FormData;
      binaryBody?: Blob | ArrayBuffer;
      headers?: Record<string, string>;
    }
  ): Promise<T> {
    // Build URL - handle both absolute and relative paths
    // When baseUrl is empty (static builds), path is used as-is (relative to current origin)
    const url = this.baseUrl ? `${this.baseUrl}${path}` : path;
    const startTime = Date.now();

    // Build headers - start with custom headers from options
    const headers: Record<string, string> = {
      ...(options?.headers || {})
    };

    // Don't set Content-Type for FormData/binaryBody (browser will set it with boundary)
    if (!options?.formData && !options?.binaryBody && !headers['Content-Type']) {
      headers['Content-Type'] = 'application/json';
    }

{% if has_session_auth %}
    // Add CSRF token for SessionAuthentication
    if (method !== 'GET') {
      const csrfToken = this.getCsrfToken();
      if (csrfToken) {
        headers['X-CSRFToken'] = csrfToken;
      }
    }
{% else %}
    // CSRF not needed - SessionAuthentication not enabled in DRF config
    // Your API uses JWT/Token authentication (no CSRF required)
{% endif %}

    // Log request
    if (this.logger) {
      this.logger.logRequest({
        method,
        url: url,
        headers,
        body: options?.formData || options?.body,
        timestamp: startTime,
      });
    }

    try {
      // Make request via HTTP adapter
      const response = await this.httpClient.request<T>({
        method,
        url: url,
        headers,
        params: options?.params,
        body: options?.body,
        formData: options?.formData,
        binaryBody: options?.binaryBody,
      });

      const duration = Date.now() - startTime;

      // Check for HTTP errors
      if (response.status >= 400) {
        const error = new APIError(
          response.status,
          response.statusText,
          response.data,
          url
        );

        // Log error
        if (this.logger) {
          this.logger.logError(
            {
              method,
              url: url,
              headers,
              body: options?.formData || options?.body,
              timestamp: startTime,
            },
            {
              message: error.message,
              statusCode: response.status,
              duration,
              timestamp: Date.now(),
            }
          );
        }

        throw error;
      }

      // Log successful response
      if (this.logger) {
        this.logger.logResponse(
          {
            method,
            url: url,
            headers,
            body: options?.formData || options?.body,
            timestamp: startTime,
          },
          {
            status: response.status,
            statusText: response.statusText,
            data: response.data,
            duration,
            timestamp: Date.now(),
          }
        );
      }

      return response.data as T;
    } catch (error) {
      const duration = Date.now() - startTime;

      // Re-throw APIError as-is
      if (error instanceof APIError) {
        throw error;
      }

      // Classify network error using PerformanceResourceTiming.
      // Browser intentionally makes CORS errors indistinguishable from network failures
      // (same TypeError: "Failed to fetch") for security reasons. We use PerformanceResourceTiming
      // as the best available heuristic:
      //   - Entry exists with responseStatus === 0 → request reached server, JS blocked → likely CORS
      //   - No entry / entry missing → connection never established → server unavailable / DNS / offline
      // All cases are dispatched as 'network-error' with a `possibly_cors` flag.
      let possiblyCors = false;
      if (error instanceof TypeError && typeof window !== 'undefined') {
        try {
          const isCrossOrigin = (() => {
            try { return new URL(url).origin !== window.location.origin; } catch { return false; }
          })();
          if (isCrossOrigin) {
            const entries = performance.getEntriesByName(url, 'resource');
            if (entries.length > 0) {
              const last = entries[entries.length - 1] as PerformanceResourceTiming;
              possiblyCors = 'responseStatus' in last && (last as any).responseStatus === 0;
            }
          }
        } catch { /* ignore — PerformanceResourceTiming not available */ }
      }

      if (this.logger) {
        this.logger.error(`⚠️  Network Error: ${method} ${url}`);
        this.logger.error(`  → ${error instanceof Error ? error.message : String(error)}`);
        if (possiblyCors) {
          this.logger.error(`  → Possibly blocked by CORS policy (configure CORS on the server)`);
        }
      }

      // Dispatch network-error event with possibly_cors hint
      if (typeof window !== 'undefined') {
        try {
          window.dispatchEvent(new CustomEvent('network-error', {
            detail: {
              url: url,
              method: method,
              error: error instanceof Error ? error.message : String(error),
              possibly_cors: possiblyCors,
              timestamp: new Date(),
            },
            bubbles: true,
            cancelable: false,
          }));
        } catch { /* silently ignore — event dispatch must never crash the app */ }
      }

      // Wrap other errors as NetworkError
      const networkError = error instanceof Error
        ? new NetworkError(error.message, url, error)
        : new NetworkError('Unknown error', url);

      // Detailed logging via logger.logError
      if (this.logger) {
        this.logger.logError(
          {
            method,
            url: url,
            headers,
            body: options?.formData || options?.body,
            timestamp: startTime,
          },
          {
            message: networkError.message,
            duration,
            timestamp: Date.now(),
          }
        );
      }

      throw networkError;
    }
  }
}
{% else %}
{# Flat client without sub-clients #}
import * as Enums from "./enums";
import { HttpClientAdapter, FetchAdapter } from "./http";
import { APIError, NetworkError } from "./errors";
import { APILogger, type LoggerConfig } from "./logger";
import { withRetry, type RetryConfig } from "./retry";


/**
 * Async API client for {{ info.title }}.
 *
 * Usage:
 * ```typescript
 * const client = new APIClient('https://api.example.com');
 * const users = await client.usersList();
 *
 * // Custom HTTP adapter
 * const client = new APIClient('https://api.example.com', {
 *   httpClient: new AxiosAdapter()
 * });
 * ```
 */
export class APIClient {
  private baseUrl: string;
  private httpClient: HttpClientAdapter;
  private logger: APILogger | null = null;
  private retryConfig: RetryConfig | null = null;
  private tokenGetter: (() => string | null) | null = null;

  constructor(
    baseUrl: string,
    options?: {
      httpClient?: HttpClientAdapter;
      loggerConfig?: Partial<LoggerConfig>;
      retryConfig?: RetryConfig;
      tokenGetter?: () => string | null;
    }
  ) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.httpClient = options?.httpClient || new FetchAdapter();
    this.tokenGetter = options?.tokenGetter || null;

    // Initialize logger if config provided
    if (options?.loggerConfig !== undefined) {
      this.logger = new APILogger(options.loggerConfig);
    }

    // Store retry configuration
    if (options?.retryConfig !== undefined) {
      this.retryConfig = options.retryConfig;
    }
  }

  /**
   * Get CSRF token from cookies.
   */
  private getCsrfToken(): string | null {
    const name = 'csrftoken';
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) {
      return parts.pop()?.split(';').shift() || null;
    }
    return null;
  }

  /**
   * Get the base URL for building streaming/download URLs.
   */
  getBaseUrl(): string {
    return this.baseUrl;
  }

  /**
   * Get JWT token for URL authentication (used in streaming endpoints).
   * Returns null if no token getter is configured or no token is available.
   */
  getToken(): string | null {
    return this.tokenGetter ? this.tokenGetter() : null;
  }

  /**
   * Make HTTP request with Django CSRF and session handling.
   * Automatically retries on network errors and 5xx server errors.
   */
  private async request<T>(
    method: string,
    path: string,
    options?: {
      params?: Record<string, any>;
      body?: any;
      formData?: FormData;
      binaryBody?: Blob | ArrayBuffer;
      headers?: Record<string, string>;
    }
  ): Promise<T> {
    // Wrap request in retry logic if configured
    if (this.retryConfig) {
      return withRetry(() => this._makeRequest<T>(method, path, options), {
        ...this.retryConfig,
        onFailedAttempt: (info) => {
          // Log retry attempts
          if (this.logger) {
            this.logger.warn(
              `Retry attempt ${info.attemptNumber}/${info.retriesLeft + info.attemptNumber} ` +
              `for ${method} ${path}: ${info.error.message}`
            );
          }
          // Call user's onFailedAttempt if provided
          this.retryConfig?.onFailedAttempt?.(info);
        },
      });
    }

    // No retry configured, make request directly
    return this._makeRequest<T>(method, path, options);
  }

  /**
   * Internal request method (without retry wrapper).
   * Used by request() method with optional retry logic.
   */
  private async _makeRequest<T>(
    method: string,
    path: string,
    options?: {
      params?: Record<string, any>;
      body?: any;
      formData?: FormData;
      binaryBody?: Blob | ArrayBuffer;
      headers?: Record<string, string>;
    }
  ): Promise<T> {
    // Build URL - handle both absolute and relative paths
    // When baseUrl is empty (static builds), path is used as-is (relative to current origin)
    const url = this.baseUrl ? `${this.baseUrl}${path}` : path;
    const startTime = Date.now();

    // Build headers - start with custom headers from options
    const headers: Record<string, string> = {
      ...(options?.headers || {})
    };

    // Don't set Content-Type for FormData/binaryBody (browser will set it with boundary)
    if (!options?.formData && !options?.binaryBody && !headers['Content-Type']) {
      headers['Content-Type'] = 'application/json';
    }

{% if has_session_auth %}
    // Add CSRF token for SessionAuthentication
    if (method !== 'GET') {
      const csrfToken = this.getCsrfToken();
      if (csrfToken) {
        headers['X-CSRFToken'] = csrfToken;
      }
    }
{% else %}
    // CSRF not needed - SessionAuthentication not enabled in DRF config
    // Your API uses JWT/Token authentication (no CSRF required)
{% endif %}

    // Log request
    if (this.logger) {
      this.logger.logRequest({
        method,
        url: url,
        headers,
        body: options?.formData || options?.body,
        timestamp: startTime,
      });
    }

    try {
      // Make request via HTTP adapter
      const response = await this.httpClient.request<T>({
        method,
        url: url,
        headers,
        params: options?.params,
        body: options?.body,
        formData: options?.formData,
        binaryBody: options?.binaryBody,
      });

      const duration = Date.now() - startTime;

      // Check for HTTP errors
      if (response.status >= 400) {
        const error = new APIError(
          response.status,
          response.statusText,
          response.data,
          url
        );

        // Log error
        if (this.logger) {
          this.logger.logError(
            {
              method,
              url: url,
              headers,
              body: options?.formData || options?.body,
              timestamp: startTime,
            },
            {
              message: error.message,
              statusCode: response.status,
              duration,
              timestamp: Date.now(),
            }
          );
        }

        throw error;
      }

      // Log successful response
      if (this.logger) {
        this.logger.logResponse(
          {
            method,
            url: url,
            headers,
            body: options?.formData || options?.body,
            timestamp: startTime,
          },
          {
            status: response.status,
            statusText: response.statusText,
            data: response.data,
            duration,
            timestamp: Date.now(),
          }
        );
      }

      return response.data as T;
    } catch (error) {
      const duration = Date.now() - startTime;

      // Re-throw APIError as-is
      if (error instanceof APIError) {
        throw error;
      }

      // Classify network error using PerformanceResourceTiming.
      // Browser intentionally makes CORS errors indistinguishable from network failures
      // (same TypeError: "Failed to fetch") for security reasons. We use PerformanceResourceTiming
      // as the best available heuristic:
      //   - Entry exists with responseStatus === 0 → request reached server, JS blocked → likely CORS
      //   - No entry / entry missing → connection never established → server unavailable / DNS / offline
      // All cases are dispatched as 'network-error' with a `possibly_cors` flag.
      let possiblyCors = false;
      if (error instanceof TypeError && typeof window !== 'undefined') {
        try {
          const isCrossOrigin = (() => {
            try { return new URL(url).origin !== window.location.origin; } catch { return false; }
          })();
          if (isCrossOrigin) {
            const entries = performance.getEntriesByName(url, 'resource');
            if (entries.length > 0) {
              const last = entries[entries.length - 1] as PerformanceResourceTiming;
              possiblyCors = 'responseStatus' in last && (last as any).responseStatus === 0;
            }
          }
        } catch { /* ignore — PerformanceResourceTiming not available */ }
      }

      if (this.logger) {
        this.logger.error(`⚠️  Network Error: ${method} ${url}`);
        this.logger.error(`  → ${error instanceof Error ? error.message : String(error)}`);
        if (possiblyCors) {
          this.logger.error(`  → Possibly blocked by CORS policy (configure CORS on the server)`);
        }
      }

      // Dispatch network-error event with possibly_cors hint
      if (typeof window !== 'undefined') {
        try {
          window.dispatchEvent(new CustomEvent('network-error', {
            detail: {
              url: url,
              method: method,
              error: error instanceof Error ? error.message : String(error),
              possibly_cors: possiblyCors,
              timestamp: new Date(),
            },
            bubbles: true,
            cancelable: false,
          }));
        } catch { /* silently ignore — event dispatch must never crash the app */ }
      }

      // Wrap other errors as NetworkError
      const networkError = error instanceof Error
        ? new NetworkError(error.message, url, error)
        : new NetworkError('Unknown error', url);

      // Detailed logging via logger.logError
      if (this.logger) {
        this.logger.logError(
          {
            method,
            url: url,
            headers,
            body: options?.formData || options?.body,
            timestamp: startTime,
          },
          {
            message: networkError.message,
            duration,
            timestamp: Date.now(),
          }
        );
      }

      throw networkError;
    }
  }

  {# Operations will be added here by generator #}
}
{% endif %}
