import { dirname } from 'node:path';
import { readdir as _readDir } from 'node:fs/promises';
import { name, singleton, autoService, location } from 'knifecycle';
import { YError, printStackTrace } from 'yerror';
import { type ResolveService, type LogService } from 'common-services';

/* Architecture Note #2.9.6.1: Plugins resolution

Whook auto loader can look for initializers in a list of
 plugins defined in the `WHOOK_PLUGINS` constant. This
 service checks their existence and computes once for all
 the details needed by the framework (to find services/handlers
 it provides).
*/

export const WHOOK_PROJECT_PLUGIN_NAME: WhookPluginName = '__project__';
export const WHOOK_DEFAULT_PLUGINS = [
  WHOOK_PROJECT_PLUGIN_NAME,
  '@whook/whook',
];
export const WHOOK_FOLDERS = [
  'routes',
  'services',
  'wrappers',
  'consumers',
  'transformers',
  'commands',
  'crons',
] as const;

export type WhookPluginName = string & { _type?: 'whook_plugin' };
export type WhookURL = string & { _type?: 'whook_url' };
export type WhookPluginFolder = (typeof WHOOK_FOLDERS)[number];
export type WhookPluginsService = WhookPluginName[];
export type WhookResolvedPlugin = {
  mainURL: WhookURL;
  types: WhookPluginFolder[];
};
export type WhookResolvedPluginsService = Record<
  WhookPluginName,
  WhookResolvedPlugin
>;
export type WhookResolvedPluginsConfig = {
  WHOOK_PLUGINS?: WhookPluginsService;
};
export type WhookResolvedPluginsDependencies = WhookResolvedPluginsConfig & {
  MAIN_FILE_URL: WhookURL;
  resolve: ResolveService;
  readDir: (path: URL) => Promise<string[]>;
  log: LogService;
};

export default location(
  singleton(
    name('WHOOK_RESOLVED_PLUGINS', autoService(initWhookResolvedPlugins)),
  ),
  import.meta.url,
);

/**
 * Resolves the Whook plugins from their names
 * @param  {Object}   services
 * The service dependencies
 * @param  {Array<String>}   [services.WHOOK_PLUGINS]
 * The activated plugins
 * @param  {Object}   [services.log=noop]
 * An optional logging service
 * @return {Promise<string>}
 * A promise of a number representing the actual port.
 */
async function initWhookResolvedPlugins({
  MAIN_FILE_URL,
  WHOOK_PLUGINS = WHOOK_DEFAULT_PLUGINS,
  resolve,
  readDir = _readDir,
  log,
}: WhookResolvedPluginsDependencies): Promise<WhookResolvedPluginsService> {
  const resolvedPlugins: WhookResolvedPluginsService = {};

  for (const pluginName of WHOOK_PLUGINS) {
    try {
      const mainURL =
        pluginName === WHOOK_PROJECT_PLUGIN_NAME
          ? MAIN_FILE_URL
          : resolve(pluginName);
      const types: WhookPluginFolder[] = [];

      resolvedPlugins[pluginName] = {
        mainURL,
        types,
      };
    } catch (err) {
      log('error', `❌ - Plugin "${pluginName}" couldn't be resolved.`);
      log('error-stack', printStackTrace(err));
      throw YError.wrap(err as Error, 'E_BAD_WHOOK_PLUGIN', [pluginName]);
    }
  }

  await Promise.all(
    WHOOK_PLUGINS.map(async (pluginName) => {
      const sourceDirectory = dirname(resolvedPlugins[pluginName].mainURL);

      try {
        for (const file of await readDir(new URL(sourceDirectory || ''))) {
          if (WHOOK_FOLDERS.includes(file as WhookPluginFolder)) {
            resolvedPlugins[pluginName].types.push(file as WhookPluginFolder);
          }
        }
      } catch (err) {
        log(
          'error',
          `🚫 - Plugin directory doesn't exist "${sourceDirectory}".`,
        );
        log('debug-stack', printStackTrace(err));
        throw YError.wrap(err as Error, 'E_BAD_PLUGIN_DIR', [
          sourceDirectory,
          resolvedPlugins[pluginName],
          MAIN_FILE_URL,
        ]);
      }
      log(
        'debug',
        `➰ - Plugin "${pluginName}" source path resolved to "${sourceDirectory}" with "${resolvedPlugins[
          pluginName
        ].types.join(', ')}" types.`,
      );
    }),
  );

  return resolvedPlugins;
}
