/**
 * HANA Query API Client - Main Client Class
 * 
 * Complete TypeScript client for the HANA Query API with full type safety,
 * error handling, retry logic, and logging.
 */

import type {
  ClientConfig,
  RequestOptions,
  RequestContext,
  SalesParams,
  HealthData,
  HealthMetadata,
  DocsData,
  DocsMetadata,
  ItemsParams,
  ItemsStatusData,
  ItemsStatusMetadata,
  ItemGroupsData,
  ItemGroupsMetadata,
  SalesData,
  SalesMetadata,
  HierarchiesData,
  HierarchiesMetadata,
  TreesData,
  TreesMetadata,
  QtyPerTagData,
  QtyPerTagMetadata,
  ProductionSectorsData,
  ProductionSectorsMetadata,
  DatabaseCompaniesData,
  DatabaseCompaniesMetadata,
  TagData,
  TagMetadata,
  WorkOrderTagsData,
  WorkOrderTagsMetadata,
  InjectionMachinesData,
  InjectionMachinesMetadata,
  SingleInjectionMachineData,
  SingleInjectionMachineMetadata,
  MoldsData,
  MoldsMetadata,
  SingleMoldData,
  SingleMoldMetadata,
  BusinessPartnersData,
  BusinessPartnersMetadata,
  BusinessPartnersSearchMetadata,
  ContactsData,
  ContactsMetadata,
  ContactsSearchMetadata,
  BusinessPartnerContactsMetadata,
  SearchCriteria,
  PlansData,
  PlansMetadata,
  SinglePlanData,
  SinglePlanMetadata,
  PlanProductsData,
  PlanProductsMetadata,
  PlanWorkOrdersData,
  PlanWorkOrdersMetadata,
  PlanTagsData,
  PlanTagsMetadata,
  PlanSectorsSummaryData,
  PlanSectorsSummaryMetadata,
  AllPlansSectorsSummaryData,
  AllPlansSectorsSummaryMetadata,
  PlanSectorSummary,
  UserData,
  UserMetadata,
  APIResponse,
  APIResponseHeaders,
  SuccessResponse
} from './types.ts';

import {
  createConfig,
  createConfigFromEnvironment,
  getApiUrl,
  replacePathParams,
  buildQueryString,
  getEndpointTimeout,
  ENDPOINTS,
  HEADERS,
  AUTH_STATUS,
  RETRY_CONFIG
} from './config.ts';

import {
  HanaQueryClientError,
  AuthorizationError,
  NotFoundError,
  createErrorFromResponse,
  createNetworkError,
  isRetryableError,
  getRetryDelay
} from './errors.ts';

/**
 * Main HANA Query API Client
 */
export class HanaQueryClient {
  private readonly config: Required<ClientConfig>;

  constructor(config: ClientConfig, customConfig?: Partial<Omit<ClientConfig, 'baseUrl'>>) {
    this.config = createConfig(config, customConfig);
    this.log('debug', 'Client initialized', { config: this.config });
  }

  // =========================================================================
  // Public API Methods
  // =========================================================================

  /**
   * Get API health status with system information
   * 
   * @param options - Optional request configuration
   * @returns Promise resolving to health data and system metadata
   * 
   * @example
   * ```typescript
   * const { data, metadata } = await client.getHealth();
   * console.log(`API Status: ${data.status}`);
   * console.log(`Uptime: ${Math.round(data.uptime / 60)} minutes`);
   * console.log(`Node Version: ${metadata.nodeVersion}`);
   * console.log(`Platform: ${metadata.platform}`);
   * ```
   * 
   * @returns {Object} result - Health status result
   * @returns {Object} result.data - Health status data
   * @returns {string} result.data.status - Always 'ok' when API is running
   * @returns {string} result.data.timestamp - Current timestamp in ISO 8601 format
   * @returns {number} result.data.uptime - Server uptime in seconds
   * @returns {Object} result.metadata - System information metadata
   * @returns {boolean} result.metadata.systemInfo - Indicates system info is included
   * @returns {string} result.metadata.serverTime - Server current time
   * @returns {string} result.metadata.nodeVersion - Node.js version
   * @returns {string} result.metadata.platform - Operating system platform
   * @returns {string} result.metadata.arch - System architecture
   */
  async getHealth(options?: RequestOptions): Promise<{ data: HealthData; metadata: HealthMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<HealthData, HealthMetadata>>(ENDPOINTS.HEALTH, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get API documentation with endpoint information
   * 
   * @param options - Optional request configuration
   * @returns Promise resolving to API documentation and metadata
   * 
   * @example
   * ```typescript
   * const { data, metadata } = await client.getDocs();
   * console.log(`API: ${data.name} v${data.version}`);
   * console.log(`Available endpoints: ${metadata.endpointCount}`);
   * console.log('Endpoints:', Object.keys(data.endpoints));
   * ```
   * 
   * @returns {Object} result - API documentation result
   * @returns {Object} result.data - API documentation data
   * @returns {string} result.data.name - API name
   * @returns {string} result.data.version - API version
   * @returns {Object} result.data.endpoints - Map of endpoint paths to descriptions
   * @returns {Object} result.metadata - Documentation metadata
   * @returns {number} result.metadata.endpointCount - Total number of available endpoints
   * @returns {string} result.metadata.apiVersion - API version identifier
   * @returns {string} result.metadata.generatedAt - Documentation generation timestamp
   * @returns {boolean} result.metadata.documentation - Indicates this is documentation endpoint
   */
  async getDocs(options?: RequestOptions): Promise<{ data: DocsData; metadata: DocsMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<DocsData, DocsMetadata>>(ENDPOINTS.DOCS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get items with optional group filtering
   * 
   * @param params - Optional filtering parameters with numeric group codes
   * @param options - Optional request configuration
   * @returns Promise resolving to items data and metadata
   * 
   * @example
   * // Get all items
   * const allItems = await client.getItems();
   * 
   * // Get items for specific group codes (numeric)
   * const filteredItems = await client.getItems({ 
   *   groups: [101, 102, 103] 
   * });
   */
  async getItems(
    params?: ItemsParams,
    options?: RequestOptions
  ): Promise<{ data: ItemsStatusData; metadata: ItemsStatusMetadata }> {
    const endpoint = params?.groups?.length 
      ? ENDPOINTS.ITEMS + buildQueryString({ groups: params.groups.join(',') })
      : ENDPOINTS.ITEMS;
    const response = await this.executeAPIRequest<SuccessResponse<ItemsStatusData, ItemsStatusMetadata>>(
      endpoint, 
      undefined, 
      options
    );
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all item groups
   * 
   * Retrieves all item groups from the SAP HANA database with their codes and names.
   * Item groups provide categorization information for inventory items.
   * 
   * @param {RequestOptions} [options] - Optional request configuration
   * @returns {Promise<{data: ItemGroupsData, metadata: ItemGroupsMetadata}>} Item groups with metadata
   * 
   * @example
   * ```typescript
   * const response = await client.getItemGroups();
   * console.log(`Found ${response.metadata.count} item groups`);
   * response.data.groups.forEach(group => {
   *   console.log(`${group.code}: ${group.name}`);
   * });
   * ```
   */
  async getItemGroups(options?: RequestOptions): Promise<{ data: ItemGroupsData; metadata: ItemGroupsMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<ItemGroupsData, ItemGroupsMetadata>>(ENDPOINTS.ITEMS_GROUPS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get sales data within a date range
   */
  async getSales(
    params: SalesParams,
    options?: RequestOptions
  ): Promise<{ data: SalesData; metadata: SalesMetadata }> {
    const endpoint = ENDPOINTS.SALES + buildQueryString(params as unknown as Record<string, string | number | boolean | undefined>);
    const response = await this.executeAPIRequest<SuccessResponse<SalesData, SalesMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all item hierarchies
   */
  async getItemHierarchies(options?: RequestOptions): Promise<{ data: HierarchiesData; metadata: HierarchiesMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<HierarchiesData, HierarchiesMetadata>>(ENDPOINTS.ITEMS_HIERARCHIES, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }
  /**
   * Get raw tree relationships (parent-child-quantity)
   */
  async getItemTrees(options?: RequestOptions): Promise<{ data: TreesData; metadata: TreesMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<TreesData, TreesMetadata>>(ENDPOINTS.ITEMS_TREES, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get children of a specific parent item (filtered by groups 131, 144)
   *
   * @param itemCode - Parent item code to get children for
   * @param options - Optional request configuration
   * @returns Promise resolving to tree data and metadata
   *
   * @example
   * ```typescript
   * const { data, metadata } = await client.getItemTree('PARENT-001');
   * console.log(`Found ${metadata.count} children for parent ${metadata.parent}`);
   * data.trees.forEach(tree => {
   *   console.log(`Child: ${tree.itemcode}, Quantity: ${tree.quantity}`);
   * });
   * ```
   *
   * @returns {Object} result - Tree result
   * @returns {Object} result.data - Tree data
   * @returns {Array} result.data.trees - Array of tree relationships (parent-child-quantity)
   * @returns {Object} result.metadata - Metadata about the response
   * @returns {number} result.metadata.count - Total number of children returned
   * @returns {string} result.metadata.parent - Parent item code queried
   */
  async getItemTree(itemCode: string, options?: RequestOptions): Promise<{ data: TreesData; metadata: TreesMetadata }> {
    const endpoint = `/items/${itemCode}/tree`;
    const response = await this.executeAPIRequest<SuccessResponse<TreesData, TreesMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get quantity per tag data for items with mold resources
   *
   * @param options - Optional request configuration
   * @returns Promise resolving to quantity per tag data and metadata
   *
   * @example
   * ```typescript
   * const { data, metadata } = await client.getQtyPerTag();
   * console.log(`Found ${metadata.count} items with quantity per tag data`);
   * data.qtyPerTag.forEach(item => {
   *   console.log(`${item.itemcode}: ${item.qtyPerTag} units per tag`);
   * });
   * ```
   *
   * @returns {Object} result - Quantity per tag result
   * @returns {Object} result.data - Quantity per tag data
   * @returns {Array} result.data.qtyPerTag - Array of items with their quantity per tag values
   * @returns {Object} result.metadata - Metadata about the response
   * @returns {number} result.metadata.count - Total number of items returned
   */
  async getQtyPerTag(options?: RequestOptions): Promise<{ data: QtyPerTagData; metadata: QtyPerTagMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<QtyPerTagData, QtyPerTagMetadata>>(ENDPOINTS.ITEMS_QTY_PER_TAG, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all production sectors
   */
  async getProductionSectors(options?: RequestOptions): Promise<{ data: ProductionSectorsData; metadata: ProductionSectorsMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<ProductionSectorsData, ProductionSectorsMetadata>>(ENDPOINTS.PRODUCTION_SECTORS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all database companies
   */
  async getDatabaseCompanies(options?: RequestOptions): Promise<{ data: DatabaseCompaniesData; metadata: DatabaseCompaniesMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<DatabaseCompaniesData, DatabaseCompaniesMetadata>>(ENDPOINTS.DATABASE_COMPANIES, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all production plans
   */
  async getPlans(options?: RequestOptions): Promise<{ data: PlansData; metadata: PlansMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<PlansData, PlansMetadata>>(ENDPOINTS.PLANS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get a specific production plan by ID
   */
  async getPlan(planId: number | string, options?: RequestOptions): Promise<{ data: SinglePlanData; metadata: SinglePlanMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.PLAN_DETAIL, { planId });
    const response = await this.executeAPIRequest<SuccessResponse<SinglePlanData, SinglePlanMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get products for a specific production plan
   * 
   * @param planId - The plan ID to get products for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan products data and metadata
   * 
   * @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
   * @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   * 
   * @example
   * ```typescript
   * try {
   *   const result = await client.getPlanProducts(123);
   *   if (result.data.products.length === 0) {
   *     console.log('Plan exists but has no products');
   *   } else {
   *     console.log(`Found ${result.data.products.length} products`);
   *   }
   * } catch (error) {
   *   if (error instanceof NotFoundError) {
   *     console.log('Plan does not exist');
   *   } else {
   *     console.log('Other error:', error.message);
   *   }
   * }
   * ```
   * 
   * @remarks
   * - Returns HTTP 200 with empty array when plan exists but has no products
   * - Returns HTTP 404 only when the plan itself doesn't exist
   * - Empty products array is a normal, successful response
   */
  async getPlanProducts(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{ data: PlanProductsData; metadata: PlanProductsMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.PLAN_PRODUCTS, { planId });
    const response = await this.executeAPIRequest<SuccessResponse<PlanProductsData, PlanProductsMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get work orders for a specific production plan
   * 
   * @param planId - The plan ID to get work orders for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan work orders data and metadata
   * 
   * @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
   * @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   * 
   * @example
   * ```typescript
   * try {
   *   const result = await client.getPlanWorkOrders(123);
   *   if (result.data.workOrders.length === 0) {
   *     console.log('Plan exists but has no work orders');
   *   } else {
   *     console.log(`Found ${result.data.workOrders.length} work orders`);
   *   }
   * } catch (error) {
   *   if (error instanceof NotFoundError) {
   *     console.log('Plan does not exist');
   *   } else {
   *     console.log('Other error:', error.message);
   *   }
   * }
   * ```
   * 
   * @remarks
   * - Returns HTTP 200 with empty array when plan exists but has no work orders
   * - Returns HTTP 404 only when the plan itself doesn't exist
   * - Empty work orders array is a normal, successful response
   */
  async getPlanWorkOrders(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{ data: PlanWorkOrdersData; metadata: PlanWorkOrdersMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.PLAN_WORK_ORDERS, { planId });
    const response = await this.executeAPIRequest<SuccessResponse<PlanWorkOrdersData, PlanWorkOrdersMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get tags for a specific production plan
   *
   * @param planId - The plan ID to get tags for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan tags data and metadata
   *
   * @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
   * @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   *
   * @example
   * ```typescript
   * try {
   *   const result = await client.getPlanTags(123);
   *   if (result.data.tags.length === 0) {
   *     console.log('Plan exists but has no tags');
   *   } else {
   *     console.log(`Found ${result.data.tags.length} tags`);
   *     result.data.tags.forEach(tag => {
   *       console.log(`Tag ${tag.tag_entry}: ${tag.tag_item_name} (WO ${tag.tag_workorder_num})`);
   *     });
   *   }
   * } catch (error) {
   *   if (error instanceof NotFoundError) {
   *     console.log('Plan does not exist');
   *   } else {
   *     console.log('Other error:', error.message);
   *   }
   * }
   * ```
   *
   * @remarks
   * - Returns HTTP 200 with empty array when plan exists but has no tags
   * - Returns HTTP 404 only when the plan itself doesn't exist
   * - Empty tags array is a normal, successful response
   */
  async getPlanTags(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{ data: PlanTagsData; metadata: PlanTagsMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.PLAN_TAGS, { planId });
    const response = await this.executeAPIRequest<SuccessResponse<PlanTagsData, PlanTagsMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get sector summaries for a specific production plan
   * 
   * @param planId - The plan ID to get sector summaries for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan sectors summary data and metadata
   * 
   * @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
   * @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   * 
   * @example
   * ```typescript
   * try {
   *   const result = await client.getPlanSectorsSummary(123);
   *   if (result.data.sectors.length === 0) {
   *     console.log('Plan exists but has no sectors');
   *   } else {
   *     console.log(`Found ${result.data.sectors.length} sectors`);
   *     console.log(`Completion rate: ${result.metadata.completion_rate}%`);
   *   }
   * } catch (error) {
   *   if (error instanceof NotFoundError) {
   *     console.log('Plan does not exist');
   *   } else {
   *     console.log('Other error:', error.message);
   *   }
   * }
   * ```
   * 
   * @remarks
   * - Returns HTTP 200 with empty array when plan exists but has no sectors
   * - Returns HTTP 404 only when the plan itself doesn't exist
   * - Empty sectors array is a normal, successful response
   * - Metadata includes aggregated statistics (total planned, completed, completion rate)
   */
  async getPlanSectorsSummary(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{ data: PlanSectorsSummaryData; metadata: PlanSectorsSummaryMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.PLAN_SECTORS, { planId });
    const response = await this.executeAPIRequest<SuccessResponse<PlanSectorsSummaryData, PlanSectorsSummaryMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get sector summaries for all production plans
   * 
   * @param options - Optional request configuration
   * @returns Promise resolving to all plans sectors summary data and metadata
   * 
   * @throws {ValidationError} When the request parameters are invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   * 
   * @example
   * ```typescript
   * const result = await client.getAllPlansSectorsSummary();
   * console.log(`Found ${result.data.sectors.length} total sectors`);
   * console.log(`Across ${result.metadata.unique_plans} unique plans`);
   * console.log(`Overall completion rate: ${result.metadata.overall_completion_rate}%`);
   * 
   * // Group sectors by plan
   * const sectorsByPlan = result.data.sectors.reduce((acc, sector) => {
   *   if (!acc[sector.plan_entry]) acc[sector.plan_entry] = [];
   *   acc[sector.plan_entry].push(sector);
   *   return acc;
   * }, {} as Record<number, PlanSectorSummary[]>);
   * ```
   * 
   * @remarks
   * - Returns all sector summaries across all plans in the system
   * - Empty array indicates no sectors exist in any plan
   * - Metadata includes overall statistics and unique counts
   */
  async getAllPlansSectorsSummary(
    options?: RequestOptions
  ): Promise<{ data: AllPlansSectorsSummaryData; metadata: AllPlansSectorsSummaryMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<AllPlansSectorsSummaryData, AllPlansSectorsSummaryMetadata>>(ENDPOINTS.ALL_PLANS_SECTORS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get user by username
   */
  async getUser(username: string, options?: RequestOptions): Promise<{ data: UserData; metadata: UserMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.USER, { username: encodeURIComponent(username) });
    const response = await this.executeAPIRequest<SuccessResponse<UserData, UserMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get a specific production tag by entry number
   */
  async getTag(tagEntry: number | string, options?: RequestOptions): Promise<{ data: TagData; metadata: TagMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.TAG, { tagEntry });
    const response = await this.executeAPIRequest<SuccessResponse<TagData, TagMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all tags for a specific work order
   *
   * @param workOrderEntry - The work order entry (DocEntry) to get tags for
   * @param options - Optional request configuration
   * @returns Promise resolving to work order tags data and metadata
   *
   * @throws {ValidationError} When the workOrderEntry parameter is invalid (HTTP 400)
   * @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
   * @throws {HanaQueryClientError} For other client or server errors
   *
   * @example
   * ```typescript
   * const result = await client.getWorkOrderTags(12345);
   * console.log(`Found ${result.data.tags.length} tags for work order ${result.metadata.workOrderEntry}`);
   *
   * // Iterate through all tags
   * result.data.tags.forEach(tag => {
   *   console.log(`Tag ${tag.tagEntry}: Status=${tag.status}, Quantity=${tag.quantity}${tag.quantityUoM}`);
   *   if (tag.activationDateTime) {
   *     console.log(`  Activated: ${tag.activationDateTime} by ${tag.activationEmployee}`);
   *   }
   *   if (tag.quantificationDateTime) {
   *     console.log(`  Quantified: ${tag.quantificationDateTime} by ${tag.quantificationEmployee}`);
   *   }
   * });
   * ```
   *
   * @remarks
   * - Returns empty array if work order has no tags
   * - No 404 if work order doesn't exist - just returns empty array
   * - Each tag includes full details (activation, quantification, use, discard info)
   * - Useful for tracking tag lifecycle for a specific production order
   */
  async getWorkOrderTags(
    workOrderEntry: number | string,
    options?: RequestOptions
  ): Promise<{ data: WorkOrderTagsData; metadata: WorkOrderTagsMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.WORK_ORDER_TAGS, { workOrderEntry });
    const response = await this.executeAPIRequest<SuccessResponse<WorkOrderTagsData, WorkOrderTagsMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all injection machines
   */
  async getInjectionMachines(options?: RequestOptions): Promise<{ data: InjectionMachinesData; metadata: InjectionMachinesMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<InjectionMachinesData, InjectionMachinesMetadata>>(ENDPOINTS.INJECTION_MACHINES, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get a specific injection machine by code
   */
  async getInjectionMachine(machineCode: string, options?: RequestOptions): Promise<{ data: SingleInjectionMachineData; metadata: SingleInjectionMachineMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.INJECTION_MACHINE, { machineCode: encodeURIComponent(machineCode) });
    const response = await this.executeAPIRequest<SuccessResponse<SingleInjectionMachineData, SingleInjectionMachineMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get all molds with specifications
   */
  async getMolds(options?: RequestOptions): Promise<{ data: MoldsData; metadata: MoldsMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<MoldsData, MoldsMetadata>>(ENDPOINTS.MOLDS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get a specific mold by code
   */
  async getMold(moldCode: string, options?: RequestOptions): Promise<{ data: SingleMoldData; metadata: SingleMoldMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.MOLD, { moldCode: encodeURIComponent(moldCode) });
    const response = await this.executeAPIRequest<SuccessResponse<SingleMoldData, SingleMoldMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * List all business partners.
   *
   * @example
   * ```typescript
   * const { data, metadata } = await client.getBusinessPartners();
   * console.log(`${metadata.count} partners`);
   * data.partners.forEach(p => console.log(p.cardcode, p.cardname));
   * ```
   *
   * @remarks Returns 404 (mapped to `NotFoundError`) if the table is empty.
   */
  async getBusinessPartners(options?: RequestOptions): Promise<{ data: BusinessPartnersData; metadata: BusinessPartnersMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<BusinessPartnersData, BusinessPartnersMetadata>>(ENDPOINTS.BUSINESS_PARTNERS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Search business partners by phone, email, name (fuzzy), and/or CardCode.
   *
   * At least one of `phone`, `email`, `q`, or `cardCode` must be supplied —
   * otherwise the server returns 400 (surfaced as `ValidationError`).
   *
   * @param criteria - Search criteria. `fuzziness` (0.1–1.0, default 0.7) tunes
   *   the FUZZY threshold for `q`.
   * @example
   * ```typescript
   * const { data, metadata } = await client.searchBusinessPartners({
   *   q: 'jose silva',
   *   fuzziness: 0.8
   * });
   * console.log(`${metadata.count} matches`);
   * ```
   *
   * @remarks
   * - `phone` is normalized to digits and matched as a substring.
   * - `email` is exact after canonical email extraction (case-insensitive).
   * - `q` is HANA `CONTAINS FUZZY` across CardName, City, State, Country,
   *   Block, Address, ZipCode, IndName. Words are OR-ed.
   * - `cardCode` is an exact match.
   * - Empty result returns 200 with `partners: []`; no NotFoundError.
   */
  async searchBusinessPartners(
    criteria: SearchCriteria,
    options?: RequestOptions
  ): Promise<{ data: BusinessPartnersData; metadata: BusinessPartnersSearchMetadata }> {
    const endpoint = ENDPOINTS.BUSINESS_PARTNERS_SEARCH + buildQueryString(criteria as unknown as Record<string, string | number | boolean | undefined>);
    const response = await this.executeAPIRequest<SuccessResponse<BusinessPartnersData, BusinessPartnersSearchMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Get the contacts for a specific business partner.
   *
   * @param cardCode - The business partner's CardCode.
   * @example
   * ```typescript
   * const { data, metadata } = await client.getBusinessPartnerContacts('C12345');
   * console.log(`${metadata.count} contacts for ${metadata.cardCode}`);
   * ```
   *
   * @remarks
   * Returns 200 with an empty array even if the cardCode does not exist —
   * this endpoint does **not** 404 on unknown partners. Use
   * `searchBusinessPartners({ cardCode })` first if you need an existence check.
   */
  async getBusinessPartnerContacts(
    cardCode: string,
    options?: RequestOptions
  ): Promise<{ data: ContactsData; metadata: BusinessPartnerContactsMetadata }> {
    const endpoint = replacePathParams(ENDPOINTS.BUSINESS_PARTNER_CONTACTS, { cardCode: encodeURIComponent(cardCode) });
    const response = await this.executeAPIRequest<SuccessResponse<ContactsData, BusinessPartnerContactsMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * List all contacts.
   *
   * @example
   * ```typescript
   * const { data, metadata } = await client.getContacts();
   * console.log(`${metadata.count} contacts`);
   * ```
   *
   * @remarks
   * - Returns 404 (NotFoundError) if the table is empty.
   * - `cardname` on the returned contacts is `null`/absent here — only the
   *   search endpoint joins to ocrd and populates it.
   */
  async getContacts(options?: RequestOptions): Promise<{ data: ContactsData; metadata: ContactsMetadata }> {
    const response = await this.executeAPIRequest<SuccessResponse<ContactsData, ContactsMetadata>>(ENDPOINTS.CONTACTS, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  /**
   * Search contacts by phone, email, name (fuzzy), and/or CardCode.
   *
   * Same criteria shape as {@link searchBusinessPartners}; at least one of
   * `phone` / `email` / `q` / `cardCode` is required (400 otherwise).
   *
   * @param criteria - Search criteria. `fuzziness` (0.1–1.0, default 0.7) tunes
   *   the FUZZY threshold for `q`.
   *
   * @example
   * ```typescript
   * // Find contacts named "jose" within partner C12345
   * const { data } = await client.searchContacts({ q: 'jose', cardCode: 'C12345' });
   * data.contacts.forEach(c => console.log(c.contactname, '@', c.cardname));
   * ```
   *
   * @remarks
   * Cross-table — `phone` / `email` / `q` also match the parent BP's fields.
   * Returned contacts include `cardname` populated from the joined BP row.
   */
  async searchContacts(
    criteria: SearchCriteria,
    options?: RequestOptions
  ): Promise<{ data: ContactsData; metadata: ContactsSearchMetadata }> {
    const endpoint = ENDPOINTS.CONTACTS_SEARCH + buildQueryString(criteria as unknown as Record<string, string | number | boolean | undefined>);
    const response = await this.executeAPIRequest<SuccessResponse<ContactsData, ContactsSearchMetadata>>(endpoint, undefined, options);
    return { data: response.data, metadata: response.metadata! };
  }

  // =========================================================================
  // Convenience Methods
  // =========================================================================

  /**
   * Check if a plan exists without fetching its full details
   * 
   * @param planId - The plan ID to check
   * @param options - Optional request configuration
   * @returns Promise resolving to true if plan exists, false otherwise
   * 
   * @example
   * ```typescript
   * const exists = await client.planExists(123);
   * if (exists) {
   *   console.log('Plan exists');
   * } else {
   *   console.log('Plan not found');
   * }
   * ```
   */
  async planExists(planId: number | string, options?: RequestOptions): Promise<boolean> {
    try {
      await this.getPlan(planId, options);
      return true;
    } catch (error) {
      if (error instanceof NotFoundError) {
        return false;
      }
      throw error;
    }
  }

  /**
   * Get plan products with explicit plan existence information
   * 
   * @param planId - The plan ID to get products for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan existence status, products, and metadata
   * 
   * @example
   * ```typescript
   * const result = await client.getPlanProductsSafe(123);
   * if (!result.planExists) {
   *   console.log('Plan does not exist');
   * } else if (result.products.length === 0) {
   *   console.log('Plan exists but has no products');
   * } else {
   *   console.log(`Plan has ${result.products.length} products`);
   * }
   * ```
   */
  async getPlanProductsSafe(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{
    planExists: boolean;
    products: PlanProductsData['products'];
    metadata: PlanProductsMetadata;
  }> {
    try {
      const result = await this.getPlanProducts(planId, options);
      return {
        planExists: true,
        products: result.data.products,
        metadata: result.metadata
      };
    } catch (error) {
      if (error instanceof NotFoundError) {
        return {
          planExists: false,
          products: [],
          metadata: { count: 0, planId: Number(planId) }
        };
      }
      throw error;
    }
  }

  /**
   * Get plan work orders with explicit plan existence information
   * 
   * @param planId - The plan ID to get work orders for
   * @param options - Optional request configuration
   * @returns Promise resolving to plan existence status, work orders, and metadata
   * 
   * @example
   * ```typescript
   * const result = await client.getPlanWorkOrdersSafe(123);
   * if (!result.planExists) {
   *   console.log('Plan does not exist');
   * } else if (result.workOrders.length === 0) {
   *   console.log('Plan exists but has no work orders');
   * } else {
   *   console.log(`Plan has ${result.workOrders.length} work orders`);
   * }
   * ```
   */
  async getPlanWorkOrdersSafe(
    planId: number | string,
    options?: RequestOptions
  ): Promise<{
    planExists: boolean;
    workOrders: PlanWorkOrdersData['workOrders'];
    metadata: PlanWorkOrdersMetadata;
  }> {
    try {
      const result = await this.getPlanWorkOrders(planId, options);
      return {
        planExists: true,
        workOrders: result.data.workOrders,
        metadata: result.metadata
      };
    } catch (error) {
      if (error instanceof NotFoundError) {
        return {
          planExists: false,
          workOrders: [],
          metadata: { count: 0, planId: Number(planId) }
        };
      }
      throw error;
    }
  }

  // =========================================================================
  // Request Builder API (Fluent Interface)
  // =========================================================================

  /**
   * Create a request builder for fluent API usage
   */
  request(endpoint: string): RequestBuilder {
    return new RequestBuilder(this, endpoint);
  }

  // =========================================================================
  // Core Request Method
  // =========================================================================

  /**
   * Core request method with retry logic and error handling
   */
  private async executeAPIRequest<T extends APIResponse>(
    endpoint: string,
    body?: any,
    options?: RequestOptions
  ): Promise<T> {
    const mergedOptions = {
      timeout: getEndpointTimeout(endpoint, this.config.timeout),
      retries: this.config.retries,
      ...options
    };

    let lastError: HanaQueryClientError | undefined;

    for (let attempt = 1; attempt <= mergedOptions.retries + 1; attempt++) {
      const context: RequestContext = {
        method: 'GET',
        url: endpoint,
        startTime: Date.now(),
        attempt,
        maxAttempts: mergedOptions.retries + 1
      };

      try {
        this.log('debug', `Request attempt ${attempt}`, { endpoint, attempt });
        
        const result = await this.executeRequest<T>(endpoint, body, mergedOptions, context);
        
        this.log('info', 'Request successful', {
          endpoint,
          attempt,
          duration: context.duration
        });

        return result;

      } catch (error) {
        context.endTime = Date.now();
        context.duration = context.endTime - context.startTime;

        if (error instanceof HanaQueryClientError) {
          lastError = error;
        } else {
          lastError = createNetworkError(error as Error, context);
        }

        this.log('warn', `Request attempt ${attempt} failed`, {
          endpoint,
          attempt,
          error: lastError.message,
          duration: context.duration
        });

        // Don't retry if this is the last attempt or error is not retryable
        if (attempt >= mergedOptions.retries + 1 || !isRetryableError(lastError)) {
          break;
        }

        // Calculate delay and wait before retrying
        const delay = getRetryDelay(attempt, this.config.retryDelay, 30000);
        this.log('debug', `Retrying in ${delay}ms`, { endpoint, attempt, delay });
        await this.sleep(delay);
      }
    }

    this.log('error', 'Request failed after all retries', {
      endpoint,
      attempts: mergedOptions.retries + 1,
      error: lastError?.message
    });

    throw lastError!;
  }

  /**
   * Execute a single HTTP request
   */
  private async executeRequest<T extends APIResponse>(
    endpoint: string,
    body: any,
    options: RequestOptions & { timeout: number },
    context: RequestContext
  ): Promise<T> {
    const url = getApiUrl(this.config.baseUrl, endpoint);
    context.url = url;

    // Setup request options
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), options.timeout);

    // Merge signal if provided
    if (options.signal) {
      options.signal.addEventListener('abort', () => controller.abort());
    }

    const requestInit: RequestInit = {
      method: 'GET',
      headers: {
        ...this.config.headers
      },
      signal: controller.signal
    };

    // Add body for POST/PUT requests (currently API only uses GET)
    if (body && requestInit.method !== 'GET') {
      requestInit.body = JSON.stringify(body);
    }

    let response: Response;

    try {
      this.log('debug', 'Sending HTTP request', { url, method: requestInit.method });
      response = await fetch(url, requestInit);
    } catch (error) {
      throw createNetworkError(error as Error, context);
    } finally {
      clearTimeout(timeoutId);
      context.endTime = Date.now();
      context.duration = context.endTime - context.startTime;
    }

    // Parse response headers
    const headers = this.parseResponseHeaders(response);
    
    // Log response info
    this.log('debug', 'Received HTTP response', {
      url,
      status: response.status,
      headers,
      duration: context.duration
    });

    // Check authorization header
    this.checkAuthorization(headers, context);

    // Parse response body
    let responseData: any;
    try {
      const responseText = await response.text();
      responseData = responseText ? JSON.parse(responseText) : null;
    } catch (error) {
      throw createNetworkError(
        new Error(`Failed to parse response JSON: ${(error as Error).message}`),
        context
      );
    }

    // Handle HTTP errors
    if (!response.ok) {
      throw createErrorFromResponse(response, responseData, context);
    }

    // Handle API errors (success: false)
    if (responseData && responseData.success === false) {
      throw createErrorFromResponse(response, responseData, context);
    }

    // Validate response structure
    if (!responseData || typeof responseData.success !== 'boolean') {
      throw createNetworkError(
        new Error('Invalid API response format'),
        context
      );
    }

    return responseData as T;
  }

  // =========================================================================
  // Helper Methods
  // =========================================================================

  /**
   * Parse response headers into typed object
   */
  private parseResponseHeaders(response: Response): APIResponseHeaders {
    const responseTime = response.headers.get(HEADERS.RESPONSE_TIME);
    const authResponse = response.headers.get(HEADERS.AUTHORIZATION_RESPONSE);
    const contentType = response.headers.get(HEADERS.CONTENT_TYPE);
    
    return {
      'x-response-time': responseTime || undefined,
      'x-authorization-response': (authResponse as 'ok' | 'unauthorized') || undefined,
      'content-type': contentType || undefined
    };
  }

  /**
   * Check authorization status from headers
   */
  private checkAuthorization(headers: APIResponseHeaders, context: RequestContext): void {
    const authStatus = headers['x-authorization-response'];
    
    if (authStatus === AUTH_STATUS.UNAUTHORIZED) {
      throw new AuthorizationError(
        'Request unauthorized',
        401,
        context
      );
    }

    if (authStatus && authStatus !== AUTH_STATUS.OK) {
      this.log('warn', `Unknown authorization status: ${authStatus}`, { context });
    }
  }

  /**
   * Sleep for specified milliseconds
   */
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Logging method
   */
  private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any): void {
    if (!this.config.enableLogging) {
      return;
    }

    const logLevels = ['debug', 'info', 'warn', 'error'];
    const configLevel = logLevels.indexOf(this.config.logLevel);
    const messageLevel = logLevels.indexOf(level);

    if (messageLevel < configLevel) {
      return;
    }

    const timestamp = new Date().toISOString();
    const prefix = `[${timestamp}] [HanaQueryClient] [${level.toUpperCase()}]`;
    
    if (data) {
      console[level === 'debug' ? 'log' : level](`${prefix} ${message}`, data);
    } else {
      console[level === 'debug' ? 'log' : level](`${prefix} ${message}`);
    }
  }

  // =========================================================================
  // Configuration and Utilities
  // =========================================================================

  /**
   * Get current client configuration
   */
  getConfig(): Readonly<Required<ClientConfig>> {
    return { ...this.config };
  }

  /**
   * Update client configuration
   */
  updateConfig(updates: Partial<ClientConfig>): void {
    Object.assign(this.config, updates);
    this.log('debug', 'Configuration updated', { updates });
  }

  /**
   * Test connection to the API
   */
  async testConnection(): Promise<boolean> {
    try {
      await this.getHealth({ timeout: 5000, retries: 1 });
      return true;
    } catch {
      return false;
    }
  }
}

/**
 * Request Builder for fluent API usage
 */
export class RequestBuilder {
  private client: HanaQueryClient;
  private endpoint: string;
  private params: Record<string, any> = {};
  private requestOptions: RequestOptions = {};

  constructor(client: HanaQueryClient, endpoint: string) {
    this.client = client;
    this.endpoint = endpoint;
  }

  /**
   * Add query parameters
   */
  query(params: Record<string, any>): this {
    this.params = { ...this.params, ...params };
    return this;
  }

  /**
   * Set request timeout
   */
  timeout(ms: number): this {
    this.requestOptions.timeout = ms;
    return this;
  }

  /**
   * Set retry count
   */
  retries(count: number): this {
    this.requestOptions.retries = count;
    return this;
  }

  /**
   * Set abort signal
   */
  signal(signal: AbortSignal): this {
    this.requestOptions.signal = signal;
    return this;
  }

  /**
   * Execute the request
   */
  async execute<T extends APIResponse>(): Promise<T> {
    const finalEndpoint = this.endpoint + buildQueryString(this.params);
    // Access the private method through the prototype
    const clientMethod = (this.client as any).executeAPIRequest;
    return clientMethod.call(this.client, finalEndpoint, undefined, this.requestOptions);
  }
}

/**
 * Factory function to create a client instance
 */
export function createClient(config: ClientConfig, customConfig?: Partial<Omit<ClientConfig, 'baseUrl'>>): HanaQueryClient {
  return new HanaQueryClient(config, customConfig);
}

/**
 * Factory function to create a client from environment configuration
 */
export function createClientFromEnvironment(environment: string, customConfig?: Partial<ClientConfig>): HanaQueryClient {
  const envConfig = createConfigFromEnvironment(environment, customConfig);
  return new HanaQueryClient(envConfig);
}

/**
 * Default client instance (requires baseUrl to be set via environment or manual configuration)
 * Example: createClient({ baseUrl: 'http://localhost:3001' })
 */
// export const defaultClient = createClient({ baseUrl: 'http://localhost:3001' });