import { FileSystem } from '@/source/storage/file-system';
import { normalize } from '@/source/path';
import type { ResolvedLoaderConfig } from '../loader';
import { isStaticSource, MetaData, PageData, type StaticSource } from '../source';

export type ContentStorage<
  P extends ContentStoragePageFile = ContentStoragePageFile,
  M extends ContentStorageMetaFile = ContentStorageMetaFile,
> = FileSystem<P | M> & {
  /** always `undefined`, for inferring types only */
  $inferPage: P;
  /** always `undefined`, for inferring types only */
  $inferMeta: M;
};

/** @internal */
export interface ContentStorageMetaFile<
  Type extends string | undefined = string | undefined,
  Data extends MetaData = MetaData,
> {
  type: Type;
  path: string;
  absolutePath?: string;

  format: 'meta';
  data: Data;
}

/** @internal */
export interface ContentStoragePageFile<
  Type extends string | undefined = string | undefined,
  Data extends PageData = PageData,
> {
  type: Type;
  path: string;
  absolutePath?: string;

  format: 'page';
  slugs: string[];
  data: Data;
}

const EmptyLang = Symbol();

/**
 * convert input files into virtual file system.
 *
 * in the storage, locale codes are removed from file paths, hence the same file will have same file paths in every storage.
 */
export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig) {
  const { input, plugins, i18n } = loaderConfig;

  let parser: (normalizedPath: string) => [string, (string | string[])?];
  if (!i18n) {
    parser = (path) => [path];
  } else if (i18n.parser === 'dir') {
    const langSet = new Set(i18n.languages);

    parser = (path) => {
      const [locale, ...segs] = path.split('/');
      if (!locale || segs.length === 0) return [path];

      if (langSet.has(locale)) return [segs.join('/'), locale];
      if (locale === '$') return [segs.join('/'), i18n.languages];

      return [path];
    };
  } else {
    const langSet = new Set(i18n.languages);

    parser = (path) => {
      const segs = path.split('/');
      const base = segs.pop();
      if (!base) return [path];

      const parts = base.split('.');
      if (parts.length < 3) return [path];

      const [locale] = parts.splice(parts.length - 2, 1);
      segs.push(parts.join('.'));

      if (langSet.has(locale)) return [segs.join('/'), locale];
      if (locale === '$') return [segs.join('/'), i18n.languages];

      return [path];
    };
  }

  const fileMap = new Map<
    string | typeof EmptyLang,
    [storageKey: string, file: ContentStorageMetaFile | ContentStoragePageFile][]
  >();

  function scan(type: string | undefined, source: StaticSource) {
    for (const inputFile of source.files) {
      let file: ContentStorageMetaFile | ContentStoragePageFile;

      if (inputFile.type === 'page') {
        file = {
          format: 'page',
          type,
          path: normalize(inputFile.path),
          // will be generated by the slugs plugin if unspecified
          slugs: inputFile.slugs as string[],
          data: inputFile.data,
          absolutePath: inputFile.absolutePath,
        };
      } else {
        file = {
          format: 'meta',
          type,
          path: normalize(inputFile.path),
          absolutePath: inputFile.absolutePath,
          data: inputFile.data,
        };
      }

      const [storageKey, locale = i18n ? i18n.defaultLanguage : EmptyLang] = parser(file.path);
      const entry = [storageKey, file] as const;
      if (Array.isArray(locale)) {
        for (const item of locale) pushMapList(fileMap, item, entry);
      } else {
        pushMapList(fileMap, locale, entry);
      }
    }
  }

  if (isStaticSource(input)) {
    scan(undefined, input);
  } else {
    for (const k in input) scan(k, input[k]);
  }

  function makeStorage(locale: string | typeof EmptyLang, inherit?: ContentStorage) {
    const storage = new FileSystem(inherit) as ContentStorage;
    for (const [storageKey, file] of fileMap.get(locale) ?? []) {
      storage.write(storageKey, file);
    }

    const context = { storage };
    for (const plugin of plugins) {
      plugin.transformStorage?.(context);
    }

    return storage;
  }

  return {
    i18n(): Record<string, ContentStorage> {
      const storages: Record<string, ContentStorage> = {};
      if (!i18n) return storages;

      const fallbackLang =
        i18n.fallbackLanguage !== null ? (i18n.fallbackLanguage ?? i18n.defaultLanguage) : null;

      function scan(lang: string): ContentStorage {
        if (storages[lang]) return storages[lang];

        return (storages[lang] = makeStorage(
          lang,
          fallbackLang && fallbackLang !== lang ? scan(fallbackLang) : undefined,
        ));
      }

      for (const lang of i18n.languages) scan(lang);
      return storages;
    },
    single(): ContentStorage {
      return makeStorage(EmptyLang);
    },
  };
}

function pushMapList<K, V>(map: Map<K, V[]>, k: K, v: V) {
  let list = map.get(k);
  if (!list) {
    list = [];
    map.set(k, list);
  }
  list.push(v);
}
