/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import _ from 'lodash';
import {mergeTranslations} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
import {CURRENT_VERSION_NAME} from './constants';
import {
  collectSidebarCategories,
  transformSidebarItems,
  collectSidebarLinks,
  collectSidebarDocItems,
  collectSidebarRefs,
} from './sidebars/utils';
import type {
  LoadedVersion,
  LoadedContent,
} from '@docusaurus/plugin-content-docs';
import type {
  Sidebar,
  SidebarItemCategory,
  SidebarItemCategoryLink,
  Sidebars,
} from './sidebars/types';
import type {
  TranslationFileContent,
  TranslationFile,
  TranslationMessage,
} from '@docusaurus/types';

function getVersionFileName(versionName: string): string {
  if (versionName === CURRENT_VERSION_NAME) {
    return versionName;
  }
  // I don't like this "version-" prefix,
  // but it's for consistency with site/versioned_docs
  return `version-${versionName}`;
}

type TranslationMessageEntry = [string, TranslationMessage];

function ensureNoSidebarDuplicateEntries(
  translationEntries: TranslationMessageEntry[],
): void {
  const grouped = _.groupBy(translationEntries, (entry) => entry[0]);
  const duplicates = Object.entries(grouped).filter(
    (entry) => entry[1].length > 1,
  );

  if (duplicates.length > 0) {
    throw new Error(`Multiple docs sidebar items produce the same translation key.
- ${duplicates
      .map(([translationKey, entries]) => {
        return `${logger.code(translationKey)}: ${logger.num(
          entries.length,
        )} duplicates found:\n  - ${entries
          .map((duplicate) => {
            const desc = duplicate[1].description;
            return `${logger.name(duplicate[1].message)} ${
              desc ? `(${logger.subdue(desc)})` : ''
            }`;
          })
          .join('\n  - ')}`;
      })
      .join('\n\n- ')}

To avoid translation key conflicts, use the ${logger.code(
      'key',
    )} attribute on the sidebar items above to uniquely identify them.

When using autogenerated sidebars, you can provide a unique translation key by adding:
- the ${logger.code('key')} attribute to category item metadata (${logger.code(
      '_category_.json',
    )} / ${logger.code('_category_.yml')})
- the ${logger.code(
      'sidebar_key',
    )} attribute to doc item metadata (front matter in ${logger.code(
      'Category/index.mdx',
    )})`);
  }
}

function getSidebarTranslationFileContent(
  sidebar: Sidebar,
  sidebarName: string,
): TranslationFileContent {
  const categories = collectSidebarCategories(sidebar);

  const categoryEntries: TranslationMessageEntry[] = categories.flatMap(
    (category) => {
      const entries: TranslationMessageEntry[] = [];
      const categoryKey = category.key ?? category.label;

      entries.push([
        `sidebar.${sidebarName}.category.${categoryKey}`,
        {
          message: category.label,
          description: `The label for category '${category.label}' in sidebar '${sidebarName}'`,
        },
      ]);

      if (category.link?.type === 'generated-index') {
        if (category.link.title) {
          entries.push([
            `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.title`,
            {
              message: category.link.title,
              description: `The generated-index page title for category '${category.label}' in sidebar '${sidebarName}'`,
            },
          ]);
        }
        if (category.link.description) {
          entries.push([
            `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.description`,
            {
              message: category.link.description,
              description: `The generated-index page description for category '${category.label}' in sidebar '${sidebarName}'`,
            },
          ]);
        }
      }

      return entries;
    },
  );

  const links = collectSidebarLinks(sidebar);
  const linksEntries: TranslationMessageEntry[] = links.map((link) => {
    const linkKey = link.key ?? link.label;
    return [
      `sidebar.${sidebarName}.link.${linkKey}`,
      {
        message: link.label,
        description: `The label for link '${link.label}' in sidebar '${sidebarName}', linking to '${link.href}'`,
      },
    ];
  });

  const docs = collectSidebarDocItems(sidebar)
    .concat(collectSidebarRefs(sidebar))
    .filter((item) => item.translatable);
  const docLinksEntries: TranslationMessageEntry[] = docs.map((doc) => {
    const docKey = doc.key ?? doc.label!;
    return [
      `sidebar.${sidebarName}.doc.${docKey}`,
      {
        message: doc.label!,
        description: `The label for the doc item '${doc.label!}' in sidebar '${sidebarName}', linking to the doc ${
          doc.id
        }`,
      },
    ];
  });

  const allEntries = [...categoryEntries, ...linksEntries, ...docLinksEntries];
  ensureNoSidebarDuplicateEntries(allEntries);
  return Object.fromEntries(allEntries);
}

function translateSidebar({
  sidebar,
  sidebarName,
  sidebarsTranslations,
}: {
  sidebar: Sidebar;
  sidebarName: string;
  sidebarsTranslations: TranslationFileContent;
}): Sidebar {
  function transformSidebarCategoryLink(
    category: SidebarItemCategory,
  ): SidebarItemCategoryLink | undefined {
    if (!category.link) {
      return undefined;
    }
    if (category.link.type === 'generated-index') {
      const categoryKey = category.key ?? category.label;
      const title =
        sidebarsTranslations[
          `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.title`
        ]?.message ?? category.link.title;
      const description =
        sidebarsTranslations[
          `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.description`
        ]?.message ?? category.link.description;
      return {
        ...category.link,
        title,
        description,
      };
    }
    return category.link;
  }

  return transformSidebarItems(sidebar, (item) => {
    if (item.type === 'category') {
      const link = transformSidebarCategoryLink(item);
      const categoryKey = item.key ?? item.label;
      return {
        ...item,
        label:
          sidebarsTranslations[`sidebar.${sidebarName}.category.${categoryKey}`]
            ?.message ?? item.label,
        ...(link && {link}),
      };
    }
    if (item.type === 'link') {
      const linkKey = item.key ?? item.label;
      return {
        ...item,
        label:
          sidebarsTranslations[`sidebar.${sidebarName}.link.${linkKey}`]
            ?.message ?? item.label,
      };
    }
    if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
      const docKey = item.key ?? item.label!;
      return {
        ...item,
        label:
          sidebarsTranslations[`sidebar.${sidebarName}.doc.${docKey}`]
            ?.message ?? item.label,
      };
    }
    return item;
  });
}

function getSidebarsTranslations(
  version: LoadedVersion,
): TranslationFileContent {
  return mergeTranslations(
    Object.entries(version.sidebars).map(([sidebarName, sidebar]) =>
      getSidebarTranslationFileContent(sidebar, sidebarName),
    ),
  );
}
function translateSidebars(
  version: LoadedVersion,
  sidebarsTranslations: TranslationFileContent,
): Sidebars {
  return _.mapValues(version.sidebars, (sidebar, sidebarName) =>
    translateSidebar({
      sidebar,
      sidebarName,
      sidebarsTranslations,
    }),
  );
}

function getVersionTranslationFiles(version: LoadedVersion): TranslationFile[] {
  const versionTranslations: TranslationFileContent = {
    'version.label': {
      message: version.label,
      description: `The label for version ${version.versionName}`,
    },
  };

  const sidebarsTranslations: TranslationFileContent =
    getSidebarsTranslations(version);

  return [
    {
      path: getVersionFileName(version.versionName),
      content: mergeTranslations([versionTranslations, sidebarsTranslations]),
    },
  ];
}

// TODO Docusaurus v4 or later
//  this temporarily works, but it is not an ideal solution
//  The docs navigation can be computed and shouldn't be part of LoadedVersion
//  We need to derive the navigation from already translated content
//  See https://github.com/facebook/docusaurus/pull/11794
function translateDocNavigation(
  docs: LoadedVersion['docs'],
  translatedSidebars: Sidebars,
): LoadedVersion['docs'] {
  // Build a map of permalink -> translated label for generated-index categories
  const translatedLabelByPermalink = new Map<string, string>();
  for (const sidebar of Object.values(translatedSidebars)) {
    for (const category of collectSidebarCategories(sidebar)) {
      if (category.link?.type === 'generated-index') {
        translatedLabelByPermalink.set(category.link.permalink, category.label);
      }
    }
  }

  if (translatedLabelByPermalink.size === 0) {
    return docs;
  }

  return docs.map((doc) => {
    const previous =
      doc.previous && translatedLabelByPermalink.has(doc.previous.permalink)
        ? {
            ...doc.previous,
            title: translatedLabelByPermalink.get(doc.previous.permalink)!,
          }
        : doc.previous;
    const next =
      doc.next && translatedLabelByPermalink.has(doc.next.permalink)
        ? {
            ...doc.next,
            title: translatedLabelByPermalink.get(doc.next.permalink)!,
          }
        : doc.next;
    if (previous === doc.previous && next === doc.next) {
      return doc;
    }
    return {...doc, previous, next};
  });
}

function translateVersion(
  version: LoadedVersion,
  translationFiles: {[fileName: string]: TranslationFile},
): LoadedVersion {
  const versionTranslations =
    translationFiles[getVersionFileName(version.versionName)]!.content;
  const translatedSidebars = translateSidebars(version, versionTranslations);
  return {
    ...version,
    label: versionTranslations['version.label']?.message ?? version.label,
    sidebars: translatedSidebars,
    docs: translateDocNavigation(version.docs, translatedSidebars),
  };
}

function getVersionsTranslationFiles(
  versions: LoadedVersion[],
): TranslationFile[] {
  return versions.flatMap(getVersionTranslationFiles);
}
function translateVersions(
  versions: LoadedVersion[],
  translationFiles: {[fileName: string]: TranslationFile},
): LoadedVersion[] {
  return versions.map((version) => translateVersion(version, translationFiles));
}

export function getLoadedContentTranslationFiles(
  loadedContent: LoadedContent,
): TranslationFile[] {
  return getVersionsTranslationFiles(loadedContent.loadedVersions);
}
export function translateLoadedContent(
  loadedContent: LoadedContent,
  translationFiles: TranslationFile[],
): LoadedContent {
  const translationFilesMap: {[fileName: string]: TranslationFile} = _.keyBy(
    translationFiles,
    (f) => f.path,
  );

  return {
    loadedVersions: translateVersions(
      loadedContent.loadedVersions,
      translationFilesMap,
    ),
  };
}
