import type * as ApiTypes from '../generated/ApiTypes.js';
import type * as RawApiTypes from '../generated/RawApiTypes.js';

/*
 * Localized Field Value Operations Utilities
 *
 * This module provides a unified interface for working with DatoCMS field values that may or may not be localized.
 *
 * Problem it solves:
 * In DatoCMS, fields can be either localized (containing values for multiple locales as an object like
 * `{ "en": "Hello", "it": "Ciao" }`) or non-localized (containing a single direct value like `"Hello"`).
 * When processing field data, developers need to handle both cases differently, leading to repetitive
 * conditional logic throughout the codebase.
 *
 * Why it's useful:
 * - Provides a consistent API for operations (map, filter, test, visit) regardless of localization status
 * - Eliminates the need for developers to manually check if a field is localized before processing
 * - Reduces code duplication and potential bugs from inconsistent handling
 * - Supports both synchronous and asynchronous operations
 * - Maintains the original data structure (localized fields remain localized, non-localized remain direct)
 */

/**
 * Represents a localized field value in DatoCMS.
 *
 * In DatoCMS, fields can be localized. In this scenario, their value contains values
 * for various locales structured as an object, such as
 * `{ "en": "Hello", "it": "Ciao" }`
 */
export type LocalizedFieldValue<
  T = unknown,
  L extends string = string,
> = Partial<Record<L, T>>;

/**
 * Determines whether a DatoCMS field is localized or not.
 *
 * This function handles both full Schema field objects and simplified Schema field objects
 * by checking the appropriate property based on the object structure.
 *
 * @returns true if the field is localized, false otherwise
 * @param field - The DatoCMS field definition (either full or simple schema)
 */
export function isLocalized(
  field: RawApiTypes.Field | ApiTypes.Field,
): boolean {
  return 'attributes' in field ? field.attributes.localized : field.localized;
}

export function isLocalizedFieldValue<T = unknown, L extends string = string>(
  value: unknown,
): value is LocalizedFieldValue<T, L> {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    return false;
  }

  const keys = Object.keys(value);

  if (keys.length === 0) {
    return false;
  }

  const localePattern =
    /^[A-Za-z]{2,4}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/;

  return keys.every((key) => localePattern.test(key));
}

/**
 * A normalized entry that represents a single value from either a localized or non-localized field.
 *
 * For localized fields, each locale produces one entry with `locale` set to the locale code (e.g., "en", "it").
 * For non-localized fields, there's one entry with `locale` set to `undefined`.
 *
 * This uniform structure allows the same processing logic to work with both field types.
 */
export type NormalizedFieldValueEntry<
  T = unknown,
  L extends string = string,
> = {
  locale: L | undefined;
  value: T;
};

/**
 * Converts a field value (localized or non-localized) into a uniform array of entries.
 *
 * This function normalizes the handling of field values by converting them into a consistent
 * array format, regardless of whether the field is localized or not.
 *
 * @param value - The field value to convert (either a localized object or direct value)
 * @param field - The DatoCMS field definition that determines localization behavior
 * @returns Array of entries where each entry contains a locale (string for localized, undefined for non-localized) and the corresponding value
 */
export function toNormalizedFieldValueEntries<
  T = unknown,
  L extends string = string,
>(
  value: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
): NormalizedFieldValueEntry<T, L>[] {
  if (isLocalized(field)) {
    const localizedValue = value as LocalizedFieldValue<T, L>;

    return Object.entries(localizedValue).map(([locale, value]) => ({
      locale: locale as L,
      value: value as T,
    }));
  }

  return [{ locale: undefined, value: value as T }];
}

/**
 * Converts an array of possibly localized entries back into the appropriate field value format.
 *
 * This function is the inverse of `toNormalizedFieldValueEntries`. It takes a uniform
 * array of entries and converts them back to either a localized object or a direct value,
 * depending on the field's localization setting.
 *
 * @param entries - Array of entries to convert back to field value format
 * @param field - The DatoCMS field definition that determines the output format
 * @returns Either a localized object (for localized fields) or the direct value (for non-localized fields)
 */
export function fromNormalizedFieldValueEntries<
  T = unknown,
  L extends string = string,
>(
  entries: NormalizedFieldValueEntry<T, L>[],
  field: RawApiTypes.Field | ApiTypes.Field,
): T | LocalizedFieldValue<T, L> {
  if (isLocalized(field)) {
    return Object.fromEntries(
      entries.map(({ locale, value }) => [locale, value]),
    ) as LocalizedFieldValue<T, L>;
  }

  if (entries.length === 0) {
    throw new Error('There must be at least one entry!');
  }

  return entries[0]!.value;
}

/**
 * Maps field values using a provided mapping function.
 * For localized fields, applies the mapping function to each locale value.
 * For non-localized fields, applies the mapping function directly to the value.
 *
 * @template T - The type that the mapping function returns
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param mapFn - The function to apply to each locale value or the direct value
 * @returns The mapped value with the same structure as the input
 */
export function mapNormalizedFieldValues<
  TInput = unknown,
  TOutput = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: TInput | LocalizedFieldValue<TInput, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  mapFn: (locale: L | undefined, localeValue: TInput) => TOutput,
): TOutput | LocalizedFieldValue<TOutput, L> {
  const entries = toNormalizedFieldValueEntries<TInput, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  const mappedEntries = entries.map(({ locale, value }) => ({
    locale,
    value: mapFn(locale, value),
  }));
  return fromNormalizedFieldValueEntries<TOutput, L>(mappedEntries, field);
}

/**
 * Maps field values using a provided mapping function (async version).
 * For localized fields, applies the mapping function to each locale value.
 * For non-localized fields, applies the mapping function directly to the value.
 *
 * @template T - The type that the mapping function returns
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param mapFn - The function to apply to each locale value or the direct value
 * @returns The mapped value with the same structure as the input
 */
export async function mapNormalizedFieldValuesAsync<
  TInput = unknown,
  TOutput = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: TInput | LocalizedFieldValue<TInput, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  mapFn: (locale: L | undefined, localeValue: TInput) => Promise<TOutput>,
): Promise<TOutput | LocalizedFieldValue<TOutput, L>> {
  const entries = toNormalizedFieldValueEntries<TInput, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  const mappedEntries = await Promise.all(
    entries.map(async ({ locale, value }) => ({
      locale,
      value: await mapFn(locale, value),
    })),
  );
  return fromNormalizedFieldValueEntries<TOutput, L>(mappedEntries, field);
}

/**
 * Filters field values using a provided filter function.
 * For localized fields, filters each locale value.
 * For non-localized fields, returns the value if the filter passes, otherwise undefined.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param filterFn - The function to test each locale value or the direct value
 * @returns The filtered value with the same structure as the input
 */
export function filterNormalizedFieldValues<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  filterFn: (locale: L | undefined, localeValue: T) => boolean,
): T | LocalizedFieldValue<T, L> | undefined {
  const entries = toNormalizedFieldValueEntries<T, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  const filteredEntries = entries.filter((entry) =>
    filterFn(entry.locale, entry.value),
  );

  if (isLocalized(field)) {
    return fromNormalizedFieldValueEntries<T, L>(filteredEntries, field);
  }

  return filteredEntries.length > 0 ? filteredEntries[0]?.value : undefined;
}

/**
 * Filters field values using a provided filter function (async version).
 * For localized fields, filters each locale value.
 * For non-localized fields, returns the value if the filter passes, otherwise undefined.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param filterFn - The function to test each locale value or the direct value
 * @returns The filtered value with the same structure as the input
 */
export async function filterNormalizedFieldValuesAsync<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  filterFn: (locale: L | undefined, localeValue: T) => Promise<boolean>,
): Promise<T | LocalizedFieldValue<T, L> | undefined> {
  const entries = toNormalizedFieldValueEntries<T, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  const results = await Promise.all(
    entries.map(async ({ locale, value }) => ({
      locale,
      value,
      passed: await filterFn(locale, value),
    })),
  );

  const filteredEntries = results
    .filter(({ passed }) => passed)
    .map(({ locale, value }) => ({ locale, value }));

  if (isLocalized(field)) {
    return fromNormalizedFieldValueEntries<T, L>(filteredEntries, field);
  }

  return filteredEntries.length > 0 ? filteredEntries[0]?.value : undefined;
}

/**
 * Tests whether at least one field value passes the test implemented by the provided function.
 * For localized fields, tests each locale value.
 * For non-localized fields, tests the direct value.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param testFn - The function to test each locale value or the direct value
 * @returns true if at least one value passes the test, false otherwise
 */
export function someNormalizedFieldValues<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  testFn: (locale: L | undefined, localeValue: T) => boolean,
): boolean {
  const entries = toNormalizedFieldValueEntries<T, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  return entries.some(({ locale, value }) => testFn(locale, value));
}

/**
 * Tests whether at least one field value passes the test implemented by the provided function (async version).
 * For localized fields, tests each locale value.
 * For non-localized fields, tests the direct value.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param testFn - The function to test each locale value or the direct value
 * @returns true if at least one value passes the test, false otherwise
 */
export async function someNormalizedFieldValuesAsync<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  testFn: (locale: L | undefined, localeValue: T) => Promise<boolean>,
): Promise<boolean> {
  const entries = toNormalizedFieldValueEntries<T, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  const results = await Promise.all(
    entries.map(({ locale, value }) => testFn(locale, value)),
  );
  return results.some((result) => result);
}

/**
 * Tests whether all field values pass the test implemented by the provided function.
 * For localized fields, tests each locale value.
 * For non-localized fields, tests the direct value.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param testFn - The function to test each locale value or the direct value
 * @returns true if all values pass the test, false otherwise
 */
export function everyNormalizedFieldValue<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  testFn: (locale: L | undefined, localeValue: T) => boolean,
): boolean {
  return !someNormalizedFieldValues(
    localizedOrNonLocalizedFieldValue,
    field,
    (locale, localeValue) => !testFn(locale, localeValue),
  );
}

/**
 * Tests whether all field values pass the test implemented by the provided function (async version).
 * For localized fields, tests each locale value.
 * For non-localized fields, tests the direct value.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param testFn - The function to test each locale value or the direct value
 * @returns true if all values pass the test, false otherwise
 */
export async function everyNormalizedFieldValueAsync<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  testFn: (locale: L | undefined, localeValue: T) => Promise<boolean>,
): Promise<boolean> {
  return !(await someNormalizedFieldValuesAsync(
    localizedOrNonLocalizedFieldValue,
    field,
    async (locale, localeValue) => !(await testFn(locale, localeValue)),
  ));
}

/**
 * Visits each field value with the provided function.
 * For localized fields, visits each locale value.
 * For non-localized fields, visits the direct value.
 *
 * @param value - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param visitFn - The function to call for each locale value or the direct value
 */
export function visitNormalizedFieldValues<
  T = unknown,
  L extends string = string,
>(
  value: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  visitFn: (locale: L | undefined, localeValue: T) => void,
): void {
  const entries = toNormalizedFieldValueEntries<T, L>(value, field);
  for (const { locale, value } of entries) {
    visitFn(locale, value);
  }
}

/**
 * Visits each field value with the provided function (async version).
 * For localized fields, visits each locale value.
 * For non-localized fields, visits the direct value.
 *
 * @param localizedOrNonLocalizedFieldValue - The field value (either localized object or direct value)
 * @param field - The DatoCMS field definition
 * @param visitFn - The function to call for each locale value or the direct value
 */
export async function visitNormalizedFieldValuesAsync<
  T = unknown,
  L extends string = string,
>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T, L>,
  field: RawApiTypes.Field | ApiTypes.Field,
  visitFn: (locale: L | undefined, localeValue: T) => Promise<void>,
): Promise<void> {
  const entries = toNormalizedFieldValueEntries<T, L>(
    localizedOrNonLocalizedFieldValue,
    field,
  );
  await Promise.all(entries.map(({ locale, value }) => visitFn(locale, value)));
}
