/** biome-ignore-all lint/style/useReadonlyClassProperties: properties are reassigned in cloneWithChanges() */
import type { FFetchOptions } from "@fetchkit/ffetch";
import { Effect } from "effect";
import buildQuery, { type QueryOptions } from "odata-query";
import { requestFromService, runLayerResult } from "../../effect";
import type { FMODataErrorType } from "../../errors";
import { BuilderInvariantError, isFMODataError, RecordCountMismatchError } from "../../errors";
import type { InternalLogger } from "../../logger";
import { type Column, isColumn } from "../../orm/column";
import { type FilterExpression, isOrderByExpression, type OrderByExpression } from "../../orm/operators";
import {
  type ExtractTableName,
  type FMTable,
  getTableName,
  type InferSchemaOutputFromFMTable,
  type ValidExpandTarget,
} from "../../orm/table";
import type { FMODataLayer, ODataConfig } from "../../services";
import { transformOrderByField } from "../../transform";
import type {
  ConditionallyWithODataAnnotations,
  ConditionallyWithSpecialColumns,
  ExecutableBuilder,
  ExecuteMethodOptions,
  ExecuteOptions,
  NormalizeIncludeSpecialColumns,
  Result,
} from "../../types";
import {
  buildSelectExpandQueryString,
  cloneQueryReadBuilderState,
  createInitialQueryReadBuilderState,
  createODataRequest,
  ExpandBuilder,
  type ExpandConfig,
  type ExpandedRelations,
  mergeExecuteOptions,
  processQueryResponse,
  processSelectWithRenames,
} from "../builders/index";
import { parseErrorResponse } from "../error-parser";
import { createClientRuntime } from "../runtime";
import { safeJsonParse } from "../sanitize-json";
import type { QueryReturnType, SystemColumnsOption, TypeSafeOrderBy } from "./types";
import { type NavigationConfig, QueryUrlBuilder } from "./url-builder";

export type { ExpandedRelations } from "../builders/index";
// Re-export types for backward compatibility
export type { CountedListResult, QueryReturnType, SystemColumnsOption, TypeSafeOrderBy } from "./types";

/**
 * Default maximum number of records to return in a list query.
 * This prevents stack overflow issues with large datasets while still
 * allowing substantial data retrieval. Users can override with .top().
 */
const _DEFAULT_TOP = 1000;

function normalizeQueryBuildError(error: unknown): FMODataErrorType {
  if (isFMODataError(error)) {
    return error;
  }
  if (error instanceof Error) {
    return new BuilderInvariantError("QueryBuilder.execute", error.message, { cause: error });
  }
  return new BuilderInvariantError("QueryBuilder.execute", String(error));
}

type QueryBuilderHasSelect<
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
  Occ extends FMTable<any, any>,
  Selected,
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
> = Selected extends Record<string, Column<any, any, any>>
  ? true
  : Selected extends keyof InferSchemaOutputFromFMTable<Occ>
    ? false
    : true;

type BaseQueryBuilderReturn<
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
  Occ extends FMTable<any, any>,
  Selected extends
    | keyof InferSchemaOutputFromFMTable<Occ>
    // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
    | Record<string, Column<any, any, ExtractTableName<Occ>>>,
  SingleMode extends "exact" | "maybe" | false,
  IsCount extends boolean,
  Expands extends ExpandedRelations,
  IncludeCount extends boolean,
  SystemCols extends SystemColumnsOption | undefined,
> = QueryReturnType<
  InferSchemaOutputFromFMTable<Occ>,
  Selected,
  SingleMode,
  IsCount,
  Expands,
  IncludeCount,
  SystemCols
>;

type ExecutableQueryBuilderReturn<
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
  Occ extends FMTable<any, any>,
  Selected extends
    | keyof InferSchemaOutputFromFMTable<Occ>
    // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
    | Record<string, Column<any, any, ExtractTableName<Occ>>>,
  SingleMode extends "exact" | "maybe" | false,
  IsCount extends boolean,
  Expands extends ExpandedRelations,
  IncludeCount extends boolean,
  SystemCols extends SystemColumnsOption | undefined,
> =
  | ConditionallyWithODataAnnotations<
      ConditionallyWithSpecialColumns<
        BaseQueryBuilderReturn<Occ, Selected, SingleMode, IsCount, Expands, IncludeCount, SystemCols>,
        true,
        QueryBuilderHasSelect<Occ, Selected>
      >,
      true
    >
  | ConditionallyWithODataAnnotations<
      ConditionallyWithSpecialColumns<
        BaseQueryBuilderReturn<Occ, Selected, SingleMode, IsCount, Expands, IncludeCount, SystemCols>,
        true,
        QueryBuilderHasSelect<Occ, Selected>
      >,
      false
    >
  | ConditionallyWithODataAnnotations<
      ConditionallyWithSpecialColumns<
        BaseQueryBuilderReturn<Occ, Selected, SingleMode, IsCount, Expands, IncludeCount, SystemCols>,
        false,
        QueryBuilderHasSelect<Occ, Selected>
      >,
      true
    >
  | ConditionallyWithODataAnnotations<
      ConditionallyWithSpecialColumns<
        BaseQueryBuilderReturn<Occ, Selected, SingleMode, IsCount, Expands, IncludeCount, SystemCols>,
        false,
        QueryBuilderHasSelect<Occ, Selected>
      >,
      false
    >;

export class QueryBuilder<
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
  Occ extends FMTable<any, any>,
  Selected extends
    | keyof InferSchemaOutputFromFMTable<Occ>
    // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
    | Record<string, Column<any, any, ExtractTableName<Occ>>> = keyof InferSchemaOutputFromFMTable<Occ>,
  SingleMode extends "exact" | "maybe" | false = false,
  IsCount extends boolean = false,
  // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
  Expands extends ExpandedRelations = {},
  IncludeCount extends boolean = false,
  DatabaseIncludeSpecialColumns extends boolean = false,
  SystemCols extends SystemColumnsOption | undefined = undefined,
> implements
    ExecutableBuilder<
      ExecutableQueryBuilderReturn<Occ, Selected, SingleMode, IsCount, Expands, IncludeCount, SystemCols>
    >
{
  private readState = createInitialQueryReadBuilderState<InferSchemaOutputFromFMTable<Occ>>();
  private readonly occurrence: Occ;
  private readonly expandBuilder: ExpandBuilder;
  private urlBuilder: QueryUrlBuilder;
  private readonly layer: FMODataLayer;
  private readonly config: ODataConfig;
  private readonly logger: InternalLogger;

  // Compatibility accessors for internal modules that inspect builder internals via `as any`.
  private get queryOptions(): Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>> {
    return this.readState.queryOptions;
  }

  private set queryOptions(queryOptions: Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>>) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      queryOptions,
    });
  }

  private get expandConfigs(): ExpandConfig[] {
    return this.readState.expandConfigs;
  }

  private set expandConfigs(expandConfigs: ExpandConfig[]) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      expandConfigs,
    });
  }

  private get singleMode(): SingleMode {
    return this.readState.singleMode as SingleMode;
  }

  private set singleMode(singleMode: SingleMode) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      singleMode,
    });
  }

  private get isCountMode(): IsCount {
    return this.readState.isCountMode as IsCount;
  }

  private set isCountMode(isCountMode: IsCount) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      isCountMode,
    });
  }

  private get includeCountMode(): IncludeCount {
    return this.readState.includeCountMode as IncludeCount;
  }

  private set includeCountMode(includeCountMode: IncludeCount) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      includeCountMode,
    });
  }

  private get fieldMapping(): Record<string, string> | undefined {
    return this.readState.fieldMapping;
  }

  private set fieldMapping(fieldMapping: Record<string, string> | undefined) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      fieldMapping,
    });
  }

  private get systemColumns(): SystemColumnsOption | undefined {
    return this.readState.systemColumns;
  }

  private set systemColumns(systemColumns: SystemColumnsOption | undefined) {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      systemColumns,
    });
  }

  private get navigation(): NavigationConfig | undefined {
    return this.readState.navigation;
  }

  private set navigation(navigation: NavigationConfig | undefined) {
    this.setNavigation(navigation);
  }

  constructor(config: {
    occurrence: Occ;
    layer: FMODataLayer;
  }) {
    this.occurrence = config.occurrence;
    const runtime = createClientRuntime(config.layer);
    this.layer = runtime.layer;
    this.config = runtime.config;
    this.logger = runtime.logger;
    this.expandBuilder = new ExpandBuilder(this.config.useEntityIds, this.logger);
    this.urlBuilder = new QueryUrlBuilder(this.config.databaseName, this.occurrence, this.config.useEntityIds);
  }

  /**
   * Helper to merge database-level useEntityIds and includeSpecialColumns with per-request options
   */
  private mergeExecuteOptions(options?: RequestInit & FFetchOptions & ExecuteOptions): RequestInit &
    FFetchOptions & {
      useEntityIds?: boolean;
      includeSpecialColumns?: boolean;
    } {
    const merged = mergeExecuteOptions(options, this.config.useEntityIds);
    return {
      ...merged,
      includeSpecialColumns: options?.includeSpecialColumns ?? this.config.includeSpecialColumns,
    };
  }

  private patchQueryOptions(patch: Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>>): void {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      queryOptions: patch,
    });
  }

  private setFilterExpression(expression: FilterExpression | undefined): void {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      filterExpression: expression,
    });
  }

  private setNavigation(navigation: NavigationConfig | undefined): void {
    this.readState = cloneQueryReadBuilderState(this.readState, {
      navigation,
    });
  }

  /**
   * Creates a new QueryBuilder with modified configuration.
   * Used by single(), maybeSingle(), count(), and select() to create new instances.
   */
  private cloneWithChanges<
    NewSelected extends
      | keyof InferSchemaOutputFromFMTable<Occ>
      // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
      | Record<string, Column<any, any, ExtractTableName<Occ>>> = Selected,
    NewSingle extends "exact" | "maybe" | false = SingleMode,
    NewCount extends boolean = IsCount,
    NewIncludeCount extends boolean = IncludeCount,
    NewSystemCols extends SystemColumnsOption | undefined = SystemCols,
  >(changes: {
    selectedFields?: NewSelected;
    singleMode?: NewSingle;
    isCountMode?: NewCount;
    includeCountMode?: NewIncludeCount;
    queryOptions?: Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>>;
    fieldMapping?: Record<string, string>;
    systemColumns?: NewSystemCols;
  }): QueryBuilder<
    Occ,
    NewSelected,
    NewSingle,
    NewCount,
    Expands,
    NewIncludeCount,
    DatabaseIncludeSpecialColumns,
    NewSystemCols
  > {
    const newBuilder = new QueryBuilder<
      Occ,
      NewSelected,
      NewSingle,
      NewCount,
      Expands,
      NewIncludeCount,
      DatabaseIncludeSpecialColumns,
      NewSystemCols
    >({
      occurrence: this.occurrence,
      layer: this.layer,
    });
    newBuilder.readState = cloneQueryReadBuilderState(this.readState, {
      queryOptions: changes.queryOptions,
      expandConfigs: this.readState.expandConfigs,
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      singleMode: (changes.singleMode ?? this.readState.singleMode) as any,
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      isCountMode: (changes.isCountMode ?? this.readState.isCountMode) as any,
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      includeCountMode: (changes.includeCountMode ?? this.readState.includeCountMode) as any,
      fieldMapping: "fieldMapping" in changes ? changes.fieldMapping : this.readState.fieldMapping,
      systemColumns: changes.systemColumns !== undefined ? changes.systemColumns : this.readState.systemColumns,
      navigation: this.readState.navigation,
    });
    newBuilder.urlBuilder = new QueryUrlBuilder(this.config.databaseName, this.occurrence, this.config.useEntityIds);
    return newBuilder;
  }

  /**
   * Select fields using column references, or pass "all" to clear any defaultSelect and fetch all fields.
   * Allows renaming fields by using different keys in the object.
   * Container fields cannot be selected and will cause a type error.
   *
   * @example
   * db.from(users).list().select({
   *   name: users.name,
   *   userEmail: users.email  // renamed!
   * })
   *
   * @example
   * // Include system columns (ROWID, ROWMODID) when using select()
   * db.from(users).list().select(
   *   { name: users.name },
   *   { ROWID: true, ROWMODID: true }
   * )
   *
   * @example
   * // Override defaultSelect to fetch all fields
   * db.from(users).list().select("all")
   *
   * @param fields - Object mapping output keys to column references (container fields excluded), or "all" to select all fields
   * @param systemColumns - Optional object to request system columns (ROWID, ROWMODID)
   * @returns QueryBuilder with updated selected fields
   */
  select(
    fields: "all",
  ): QueryBuilder<
    Occ,
    keyof InferSchemaOutputFromFMTable<Occ>,
    SingleMode,
    IsCount,
    Expands,
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    undefined
  >;
  select<
    // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
    TSelect extends Record<string, Column<any, any, ExtractTableName<Occ>, false>>,
    // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no system columns by default
    TSystemCols extends SystemColumnsOption = {},
  >(
    fields: TSelect,
    systemColumns?: TSystemCols,
  ): QueryBuilder<Occ, TSelect, SingleMode, IsCount, Expands, IncludeCount, DatabaseIncludeSpecialColumns, TSystemCols>;
  // biome-ignore lint/suspicious/noExplicitAny: Implementation signature hidden from callers
  select(fields: any, systemColumns?: any): any {
    if (fields === "all") {
      return this.cloneWithChanges({
        queryOptions: {
          select: undefined,
        },
        fieldMapping: undefined,
        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
        systemColumns: undefined as any,
      });
    }

    const tableName = getTableName(this.occurrence);
    const { selectedFields, fieldMapping } = processSelectWithRenames(fields, tableName, this.logger);

    // Add system columns to selectedFields if requested
    const finalSelectedFields = [...selectedFields];
    if (systemColumns?.ROWID) {
      finalSelectedFields.push("ROWID");
    }
    if (systemColumns?.ROWMODID) {
      finalSelectedFields.push("ROWMODID");
    }

    return this.cloneWithChanges({
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      selectedFields: fields as any,
      queryOptions: {
        select: finalSelectedFields,
      },
      fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      systemColumns: systemColumns as any,
    });
  }

  /**
   * Filter results using operator expressions (new ORM-style API).
   * Supports eq, gt, lt, and, or, etc. operators with Column references.
   * Also supports raw OData filter strings as an escape hatch.
   *
   * @example
   * .where(eq(users.hobby, "reading"))
   * .where(and(eq(users.active, true), gt(users.age, 18)))
   * .where("status eq 'active'")  // Raw OData string escape hatch
   */
  where(
    expression: FilterExpression | string,
  ): QueryBuilder<
    Occ,
    Selected,
    SingleMode,
    IsCount,
    Expands,
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    SystemCols
  > {
    // Handle raw string filters (escape hatch)
    if (typeof expression === "string") {
      this.setFilterExpression(undefined);
      this.patchQueryOptions({ filter: expression });
      return this;
    }

    // Defer serialization until execute/getQueryString so per-request useEntityIds is honored
    this.setFilterExpression(expression);
    this.patchQueryOptions({ filter: undefined });
    return this;
  }

  /**
   * Specify the sort order for query results.
   *
   * @example Single field (ascending by default)
   * ```ts
   * .orderBy("name")
   * .orderBy(users.name)  // Column reference
   * .orderBy(asc(users.name))  // Explicit ascending
   * ```
   *
   * @example Single field with explicit direction
   * ```ts
   * .orderBy(["name", "desc"])
   * .orderBy([users.name, "desc"])  // Column reference
   * .orderBy(desc(users.name))  // Explicit descending
   * ```
   *
   * @example Multiple fields with directions
   * ```ts
   * .orderBy([["name", "asc"], ["createdAt", "desc"]])
   * .orderBy([[users.name, "asc"], [users.createdAt, "desc"]])  // Column references
   * .orderBy(users.name, desc(users.age))  // Variadic with helpers
   * ```
   */
  orderBy(
    ...orderByArgs:
      | [
          | TypeSafeOrderBy<InferSchemaOutputFromFMTable<Occ>>
          // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
          | Column<any, any, ExtractTableName<Occ>>
          | OrderByExpression<ExtractTableName<Occ>>,
        ]
      | [
          // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
          Column<any, any, ExtractTableName<Occ>>,
          // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
          ...Array<Column<any, any, ExtractTableName<Occ>> | OrderByExpression<ExtractTableName<Occ>>>,
        ]
  ): QueryBuilder<
    Occ,
    Selected,
    SingleMode,
    IsCount,
    Expands,
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    SystemCols
  > {
    const tableName = getTableName(this.occurrence);

    // Handle variadic arguments (multiple fields)
    if (orderByArgs.length > 1) {
      const orderByParts = orderByArgs.map((arg) => {
        if (isOrderByExpression(arg)) {
          // Validate table match
          if (arg.column.tableName !== tableName) {
            this.logger.warn(
              `Column ${arg.column.toString()} is from table "${arg.column.tableName}", but query is for table "${tableName}"`,
            );
          }
          const fieldName = arg.column.fieldName;
          const transformedField = this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName;
          return `${transformedField} ${arg.direction}`;
        }
        if (isColumn(arg)) {
          // Validate table match
          if (arg.tableName !== tableName) {
            this.logger.warn(
              `Column ${arg.toString()} is from table "${arg.tableName}", but query is for table "${tableName}"`,
            );
          }
          const fieldName = arg.fieldName;
          const transformedField = this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName;
          return transformedField; // Default to ascending
        }
        throw new Error("Variadic orderBy() only accepts Column or OrderByExpression arguments");
      });
      this.patchQueryOptions({ orderBy: orderByParts });
      return this;
    }

    // Handle single argument
    const orderBy = orderByArgs[0];

    // Handle OrderByExpression
    if (isOrderByExpression(orderBy)) {
      // Validate table match
      if (orderBy.column.tableName !== tableName) {
        this.logger.warn(
          `Column ${orderBy.column.toString()} is from table "${orderBy.column.tableName}", but query is for table "${tableName}"`,
        );
      }
      const fieldName = orderBy.column.fieldName;
      const transformedField = this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName;
      this.patchQueryOptions({ orderBy: `${transformedField} ${orderBy.direction}` });
      return this;
    }

    // Handle Column references
    if (isColumn(orderBy)) {
      // Validate table match
      if (orderBy.tableName !== tableName) {
        this.logger.warn(
          `Column ${orderBy.toString()} is from table "${orderBy.tableName}", but query is for table "${tableName}"`,
        );
      }
      // Single Column reference without direction (defaults to ascending)
      const fieldName = orderBy.fieldName;
      this.patchQueryOptions({
        orderBy: this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName,
      });
      return this;
    }
    // Transform field names to FMFIDs if using entity IDs
    if (this.occurrence && orderBy) {
      if (Array.isArray(orderBy)) {
        // Check if it's a single tuple [field, direction] or array of tuples
        if (
          orderBy.length === 2 &&
          (typeof orderBy[0] === "string" || isColumn(orderBy[0])) &&
          (orderBy[1] === "asc" || orderBy[1] === "desc")
        ) {
          // Single tuple: [field, direction] or [column, direction]
          const field = isColumn(orderBy[0]) ? orderBy[0].fieldName : orderBy[0];
          const direction = orderBy[1] as "asc" | "desc";
          this.patchQueryOptions({ orderBy: `${transformOrderByField(field, this.occurrence)} ${direction}` });
        } else {
          // Array of tuples: [[field, dir], [field, dir], ...]
          this.patchQueryOptions({
            orderBy: (orderBy as [unknown, "asc" | "desc"][]).map(([fieldOrCol, direction]) => {
              const field = isColumn(fieldOrCol) ? fieldOrCol.fieldName : String(fieldOrCol);
              const transformedField = this.occurrence ? transformOrderByField(field, this.occurrence) : field;
              return `${transformedField} ${direction}`;
            }),
          });
        }
      } else {
        // Single field name (string)
        this.patchQueryOptions({ orderBy: transformOrderByField(String(orderBy), this.occurrence) });
      }
    } else if (Array.isArray(orderBy)) {
      if (
        orderBy.length === 2 &&
        (typeof orderBy[0] === "string" || isColumn(orderBy[0])) &&
        (orderBy[1] === "asc" || orderBy[1] === "desc")
      ) {
        // Single tuple: [field, direction] or [column, direction]
        const field = isColumn(orderBy[0]) ? orderBy[0].fieldName : orderBy[0];
        const direction = orderBy[1] as "asc" | "desc";
        this.patchQueryOptions({ orderBy: `${field} ${direction}` });
      } else {
        // Array of tuples
        this.patchQueryOptions({
          orderBy: (orderBy as [unknown, "asc" | "desc"][]).map(([fieldOrCol, direction]) => {
            const field = isColumn(fieldOrCol) ? fieldOrCol.fieldName : String(fieldOrCol);
            return `${field} ${direction}`;
          }),
        });
      }
    } else {
      this.patchQueryOptions({ orderBy });
    }
    return this;
  }

  top(
    count: number,
  ): QueryBuilder<
    Occ,
    Selected,
    SingleMode,
    IsCount,
    Expands,
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    SystemCols
  > {
    this.patchQueryOptions({ top: count });
    return this;
  }

  skip(
    count: number,
  ): QueryBuilder<
    Occ,
    Selected,
    SingleMode,
    IsCount,
    Expands,
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    SystemCols
  > {
    this.patchQueryOptions({ skip: count });
    return this;
  }

  expand<
    // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
    TargetTable extends FMTable<any, any>,
    TSelected extends
      | keyof InferSchemaOutputFromFMTable<TargetTable>
      | Record<
          string,
          // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
          Column<any, any, ExtractTableName<TargetTable>>
        > = keyof InferSchemaOutputFromFMTable<TargetTable>,
    // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no nested expands by default
    TNestedExpands extends ExpandedRelations = {},
  >(
    targetTable: ValidExpandTarget<Occ, TargetTable>,
    callback?: (
      // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands in initial builder
      builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}, false>,
      // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
    ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands, any, any>,
  ): QueryBuilder<
    Occ,
    Selected,
    SingleMode,
    IsCount,
    Expands & {
      [K in ExtractTableName<TargetTable>]: {
        schema: InferSchemaOutputFromFMTable<TargetTable>;
        selected: TSelected;
        nested: TNestedExpands;
      };
    },
    IncludeCount,
    DatabaseIncludeSpecialColumns,
    SystemCols
  > {
    // Use ExpandBuilder.processExpand to handle the expand logic
    type TargetBuilder = QueryBuilder<
      TargetTable,
      keyof InferSchemaOutputFromFMTable<TargetTable>,
      false,
      false,
      // biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands in new builder
      {},
      false,
      DatabaseIncludeSpecialColumns
    >;
    const expandConfig = this.expandBuilder.processExpand<TargetTable, TargetBuilder>(
      targetTable,
      this.occurrence,
      callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,
      () =>
        // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
        new QueryBuilder<TargetTable, any, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({
          occurrence: targetTable,
          layer: this.layer,
        }),
    );

    this.readState = cloneQueryReadBuilderState(this.readState, {
      expandConfigs: [...this.readState.expandConfigs, expandConfig],
    });
    // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
    return this as any;
  }

  single(
    this: QueryBuilder<Occ, Selected, false, IsCount, Expands, false, DatabaseIncludeSpecialColumns, SystemCols>,
  ): QueryBuilder<Occ, Selected, "exact", IsCount, Expands, false, DatabaseIncludeSpecialColumns, SystemCols> {
    if (this.readState.includeCountMode) {
      throw new BuilderInvariantError("QueryBuilder.single", "count-enabled list queries cannot use single()");
    }
    return this.cloneWithChanges({ singleMode: "exact" as const });
  }

  maybeSingle(
    this: QueryBuilder<Occ, Selected, false, IsCount, Expands, false, DatabaseIncludeSpecialColumns, SystemCols>,
  ): QueryBuilder<Occ, Selected, "maybe", IsCount, Expands, false, DatabaseIncludeSpecialColumns, SystemCols> {
    if (this.readState.includeCountMode) {
      throw new BuilderInvariantError(
        "QueryBuilder.maybeSingle",
        "count-enabled list queries cannot use maybeSingle()",
      );
    }
    return this.cloneWithChanges({ singleMode: "maybe" as const });
  }

  count(
    this: QueryBuilder<Occ, Selected, false, IsCount, Expands, false, DatabaseIncludeSpecialColumns, SystemCols>,
  ): QueryBuilder<Occ, Selected, false, IsCount, Expands, true, DatabaseIncludeSpecialColumns, SystemCols> {
    if (this.readState.singleMode !== false) {
      throw new BuilderInvariantError(
        "QueryBuilder.count",
        "single() and maybeSingle() cannot be combined with count()",
      );
    }
    return this.cloneWithChanges({
      includeCountMode: true as const,
      queryOptions: { count: true },
    });
  }

  /**
   * Builds the OData query string from current query options and expand configs.
   */
  private buildQueryString(includeSpecialColumns?: boolean, useEntityIds?: boolean): string {
    // Use provided useEntityIds if provided, otherwise use database-level default
    const finalUseEntityIds = useEntityIds ?? this.config.useEntityIds;

    // Build query without expand and select (we'll add them manually if using entity IDs)
    const queryOptionsWithoutExpandAndSelect = { ...this.readState.queryOptions };
    if (this.readState.filterExpression) {
      queryOptionsWithoutExpandAndSelect.filter = this.readState.filterExpression.toODataFilter(finalUseEntityIds);
    }
    const originalSelect = queryOptionsWithoutExpandAndSelect.select;
    queryOptionsWithoutExpandAndSelect.expand = undefined;
    queryOptionsWithoutExpandAndSelect.select = undefined;

    let queryString = buildQuery(queryOptionsWithoutExpandAndSelect);

    // Use shared helper for select/expand portion
    let selectArray: string[] | undefined;
    if (originalSelect) {
      selectArray = Array.isArray(originalSelect) ? originalSelect.map(String) : [String(originalSelect)];
    }

    // Use merged includeSpecialColumns if provided, otherwise use database-level default
    const finalIncludeSpecialColumns = includeSpecialColumns ?? this.config.includeSpecialColumns;

    const selectExpandString = buildSelectExpandQueryString({
      selectedFields: selectArray,
      expandConfigs: this.readState.expandConfigs,
      table: this.occurrence,
      useEntityIds: finalUseEntityIds,
      logger: this.logger,
      includeSpecialColumns: finalIncludeSpecialColumns,
    });

    // Append select/expand to existing query string
    if (selectExpandString) {
      // Strip leading ? from helper result and append with appropriate separator
      const params = selectExpandString.startsWith("?") ? selectExpandString.slice(1) : selectExpandString;
      const separator = queryString.includes("?") ? "&" : "?";
      queryString = `${queryString}${separator}${params}`;
    }

    return queryString;
  }

  execute<EO extends ExecuteOptions>(
    options?: ExecuteMethodOptions<EO>,
  ): Promise<
    Result<
      ConditionallyWithODataAnnotations<
        ConditionallyWithSpecialColumns<
          QueryReturnType<
            InferSchemaOutputFromFMTable<Occ>,
            Selected,
            SingleMode,
            IsCount,
            Expands,
            IncludeCount,
            SystemCols
          >,
          // Use the merged value: if explicitly provided in options, use that; otherwise use database default
          NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>,
          // Check if select was applied: if Selected is Record (object select) or a subset of keys, select was applied
          // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
          Selected extends Record<string, Column<any, any, any>>
            ? true
            : Selected extends keyof InferSchemaOutputFromFMTable<Occ>
              ? false
              : true
        >,
        EO["includeODataAnnotations"] extends true ? true : false
      >
    >
  > {
    type ExecuteResponse = ConditionallyWithODataAnnotations<
      ConditionallyWithSpecialColumns<
        QueryReturnType<
          InferSchemaOutputFromFMTable<Occ>,
          Selected,
          SingleMode,
          IsCount,
          Expands,
          IncludeCount,
          SystemCols
        >,
        NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>,
        // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
        Selected extends Record<string, Column<any, any, any>>
          ? true
          : Selected extends keyof InferSchemaOutputFromFMTable<Occ>
            ? false
            : true
      >,
      EO["includeODataAnnotations"] extends true ? true : false
    >;

    const mergedOptions = this.mergeExecuteOptions(options);
    let queryString: string;
    try {
      queryString = this.buildQueryString(mergedOptions.includeSpecialColumns, mergedOptions.useEntityIds);
    } catch (error) {
      return Promise.resolve({
        data: undefined,
        error: normalizeQueryBuildError(error),
      }) as Promise<Result<ExecuteResponse>>;
    }

    // Handle $count endpoint
    if (this.readState.isCountMode) {
      let url: string;
      try {
        url = this.urlBuilder.build(queryString, {
          isCount: true,
          useEntityIds: mergedOptions.useEntityIds,
          navigation: this.readState.navigation,
        });
      } catch (error) {
        return Promise.resolve({
          data: undefined,
          error: normalizeQueryBuildError(error),
        }) as Promise<Result<ExecuteResponse>>;
      }

      const pipeline = requestFromService(url, mergedOptions).pipe(
        Effect.map((data) => {
          const count = typeof data === "string" ? Number(data) : data;
          return count as number;
        }),
      );

      return runLayerResult(this.layer, pipeline, "fmodata.query.count", {
        "fmodata.table": getTableName(this.occurrence),
      }) as Promise<Result<ExecuteResponse>>;
    }

    let url: string;
    try {
      url = this.urlBuilder.build(queryString, {
        isCount: this.readState.isCountMode,
        useEntityIds: mergedOptions.useEntityIds,
        navigation: this.readState.navigation,
      });
    } catch (error) {
      return Promise.resolve({
        data: undefined,
        error: normalizeQueryBuildError(error),
      }) as Promise<Result<ExecuteResponse>>;
    }

    const pipeline = requestFromService(url, mergedOptions).pipe(
      Effect.flatMap((data) =>
        Effect.tryPromise({
          try: () =>
            processQueryResponse(data, {
              occurrence: this.occurrence,
              singleMode: this.readState.singleMode as SingleMode,
              queryOptions: this.readState.queryOptions as {
                select?: (keyof InferSchemaOutputFromFMTable<Occ>)[] | string[];
              },
              expandConfigs: this.readState.expandConfigs,
              skipValidation: options?.skipValidation,
              useEntityIds: mergedOptions.useEntityIds,
              includeSpecialColumns: mergedOptions.includeSpecialColumns,
              includeCount: this.readState.includeCountMode,
              fieldMapping: this.readState.fieldMapping,
              logger: this.logger,
            }),
          catch: (e) => (e instanceof Error ? e : new Error(String(e))),
        }),
      ),
      // processQueryResponse returns a Result, so we need to unwrap it
      Effect.flatMap((result) => (result.error ? Effect.fail(result.error) : Effect.succeed(result.data))),
    );

    return runLayerResult(
      this.layer,
      pipeline,
      this.readState.singleMode ? "fmodata.query.single" : "fmodata.query.list",
      { "fmodata.table": getTableName(this.occurrence) },
    ) as Promise<Result<ExecuteResponse>>;
  }

  getQueryString(options?: { useEntityIds?: boolean }): string {
    const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds;
    const queryString = this.buildQueryString(undefined, useEntityIds);
    return this.urlBuilder.buildPath(queryString, {
      useEntityIds,
      navigation: this.readState.navigation,
    });
  }

  // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
  getRequestConfig(): { method: string; url: string; body?: any } {
    const queryString = this.buildQueryString();
    const url = this.urlBuilder.build(queryString, {
      isCount: this.readState.isCountMode,
      useEntityIds: this.config.useEntityIds,
      navigation: this.readState.navigation,
    });

    return {
      method: "GET",
      url,
    };
  }

  toRequest(baseUrl: string, options?: ExecuteOptions): Request {
    const config = this.getRequestConfig();
    return createODataRequest(baseUrl, config, {
      ...options,
      normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName,
    });
  }

  async processResponse(
    response: Response,
    options?: ExecuteOptions,
  ): Promise<
    Result<
      QueryReturnType<
        InferSchemaOutputFromFMTable<Occ>,
        Selected,
        SingleMode,
        IsCount,
        Expands,
        IncludeCount,
        SystemCols
      >
    >
  > {
    // Check for error responses (important for batch operations)
    if (!response.ok) {
      const error = await parseErrorResponse(
        response,
        response.url || `/${this.config.databaseName}/${getTableName(this.occurrence)}`,
      );
      return { data: undefined, error };
    }

    // Handle 204 No Content (shouldn't happen for queries, but handle it gracefully)
    if (response.status === 204) {
      // Return empty list for list queries, null for single queries
      if (this.readState.singleMode !== false) {
        if (this.readState.singleMode === "maybe") {
          // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
          return { data: null as any, error: undefined };
        }
        return {
          data: undefined,
          error: new RecordCountMismatchError("one", 0),
        };
      }
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
      return { data: [] as any, error: undefined };
    }

    // Parse the response body (using safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values)
    let rawData: unknown;
    try {
      rawData = await safeJsonParse(response);
    } catch (err) {
      // Check if it's an empty body error (common with 204 responses)
      if (err instanceof SyntaxError && response.status === 204) {
        // Handled above, but just in case
        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
        return { data: [] as any, error: undefined };
      }
      return {
        data: undefined,
        error: {
          name: "ResponseParseError",
          message: `Failed to parse response JSON: ${err instanceof Error ? err.message : "Unknown error"}`,
          timestamp: new Date(),
          // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
        } as any,
      };
    }

    if (!rawData) {
      return {
        data: undefined,
        error: {
          name: "ResponseError",
          message: "Response body was empty or null",
          timestamp: new Date(),
          // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
        } as any,
      };
    }

    const mergedOptions = this.mergeExecuteOptions(options);
    // Check if select was applied (runtime check)
    const _hasSelect = this.readState.queryOptions.select !== undefined;

    return processQueryResponse(rawData, {
      occurrence: this.occurrence,
      singleMode: this.readState.singleMode as SingleMode,
      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
      queryOptions: this.readState.queryOptions as any,
      expandConfigs: this.readState.expandConfigs,
      skipValidation: options?.skipValidation,
      useEntityIds: mergedOptions.useEntityIds,
      includeSpecialColumns: mergedOptions.includeSpecialColumns,
      includeCount: this.readState.includeCountMode,
      fieldMapping: this.readState.fieldMapping,
      logger: this.logger,
    });
  }
}
