import { dirname } from 'path';

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { join, json, logging, normalize, relative, workspaces } from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';

import {
  AppModule,
  PersistenceStrategy,
  SerializationOptions,
  SerializationStrategy,
  TargetInfo,
  TargetPathBuilder,
  TranslationDeserializer,
  TranslationSerializer,
  TranslationSource,
  TranslationTarget,
  TranslationTargetRegistry,
  WorkspaceHost,
  Xlf2Deserializer,
  Xlf2Serializer,
  XlfDeserializer,
  XlfSerializer,
  XmlParser,
} from '../../server';

import { AngularI18n, AngularJsonPersistenceStrategy } from './persistence';
import { Schema as Options } from './schema';

export * from '../../server';
export * from './persistence';
export { Schema as t9nOptions } from './schema';

export default createBuilder<Options & json.JsonObject, BuilderOutput>(t9n);

export async function t9n(options: Options, context: BuilderContext): Promise<BuilderOutput> {
  if (!context.target) {
    throw new Error('To run this builder context.target is required!');
  }

  const nodeHost = new NodeJsSyncHost();
  const host = workspaces.createWorkspaceHost(nodeHost);
  const workspaceRoot = normalize(context.workspaceRoot);
  const sourceFile = join(workspaceRoot, options.translationFile);
  const targetTranslationPath = options.targetTranslationPath || dirname(options.translationFile);
  const targetDirectory = join(workspaceRoot, targetTranslationPath);
  context.logger.info('angular-t9n');
  context.logger.info('===========');
  context.logger.info(` - workspace root:   ${workspaceRoot}`);
  context.logger.info(` - source file:      ${sourceFile}`);
  context.logger.info(` - target directory: ${targetDirectory}`);
  context.logger.info('');

  if (!(await host.isFile(sourceFile))) {
    return { success: false, error: `${options.translationFile} does not exist or is not a file!` };
  } else if (!(await host.isDirectory(targetDirectory))) {
    return {
      success: false,
      error: `targetTranslationPath ${targetTranslationPath} is not a valid directory!`,
    };
  }

  const xliffVersion = await detectXliffVersion();
  context.logger.info(`Detected version ${xliffVersion} of XLIFF`);
  const targetPathBuilder = new TargetPathBuilder(targetDirectory, sourceFile);
  let translationContext: {
    source: TranslationSource;
    targetRegistry: TranslationTargetRegistry;
  } = null!;
  const angularI18n = new AngularI18n(
    host,
    workspaceRoot,
    context.target.project,
    targetPathBuilder,
    () => translationContext,
  );
  const sourceLocale = await angularI18n.sourceLocale();

  context.logger.info(`Loading translations. Depending on the amount, this might take a moment.`);
  const app = await NestFactory.create(
    AppModule.forRoot([
      { provide: logging.Logger, useValue: context.logger.createChild('NestJS') },
      { provide: WorkspaceHost, useValue: host },
      {
        provide: TargetInfo,
        useValue: new TargetInfo(
          context.target.project,
          options.translationFile,
          sourceLocale.code,
        ),
      },
      { provide: SerializationOptions, useValue: options },
      { provide: TargetPathBuilder, useValue: targetPathBuilder },
      { provide: AngularI18n, useValue: angularI18n },
      {
        provide: TranslationDeserializer,
        useExisting: xliffVersion === '1.2' ? XlfDeserializer : Xlf2Deserializer,
      },
      {
        provide: TranslationSerializer,
        useExisting: xliffVersion === '1.2' ? XlfSerializer : Xlf2Serializer,
      },
      {
        provide: TranslationSource,
        useFactory: TRANSLATION_SOURCE_FACTORY,
        inject: [SerializationStrategy],
      },
      {
        provide: TranslationTargetRegistry,
        useFactory: TRANSLATION_TARGET_REGISTRY_FACTORY,
        inject: [TranslationSource, SerializationStrategy, PersistenceStrategy],
      },
      { provide: PersistenceStrategy, useClass: AngularJsonPersistenceStrategy },
    ]),
    {
      cors: true,
      logger: ['error', 'warn'],
    },
  );
  app.setGlobalPrefix('api');
  app.useWebSocketAdapter(new WsAdapter(app));
  app.useGlobalPipes(new ValidationPipe({ skipMissingProperties: true, whitelist: true }));
  await app.listen(options.port ?? 4300, () =>
    context.logger.info(`Translation server started: http://localhost:${options.port}\n`),
  );
  return new Promise(() => {});

  async function detectXliffVersion(): Promise<'2.0' | '1.2'> {
    const content = await host.readFile(sourceFile);
    const doc = new XmlParser().parse(content);
    const version = doc.documentElement.getAttribute('version');
    if (doc.documentElement.tagName !== 'xliff') {
      throw new Error('Only xliff is supported!');
    } else if (version !== '2.0' && version !== '1.2') {
      throw new Error('Unsupported xliff version!');
    } else {
      return version;
    }
  }

  async function TRANSLATION_SOURCE_FACTORY(
    serializationStrategy: SerializationStrategy,
  ): Promise<TranslationSource> {
    try {
      context.logger.info(`Attempting to serialize source file ${sourceFile}`);
      const result = await serializationStrategy.deserializeSource(sourceFile);
      if (result.language && sourceLocale.code && result.language !== sourceLocale.code) {
        context.logger.warn(
          `Source locale in angular.json is ${sourceLocale} but in the ` +
            ` source file ${sourceFile} it is ${result.language}.`,
        );
      }

      const source = new TranslationSource(sourceLocale.code || result.language, result.unitMap);
      if (sourceLocale.baseHref) {
        source.baseHref = sourceLocale.baseHref;
      }

      context.logger.info(`Successfully serialized source file ${sourceFile}`);
      return source;
    } catch (e) {
      context.logger.error(`Failed to serialize source file ${sourceFile}`);
      throw e;
    }
  }

  async function TRANSLATION_TARGET_REGISTRY_FACTORY(
    source: TranslationSource,
    serializationStrategy: SerializationStrategy,
    persistenceStrategy: PersistenceStrategy,
  ): Promise<TranslationTargetRegistry> {
    try {
      context.logger.info(`Attempting to serialize target files`);
      const targetRegistry = new TranslationTargetRegistry(source, persistenceStrategy);
      translationContext = { source, targetRegistry };
      const locales = await angularI18n.locales();
      await Promise.all(
        Object.keys(locales).map(async (language) => {
          const locale = locales[language];
          const normalizedPath = normalize(targetPathBuilder.createPath(language));
          const relativePath = relative(workspaceRoot, normalizedPath);
          if (locale.translation.every((t) => join(workspaceRoot, t) !== normalizedPath)) {
            context.logger.warn(
              `Expected translation file ${relativePath} not found listed in i18n! It will be created and added to the i18n entry.`,
            );
            const target = await targetRegistry.create(language, locale.baseHref);
            await importExistingTranslationUnits(target, locale.translation, serializationStrategy);
          } else if (!host.isFile(normalizedPath)) {
            context.logger.warn(
              `Expected translation file ${relativePath} does not exist! It will be created.`,
            );
            await targetRegistry.create(language, locale.baseHref);
          } else {
            const result = await serializationStrategy.deserializeTarget(normalizedPath);
            targetRegistry.register(result.language, result.unitMap, locale.baseHref);
          }
        }),
      );

      await angularI18n.update();
      context.logger.info(`Successfully serialized target files`);
      return targetRegistry;
    } catch (e) {
      context.logger.error(`Failed to serialize target files`);
      throw e;
    }
  }

  async function importExistingTranslationUnits(
    target: TranslationTarget,
    translationFiles: string[],
    serializationStrategy: SerializationStrategy,
  ) {
    for (const translation of translationFiles) {
      const targetPath = join(workspaceRoot, translation);
      const result = await serializationStrategy.deserializeTarget(targetPath);
      result.unitMap.forEach((unit, key) => {
        const targetUnit = target.unitMap.get(key);
        if (targetUnit) {
          target.translateUnit(targetUnit, unit);
        }
      });
    }
  }
}
