/**
 * Types of `@cubejs-client/react`.
 *
 * Everything exported from `src/index.ts` is part of the published API: the
 * declarations shipped in `dist` are emitted from these sources, so a change
 * here is a change to what consumers see.
 */
import type * as React from 'react';
import type {
  BinaryOperator,
  CacheMode,
  CubeApi,
  DateRange,
  DeeplyReadonly,
  DryRunResponse,
  Filter,
  FilterOperator,
  LoadMethodOptions,
  MemberType,
  Meta,
  PivotConfig,
  ProgressResponse,
  Query,
  QueryOrder,
  RequestError,
  ResultSet,
  SqlQuery,
  TCubeDimension,
  TCubeMeasure,
  TCubeSegment,
  TimeDimension,
  TimeDimensionGranularity,
  TOrderMember,
  TSourceAxis,
  UnaryOperator,
} from '@cubejs-client/core';

/**
 * Object the API methods store their MUTEX in.
 */
export type MutexObj = NonNullable<LoadMethodOptions['mutexObj']>;

/**
 * A query as accepted by `useCubeQuery`.
 */
export type ReadonlyQueryInput = DeeplyReadonly<Query | Query[]>;

export type CubeProviderOptions = {
  castNumerics?: boolean;
};

export type CubeProviderProps = {
  cubeApi: CubeApi | null;
  options?: CubeProviderOptions;
  children: React.ReactNode;
};

export type CubeContextProps = {
  cubeApi: CubeApi;
  options?: CubeProviderOptions;
};

export type TLoadingState = {
  isLoading: boolean;
};

export type ChartType = 'line' | 'bar' | 'table' | 'area' | 'number' | 'pie';

export type VizState = {
  query?: Query;
  pivotConfig?: PivotConfig;
  chartType?: ChartType;
};

export type QueryRendererRenderProps = {
  resultSet: ResultSet | null;
  error: Error | null;
  loadingState: TLoadingState;
  sqlQuery: SqlQuery | null;
};

export type QueryRendererProps = {
  /**
   * Analytic query. [Learn more about it's format](/product/apis-integrations/rest-api/query-format)
   */
  query: Query | Query[];
  queries?: { [key: string]: Query };
  /**
   * Indicates whether the generated by `Cube.js` SQL Code should be requested. See [rest-api#sql](/reference/rest-api#v1sql). When set to `only` then only the request to [/v1/sql](/reference/rest-api#v1sql) will be performed. When set to `true` the sql request will be performed along with the query request. Will not be performed if set to `false`
   */
  loadSql?: 'only' | boolean;
  /**
   * When `true` the **resultSet** will be reset to `null` first on every state change
   */
  resetResultSetOnChange?: boolean;
  updateOnlyOnStateChange?: boolean;
  /**
   * `CubeApi` instance to use
   */
  cubeApi?: CubeApi;
  /**
   * Server-side cache policy for query execution. Does not control client-side caching.
   */
  cache?: CacheMode;
  /**
   * Output of this function will be rendered by the `QueryRenderer`
   */
  render: (renderProps: QueryRendererRenderProps) => void;
  /**
   * @hidden
   */
  children?: never;
};

/**
 * `resultSet` holds a map of result sets keyed by query name when the `queries`
 * prop is used, and a single result set otherwise.
 *
 * @hidden
 */
export type QueryRendererState = {
  isLoading?: boolean;
  error?: RequestError | null;
  sqlQuery?: SqlQuery | null;
  resultSet?: ResultSet | { [key: string]: ResultSet } | null;
  /**
   * Never assigned — `render()` has always read it from state rather than from
   * props, so the empty-object fallback for `resultSet` does not kick in.
   */
  queries?: { [key: string]: Query };
};

export type QueryRendererWithTotalsProps = Omit<QueryRendererProps, 'queries' | 'query'> & {
  query: Query;
};

/**
 * What `QueryRenderer` passes to its render prop. `resultSet`,
 * `loadingState.isLoading` and `sqlQuery` are only set once a request resolves.
 *
 * @hidden
 */
export type QueryRendererLoadState = {
  error: Error | null;
  resultSet: ResultSet | { [key: string]: ResultSet } | null | undefined;
  loadingState: { isLoading?: boolean };
  sqlQuery: SqlQuery | null | undefined;
};

/**
 * @hidden
 */
export type SchemaChangeProps = {
  schemaVersion: number;
  refresh: () => Promise<void>;
};

export type QueryBuilderState = VizState & {
  query?: Query;
};

export type QueryBuilderProps = {
  /**
   * `CubeApi` instance to use
   */
  cubeApi?: CubeApi;
  /**
   * State for the QueryBuilder to start with. Pass in the value previously saved from onVizStateChanged to restore a session.
   */
  initialVizState?: VizState;
  /**
   * Called by the `QueryBuilder` when the viz state has changed. Use it to save state outside of the `QueryBuilder` component.
   */
  onVizStateChanged?: (vizState: VizState) => void;
  /**
   * @default defaultChartType line (used when initialVizState is not set or does not contain chartType)
   */
  defaultChartType?: ChartType;
  /**
   * Default query (used when initialVizState is not set or does not contain query)
   */
  defaultQuery?: Query;
  /**
   * Defaults to `false`. This means that the default heuristics will be applied. For example: when the query is empty and you select a measure that has a default time dimension it will be pushed to the query.
   * @default disableHeuristics false
   */
  disableHeuristics?: boolean;
  wrapWithQueryRenderer?: boolean;
  render: (renderProps: QueryBuilderRenderProps) => React.ReactNode;
  /**
   * A function that accepts the `newState` just before it's applied. You can use it to override the **defaultHeuristics** or to tweak the query or the vizState in any way.
   */
  stateChangeHeuristics?: (state: QueryBuilderState, newState: QueryBuilderState) => QueryBuilderState;
  /**
   * @ignore @deprecated Controlled query
   */
  query?: Query;
  /**
   * @ignore @deprecated Controlled query setter
   */
  setQuery?: (query: Query) => void;
  /**
   * @ignore @deprecated Controlled vizState
   */
  vizState?: VizState;
  /**
   * @ignore @deprecated Controlled vizState setter
   */
  setVizState?: (vizState: VizState) => void;
  /**
   * @hidden
   */
  schemaVersion?: number;
  /**
   * @hidden
   */
  queryVersion?: number | string;
  /**
   * @hidden
   */
  onSchemaChange?: (props: SchemaChangeProps) => void;
};

export type QueryBuilderRenderProps = {
  // Todo: should fix DRY, duplicate props from QueryRendererRenderProps, see https://github.com/cube-js/cube.js/issues/1192
  resultSet?: ResultSet | null;
  error?: Error | null;
  loadingState?: TLoadingState;

  meta: Meta | undefined;
  metaError?: Error | null;
  richMetaError?: Error | null;
  metaErrorStack?: string | null;
  isFetchingMeta: boolean;
  /**
   * Indicates whether the query is ready to be displayed or not
   */
  isQueryPresent: boolean;
  measures: (TCubeMeasure & { index: number })[];
  dimensions: (TCubeDimension & { index: number })[];
  segments: (TCubeSegment & { index: number })[];
  timeDimensions: (TimeDimensionWithExtraFields & { index: number })[];

  availableMembers: AvailableMembers;

  availableFilterMembers: Array<AvailableCube<TCubeMeasure> | AvailableCube<TCubeDimension>>;
  /**
   * An array of available measures to select. They are loaded via the API from Cube.js Backend.
   */
  availableMeasures: TCubeMeasure[];
  /**
   * An array of available dimensions to select. They are loaded via the API from Cube.js Backend.
   */
  availableDimensions: TCubeDimension[];
  /**
   * An array of available time dimensions to select. They are loaded via the API from Cube.js Backend.
   */
  availableTimeDimensions: TCubeDimension[];
  /**
   * An array of available segments to select. They are loaded via the API from Cube.js Backend.
   */
  availableSegments: TCubeSegment[];

  updateMeasures: MeasureUpdater;
  updateDimensions: DimensionUpdater;
  updateSegments: SegmentUpdater;
  updateTimeDimensions: TimeDimensionUpdater;
  updateFilters: FilterUpdater;
  /**
   * Used for partial of full query update
   */
  updateQuery: (query: Query) => void;
  filters: (FilterWithExtraFields & { index: number })[];
  /**
   * All possible order members for the query
   */
  orderMembers: TOrderMember[];
  /**
   * Used for query order update
   */
  updateOrder: OrderUpdater;
  /**
   * See [Pivot Config](@cubejs-client-core#types-pivot-config)
   */
  pivotConfig?: PivotConfig;
  /**
   * Helper method for `pivotConfig` updates
   */
  updatePivotConfig: PivotConfigUpdater;

  /**
   * Selected chart type
   */
  chartType?: ChartType;

  /**
   * Used for chart type update
   */
  updateChartType: (chartType: ChartType) => void;

  /**
   * Used to set the initial query for this component. Note that adding this prop turns this into an
   * uncontrolled component and will only be able to execute `dryRun` queries. To use this component
   * as a controlled component, use `setQuery` instead.
   */
  query: Query;
  validatedQuery: Query;
  refresh: () => void;
  missingMembers: string[];
  dryRunResponse?: DryRunResponse;
};

/**
 * The state `QueryBuilder` keeps. It is a superset of `QueryBuilderState`:
 * everything beyond `query`, `pivotConfig` and `chartType` is internal
 * bookkeeping.
 *
 * @hidden
 */
export type QueryBuilderInternalState = QueryBuilderState & {
  query: Query;
  validatedQuery?: Query;
  missingMembers: string[];
  isFetchingMeta: boolean;
  dryRunResponse?: DryRunResponse | null;
  meta?: Meta;
  metaError?: Error | null;
  richMetaError?: Error | null;
  metaErrorStack?: string | null;
  queryError?: Error | null;
  richQueryError?: Error | null;
  sessionGranularity?: TimeDimensionGranularity | null;
  shouldApplyHeuristicOrder?: boolean;
};

/**
 * A state update on its way through `updateVizState`. Heuristics may return a
 * partial state, and the missing pieces are filled in before it is applied.
 *
 * @hidden
 */
export type QueryBuilderStateUpdate = Partial<QueryBuilderInternalState>;

/**
 * @hidden
 */
export type QueryMemberType = MemberType | 'timeDimensions';

/**
 * State `QueryBuilder.resolveMember` resolves members against.
 *
 * @hidden
 */
export type ResolveMemberArgs = {
  meta?: Meta;
  query: Query | Query[];
};

/**
 * Resolved query members carry the position they hold in the query.
 *
 * @hidden
 */
export type IndexedMeasure = TCubeMeasure & { index: number };

/**
 * @hidden
 */
export type IndexedDimension = TCubeDimension & { index: number };

/**
 * @hidden
 */
export type IndexedSegment = TCubeSegment & { index: number };

export type AvailableMembers = {
  measures: AvailableCube<TCubeMeasure>[];
  dimensions: AvailableCube<TCubeDimension>[];
  segments: AvailableCube<TCubeSegment>[];
  timeDimensions: AvailableCube<TCubeDimension>[];
};

// todo: CubeMember
export type AvailableCube<T = any> = {
  type: 'cube' | 'view';
  public: boolean;
  cubeName: string;
  cubeTitle: string;
  members: T[];
};

export type UseCubeQueryOptions = {
  /**
   * A `CubeApi` instance to use. Taken from the context if the param is not passed
   */
  cubeApi?: CubeApi;
  /**
   * Query execution will be skipped when `skip` is set to `true`. You can use this flag to avoid sending incomplete queries.
   */
  skip?: boolean;
  /**
   * Use continuous fetch behavior. See [Real-Time Data Fetch](/product/apis-integrations/rest-api/real-time-data-fetch)
   */
  subscribe?: boolean;
  /**
   * When `true` the resultSet will be reset to `null` first
   */
  resetResultSetOnChange?: boolean;
  /**
   * If enabled, all members of the 'number' type will be automatically converted to numerical values on the client side
   */
  castNumerics?: boolean;
  /**
   * Server-side cache policy for query execution. Does not control client-side caching.
   */
  cache?: CacheMode;
};

/**
 * `ResultSet` constrains its data to `Record<string, any>`, while `Data` is
 * unconstrained here, as it is in `useCubeQuery`.
 */
type ResultSetData<Data> = Data extends Record<string, any> ? Data : any;

export type UseCubeQueryResult<QueryInput, Data> = {
  error: Error | null;
  isLoading: boolean;
  resultSet: ResultSet<ResultSetData<Data>> | null;
  /**
   * Set from the `Continue wait` messages of a long-running query.
   *
   * Note: this is `null` until the first such message arrives — the type is
   * kept non-nullable for backwards compatibility, so read it with `progress?.`
   */
  progress: ProgressResponse;
  /**
   * The query the current `resultSet` was loaded for.
   *
   * Note: this is `null` until the first query runs — the type is kept
   * non-nullable for backwards compatibility.
   */
  previousQuery: QueryInput;
  refetch: () => Promise<void>;
};

/**
 * What `useCubeQuery` builds, with `progress` and `previousQuery` as they
 * actually are before the first response.
 *
 * @hidden
 */
export type UseCubeQueryInternalResult = {
  error: Error | null;
  isLoading: boolean;
  resultSet: ResultSet | null;
  progress: ProgressResponse | null;
  previousQuery: ReadonlyQueryInput | null;
  refetch: () => Promise<void>;
};

/**
 * @hidden
 */
export type CubeFetchOptions = {
  skip?: boolean;
  cubeApi?: CubeApi;
  query?: Query;
};

/**
 * @hidden
 */
export type CubeFetchResult<T> = {
  isLoading: boolean;
  error: Error | null;
  /**
   * The response of the last request.
   *
   * Note: this is `null` until the first request resolves — the type is kept
   * non-nullable for backwards compatibility, so check `isLoading` first.
   */
  response: T;
};

/**
 * The result of `useCubeMeta`, `useCubeSql` and `useDryRun`: a
 * `CubeFetchResult` plus the `refetch` they expose.
 */
export type UseCubeFetchResult<T> = CubeFetchResult<T> & {
  refetch: (options?: UseCubeFetchLoadOptions) => Promise<void>;
};

/**
 * What `useCubeFetch` returns before the first request resolves.
 *
 * @hidden
 */
export type UseCubeFetchInternalResult<T> = CubeFetchState<T> & {
  error: Error | null;
  refetch: (options?: UseCubeFetchLoadOptions) => Promise<void>;
};

/**
 * @hidden
 */
export type UseDryRunResult = CubeFetchResult<DryRunResponse>;

/**
 * @hidden
 */
export type UseCubeSqlResponse = {
  sql: string;
};

/**
 * What `useCubeSql` resolves with. Its declared `response` is a
 * `DryRunResponse` for backwards compatibility; cast to this to get at the
 * `SqlQuery` it actually resolves with.
 */
export type UseCubeSqlResult = UseCubeFetchResult<SqlQuery>;

/**
 * @hidden
 */
export type LoadLazyDryRunOptions = {
  query?: Query | Query[];
};

/**
 * `CubeApi` methods `useCubeFetch` can dispatch to.
 *
 * @hidden
 */
export type CubeFetchMethod = 'meta' | 'sql' | 'dryRun';

/**
 * Options of `useCubeSql`, `useDryRun` and `useLazyDryRun`. Unlike
 * `CubeFetchOptions` they accept an array of queries and a `baseRequestId`.
 */
export type UseCubeFetchOptions = Omit<CubeFetchOptions, 'query'> & {
  query?: Query | Query[];
  baseRequestId?: string;
  onlyViews?: boolean;
};

/**
 * The query a `refetch` — or `useLazyDryRun`'s loader — runs instead of the one
 * the hook was called with.
 */
export type UseCubeFetchLoadOptions = {
  query?: Query | Query[];
  onlyViews?: boolean;
};

/**
 * @hidden
 */
export type CubeMetaFetchOptions = Omit<CubeFetchOptions, 'query'> & {
  /**
   * Request views only — the response's `cubes` array then contains only entries
   * whose `type` is `view`. Over HTTP, servers predating the flag ignore it and
   * return the full model, so callers should not assume the response is filtered;
   * over `WebSocketTransport` such servers reject the message with a 400.
   */
  onlyViews?: boolean;
  /**
   * @hidden
   */
  baseRequestId?: string;
};

/**
 * What `useCubeFetch` keeps in state.
 *
 * @hidden
 */
export type CubeFetchState<T> = {
  isLoading: boolean;
  response: T | null;
};

/**
 * Arguments the dispatched `CubeApi` method is called with: `meta` takes only
 * options, `sql` and `dryRun` take a query as well.
 *
 * @hidden
 */
export type CubeFetchArgs = [LoadMethodOptions] | [Query | Query[], LoadMethodOptions];

/**
 * The `CubeApi` method `useCubeFetch` dispatches to. The three methods share
 * neither their signatures nor their response type, so the response is narrowed
 * by the caller.
 *
 * @hidden
 */
export type CubeFetchDispatch = (this: CubeApi, ...args: CubeFetchArgs) => Promise<unknown>;

/**
 * `ProgressResult` keeps `progressResponse` private, but `useCubeQuery` has
 * always read it to expose the raw response.
 *
 * @hidden
 */
export type ProgressResultWithResponse = {
  progressResponse: ProgressResponse;
};

/**
 * @hidden
 */
export type ProgressCallback = NonNullable<LoadMethodOptions['progressCallback']>;

/**
 * You can use the following methods for member manipulaltion
 * ```js
 * <QueryBuilder
 *   // ...
 *   cubeApi={cubeApi}
 *   render={({
 *     // ...
 *     availableMeasures,
 *     updateMeasures,
 *   }) => {
 *     return (
 *       // ...
 *       <Select
 *         mode="multiple"
 *         placeholder="Please select"
 *         onSelect={(measure) => updateMeasures.add(measure)}
 *         onDeselect={(measure) => updateMeasures.remove(measure)}
 *       >
 *         {availableMeasures.map((measure) => (
 *           <Select.Option key={measure.name} value={measure}>
 *             {measure.title}
 *           </Select.Option>
 *         ))}
 *       </Select>
 *     );
 *   }}
 * />
 * ```
 *
 * NOTE: if you need to add or remove more than one member at a time you should use `updateQuery` prop of {@see QueryBuilderRenderProps}
 * ```js
 * <QueryBuilder
 *   // ...
 *   cubeApi={cubeApi}
 *   render={({
 *     // ...
 *     measures,
 *     updateMeasures,
 *     updateQuery,
 *   }) => {
 *     // ...
 *     return (
 *       <>
 *         // WRONG: This code will not work properly
 *         <button
 *           onClick={() =>
 *             measures.forEach((measure) => updateMeasures.remove(measure))
 *           }
 *         >
 *           Remove all
 *         </button>
 *
 *         // CORRECT: Using `updateQuery` for removing all measures
 *         <button
 *           onClick={() =>
 *             updateQuery({
 *               measures: [],
 *             })
 *           }
 *         >
 *           Remove all
 *         </button>
 *       </>
 *     );
 *   }}
 * />
 * ```
 */
export type MemberUpdater<T> = {
  add: (member: T) => void;
  remove: (member: { index: number }) => void;
  update: (member: { index: number }, updateWith: T) => void;
};

/**
 * Builds the updater for one kind of query member. `toQuery` turns a member
 * into its query representation, and defaults to reading its name.
 *
 * @hidden
 */
export type MemberUpdaterFactory = <Member>(
  memberType: QueryMemberType | 'filters',
  toQuery?: (member: Member) => unknown
) => MemberUpdater<Member>;

export type FilterExtraFields = {
  dimension: TCubeDimension | TCubeMeasure;
  operators: { name: string; title: string }[];
};

export type FilterWithExtraFields = Omit<Filter, 'dimension'> & FilterExtraFields;

export type GranularityOptions = {
  granularities: { name: string; title: string }[];
};

export type TimeDimensionExtraFields = {
  dimension: TCubeDimension & GranularityOptions;
};

export type TimeDimensionWithExtraFields = Omit<TimeDimension, 'dimension'> & TimeDimensionExtraFields;

export type DimensionUpdater = MemberUpdater<TCubeDimension>;
export type MeasureUpdater = MemberUpdater<TCubeMeasure>;
export type SegmentUpdater = MemberUpdater<TCubeSegment>;

// Only require the fields that are actually used (otherwise fields like `operators` are required just to add/update)
export type TimeDimensionRangedUpdateFields = {
  granularity?: TimeDimensionGranularity;
  dateRange?: DateRange;
  dimension: TCubeDimension;
};

export type TimeDimensionComparisonUpdateFields = {
  granularity?: TimeDimensionGranularity;
  compareDateRange: Array<DateRange>;
  dimension: TCubeDimension;
};

export type TimeDimensionUpdater = MemberUpdater<
  TimeDimensionRangedUpdateFields | TimeDimensionComparisonUpdateFields
>;

export type FilterUpdateFields = {
  member?: string;
  operator: BinaryOperator | UnaryOperator;
  values?: string[];
  dimension: TCubeDimension | TCubeMeasure;
};

export type FilterUpdater = MemberUpdater<FilterUpdateFields>;

/**
 * The fields `toTimeDimension` reads off a member. The updater takes either the
 * ranged or the comparison variant, so both range fields are optional here.
 *
 * @hidden
 */
export type TimeDimensionUpdateInput = {
  granularity?: TimeDimensionGranularity;
  dateRange?: DateRange;
  compareDateRange?: Array<DateRange>;
  dimension: TCubeDimension;
};

/**
 * The fields `toFilter` reads off a member. Unlike `FilterUpdateFields`, where
 * `member` is declared as a string, the resolved member object is what is read
 * here.
 *
 * @hidden
 */
export type FilterUpdateInput = {
  member?: TCubeDimension | TCubeMeasure;
  dimension?: TCubeDimension | TCubeMeasure;
  operator: BinaryOperator | UnaryOperator;
  values?: string[];
};

/**
 * A time dimension as `QueryBuilder.resolveMember` returns it: the resolved
 * dimension carries the granularity options the query builder offers.
 *
 * @hidden
 */
export type ResolvedTimeDimension = Omit<TimeDimension, 'dimension'> & {
  dimension: TCubeDimension & { granularities: GranularityOption[] };
  index: number;
};

/**
 * A granularity the query builder offers. `name` is left out for the
 * "w/o grouping" option.
 */
export type GranularityOption = {
  name?: TimeDimensionGranularity;
  title: string;
};

/**
 * Either an `Error` or the `plainError` string from the API response, of which
 * `fetchMeta` only reads `message`/`toString()`.
 *
 * @hidden
 */
export type MetaErrorSource = {
  message?: string;
  toString(): string;
};

/**
 * A filter as it reaches the render props: the resolved member and the
 * operators available for it, alongside the fields of the query filter.
 *
 * @hidden
 */
export type ResolvedFilter = Omit<Filter, 'dimension'> & {
  dimension: TCubeDimension | TCubeMeasure;
  operators: FilterOperator[];
  index: number;
};

export type OrderUpdater = {
  set: (memberId: string, order: QueryOrder | 'none') => void;
  update: (order: Query['order']) => void;
  reorder: (sourceIndex: number, destinationIndex: number) => void;
};

export type PivotConfigUpdaterArgs = {
  sourceIndex: number;
  destinationIndex: number;
  sourceAxis: TSourceAxis;
  destinationAxis: TSourceAxis;
};

export type PivotConfigExtraUpdateFields = {
  limit?: number;
};

export type PivotConfigUpdater = {
  moveItem: (args: PivotConfigUpdaterArgs) => void;
  update: (pivotConfig: PivotConfig & PivotConfigExtraUpdateFields) => void;
};
