import {
  apiFetch,
  apiPath,
  type ApiQueryParams
} from "../api/client";
import { fetchPlatformModules } from "../api/platform";
import { filterSearchableSelectOptions } from "../components/SearchableSelect";
import {
  unavailableReferenceOption,
  type ReferenceOption,
  type ReferenceOptionProvider
} from "../components/ReferenceSelect";
import type { ApiSettings } from "../types";

type ApiReferenceOptionPayload = {
  value: string;
  label: string;
  description?: string | null;
  kind?: string | null;
  availability?: ReferenceOption["availability"];
  disabled?: boolean;
  source_module?: string | null;
  provenance?: Record<string, unknown>;
};

export function apiReferenceOptionProvider(
  settings: ApiSettings,
  path: string,
  params: ApiQueryParams = {}
): ReferenceOptionProvider {
  async function load(
    query: string,
    selectedValues: readonly string[],
    limit: number,
    signal: AbortSignal
  ): Promise<ReferenceOption[]> {
    const response = await apiFetch<{
      options: ApiReferenceOptionPayload[];
      provider_available: boolean;
    }>(
      settings,
      apiPath(path, {
        ...params,
        q: query,
        selected: selectedValues,
        limit
      }),
      { signal }
    );
    return response.options.map(referenceOptionFromPayload);
  }

  return {
    search: (query, context) =>
      load(query, context.selectedValues, context.limit, context.signal),
    async resolve(values, context) {
      const uniqueValues = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
      const chunks: string[][] = [];
      for (let index = 0; index < uniqueValues.length; index += 200) {
        chunks.push(uniqueValues.slice(index, index + 200));
      }
      const pages = await Promise.all(
        chunks.map((chunk) =>
          load("", chunk, Math.min(200, Math.max(1, chunk.length)), context.signal)
        )
      );
      const byValue = new Map(
        pages.flat().map((option) => [option.value, option])
      );
      return uniqueValues.map(
        (value) =>
          byValue.get(value)
          ?? unavailableReferenceOption(value)
      );
    }
  };
}

export function platformModuleReferenceProvider(
  settings: ApiSettings,
  presentedOptions: readonly ReferenceOption[] = []
): ReferenceOptionProvider {
  const presentedByValue = new Map(
    presentedOptions.map((option) => [option.value, option])
  );
  let cataloguePromise: Promise<ReferenceOption[]> | null = null;

  async function catalogue(signal: AbortSignal): Promise<ReferenceOption[]> {
    cataloguePromise ??= loadPlatformModuleOptions(
      settings,
      presentedOptions,
      presentedByValue
    ).catch((error: unknown) => {
      cataloguePromise = null;
      throw error;
    });
    const options = await cataloguePromise;
    if (signal.aborted) throw abortError();
    return options;
  }

  return {
    async search(query, context) {
      const options = await catalogue(context.signal);
      return filterSearchableSelectOptions(
        options,
        query,
        context.limit
      ) as ReferenceOption[];
    },
    async resolve(values, context) {
      const options = await catalogue(context.signal);
      const byValue = new Map(
        [...presentedOptions, ...options].map((option) => [
          option.value,
          option
        ])
      );
      return values.map(
        (value) =>
          byValue.get(value)
          ?? unavailableReferenceOption(value, "Unavailable module")
      );
    }
  };
}

async function loadPlatformModuleOptions(
  settings: ApiSettings,
  presentedOptions: readonly ReferenceOption[],
  presentedByValue: ReadonlyMap<string, ReferenceOption>
): Promise<ReferenceOption[]> {
  let modules;
  try {
    modules = (await fetchPlatformModules(settings)).modules;
  } catch (error) {
    if (presentedOptions.length) return [...presentedOptions];
    throw error;
  }
  return modules
    .filter((module) => module.enabled)
    .map((module) => {
      const presented = presentedByValue.get(module.id);
      return {
        value: module.id,
        label: presented?.label ?? module.name ?? module.id,
        description: presented?.description
          ?? `${module.id} · version ${module.version}`,
        searchText: presented?.searchText
          ?? `${module.id} ${module.name} ${module.version}`,
        kind: "module",
        availability: presented?.availability ?? "available",
        disabled: presented?.disabled ?? false,
        sourceModule: "core",
        provenance: {
          version: module.version,
          activeRegistry: true,
          ...(presented?.provenance ?? {})
        }
      } satisfies ReferenceOption;
    });
}

function referenceOptionFromPayload(
  option: ApiReferenceOptionPayload
): ReferenceOption {
  return {
    value: option.value,
    label: option.label,
    description: option.description,
    kind: option.kind,
    availability: option.availability,
    disabled: option.disabled,
    sourceModule: option.source_module,
    provenance: option.provenance
  };
}

function abortError(): DOMException {
  return new DOMException("The operation was aborted.", "AbortError");
}
