import * as path from 'path';
import {QuickPickItem, Uri, commands, env, window, workspace} from 'vscode';
import {SampleConfigsRequest, SampleConfigsResponse} from './rpc/sample_configs_pb';
import {SampleCreateRequest, SampleCreateResponse} from './rpc/sample_create_pb';
import {SamplesListRequest, SamplesListResponse} from './rpc/samples_list_pb';
import {StripeClient} from './stripeClient';
import {StripeDaemon} from './daemon/stripeDaemon';
import {StripeCLIClient as StripeDaemonClient} from './rpc/commands_grpc_pb';

/**
 * SampleQuickPickItem contains the data for each Sample quick pick item.
 */
type SampleQuickPickItem = QuickPickItem & {
  sampleData: {
    name: string;
    url: string;
  };
};

/**
 * StripeSamples prompts the user for a Stripe sample and delegates sample creation to the
 * underlying Stripe daemon process.
 */
export class StripeSamples {
  private daemonClient?: StripeDaemonClient;
  private stripeClient: StripeClient;
  private stripeDaemon: StripeDaemon;

  constructor(stripeClient: StripeClient, stripeDaemon: StripeDaemon) {
    this.stripeClient = stripeClient;
    this.stripeDaemon = stripeDaemon;
  }

  /**
   * Show a menu with a list of Stripe samples, prompt for sample options, clone the sample, and
   * prompt to open the sample.
   */
  selectAndCloneSample = async () => {
    try {
      this.daemonClient = await this.stripeDaemon.setupClient();
    } catch (e: any) {
      if (e.name === 'NoDaemonCommandError') {
        this.stripeClient.promptUpdateForDaemon();
      }
      console.error(e);
      return;
    }

    try {
      const selectedSample = await this.promptSample();
      if (!selectedSample) {
        return;
      }

      const sampleName = selectedSample.sampleData.name;

      const selectedIntegration = await this.promptIntegration(selectedSample);
      if (!selectedIntegration) {
        return;
      }

      const selectedClient = await this.promptClient(selectedIntegration);
      if (!selectedClient) {
        return;
      }

      const selectedServer = await this.promptServer(selectedIntegration);
      if (!selectedServer) {
        return;
      }

      const cloneSampleAsName = await this.promptSampleName(sampleName);

      const clonePath = await this.promptPath(selectedSample, cloneSampleAsName);
      if (!clonePath) {
        return;
      }

      await window.showInformationMessage(
        `Sample "${sampleName}" cloning in progress...`,
        'OK',
      );

      const sampleCreateResponse = await this.createSample(
        sampleName,
        selectedIntegration.getIntegrationName(),
        selectedServer,
        selectedClient,
        clonePath,
      );

      const sampleIsReady = `Your sample "${cloneSampleAsName}" is all ready to go`;
      // eslint-disable-next-line no-nested-ternary
      const postInstallMessage = !!sampleCreateResponse
        ? !!sampleCreateResponse.getPostInstall()
          ? sampleCreateResponse.getPostInstall()
          : `${sampleIsReady}.`
        : `${sampleIsReady}, but we could not set the API keys in the .env file. Please set them manually.`;

      await this.promptOpenFolder(postInstallMessage, clonePath, sampleName);
    } catch (e: any) {
      window.showErrorMessage(`Cannot create Stripe sample: ${e.message}`);
    }
  };

  /**
   * Get a list of Stripe Samples items to show in a quick pick menu.
   */
  private getQuickPickItems = async () => {
    const rawSamples = await new Promise<SamplesListResponse.SampleData[]>((resolve, reject) => {
      this.daemonClient?.samplesList(new SamplesListRequest(), (error, response) => {
        if (error) {
          reject(error);
        } else if (response) {
          resolve(response.getSamplesList());
        }
      });
    });

    // alphabetical order
    rawSamples.sort((a, b) => {
      if (a.getName() < b.getName()) {
        return -1;
      }
      if (a.getName() > b.getName()) {
        return 1;
      }
      return 0;
    });

    const samplesQuickPickItems: SampleQuickPickItem[] = rawSamples.map((s) => {
      return {
        label: `$(repo) ${s.getName()}`,
        detail: s.getDescription(),
        sampleData: {
          name: s.getName(),
          url: s.getUrl(),
        },
      };
    });

    return samplesQuickPickItems;
  };

  /**
   *  Get the available configs for this sample.
   */
  private getConfigsForSample(sampleName: string): Promise<SampleConfigsResponse.Integration[]> {
    const request = new SampleConfigsRequest();
    request.setSampleName(sampleName);

    return new Promise((resolve, reject) => {
      this.daemonClient?.sampleConfigs(request, (error, response) => {
        if (error) {
          reject(error);
        } else if (response) {
          resolve(response.getIntegrationsList());
        }
      });
    });
  }

  /**
   * Ask for which sample to clone.
   */
  private promptSample = async (): Promise<SampleQuickPickItem | undefined> => {
    const selectedSample = await window.showQuickPick(this.getQuickPickItems(), {
      matchOnDetail: true,
      placeHolder: 'Select a sample to clone',
    });
    return selectedSample;
  };

  /**
   * Ask for which integration to copy for this sample.
   */
  private promptIntegration = async (
    sample: SampleQuickPickItem,
  ): Promise<SampleConfigsResponse.Integration | undefined> => {
    const integrationsPromise = this.getConfigsForSample(sample.sampleData.name);

    // Don't resolve the promise now. Instead, pass the promise to showQuickPick.
    // The quick pick will show a progress indicator while the promise is resolving.
    const getIntegrationNames = async (): Promise<string[]> => {
      return ((await integrationsPromise) || []).map((i) => i.getIntegrationName());
    };

    const selectedIntegrationName = await window.showQuickPick(getIntegrationNames(), {
      placeHolder: 'Select an integration',
    });
    if (!selectedIntegrationName) {
      return;
    }

    const integrations = await integrationsPromise;
    if (!integrations) {
      return undefined;
    }

    const selectedIntegration = integrations.find(
      (i) => i.getIntegrationName() === selectedIntegrationName,
    );
    return selectedIntegration;
  };

  /**
   * Ask for the sample client language
   */
  private promptClient = (
    integration: SampleConfigsResponse.Integration,
  ): Thenable<string | undefined> => {
    return window.showQuickPick(integration.getClientsList(), {
      placeHolder: 'Select a client language',
    });
  };

  /**
   * Ask for the sample server language
   */
  private promptServer = (
    integration: SampleConfigsResponse.Integration,
  ): Thenable<string | undefined> => {
    return window.showQuickPick(integration.getServersList(), {
      placeHolder: 'Select a server language',
    });
  };

  /**
   * Ask for where to clone the sample
   */
  private promptPath = async (sample: SampleQuickPickItem, cloneSampleAsName: string): Promise<string | undefined> => {
    const cloneDirectoryUri = await window.showOpenDialog({
      canSelectFiles: false,
      canSelectFolders: true,
      canSelectMany: false,
      defaultUri: workspace.workspaceFolders ? workspace.workspaceFolders[0].uri : undefined,
      openLabel: 'Clone sample',
    });

    if (!cloneDirectoryUri) {
      return;
    }

    const clonePath = path.resolve(cloneDirectoryUri[0].fsPath, cloneSampleAsName);

    return clonePath;
  };

  /**
   * Ask for sample name
   */
  private promptSampleName = async (sampleName: string): Promise<string> => {
    const inputName = await window.showInputBox({
      value: sampleName,
      prompt: 'Enter a sample name',
    });

    return !!inputName ? inputName : sampleName;
  };

  /**
   * Execute the sample creation with the given config at the given path
   */
  private createSample = (
    sampleName: string,
    integrationName: string,
    server: string,
    client: string,
    path: string,
  ): Promise<SampleCreateResponse | null> => {
    const sampleCreateRequest = new SampleCreateRequest();
    sampleCreateRequest.setSampleName(sampleName);
    sampleCreateRequest.setIntegrationName(integrationName);
    sampleCreateRequest.setServer(server);
    sampleCreateRequest.setClient(client);
    sampleCreateRequest.setPath(path);

    return new Promise<SampleCreateResponse | null>((resolve, reject) => {
      this.daemonClient?.sampleCreate(sampleCreateRequest, (error, response) => {
        if (error) {
          // The error message that starts with 'we could not set...' is a special case that we want to
          // handle differently. Unfortunately, the server does not distinguish this error from other
          // ones, so we have to do our own handling.
          if (error.details.startsWith('we could not set')) {
            resolve(null);
          } else {
            reject(error);
          }
        } else if (response) {
          resolve(response);
        }
      });
    });
  };

  /**
   * Ask if the user wants to open the sample in the same or new window
   */
  private promptOpenFolder = async (postInstallMessage: string, clonePath: string, sampleName: string): Promise<void> => {
    const openFolderOptions = {
      sameWindow: 'Open in same window',
      newWindow: 'Open in new window',
    };

    const selectedOption = await window.showInformationMessage(
      postInstallMessage,
      {modal: true},
      ...Object.values(openFolderOptions),
    );

    // open the readme file in a new browser window
    // cant open in the editor because cannot update user setting 'workbench.startupEditor​' from stripe extension
    // preview markdown also does not work because opening new workspace will terminate the stripe extension process
    env.openExternal(Uri.parse(`https://github.com/stripe-samples/${sampleName}#readme`));

    switch (selectedOption) {
      case openFolderOptions.sameWindow:
        await commands.executeCommand('vscode.openFolder', Uri.file(clonePath), {
          forceNewWindow: false,
        });
        break;
      case openFolderOptions.newWindow:
        await commands.executeCommand('vscode.openFolder', Uri.file(clonePath), {
          forceNewWindow: true,
        });
        break;
      default:
        break;
    }
  };
}