import {
  asyncMap,
  decodeIfEncoded,
  delay,
  flatMap,
  getOptionsPage,
  getRegionURL,
  isCDNUrl,
  range,
  stringify,
} from './utils';
import SbFetch from './sbFetch';
import type Method from './constants';
import type { StoryblokContentVersionKeys } from './constants';
import { StoryblokContentVersion } from './constants';
import { createRateLimitConfig, determineRateLimit, MANAGEMENT_API_DEFAULT_RATE_LIMIT, parseRateLimitHeaders, type RateLimitConfig } from './rateLimit';
import { ThrottleQueueManager } from './throttleQueueManager';

import type {
  ICacheProvider,
  IMemoryType,
  ISbCache,
  ISbComponentType,
  ISbConfig,
  ISbContentMangmntAPI,
  ISbCustomFetch,
  ISbField,
  ISbLinksParams,
  ISbLinksResult,
  ISbLinkURLObject,
  ISbResponse,
  ISbResponseData,
  ISbResult,
  ISbStories,
  ISbStoriesParams,
  ISbStory,
  ISbStoryData,
  ISbStoryParams,
} from './interfaces';

export * from './interfaces';

let memory: Partial<IMemoryType> = {};

const cacheVersions = {} as CachedVersions;

interface CachedVersions {
  [key: string]: number;
}

interface LinksType {
  [key: string]: any;
}

interface RelationsType {
  [key: string]: any;
}

interface ISbFlatMapped {
  data: any;
}

const _VERSION = {
  V1: 'v1',
  V2: 'v2',
} as const;

type ObjectValues<T> = T[keyof T];
type Version = ObjectValues<typeof _VERSION>;

export class Storyblok {
  private client: SbFetch;
  private maxRetries: number;
  private retriesDelay: number;
  private throttleManager: ThrottleQueueManager;
  private accessToken: string;
  private cache: ISbCache;
  private resolveCounter: number;
  public relations: RelationsType;
  public links: LinksType;
  public version: StoryblokContentVersionKeys | undefined;
  private rateLimitConfig: RateLimitConfig;
  private cvMode: 'auto' | 'manual';
  /**
   * @deprecated This property is deprecated. Use the standalone `richTextResolver` from `@storyblok/richtext` instead.
   * @see https://github.com/storyblok/richtext
   */
  public richTextResolver: unknown;
  public resolveNestedRelations: boolean;
  private stringifiedStoriesCache: Record<string, string>;
  private inlineAssets: boolean;

  /**
   *
   * @param config ISbConfig interface
   * @param pEndpoint string, optional
   */
  public constructor(config: ISbConfig, pEndpoint?: string) {
    let endpoint = config.endpoint || pEndpoint;

    if (!endpoint) {
      const protocol = config.https === false ? 'http' : 'https';

      if (!config.oauthToken) {
        endpoint = `${protocol}://${getRegionURL(config.region)}/${'v2' as Version}`;
      }
      else {
        endpoint = `${protocol}://${getRegionURL(config.region)}/${'v1' as Version}`;
      }
    }

    const headers: Headers = new Headers();

    // Skip Content-Type for GET browser CDN requests to avoid CORS preflight.
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#simple_requests
    const isBrowserCdn = !config.oauthToken && typeof window !== 'undefined';
    if (!isBrowserCdn) {
      headers.set('Content-Type', 'application/json');
    }

    headers.set('Accept', 'application/json');

    if (config.headers) {
      const entries
        = config.headers.constructor.name === 'Headers'
          ? config.headers.entries().toArray()
          : Object.entries(config.headers);

      entries.forEach(([key, value]: [string, string]) => {
        headers.set(key, value);
      });
    }

    if (config.oauthToken) {
      headers.set('Authorization', config.oauthToken);
    }

    // Create rate limit config - user's rateLimit applies only to uncached requests
    // Pass isManagementApi flag to handle Management API default rate limit
    this.rateLimitConfig = createRateLimitConfig(config.rateLimit, !!config.oauthToken);

    this.maxRetries = config.maxRetries || 10;
    this.retriesDelay = config.retriesDelay ?? 300;

    // Initialize throttle queue manager
    this.throttleManager = new ThrottleQueueManager(
      this.throttledRequest.bind(this),
      1000,
    );

    this.accessToken = config.accessToken || '';
    this.relations = {} as RelationsType;
    this.links = {} as LinksType;
    this.cache = config.cache || { clear: 'manual' };
    this.cvMode = config.cache?.cv ?? 'auto';
    this.resolveCounter = 0;
    this.resolveNestedRelations = config.resolveNestedRelations || true;
    this.stringifiedStoriesCache = {} as Record<string, string>;
    this.version = config.version || StoryblokContentVersion.PUBLISHED; // the default version is published as per API documentation
    this.inlineAssets = config.inlineAssets || false;

    this.client = new SbFetch({
      baseURL: endpoint,
      timeout: config.timeout || 0,
      headers,
      responseInterceptor: config.responseInterceptor,
      fetch: config.fetch,
    });
  }

  private parseParams(params: ISbStoriesParams): ISbStoriesParams {
    if (!params.token) {
      params.token = this.getToken();
    }

    if (!params.cv && this.cvMode === 'auto') {
      params.cv = cacheVersions[params.token];
    }

    if (Array.isArray(params.resolve_relations)) {
      // Decode URL-encoded strings in array before joining
      params.resolve_relations = params.resolve_relations.map(decodeIfEncoded).join(',');
    }
    else if (typeof params.resolve_relations === 'string') {
      // Decode URL-encoded strings to prevent double-encoding
      params.resolve_relations = decodeIfEncoded(params.resolve_relations);
    }

    if (typeof params.resolve_relations !== 'undefined') {
      params.resolve_level = 2;
    }

    return params;
  }

  private factoryParamOptions(
    url: string,
    params: ISbStoriesParams,
  ): ISbStoriesParams {
    if (isCDNUrl(url)) {
      return this.parseParams(params);
    }

    return params;
  }

  private makeRequest(
    url: string,
    params: ISbStoriesParams,
    per_page: number,
    page: number,
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResult> {
    const query = this.factoryParamOptions(
      url,
      getOptionsPage(params, per_page, page),
    );

    return this.cacheResponse(url, query, undefined, fetchOptions);
  }

  public get(
    slug: 'cdn/links',
    params?: ISbLinksParams,
    fetchOptions?: ISbCustomFetch
  ): Promise<ISbLinksResult>;

  public get(
    slug: string,
    params?: ISbStoriesParams,
    fetchOptions?: ISbCustomFetch
  ): Promise<ISbResult>;

  public get(
    slug: string,
    params: ISbStoriesParams | ISbLinksParams = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResult | ISbLinksResult> {
    if (!params) {
      params = {} as ISbStoriesParams;
    }
    const url = `/${slug}`;

    // Only add version parameter for CDN URLs
    if (isCDNUrl(url)) {
      params.version = params.version || this.version;
    }

    const query = this.factoryParamOptions(url, params);

    return this.cacheResponse(url, query, undefined, fetchOptions);
  }

  public async getAll(
    slug: string,
    params: ISbStoriesParams = {},
    entity?: string,
    fetchOptions?: ISbCustomFetch,
  ): Promise<any[]> {
    const perPage = params?.per_page || 25;
    const url = `/${slug}`.replace(/\/$/, '');
    const e = entity ?? url.substring(url.lastIndexOf('/') + 1);
    params.version = params.version || this.version;

    const firstPage = 1;
    const firstRes = await this.makeRequest(
      url,
      params,
      perPage,
      firstPage,
      fetchOptions,
    );
    const lastPage = firstRes.total ? Math.ceil(firstRes.total / (firstRes.perPage || perPage)) : 1;

    const restRes: any = await asyncMap(
      range(firstPage, lastPage),
      (i: number) => {
        return this.makeRequest(url, params, perPage, i + 1, fetchOptions);
      },
    );

    return flatMap([firstRes, ...restRes], (res: ISbFlatMapped) =>
      Object.values(res.data[e]));
  }

  public post(
    slug: string,
    params: ISbStoriesParams | ISbContentMangmntAPI = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResponse> {
    const url = `/${slug}`;

    const rateLimit = determineRateLimit(undefined, undefined, this.rateLimitConfig, MANAGEMENT_API_DEFAULT_RATE_LIMIT);
    return this.throttleManager.execute(rateLimit, 'post', url, params, fetchOptions) as Promise<ISbResponse>;
  }

  public put(
    slug: string,
    params: ISbStoriesParams | ISbContentMangmntAPI = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResponse> {
    const url = `/${slug}`;

    const rateLimit = determineRateLimit(undefined, undefined, this.rateLimitConfig, MANAGEMENT_API_DEFAULT_RATE_LIMIT);
    return this.throttleManager.execute(rateLimit, 'put', url, params, fetchOptions) as Promise<ISbResponse>;
  }

  public patch(
    slug: string,
    params: ISbStoriesParams | ISbContentMangmntAPI = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResponse> {
    const url = `/${slug}`;

    const rateLimit = determineRateLimit(undefined, undefined, this.rateLimitConfig, MANAGEMENT_API_DEFAULT_RATE_LIMIT);
    return this.throttleManager.execute(rateLimit, 'patch', url, params, fetchOptions) as Promise<ISbResponse>;
  }

  public delete(
    slug: string,
    params: ISbStoriesParams | ISbContentMangmntAPI = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResponse> {
    if (!params) {
      params = {} as ISbStoriesParams;
    }
    const url = `/${slug}`;

    const rateLimit = determineRateLimit(undefined, undefined, this.rateLimitConfig, MANAGEMENT_API_DEFAULT_RATE_LIMIT);
    return this.throttleManager.execute(rateLimit, 'delete', url, params, fetchOptions) as Promise<ISbResponse>;
  }

  public getStories(
    params: ISbStoriesParams = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbStories> {
    this._addResolveLevel(params);

    return this.get('cdn/stories', params, fetchOptions);
  }

  public getStory(
    slug: string,
    params: ISbStoryParams = {},
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbStory> {
    this._addResolveLevel(params);

    return this.get(`cdn/stories/${slug}`, params, fetchOptions);
  }

  private getToken(): string {
    return this.accessToken;
  }

  public ejectInterceptor(): void {
    this.client.eject();
  }

  private _addResolveLevel(params: ISbStoriesParams | ISbStoryParams): void {
    if (typeof params.resolve_relations !== 'undefined') {
      params.resolve_level = 2;
    }
  }

  private _cleanCopy(value: LinksType): JSON {
    return JSON.parse(JSON.stringify(value));
  }

  private _insertLinks(
    jtree: ISbStoriesParams,
    treeItem: keyof ISbStoriesParams,
    resolveId: string,
  ): void {
    const node = jtree[treeItem];

    if (
      node
      && node.fieldtype === 'multilink'
      && node.linktype === 'story'
      && typeof node.id === 'string'
      && this.links[resolveId][node.id]
    ) {
      node.story = this._cleanCopy(this.links[resolveId][node.id]);
    }
    else if (
      node
      && node.linktype === 'story'
      && typeof node.uuid === 'string'
      && this.links[resolveId][node.uuid]
    ) {
      node.story = this._cleanCopy(this.links[resolveId][node.uuid]);
    }
  }

  /**
   *
   * @param resolveId A counter number as a string
   * @param uuid The uuid of the story
   * @returns string | object
   */
  private getStoryReference(resolveId: string, uuid: string): string | JSON {
    const result = this.relations[resolveId][uuid]
      ? JSON.parse(this.stringifiedStoriesCache[uuid] || JSON.stringify(this.relations[resolveId][uuid]))
      : uuid;
    return result;
  }

  /**
   * Resolves a field's value by replacing UUIDs with their corresponding story references
   * @param jtree - The JSON tree object containing the field to resolve
   * @param treeItem - The key of the field to resolve
   * @param resolveId - The unique identifier for the current resolution context
   *
   * This method handles both single string UUIDs and arrays of UUIDs:
   * - For single strings: directly replaces the UUID with the story reference
   * - For arrays: maps through each UUID and replaces with corresponding story references
   */
  private _resolveField(
    jtree: ISbStoriesParams,
    treeItem: keyof ISbStoriesParams,
    resolveId: string,
  ): void {
    const item = jtree[treeItem];
    if (typeof item === 'string') {
      jtree[treeItem] = this.getStoryReference(resolveId, item);
    }
    else if (Array.isArray(item)) {
      jtree[treeItem] = item.map(uuid =>
        this.getStoryReference(resolveId, uuid),
      ).filter(Boolean);
    }
  }

  /**
   * Inserts relations into the JSON tree by resolving references
   * @param jtree - The JSON tree object to process
   * @param treeItem - The current field being processed
   * @param fields - The relation patterns to resolve (string or array of strings)
   * @param resolveId - The unique identifier for the current resolution context
   *
   * This method handles two types of relation patterns:
   * 1. Nested relations: matches fields that end with the current field name
   *    Example: If treeItem is "event_type", it matches patterns like "*.event_type"
   *
   * 2. Direct component relations: matches exact component.field patterns
   *    Example: "event.event_type" for component "event" and field "event_type"
   *
   * The method supports both string and array formats for the fields parameter,
   * allowing flexible specification of relation patterns.
   */
  private _insertRelations(
    jtree: ISbStoriesParams,
    treeItem: keyof ISbStoriesParams,
    fields: string | string[],
    resolveId: string,
  ): void {
    // Check for nested relations (e.g., "*.event_type" or "spots.event_type")
    const fieldPattern = Array.isArray(fields)
      ? fields.find(f => f.endsWith(`.${treeItem}`))
      : fields.endsWith(`.${treeItem}`);

    if (fieldPattern) {
      // If we found a matching pattern, resolve this field
      this._resolveField(jtree, treeItem, resolveId);
      return;
    }

    // If no nested pattern matched, check for direct component.field pattern
    // e.g., "event.event_type" for a field within its immediate parent component
    const fieldPath = jtree.component ? `${jtree.component}.${treeItem}` : treeItem;
    // Check if this exact pattern exists in the fields to resolve
    if (Array.isArray(fields) ? fields.includes(fieldPath) : fields === fieldPath) {
      this._resolveField(jtree, treeItem, resolveId);
    }
  }

  /**
   * Recursively traverses and resolves relations in the story content tree
   * @param story - The story object containing the content to process
   * @param fields - The relation patterns to resolve
   * @param resolveId - The unique identifier for the current resolution context
   */
  private iterateTree(
    story: ISbStoryData,
    fields: string | Array<string>,
    resolveId: string,
  ): void {
    // Internal recursive function to process each node in the tree
    const enrich = (jtree: ISbStoriesParams | any, path = '') => {
      // Skip processing if node is null/undefined or marked to stop resolving
      if (!jtree || jtree._stopResolving) {
        return;
      }

      // Handle arrays by recursively processing each element
      // Maintains path context by adding array indices
      if (Array.isArray(jtree)) {
        jtree.forEach((item, index) => enrich(item, `${path}[${index}]`));
      }
      // Handle object nodes
      else if (typeof jtree === 'object') {
        // Process each property in the object
        for (const key in jtree) {
          // Build the current path for the context
          const newPath = path ? `${path}.${key}` : key;

          // If this is a component (has component and _uid) or a link,
          // attempt to resolve its relations and links
          if ((jtree.component && jtree._uid) || jtree.type === 'link') {
            this._insertRelations(jtree, key as keyof ISbStoriesParams, fields, resolveId);
            this._insertLinks(jtree, key as keyof ISbStoriesParams, resolveId);
          }

          // Continue traversing deeper into the tree
          // This ensures we process nested components and their relations
          enrich(jtree[key], newPath);
        }
      }
    };

    // Start the traversal from the story's content
    enrich(story.content);
  }

  private async resolveLinks(
    responseData: ISbResponseData,
    params: ISbStoriesParams,
    resolveId: string,
  ): Promise<void> {
    let links: (ISbStoryData | ISbLinkURLObject | string)[] = [];

    if (responseData.link_uuids) {
      const relSize = responseData.link_uuids.length;
      const chunks = [];
      const chunkSize = 50;

      for (let i = 0; i < relSize; i += chunkSize) {
        const end = Math.min(relSize, i + chunkSize);
        chunks.push(responseData.link_uuids.slice(i, end));
      }

      for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
        const linksRes = await this.getStories({
          per_page: chunkSize,
          language: params.language,
          version: params.version,
          starts_with: params.starts_with,
          by_uuids: chunks[chunkIndex].join(','),
        });

        linksRes.data.stories.forEach(
          (rel: ISbStoryData | ISbLinkURLObject | string) => {
            links.push(rel);
          },
        );
      }
    }
    else {
      links = responseData.links;
    }

    links.forEach((story: ISbStoryData | any) => {
      this.links[resolveId][story.uuid] = {
        ...story,
        ...{ _stopResolving: true },
      };
    });
  }

  private async resolveRelations(
    responseData: ISbResponseData,
    params: ISbStoriesParams,
    resolveId: string,
  ): Promise<void> {
    let relations: ISbStoryData<ISbComponentType<string> & { [index: string]: any }>[] = [];

    if (responseData.rel_uuids) {
      const relSize = responseData.rel_uuids.length;
      const chunks = [];
      const chunkSize = 50;

      for (let i = 0; i < relSize; i += chunkSize) {
        const end = Math.min(relSize, i + chunkSize);
        chunks.push(responseData.rel_uuids.slice(i, end));
      }

      for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
        const relationsRes = await this.getStories({
          per_page: chunkSize,
          language: params.language,
          version: params.version,
          starts_with: params.starts_with,
          by_uuids: chunks[chunkIndex].join(','),
          excluding_fields: params.excluding_fields,
        });

        relationsRes.data.stories.forEach((rel: ISbStoryData) => {
          relations.push(rel);
        });
      }

      // Replace rel_uuids with the fully resolved stories and clear it
      if (relations.length > 0) {
        responseData.rels = relations;
        delete responseData.rel_uuids;
      }
    }
    else {
      relations = responseData.rels;
    }

    if (relations && relations.length > 0) {
      relations.forEach((story: ISbStoryData) => {
        this.relations[resolveId][story.uuid] = {
          ...story,
          ...{ _stopResolving: true },
        };
      });
    }
  }

  /**
   *
   * @param responseData
   * @param params
   * @param resolveId
   * @description Resolves the relations and links of the stories
   * @returns Promise<void>
   *
   */
  private async resolveStories(
    responseData: ISbResponseData,
    params: ISbStoriesParams,
    resolveId: string,
  ): Promise<void> {
    let relationParams: string[] = [];

    this.links[resolveId] = {};
    this.relations[resolveId] = {};

    if (
      typeof params.resolve_relations !== 'undefined'
      && params.resolve_relations.length > 0
    ) {
      if (typeof params.resolve_relations === 'string') {
        relationParams = params.resolve_relations.split(',');
      }
      await this.resolveRelations(responseData, params, resolveId);
    }

    if (
      params.resolve_links
      && ['1', 'story', 'url', 'link'].includes(params.resolve_links)
      && (responseData.links?.length || responseData.link_uuids?.length)
    ) {
      await this.resolveLinks(responseData, params, resolveId);
    }

    if (this.resolveNestedRelations) {
      for (const relUuid in this.relations[resolveId]) {
        this.iterateTree(
          this.relations[resolveId][relUuid],
          relationParams,
          resolveId,
        );
      }
    }

    if (responseData.story) {
      this.iterateTree(responseData.story, relationParams, resolveId);
    }
    else {
      responseData.stories.forEach((story: ISbStoryData) => {
        this.iterateTree(story, relationParams, resolveId);
      });
    }

    this.stringifiedStoriesCache = {};

    delete this.links[resolveId];
    delete this.relations[resolveId];
  }

  private async cacheResponse(
    url: string,
    params: ISbStoriesParams,
    retries?: number,
    fetchOptions?: ISbCustomFetch,
  ): Promise<ISbResult> {
    const cacheKey = stringify({ url, params });
    const provider = this.cacheProvider();

    // Check in-memory cache first for published content
    // If cached, skip API call and rate limiting entirely
    if (params.version === 'published' && url !== '/cdn/spaces/me') {
      const cache = await provider.get(cacheKey);
      if (cache) {
        return Promise.resolve(cache);
      }
    }

    // Calculate appropriate rate limit for this request
    // For Management API requests (non-CDN URLs), use the MAPI rate limit
    const isMapi = !isCDNUrl(url) && this.rateLimitConfig.isManagementApi;
    const defaultLimit = isMapi ? MANAGEMENT_API_DEFAULT_RATE_LIMIT : undefined;
    const rateLimit = determineRateLimit(url, params, this.rateLimitConfig, defaultLimit);

    return new Promise(async (resolve, reject) => {
      try {
        // Execute through the appropriate throttle queue based on rate limit
        const res = (await this.throttleManager.execute(
          rateLimit,
          'get',
          url,
          params,
          fetchOptions,
        )) as ISbResponse;
        if (res.status !== 200) {
          return reject(res);
        }

        let response = { data: res.data, headers: res.headers } as ISbResult;

        // Parse rate limit headers and update config if present
        const rateLimitHeaders = parseRateLimitHeaders(res.headers);
        if (rateLimitHeaders?.max !== undefined) {
          // Update server rate limit for subsequent requests
          this.rateLimitConfig.serverHeadersRateLimit = rateLimitHeaders.max;
        }

        if (res.headers?.['per-page']) {
          response = Object.assign({}, response, {
            perPage: res.headers['per-page']
              ? Number.parseInt(res.headers['per-page'])
              : 0,
            total: res.headers['per-page']
              ? Number.parseInt(res.headers.total)
              : 0,
          });
        }

        if (response.data.story || response.data.stories) {
          const resolveId = (this.resolveCounter
            = ++this.resolveCounter % 1000);
          await this.resolveStories(response.data, params, `${resolveId}`);
          response = await this.processInlineAssets(response);
        }

        if (params.version === 'published' && url !== '/cdn/spaces/me') {
          await provider.set(cacheKey, response);
        }

        const isCacheClearable = (this.cache.clear === 'onpreview' && params.version === 'draft')
          || this.cache.clear === 'auto';

        if (params.token && response.data.cv) {
          if (isCacheClearable
            && cacheVersions[params.token] // there is a cache
            && cacheVersions[params.token] !== response.data.cv // a new cv is incoming
          ) {
            await this.flushCache();
          }
          cacheVersions[params.token] = response.data.cv;
        }

        return resolve(response);
      }
      catch (error: Error | any) {
        if (error.response && error.status === 429) {
          retries = typeof retries === 'undefined' ? 0 : retries + 1;

          if (retries < this.maxRetries) {
            // eslint-disable-next-line no-console
            console.log(
              `Hit rate limit. Retrying in ${this.retriesDelay / 1000} seconds.`,
            );
            await delay(this.retriesDelay);
            return this.cacheResponse(url, params, retries)
              .then(resolve)
              .catch(reject);
          }
        }
        reject(error);
      }
    });
  }

  private throttledRequest(
    type: Method,
    url: string,
    params: ISbStoriesParams,
    fetchOptions?: ISbCustomFetch,
  ): Promise<unknown> {
    this.client.setFetchOptions(fetchOptions);
    return this.client[type](url, params);
  }

  public cacheVersions(): CachedVersions {
    return cacheVersions;
  }

  public cacheVersion(): number {
    return cacheVersions[this.accessToken];
  }

  public setCacheVersion(cv: number): void {
    if (this.accessToken) {
      cacheVersions[this.accessToken] = cv;
    }
  }

  public clearCacheVersion(): void {
    if (this.accessToken) {
      cacheVersions[this.accessToken] = 0;
    }
  }

  public cacheProvider(): ICacheProvider {
    switch (this.cache.type) {
      case 'memory':
        return {
          get(key: string) {
            return Promise.resolve(memory[key]);
          },
          getAll() {
            return Promise.resolve(memory as IMemoryType);
          },
          set(key: string, content: ISbResult) {
            memory[key] = content;
            return Promise.resolve(undefined);
          },
          flush() {
            memory = {};
            return Promise.resolve(undefined);
          },
        };
      case 'custom':
        if (this.cache.custom) {
          return this.cache.custom;
        }
      // eslint-disable-next-line no-fallthrough
      default:
        return {
          get() {
            return Promise.resolve();
          },
          getAll() {
            return Promise.resolve(undefined);
          },
          set() {
            return Promise.resolve(undefined);
          },
          flush() {
            return Promise.resolve(undefined);
          },
        };
    }
  }

  public async flushCache(): Promise<this> {
    await this.cacheProvider().flush();
    this.clearCacheVersion();
    return this;
  }

  private async processInlineAssets(response: ISbResult): Promise<ISbResult> {
    if (!this.inlineAssets) {
      return response;
    }

    const processNode = (node: ISbField): unknown => {
      if (!node || typeof node !== 'object') {
        return node;
      }

      if (Array.isArray(node)) {
        return node.map(item => processNode(item));
      }

      let processedNode = { ...node };
      if (processedNode.fieldtype === 'asset' && Array.isArray(response.data.assets)) {
        // Enrich the asset with an actual asset object
        processedNode = {
          ...response.data.assets.find((asset: any) => asset.id === processedNode.id),
          ...processedNode,
        };
      }

      // Recursively process all properties
      for (const key in processedNode) {
        if (typeof processedNode[key] === 'object') {
          processedNode[key] = processNode(processedNode[key] as ISbField);
        }
      }

      return processedNode;
    };

    // Process the story content
    if (response.data.story) {
      response.data.story.content = processNode(response.data.story.content);
    }

    // Process all stories if present
    if (response.data.stories) {
      response.data.stories = response.data.stories.map((story: any) => {
        story.content = processNode(story.content);
        return story;
      });
    }

    return response;
  }
}

export default Storyblok;
