import {BaseCommand, WorkspaceRequiredError}                                                  from '@yarnpkg/cli';
import {Configuration, Descriptor, DescriptorHash, Manifest, Project, structUtils, Workspace} from '@yarnpkg/core';
import {npath, xfs, ppath, Filename}                                                          from '@yarnpkg/fslib';
import {Command, Option, Usage, UsageError}                                                   from 'clipanion';

import * as patchUtils                                                                        from '../patchUtils';

// eslint-disable-next-line arca/no-default-export
export default class PatchCommitCommand extends BaseCommand {
  static paths = [
    [`patch-commit`],
  ];

  static usage: Usage = Command.Usage({
    description: `generate a patch out of a directory`,
    details: `
      By default, this will print a patchfile on stdout based on the diff between the folder passed in and the original version of the package. Such file is suitable for consumption with the \`patch:\` protocol.

      With the \`-s,--save\` option set, the patchfile won't be printed on stdout anymore and will instead be stored within a local file (by default kept within \`.yarn/patches\`, but configurable via the \`patchFolder\` setting). A \`resolutions\` entry will also be added to your top-level manifest, referencing the patched package via the \`patch:\` protocol.

      Note that only folders generated by \`yarn patch\` are accepted as valid input for \`yarn patch-commit\`.
    `,
  });

  save = Option.Boolean(`-s,--save`, false, {
    description: `Add the patch to your resolution entries`,
  });

  patchFolder = Option.String();

  async execute() {
    const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
    const {project, workspace} = await Project.find(configuration, this.context.cwd);

    if (!workspace)
      throw new WorkspaceRequiredError(project.cwd, this.context.cwd);

    await project.restoreInstallState();

    const folderPath = ppath.resolve(this.context.cwd, npath.toPortablePath(this.patchFolder));
    const sourcePath = ppath.join(folderPath, `../source`);
    const metaPath = ppath.join(folderPath, `../.yarn-patch.json`);

    if (!xfs.existsSync(sourcePath))
      throw new UsageError(`The argument folder didn't get created by 'yarn patch'`);

    const diff = await patchUtils.diffFolders(sourcePath, folderPath);

    const meta = await xfs.readJsonPromise(metaPath);
    const locator = structUtils.parseLocator(meta.locator, true);

    if (!project.storedPackages.has(locator.locatorHash))
      throw new UsageError(`No package found in the project for the given locator`);

    if (!this.save) {
      this.context.stdout.write(diff);
      return;
    }

    const patchFolder = configuration.get(`patchFolder`);
    const patchPath = ppath.join(patchFolder, `${structUtils.slugifyLocator(locator)}.patch`);

    await xfs.mkdirPromise(patchFolder, {recursive: true});
    await xfs.writeFilePromise(patchPath, diff);

    const workspaceDependents: Array<Workspace> = [];
    const transitiveDependencies = new Map<DescriptorHash, Descriptor>();

    for (const pkg of project.storedPackages.values()) {
      if (structUtils.isVirtualLocator(pkg))
        continue;

      const descriptor = pkg.dependencies.get(locator.identHash);
      if (!descriptor)
        continue;

      const devirtualizedDescriptor = structUtils.ensureDevirtualizedDescriptor(descriptor);
      const unpatchedDescriptor = patchUtils.ensureUnpatchedDescriptor(devirtualizedDescriptor);

      const resolution = project.storedResolutions.get(unpatchedDescriptor.descriptorHash);
      if (!resolution)
        throw new Error(`Assertion failed: Expected the resolution to have been registered`);

      const dependency = project.storedPackages.get(resolution);
      if (!dependency)
        throw new Error(`Assertion failed: Expected the package to have been registered`);

      const workspace = project.tryWorkspaceByLocator(pkg);
      if (workspace) {
        workspaceDependents.push(workspace);
      } else {
        const originalPkg = project.originalPackages.get(pkg.locatorHash);
        if (!originalPkg)
          throw new Error(`Assertion failed: Expected the original package to have been registered`);

        const originalDependency = originalPkg.dependencies.get(descriptor.identHash);
        if (!originalDependency)
          throw new Error(`Assertion failed: Expected the original dependency to have been registered`);

        transitiveDependencies.set(originalDependency.descriptorHash, originalDependency);
      }
    }

    for (const workspace of workspaceDependents) {
      for (const dependencyType of Manifest.hardDependencies) {
        const originalDescriptor = workspace.manifest[dependencyType].get(locator.identHash);
        if (!originalDescriptor)
          continue;

        const newDescriptor = patchUtils.makeDescriptor(originalDescriptor, {
          parentLocator: null,
          sourceDescriptor: structUtils.convertLocatorToDescriptor(locator),
          patchPaths: [ppath.join(Filename.home, ppath.relative(project.cwd, patchPath))],
        });

        workspace.manifest[dependencyType].set(originalDescriptor.identHash, newDescriptor);
      }
    }

    for (const originalDescriptor of transitiveDependencies.values()) {
      const newDescriptor = patchUtils.makeDescriptor(originalDescriptor, {
        parentLocator: null,
        sourceDescriptor: structUtils.convertLocatorToDescriptor(locator),
        patchPaths: [ppath.join(Filename.home, ppath.relative(project.cwd, patchPath))],
      });

      project.topLevelWorkspace.manifest.resolutions.push({
        pattern: {descriptor: {fullName: structUtils.stringifyIdent(newDescriptor), description: originalDescriptor.range}},
        reference: newDescriptor.range,
      });
    }

    await project.persist();
  }
}
