import path from 'node:path';

import { PublisherBase } from '@electron-forge/publisher-base';
import {
  ForgeConfigPublisher,
  ForgeListrOptions,
  ForgeListrTask,
  ForgeListrTaskFn,
  ForgeMakeResult,
  IForgePublisher,
  IForgeResolvablePublisher,
  ResolvedForgeConfig,
  // ForgePlatform,
} from '@electron-forge/shared-types';
import { autoTrace, delayTraceTillSignal } from '@electron-forge/tracer';
import chalk from 'chalk';
import debug from 'debug';
import fs from 'fs-extra';
import { Listr } from 'listr2';

import getForgeConfig from '../util/forge-config';
import importSearch from '../util/import-search';
import getCurrentOutDir from '../util/out-dir';
import PublishState from '../util/publish-state';
import resolveDir from '../util/resolve-dir';

import { listrMake, MakeOptions } from './make';

const d = debug('electron-forge:publish');

type PublishContext = {
  dir: string;
  forgeConfig: ResolvedForgeConfig;
  publishers: PublisherBase<unknown>[];
  makeResults: ForgeMakeResult[];
};

export interface PublishOptions {
  /**
   * The path to the app to be published
   */
  dir?: string;
  /**
   * Whether to use sensible defaults or prompt the user visually
   */
  interactive?: boolean;
  /**
   * The publish targets, by default pulled from forge config, set this prop to
   * override that list
   */
  publishTargets?: ForgeConfigPublisher[] | string[];
  /**
   * Options object to passed through to make()
   */
  makeOptions?: MakeOptions;
  /**
   * The path to the directory containing generated distributables
   */
  outDir?: string;
  /**
   * Whether to generate dry run meta data but not actually publish
   */
  dryRun?: boolean;
  /**
   * Whether or not to attempt to resume a previously saved `dryRun` and publish
   *
   * You can't use this combination at the same time as dryRun=true
   */
  dryRunResume?: boolean;
}

export default autoTrace(
  { name: 'publish()', category: '@electron-forge/core' },
  async (
    childTrace,
    {
      dir: providedDir = process.cwd(),
      interactive = false,
      makeOptions = {},
      publishTargets = undefined,
      dryRun = false,
      dryRunResume = false,
      outDir,
    }: PublishOptions,
  ): Promise<void> => {
    if (dryRun && dryRunResume) {
      throw new Error("Can't dry run and resume a dry run at the same time");
    }

    const listrOptions: ForgeListrOptions<PublishContext> = {
      concurrent: false,
      rendererOptions: {
        collapseErrors: false,
      },
      silentRendererCondition: !interactive,
      fallbackRendererCondition:
        Boolean(process.env.DEBUG) || Boolean(process.env.CI),
    };

    const publishDistributablesTasks = (childTrace: typeof autoTrace) => [
      {
        title: 'Publishing distributables',
        task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
          { name: 'publish-distributables', category: '@electron-forge/core' },
          async (
            childTrace,
            { dir, forgeConfig, makeResults, publishers },
            task: ForgeListrTask<PublishContext>,
          ) => {
            if (publishers.length === 0) {
              task.output = 'No publishers configured';
              task.skip();
              return;
            }

            return delayTraceTillSignal(
              childTrace,
              task.newListr<never>(
                publishers.map((publisher) => ({
                  title: `${chalk.cyan(`[publisher-${publisher.name}]`)} Running the ${chalk.yellow('publish')} command`,
                  task: childTrace<Parameters<ForgeListrTaskFn>>(
                    {
                      name: `publish-${publisher.name}`,
                      category: '@electron-forge/core',
                    },
                    async (childTrace, _, task) => {
                      const setStatusLine = (s: string) => {
                        task.output = s;
                      };
                      await publisher.publish({
                        dir,
                        makeResults: makeResults!,
                        forgeConfig,
                        setStatusLine,
                      });
                    },
                  ),
                  rendererOptions: {
                    persistentOutput: true,
                  },
                })),
                {
                  rendererOptions: {
                    collapseSubtasks: false,
                    collapseErrors: false,
                  },
                },
              ),
              'run',
            );
          },
        ),
        rendererOptions: {
          persistentOutput: true,
        },
      },
    ];

    const runner = new Listr<PublishContext>(
      [
        {
          title: 'Loading configuration',
          task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
            { name: 'load-forge-config', category: '@electron-forge/core' },
            async (childTrace, ctx) => {
              const resolvedDir = await resolveDir(providedDir);
              if (!resolvedDir) {
                throw new Error(
                  'Failed to locate publishable Electron application',
                );
              }

              ctx.dir = resolvedDir;
              ctx.forgeConfig = await getForgeConfig(resolvedDir);
            },
          ),
        },
        {
          title: 'Resolving publish targets',
          task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
            {
              name: 'resolve-publish-targets',
              category: '@electron-forge/core',
            },
            async (childTrace, ctx, task) => {
              const { dir, forgeConfig } = ctx;

              if (!publishTargets) {
                publishTargets = forgeConfig.publishers || [];
              }
              publishTargets = (publishTargets as ForgeConfigPublisher[]).map(
                (target) => {
                  if (typeof target === 'string') {
                    return (
                      (forgeConfig.publishers || []).find(
                        (p: ForgeConfigPublisher) => {
                          if (typeof p === 'string') return false;
                          if ((p as IForgePublisher).__isElectronForgePublisher)
                            return false;
                          return (
                            (p as IForgeResolvablePublisher).name === target
                          );
                        },
                      ) || { name: target }
                    );
                  }
                  return target;
                },
              );

              ctx.publishers = [];
              for (const publishTarget of publishTargets) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                let publisher: PublisherBase<any>;
                if (
                  (publishTarget as IForgePublisher).__isElectronForgePublisher
                ) {
                  // eslint-disable-next-line @typescript-eslint/no-explicit-any
                  publisher = publishTarget as PublisherBase<any>;
                } else {
                  const resolvablePublishTarget =
                    publishTarget as IForgeResolvablePublisher;
                  // eslint-disable-next-line @typescript-eslint/no-explicit-any
                  const PublisherClass: any = await importSearch(dir, [
                    resolvablePublishTarget.name,
                  ]);
                  if (!PublisherClass) {
                    throw new Error(
                      `Could not find a publish target with the name: ${resolvablePublishTarget.name}. Make sure it's listed in the devDependencies of your package.json`,
                    );
                  }

                  publisher = new PublisherClass(
                    resolvablePublishTarget.config || {},
                    resolvablePublishTarget.platforms,
                  );
                }

                ctx.publishers.push(publisher);
              }

              if (ctx.publishers.length) {
                task.output = `Publishing to the following targets: ${chalk.magenta(`${ctx.publishers.map((publisher) => publisher.name).join(', ')}`)}`;
              }
            },
          ),
          rendererOptions: {
            persistentOutput: true,
          },
        },
        {
          title: dryRunResume
            ? 'Resuming from dry run...'
            : `Running ${chalk.yellow('make')} command`,
          task: childTrace<Parameters<ForgeListrTaskFn<PublishContext>>>(
            {
              name: dryRunResume ? 'resume-dry-run' : 'make()',
              category: '@electron-forge/core',
            },
            async (childTrace, ctx, task) => {
              const { dir, forgeConfig } = ctx;
              const calculatedOutDir =
                outDir || getCurrentOutDir(dir, forgeConfig);
              const dryRunDir = path.resolve(
                calculatedOutDir,
                'publish-dry-run',
              );

              if (dryRunResume) {
                d('attempting to resume from dry run');
                const publishes = await PublishState.loadFromDirectory(
                  dryRunDir,
                  dir,
                );
                task.title = `Resuming ${publishes.length} found dry runs...`;

                return delayTraceTillSignal(
                  childTrace,
                  task.newListr<PublishContext>(
                    publishes.map((publishStates, index) => {
                      return {
                        title: `Publishing dry-run ${chalk.blue(`#${index + 1}`)}`,
                        task: childTrace<
                          Parameters<ForgeListrTaskFn<PublishContext>>
                        >(
                          {
                            name: `publish-dry-run-${index + 1}`,
                            category: '@electron-forge/core',
                          },
                          async (childTrace, ctx, task) => {
                            const restoredMakeResults = publishStates.map(
                              ({ state }) => state,
                            );
                            d('restoring publish settings from dry run');

                            for (const makeResult of restoredMakeResults) {
                              makeResult.artifacts = await Promise.all(
                                makeResult.artifacts.map(
                                  async (makePath: string) => {
                                    // standardize the path to artifacts across platforms
                                    const normalizedPath = makePath
                                      .split(/\/|\\/)
                                      .join(path.sep);
                                    if (
                                      !(await fs.pathExists(normalizedPath))
                                    ) {
                                      throw new Error(
                                        `Attempted to resume a dry run, but an artifact (${normalizedPath}) could not be found`,
                                      );
                                    }
                                    return normalizedPath;
                                  },
                                ),
                              );
                            }

                            d('publishing for given state set');
                            return delayTraceTillSignal(
                              childTrace,
                              task.newListr(
                                publishDistributablesTasks(childTrace),
                                {
                                  ctx: {
                                    ...ctx,
                                    makeResults: restoredMakeResults,
                                  },
                                  rendererOptions: {
                                    collapseSubtasks: false,
                                    collapseErrors: false,
                                  },
                                },
                              ),
                              'run',
                            );
                          },
                        ),
                      };
                    }),
                    {
                      rendererOptions: {
                        collapseSubtasks: false,
                        collapseErrors: false,
                      },
                    },
                  ),
                  'run',
                );
              }

              d('triggering make');
              return delayTraceTillSignal(
                childTrace,
                listrMake(
                  childTrace,
                  {
                    dir,
                    interactive,
                    ...makeOptions,
                  },
                  (results) => {
                    ctx.makeResults = results;
                  },
                ),
                'run',
              );
            },
          ),
        },
        ...(dryRunResume
          ? []
          : dryRun
            ? [
                {
                  title: 'Saving dry-run state',
                  task: childTrace<
                    Parameters<ForgeListrTaskFn<PublishContext>>
                  >(
                    { name: 'save-dry-run', category: '@electron-forge/core' },
                    async (childTrace, { dir, forgeConfig, makeResults }) => {
                      d('saving results of make in dry run state', makeResults);
                      const calculatedOutDir =
                        outDir || getCurrentOutDir(dir, forgeConfig);
                      const dryRunDir = path.resolve(
                        calculatedOutDir,
                        'publish-dry-run',
                      );

                      await fs.remove(dryRunDir);
                      await PublishState.saveToDirectory(
                        dryRunDir,
                        makeResults!,
                        dir,
                      );
                    },
                  ),
                },
              ]
            : publishDistributablesTasks(childTrace)),
      ],
      listrOptions,
    );

    await runner.run();
  },
);
