import { useEffect, useMemo, useState } from "react";
import { X } from "lucide-react";
import SearchableSelect, {
  filterSearchableSelectOptions,
  type SearchableSelectCreateCustomOption,
  type SearchableSelectOption,
  type SearchableSelectProps
} from "./SearchableSelect";
import IconButton from "./IconButton";
import type { ApiSettings } from "../types";

export type ReferenceAvailability =
  | "available"
  | "inactive"
  | "unavailable";

export type ReferenceOption = SearchableSelectOption & {
  kind?: string | null;
  availability?: ReferenceAvailability;
  sourceModule?: string | null;
  provenance?: Record<string, unknown>;
  custom?: boolean;
};

export type ReferenceProviderContext = {
  limit: number;
  selectedValues: readonly string[];
  signal: AbortSignal;
};

export type ReferenceOptionProvider = {
  search: (
    query: string,
    context: ReferenceProviderContext
  ) => Promise<readonly ReferenceOption[]>;
  resolve?: (
    values: readonly string[],
    context: Omit<ReferenceProviderContext, "limit"> & { limit?: number }
  ) => Promise<readonly ReferenceOption[]>;
};

export type ConfigurationReferenceSelectorsUiCapability = {
  tenantProvider: (settings: ApiSettings) => ReferenceOptionProvider;
  changeRequestProvider: (
    settings: ApiSettings,
    options: {
      purpose: string;
      tenantId?: string | null;
    }
  ) => ReferenceOptionProvider;
};

export type CredentialReferenceSelectorContext = {
  scopeType: "system" | "tenant" | "user" | "group";
  scopeId?: string | null;
};

export type CredentialReferenceSelectorsUiCapability = {
  serverProvider: (
    settings: ApiSettings,
    context: CredentialReferenceSelectorContext
  ) => ReferenceOptionProvider;
};

export type ReferenceSelectProps = Omit<
  SearchableSelectProps,
  "options" | "loadOptions" | "selectedOption"
> & {
  provider: ReferenceOptionProvider;
  selectedOption?: ReferenceOption | null;
};

export type ReferenceMultiSelectProps = {
  id?: string;
  values: readonly string[];
  onChange: (values: string[]) => void;
  provider: ReferenceOptionProvider;
  selectedOptions?: readonly ReferenceOption[];
  createCustomOption?: SearchableSelectCreateCustomOption;
  "aria-label"?: string;
  placeholder?: string;
  searchPlaceholder?: string;
  emptyText?: string;
  disabled?: boolean;
  minQueryLength?: number;
  searchLimit?: number;
  className?: string;
};

const EMPTY_REFERENCE_OPTIONS: readonly ReferenceOption[] = [];

export default function ReferenceSelect({
  provider,
  value,
  selectedOption,
  searchLimit = 50,
  ...props
}: ReferenceSelectProps) {
  const [resolved, setResolved] = useState<ReferenceOption | null>(
    selectedOption?.value === value ? selectedOption : null
  );

  useEffect(() => {
    if (!value) {
      setResolved(null);
      return;
    }
    setResolved(
      selectedOption?.value === value
        ? selectedOption
        : unavailableReferenceOption(value)
    );
  }, [selectedOption, value]);

  useEffect(() => {
    if (!value || selectedOption?.value === value || !provider.resolve) return;
    const controller = new AbortController();
    void provider
      .resolve([value], {
        selectedValues: [value],
        signal: controller.signal,
        limit: 1
      })
      .then((options) => {
        if (!controller.signal.aborted) {
          setResolved(
            options.find((option) => option.value === value)
              ?? unavailableReferenceOption(value)
          );
        }
      })
      .catch((error: unknown) => {
        if (
          !controller.signal.aborted
          && !(error instanceof DOMException && error.name === "AbortError")
        ) {
          setResolved(unavailableReferenceOption(value));
        }
      });
    return () => controller.abort();
  }, [provider, selectedOption?.value, value]);

  return (
    <SearchableSelect
      {...props}
      value={value}
      searchLimit={searchLimit}
      selectedOption={resolved}
      loadOptions={(query, context) =>
        provider.search(query, {
          ...context,
          selectedValues: value ? [value] : []
        })
      }
    />
  );
}

export function ReferenceMultiSelect({
  id,
  values,
  onChange,
  provider,
  selectedOptions = EMPTY_REFERENCE_OPTIONS,
  createCustomOption,
  "aria-label": ariaLabel = "Select references",
  placeholder = "Add a reference",
  searchPlaceholder,
  emptyText,
  disabled = false,
  minQueryLength,
  searchLimit = 50,
  className = ""
}: ReferenceMultiSelectProps) {
  const normalizedValues = useMemo(
    () => [...new Set(values.map((value) => value.trim()).filter(Boolean))],
    [values]
  );
  const normalizedValuesKey = normalizedValues.join("\u0000");
  const [resolvedOptions, setResolvedOptions] = useState<
    readonly ReferenceOption[]
  >(selectedOptions);

  useEffect(() => {
    setResolvedOptions(selectedOptions);
  }, [selectedOptions]);

  useEffect(() => {
    if (!normalizedValues.length || !provider.resolve) return;
    const controller = new AbortController();
    void provider
      .resolve(normalizedValues, {
        selectedValues: normalizedValues,
        signal: controller.signal,
        limit: normalizedValues.length
      })
      .then((options) => {
        if (!controller.signal.aborted) setResolvedOptions(options);
      })
      .catch((error: unknown) => {
        if (
          !controller.signal.aborted
          && !(error instanceof DOMException && error.name === "AbortError")
        ) {
          setResolvedOptions(EMPTY_REFERENCE_OPTIONS);
        }
      });
    return () => controller.abort();
  }, [normalizedValuesKey, provider]);

  const optionsByValue = useMemo(
    () =>
      new Map(
        [...selectedOptions, ...resolvedOptions].map((option) => [
          option.value,
          option
        ])
      ),
    [resolvedOptions, selectedOptions]
  );
  const selected = normalizedValues.map(
    (value) =>
      optionsByValue.get(value)
      ?? (createCustomOption?.(value) as ReferenceOption | null | undefined)
      ?? unavailableReferenceOption(value)
  );
  const rootClassName = ["reference-multi-select", className]
    .filter(Boolean)
    .join(" ");

  function add(value: string, option: SearchableSelectOption | null) {
    if (!value || normalizedValues.includes(value)) return;
    if (option) {
      setResolvedOptions((current) => [
        ...current.filter((item) => item.value !== value),
        option as ReferenceOption
      ]);
    }
    onChange([...normalizedValues, value]);
  }

  function remove(value: string) {
    onChange(normalizedValues.filter((item) => item !== value));
  }

  return (
    <div className={rootClassName}>
      {selected.length ? (
        <div
          className="reference-multi-select-values"
          role="list"
          aria-label={`${ariaLabel} selected`}
        >
          {selected.map((option) => (
            <div
              key={option.value}
              className={[
                "reference-multi-select-value",
                option.availability
                  ? `is-${option.availability}`
                  : "",
                option.custom ? "is-custom" : ""
              ]
                .filter(Boolean)
                .join(" ")}
              role="listitem"
            >
              <span className="reference-multi-select-value-copy">
                <strong>{option.label}</strong>
                <small>{referenceOptionDetail(option)}</small>
              </span>
              <IconButton
                label={`Remove ${option.label}`}
                icon={<X size={15} aria-hidden="true" />}
                onClick={() => remove(option.value)}
                disabled={disabled}
              />
            </div>
          ))}
        </div>
      ) : null}
      <SearchableSelect
        id={id}
        aria-label={ariaLabel}
        value=""
        onChange={add}
        loadOptions={async (query, context) => {
          const options = await provider.search(query, {
            ...context,
            selectedValues: normalizedValues
          });
          return options.filter(
            (option) => !normalizedValues.includes(option.value)
          );
        }}
        createCustomOption={(value) => {
          if (normalizedValues.includes(value)) return null;
          return createCustomOption?.(value) ?? null;
        }}
        placeholder={placeholder}
        searchPlaceholder={searchPlaceholder}
        emptyText={emptyText}
        disabled={disabled}
        minQueryLength={minQueryLength}
        searchLimit={searchLimit}
      />
    </div>
  );
}

export function staticReferenceOptionProvider(
  options: readonly ReferenceOption[]
): ReferenceOptionProvider {
  const byValue = new Map(options.map((option) => [option.value, option]));
  return {
    async search(query, context) {
      if (context.signal.aborted) throw abortError();
      return filterSearchableSelectOptions(
        options,
        query,
        context.limit
      ) as ReferenceOption[];
    },
    async resolve(values, context) {
      if (context.signal.aborted) throw abortError();
      return values.map(
        (value) => byValue.get(value) ?? unavailableReferenceOption(value)
      );
    }
  };
}

export function combineReferenceOptionProviders(
  providers: readonly ReferenceOptionProvider[]
): ReferenceOptionProvider {
  if (providers.length === 1) return providers[0];

  async function collect(
    operation: (
      provider: ReferenceOptionProvider
    ) => Promise<readonly ReferenceOption[]>
  ): Promise<ReferenceOption[]> {
    const results = await Promise.allSettled(providers.map(operation));
    const successful = results.filter(
      (
        result
      ): result is PromiseFulfilledResult<readonly ReferenceOption[]> =>
        result.status === "fulfilled"
    );
    if (!successful.length) {
      const failure = results.find(
        (result): result is PromiseRejectedResult =>
          result.status === "rejected"
      );
      throw failure?.reason ?? new Error("Reference options are unavailable.");
    }
    return mergeReferenceOptions(
      successful.flatMap((result) => [...result.value])
    );
  }

  return {
    async search(query, context) {
      const options = await collect((provider) =>
        provider.search(query, context)
      );
      return options.slice(0, context.limit);
    },
    async resolve(values, context) {
      const options = await collect((provider) =>
        provider.resolve
          ? provider.resolve(values, context)
          : provider.search("", {
              ...context,
              limit: context.limit ?? Math.max(values.length, 50)
            })
      );
      const byValue = new Map(options.map((option) => [option.value, option]));
      return values.map(
        (value) =>
          byValue.get(value)
          ?? unavailableReferenceOption(value)
      );
    }
  };
}

export function wildcardReferenceOption(
  value: string,
  kind = "pattern"
): ReferenceOption | null {
  const clean = value.trim();
  if (!clean || !/[*?[\]]/.test(clean)) return null;
  return {
    value: clean,
    label: clean,
    description: "Custom wildcard pattern",
    kind,
    availability: "available",
    custom: true,
    provenance: { input: "custom_pattern" }
  };
}

export function customReferenceOption(
  value: string,
  kind = "custom"
): ReferenceOption | null {
  const clean = value.trim();
  if (!clean) return null;
  return {
    value: clean,
    label: clean,
    description: "Custom reference",
    kind,
    availability: "available",
    custom: true,
    provenance: { input: "custom_reference" }
  };
}

export function unavailableReferenceOption(
  value: string,
  label = "Unavailable reference"
): ReferenceOption {
  return {
    value,
    label,
    description: value,
    availability: "unavailable",
    disabled: true,
    provenance: { retainedReference: true }
  };
}

function mergeReferenceOptions(
  options: readonly ReferenceOption[]
): ReferenceOption[] {
  const merged = new Map<string, ReferenceOption>();
  for (const option of options) {
    const current = merged.get(option.value);
    if (!current || referenceOptionPriority(option) > referenceOptionPriority(current)) {
      merged.set(option.value, option);
    }
  }
  return [...merged.values()];
}

function referenceOptionPriority(option: ReferenceOption): number {
  if (option.availability === "unavailable") return 0;
  if (option.availability === "inactive" || option.disabled) return 1;
  return 2;
}

function referenceOptionDetail(option: ReferenceOption): string {
  const state = option.custom
    ? option.kind === "pattern"
      ? "Pattern"
      : "Custom"
    : option.availability === "inactive"
      ? "Inactive"
      : option.availability === "unavailable"
        ? "Unavailable"
        : "";
  return [state, option.description, option.value !== option.label ? option.value : ""]
    .filter(Boolean)
    .join(" · ");
}

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