/*
 * Copyright 2026 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { useOsdkClient, useOsdkFunctions } from "@osdk/react";
import { useMemo, useRef } from "react";
import { createAsyncCellData } from "../utils/AsyncCellData.js";
import { DEFAULT_FUNCTION_COLUMN_DEDUPE_INTERVAL_MS, DEFAULT_MAX_CONCURRENT_REQUESTS, DEFAULT_PAGE_SIZE } from "../utils/constants.js";
import { buildPagedObjectSets, extractFunctionLocators } from "../utils/functionColumns.js";
export function useFunctionColumnsData({
  objectOrInterfaceType,
  objects,
  columnDefinitions,
  pageSize = DEFAULT_PAGE_SIZE
}) {
  const client = useOsdkClient();
  const prevDataRef = useRef({});
  const stableObjects = useStableObjects(objects);
  const functionColDefs = useMemo(() => extractFunctionLocators(columnDefinitions), [columnDefinitions]);

  // Construct an object set per page (base set filtered to each page's
  // primary keys). `buildPagedObjectSets` returns `[]` for interfaces and
  // when there's no work to do, which short-circuits the downstream queries.
  //
  // When a new page loads, only that page's queries fire — old pages hit the
  // dedupeIntervalMs cache since their params are unchanged.
  const pagedObjectSets = useMemo(() => {
    if (!stableObjects?.length) return [];
    return buildPagedObjectSets(client, objectOrInterfaceType, stableObjects, pageSize);
  }, [client, objectOrInterfaceType, stableObjects, pageSize]);
  const disabled = pagedObjectSets.length === 0 || functionColDefs.length === 0;
  const queryGrid = useMemo(() => {
    if (pagedObjectSets.length === 0 || functionColDefs.length === 0) {
      return EMPTY_QUERY_GRID;
    }
    return buildQueryGrid(pagedObjectSets, functionColDefs);
  }, [pagedObjectSets, functionColDefs]);
  const results = useOsdkFunctions({
    queries: queryGrid.queries,
    enabled: !disabled,
    maxConcurrent: DEFAULT_MAX_CONCURRENT_REQUESTS
  });
  const mergedResults = useMemo(() => mergePagedResults(results, queryGrid.numColumns), [results, queryGrid.numColumns]);
  const data = useMemo(() => {
    const columnData = buildFunctionColumnData(mergedResults, functionColDefs, stableObjects, disabled, prevDataRef.current);
    prevDataRef.current = columnData;
    return columnData;
  }, [mergedResults, functionColDefs, stableObjects, disabled]);
  return data;
}

/**
 * Pairs a flat queries array with the layout metadata needed to recover per-column
 * results back into per-column groups. The numColumns value is produced by
 * the same function that builds the queries, so the two are always in sync.
 */

const EMPTY_QUERY_GRID = {
  queries: [],
  numColumns: 0
};

/**
 * Builds a flat query array and the layout metadata needed to recover per-column results.
 *
 * Layout: [page0_col0, page0_col1, ..., page1_col0, page1_col1, ...]
 * Page-first ordering ensures first concurrent queries prioritizes the first page,
 * so visible rows get all their columns populated before later pages.
 *
 * Each query carries `dependsOnObjects` (the page's row instances) so that
 * editing any of those rows fans out to invalidate this function query, and
 * `dependsOn` from the locator so that linked-type edits also invalidate.
 */
function buildQueryGrid(pagedObjects, functionColDefs) {
  const queries = [];
  for (const {
    objectSet: pagedObjectSet,
    objects: pageObjects
  } of pagedObjects) {
    for (const locator of functionColDefs) {
      queries.push({
        queryDefinition: locator.queryDefinition,
        options: {
          params: locator.getFunctionParams(pagedObjectSet),
          dedupeIntervalMs: locator.dedupeIntervalMs ?? DEFAULT_FUNCTION_COLUMN_DEDUPE_INTERVAL_MS,
          dependsOn: locator.dependsOn,
          dependsOnObjects: pageObjects
        }
      });
    }
  }
  return {
    queries,
    numColumns: functionColDefs.length
  };
}

/**
 * Merges paged results into one merged result per column.
 * Each column has results spread across pages — this combines their
 * functionsMaps so buildFunctionColumnData can look up any object by key.
 *
 * Relies on QueryGrid layout: results[i] belongs to column (i % numColumns).
 * i.e. the first N results are the first page of each column, the next N results are the second page, etc.
 */
function mergePagedResults(results, numColumns) {
  if (numColumns === 0) return [];
  const merged = Array.from({
    length: numColumns
  }, () => ({
    isLoading: false,
    error: undefined,
    functionsMap: {}
  }));
  results.forEach((result, index) => {
    const entry = merged[index % numColumns];
    if (result.isLoading) {
      entry.isLoading = true;
    }
    if (result.error) {
      entry.error = result.error;
    }
    const pageData = result.data;
    if (pageData) {
      Object.assign(entry.functionsMap, pageData);
    }
  });
  return merged;
}
function buildFunctionColumnData(results, functionColDefs, objects, disabled, prevColumnData) {
  const columnData = {};
  if (disabled || !objects) return columnData;
  results.forEach((result, index) => {
    const locator = functionColDefs[index];
    if (!locator) return;
    const columnId = String(locator.id);
    columnData[columnId] = {};
    objects.forEach(obj => {
      const key = String(obj.$primaryKey);
      const prevData = prevColumnData[columnId]?.[key]?.data;
      columnData[columnId][key] = createAsyncCellData(resolveCell(result, locator.getKey(obj), locator.getValue, prevData));
    });
  });
  return columnData;
}

/** Resolves the cell state: error, loaded, or loading with previous data. */
function resolveCell(result, objectKey, getValue, prevData) {
  if (result.error) {
    return {
      isLoading: false,
      error: result.error
    };
  }
  if (objectKey in result.functionsMap) {
    const rawData = result.functionsMap[objectKey];
    return {
      isLoading: false,
      data: getValue ? getValue(rawData) : rawData
    };
  }
  // Key not in results. While the query is still loading, keep showing the
  // previous data so the cell doesn't flash empty during a refetch. Once the
  // query has resolved without an entry for this object, its value is gone —
  // clear the cell instead of retaining the stale previous value (otherwise a
  // cell whose derived value became null keeps displaying the old value).
  return {
    isLoading: result.isLoading,
    data: result.isLoading ? prevData : undefined
  };
}
const useStableObjects = objects => {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useMemo(() => objects, [
  // eslint-disable-next-line react-hooks/exhaustive-deps
  JSON.stringify((objects ?? []).map(item => ({
    $apiName: item.$apiName,
    $primaryKey: item.$primaryKey
  })).sort((a, b) => {
    if (a.$apiName !== b.$apiName) {
      return a.$apiName.localeCompare(b.$apiName);
    }
    return String(a.$primaryKey).localeCompare(String(b.$primaryKey));
  }))]);
};
//# sourceMappingURL=useFunctionColumnsData.js.map