import type { FFetchOptions } from "@fetchkit/ffetch";
import type { StandardSchemaV1 } from "@standard-schema/spec";

export interface BasicAuth {
  username: string;
  password: string;
}

export interface ApiKeyAuth {
  apiKey: string;
}

export interface ClarisIdAuth {
  clarisId: {
    username: string;
    password: string;
  };
}

export type Auth = BasicAuth | ApiKeyAuth | ClarisIdAuth;

export interface CountedListResult<T> {
  records: T[];
  count: number;
}

export interface ExecutableBuilder<T> {
  execute(options?: ExecuteOptions): Promise<Result<T>>;
  // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
  getRequestConfig(): { method: string; url: string; body?: any };

  /**
   * Convert this builder to a native Request object for batch processing.
   * @param baseUrl - The base URL for the OData service
   * @param options - Optional execution options (e.g., includeODataAnnotations)
   * @returns A native Request object
   */
  toRequest(baseUrl: string, options?: ExecuteOptions): Request;

  /**
   * Process a raw Response object into a typed Result.
   * This allows builders to apply their own validation and transformation logic.
   * @param response - The native Response object from the batch operation
   * @param options - Optional execution options (e.g., skipValidation, includeODataAnnotations)
   * @returns A typed Result with the builder's expected return type
   */
  processResponse(response: Response, options?: ExecuteOptions): Promise<Result<T>>;
}

export interface ExecutionContext {
  /**
   * @internal
   * Returns the Effect Layer for this context, enabling service-based Effect pipelines.
   * All HTTP requests are made through the Layer's HttpClient service.
   */
  _getLayer?(): import("./services").FMODataLayer;
}

export type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = {
  // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
  [K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output> ? Output : never;
};

export type WithSpecialColumns<T> =
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
  T extends Record<string, any>
    ? T & {
        ROWID: number;
        ROWMODID: number;
      }
    : never;

// Helper type to exclude special columns from a union of keys
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type
export type ExcludeSystemFields<T extends keyof any> = Exclude<T, "ROWID" | "ROWMODID">;

// OData record metadata fields (present on each record)
export interface ODataRecordMetadata {
  "@id": string;
  "@editLink": string;
}

// OData response wrapper (top-level, internal use only)
export interface ODataListResponse<T> {
  "@context": string;
  value: (T & ODataRecordMetadata)[];
}

export type ODataSingleResponse<T> = T &
  ODataRecordMetadata & {
    "@context": string;
  };

// OData response for single field values
export interface ODataFieldResponse<T> {
  "@context": string;
  value: T;
}

// Result pattern for execute responses
export type Result<T, E = import("./errors").FMODataErrorType> =
  | { data: T; error: undefined }
  | { data: undefined; error: E };

// Batch operation result types
export interface BatchItemResult<T> {
  data: T | undefined;
  error: import("./errors").FMODataErrorType | undefined;
  status: number; // HTTP status code (0 for truncated)
}

// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any tuple type
export interface BatchResult<T extends readonly any[]> {
  results: { [K in keyof T]: BatchItemResult<T[K]> };
  successCount: number;
  errorCount: number;
  truncated: boolean;
  firstErrorIndex: number | null;
}

// Make specific keys required, rest optional
export type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> & Required<Pick<T, Keys>>;

// Extract keys from schema where validator doesn't allow null/undefined (auto-required fields)
export type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> = {
  [K in keyof Schema]: Extract<StandardSchemaV1.InferOutput<Schema[K]>, null | undefined> extends never ? K : never;
}[keyof Schema];

// Helper type to compute excluded fields (readOnly fields + idField)
export type ExcludedFields<
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type
  IdField extends keyof any | undefined,
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type
  ReadOnly extends readonly any[],
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type
> = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number];

// Helper type for InsertData computation
type _ComputeInsertData<
  Schema extends Record<string, StandardSchemaV1>,
  IdField extends keyof Schema | undefined,
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type
  Required extends readonly any[],
  // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type
  ReadOnly extends readonly any[],
> = [Required[number]] extends [keyof InferSchemaType<Schema>]
  ? Required extends readonly (keyof InferSchemaType<Schema>)[]
    ? MakeFieldsRequired<
        Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
        Exclude<AutoRequiredKeys<Schema> | Required[number], ExcludedFields<IdField, ReadOnly>>
      >
    : MakeFieldsRequired<
        Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
        Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>
      >
  : MakeFieldsRequired<
      Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
      Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>
    >;

/**
 * Configuration for automatic retry of transient errors.
 * Uses exponential backoff with optional jitter.
 */
export interface RetryPolicy {
  /**
   * Maximum number of retry attempts (default: 3)
   */
  maxRetries?: number;
  /**
   * Base delay in milliseconds for exponential backoff (default: 500)
   */
  baseDelay?: number;
  /**
   * Whether to add random jitter to delay to prevent thundering herd (default: true)
   */
  jitter?: boolean;
}

export interface ExecuteOptions {
  includeODataAnnotations?: boolean;
  skipValidation?: boolean;
  /**
   * Overrides the default behavior of the database to normalize the database name in THIS REQUEST ONLY.
   * Defaults to true at the database level.
   */
  normalizeDatabaseName?: boolean;
  /**
   * Overrides the default behavior of the database to use entity IDs (rather than field names) in THIS REQUEST ONLY
   */
  useEntityIds?: boolean;
  /**
   * Overrides the default behavior of the database to include special columns (ROWID and ROWMODID) in THIS REQUEST ONLY.
   * Note: Special columns are only included when there is no $select query.
   */
  includeSpecialColumns?: boolean;
  /**
   * Optional retry policy for transient errors (SchemaLockedError, NetworkError, TimeoutError, HTTP 5xx).
   * When set, failed requests matching transient error conditions will be retried
   * with exponential backoff.
   */
  retryPolicy?: RetryPolicy;
}

/**
 * Type for the fetchHandler callback function.
 * This is a convenience type export that matches the fetchHandler signature in FFetchOptions.
 *
 * @example
 * ```typescript
 * import type { FetchHandler } from '@proofkit/fmodata';
 *
 * const myFetchHandler: FetchHandler = (input, init) => {
 *   console.log('Custom fetch:', input);
 *   return fetch(input, init);
 * };
 *
 * await query.execute({
 *   fetchHandler: myFetchHandler
 * });
 * ```
 */
export type FetchHandler = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;

/**
 * Combined type for execute() method options.
 *
 * Uses FFetchOptions from @fetchkit/ffetch to ensure proper type inference.
 * FFetchOptions is re-exported in the package to ensure type availability in consuming packages.
 */
export type ExecuteMethodOptions<EO extends ExecuteOptions = ExecuteOptions> = RequestInit &
  FFetchOptions &
  ExecuteOptions &
  EO;

/**
 * Get the Accept header value based on includeODataAnnotations option
 * @param includeODataAnnotations - Whether to include OData annotations
 * @returns Accept header value
 */
export function getAcceptHeader(includeODataAnnotations?: boolean): string {
  return includeODataAnnotations === true ? "application/json" : "application/json;odata.metadata=none";
}

type WithODataAnnotations<T> =
  T extends CountedListResult<infer U>
    ? CountedListResult<WithODataAnnotations<U>>
    : T extends readonly (infer U)[]
      ? WithODataAnnotations<U>[]
      : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
        T extends Record<string, any>
        ? T & ODataRecordMetadata
        : T;

export type ConditionallyWithODataAnnotations<
  T,
  IncludeODataAnnotations extends boolean,
> = IncludeODataAnnotations extends true ? WithODataAnnotations<T> : T;

/**
 * Normalizes includeSpecialColumns with a database-level default.
 * Uses distributive conditional types to handle unions correctly.
 * @template IncludeSpecialColumns - The includeSpecialColumns value from execute options
 * @template DatabaseDefault - The database-level includeSpecialColumns setting (defaults to false)
 */
export type NormalizeIncludeSpecialColumns<
  IncludeSpecialColumns extends boolean | undefined,
  DatabaseDefault extends boolean = false,
> = [IncludeSpecialColumns] extends [true] ? true : [IncludeSpecialColumns] extends [false] ? false : DatabaseDefault; // When undefined, use database-level default

type WithSpecialColumnsDeep<T> =
  T extends CountedListResult<infer U>
    ? CountedListResult<WithSpecialColumnsDeep<U>>
    : T extends readonly (infer U)[]
      ? WithSpecialColumnsDeep<U>[]
      : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
        T extends Record<string, any>
        ? T & {
            ROWID: number;
            ROWMODID: number;
          }
        : T;

/**
 * Conditionally adds ROWID and ROWMODID special columns to a type.
 * Special columns are only included when:
 * - includeSpecialColumns is true AND
 * - hasSelect is false (no $select query was applied) AND
 * - T is an object type (not a primitive like string or number)
 *
 * Handles both single objects and arrays of objects.
 */
export type ConditionallyWithSpecialColumns<
  T,
  IncludeSpecialColumns extends boolean,
  HasSelect extends boolean,
> = IncludeSpecialColumns extends true ? (HasSelect extends false ? WithSpecialColumnsDeep<T> : T) : T;

// Helper type to extract schema from a FMTable
export type ExtractSchemaFromOccurrence<Occ> = Occ extends {
  baseTable: { schema: infer S };
}
  ? S extends Record<string, StandardSchemaV1>
    ? S
    : Record<string, StandardSchemaV1>
  : Record<string, StandardSchemaV1>;

export interface GenericFieldMetadata {
  $Nullable?: boolean;
  "@Index"?: boolean;
  "@Calculation"?: boolean;
  "@Summary"?: boolean;
  "@Global"?: boolean;
  "@Org.OData.Core.V1.Permissions"?: "Org.OData.Core.V1.Permission@Read";
}

export type StringFieldMetadata = GenericFieldMetadata & {
  $Type: "Edm.String";
  $DefaultValue?: "USER" | "USERNAME" | "CURRENT_USER";
  $MaxLength?: number;
};

export type DecimalFieldMetadata = GenericFieldMetadata & {
  $Type: "Edm.Decimal";
  "@AutoGenerated"?: boolean;
};

export type DateFieldMetadata = GenericFieldMetadata & {
  $Type: "Edm.Date";
  $DefaultValue?: "CURDATE" | "CURRENT_DATE";
};

export type TimeOfDayFieldMetadata = GenericFieldMetadata & {
  $Type: "Edm.TimeOfDay";
  $DefaultValue?: "CURTIME" | "CURRENT_TIME";
};

export type DateTimeOffsetFieldMetadata = GenericFieldMetadata & {
  $Type: "Edm.Date";
  $DefaultValue?: "CURTIMESTAMP" | "CURRENT_TIMESTAMP";
  "@VersionId"?: boolean;
};

export interface StreamFieldMetadata {
  $Type: "Edm.Stream";
  $Nullable?: boolean;
  "@EnclosedPath": string;
  "@ExternalOpenPath": string;
  "@ExternalSecurePath"?: string;
}

export type FieldMetadata =
  | StringFieldMetadata
  | DecimalFieldMetadata
  | DateFieldMetadata
  | TimeOfDayFieldMetadata
  | DateTimeOffsetFieldMetadata
  | StreamFieldMetadata;

export type EntityType = {
  $Kind: "EntityType";
  $Key: string[];
} & Record<string, FieldMetadata>;

export interface EntitySet {
  $Kind: "EntitySet";
  $Type: string;
}

export type Metadata = Record<string, EntityType | EntitySet>;
