import {
  Rule,
  SchematicContext,
  Tree,
  mergeWith,
  apply,
  url,
  template,
  chain,
  forEach,
  FileEntry,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { strings } from '@angular-devkit/core';
import {
  addPackageJsonDependency,
  NodeDependencyType,
} from '@schematics/angular/utility/dependencies';
import { getProject, getWorkspace, parseJson } from './utils';

export interface Schema {
  project?: string;
  output?: string;
}

/**
 * Retrieves the contents of the tsconfig.json file from the given tree.
 * @param {Tree} tree - The tree object representing the file system.
 * @returns The contents of the tsconfig.json file as a JSON object, or an empty string if the file does not exist.
 */
const getTsConfig = (tree: Tree) => 
  tree.readJson('tsconfig.json') ?? '';

/**
 * Retrieves the Angular configuration from the given tree.
 * @param {Tree} tree - The tree object representing the file system.
 * @returns The Angular configuration object if found, otherwise an empty string.
 */
const getAngularConfig = (tree: Tree) =>
  tree.readJson('angular.json') ?? '';

/**
 * Updates the tsconfig.json file in the given tree by adding or modifying the compiler options.
 * @param {Tree} tree - The tree representing the file system.
 * @returns The updated tree.
 */
const updateTSConfig = () => {
  return (tree: Tree) => {
    const config: any = getTsConfig(tree);

    config.compilerOptions = {
      ...config.compilerOptions,
      resolveJsonModule: true,
      allowSyntheticDefaultImports: true,
    };

    tree.overwrite('tsconfig.json', JSON.stringify(config, null, 2));
    return tree;
  };
};

/**
 * Updates the angular.json file with the specified options.
 * @param {Schema} options - The options to update the angular.json file with.
 * @returns A function that takes in a Tree object and updates the angular.json file.
 */
const updateAngularJSON = (options: Schema) => {
  return (tree: Tree) => {
    const workspace = getWorkspace(tree);
    const project = getProject(workspace, options.project!);
    (project.architect!.build!
      .builder) = '@angular-builders/custom-webpack:browser';
    (project.architect!.serve!
      .builder) = '@angular-builders/custom-webpack:dev-server';
    (project.architect!.build!.options)['customWebpackConfig'] = {
      path: './extra-webpack.config.ts',
    };

    // Options
    const mainSrc = project.architect!.build!.options!.browser ?? 'src/main.ts';
    (project.architect!.build!
      .options!.main) = mainSrc;
    delete project.architect!.build!.options!.browser; // Remove browser option for Custom Webpack

    tree.overwrite('angular.json', JSON.stringify(workspace, null, 2));
    return tree;
  };
};

/**
 * Retrieves the version number of the project from the package.json file in the given tree.
 * @param {Tree} tree - The tree object representing the project file structure.
 * @returns The version number of the project.
 */
const getProjectVersion = (tree: Tree) => {
  const packageBuffer = tree.read('package.json') ?? '';
  const pack: any = parseJson(packageBuffer.toString());
  return pack.version;
};

/**
 * Copies Domo files to the specified tree directory.
 * @param {Tree} tree - The tree directory to copy the files to.
 * @param {Schema} options - The options for copying the files.
 * @returns A merged tree with the copied files.
 */
const copyDomoFiles = (tree: Tree, options: Schema) => {
  const version = getProjectVersion(tree);
  return mergeWith(
    apply(url('./files'), [
      template({
        ...options,
        version,
        ...strings,
      }),
      forEach((file: FileEntry) => {
        if (tree.exists(file.path)) return null;
        return file;
      }),
    ]),
  );
};

/**
 * Adds the specified dependencies to the package.json file.
 * @param {Tree} tree - The tree object representing the file system.
 * @returns A function that takes in a tree object and adds the dependencies to the package.json file.
 */
const addDepsToPackage = () => {
  return (tree: Tree) => {
    const deps: any = {
      '@angular-builders/custom-webpack': '^18.0.0',
      '@domoinc/ryuu-proxy': '^4.3.4',
      'copy-webpack-plugin': '^11.0.0',
    };

    Object.keys(deps).forEach((key) => {
      addPackageJsonDependency(tree, {
        name: key,
        type: NodeDependencyType.Dev,
        version: deps[key],
      });
    });
  };
};

/**
 * Adds helper scripts to the package.json file based on the provided options.
 * @param {Schema} options - The options object containing project information.
 * @returns {Tree} The updated tree with the modified package.json file.
 */
const addHelperScripts = (options: Schema) => {
  return (tree: Tree) => {
    const packageBuffer = tree.read('package.json') ?? '';
    const pack: any = parseJson(packageBuffer.toString());
    const distFolder = options.output ?? `dist/${options.project}`;
    pack.scripts = {
      ...pack.scripts,
      'domo:upload': `npm run build && cd ${distFolder} && domo publish`,
      'postdomo:upload': `cp ${distFolder}/manifest.json domo/`,
    };
    tree.overwrite('package.json', JSON.stringify(pack, null, 2));
    return tree;
  };
};

/**
 * Angular schematic rule that adds dependencies and configuration to an Angular project.
 * @param {any} _options - The options for the schematic.
 * @returns {Rule} - A rule function that modifies the project tree.
 */
export function ngAdd(options: any): Rule {
  return (tree: Tree, context: SchematicContext) => {
    context.addTask(new NodePackageInstallTask());
    // Check for Project Name
    if (!options.project) {
      const config: any = getAngularConfig(tree);
      if (config.projects) {
        const projects = Object.keys(config.projects);
        const defaultProjectName = projects.length > 0 ? projects[0] : '';
        const defaultProject = config.projects[defaultProjectName];
        const buildOutput = defaultProject.architect!.build!.options!.outputPath;
        options = { ...options, project: defaultProjectName, output: buildOutput };
      }
    }
    console.log('[ngAdd] options', options);
    return chain([
      addDepsToPackage(),
      copyDomoFiles(tree, options),
      updateAngularJSON(options),
      updateTSConfig(),
      addHelperScripts(options),
    ]);
  };
}
