import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MainMenuManager } from '../../common/main-menu-manager';
import type { AuthenticationSession } from '../../node/auth/types';
import { AuthenticationClientService } from '../auth/authentication-client-service';
import { CreateApi } from '../create/create-api';
import { CreateUri } from '../create/create-uri';
import { Create } from '../create/typings';
import { ArduinoMenus } from '../menu/arduino-menus';
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
import { Command, CommandRegistry, Contribution, URI } from './contribution';

@injectable()
export class NewCloudSketch extends Contribution {
  @inject(CreateApi)
  private readonly createApi: CreateApi;
  @inject(SketchbookWidgetContribution)
  private readonly widgetContribution: SketchbookWidgetContribution;
  @inject(AuthenticationClientService)
  private readonly authenticationService: AuthenticationClientService;
  @inject(MainMenuManager)
  private readonly mainMenuManager: MainMenuManager;

  private readonly toDispose = new DisposableCollection();
  private _session: AuthenticationSession | undefined;
  private _enabled: boolean;

  override onReady(): void {
    this.toDispose.pushAll([
      this.authenticationService.onSessionDidChange((session) => {
        const oldSession = this._session;
        this._session = session;
        if (!!oldSession !== !!this._session) {
          this.mainMenuManager.update();
        }
      }),
      this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
        if (preferenceName === 'arduino.cloud.enabled') {
          const oldEnabled = this._enabled;
          this._enabled = Boolean(newValue);
          if (this._enabled !== oldEnabled) {
            this.mainMenuManager.update();
          }
        }
      }),
    ]);
    this._enabled = this.preferences['arduino.cloud.enabled'];
    this._session = this.authenticationService.session;
    if (this._session) {
      this.mainMenuManager.update();
    }
  }

  onStop(): void {
    this.toDispose.dispose();
  }

  override registerCommands(registry: CommandRegistry): void {
    registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
      execute: () => this.createNewSketch(),
      isEnabled: () => !!this._session,
      isVisible: () => this._enabled,
    });
  }

  override registerMenus(registry: MenuModelRegistry): void {
    registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
      commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
      label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
      order: '1',
    });
  }

  override registerKeybindings(registry: KeybindingRegistry): void {
    registry.registerKeybinding({
      command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
      keybinding: 'CtrlCmd+Alt+N',
    });
  }

  private async createNewSketch(
    initialValue?: string | undefined
  ): Promise<URI | undefined> {
    const widget = await this.widgetContribution.widget;
    const treeModel = this.treeModelFrom(widget);
    if (!treeModel) {
      return undefined;
    }
    const rootNode = CompositeTreeNode.is(treeModel.root)
      ? treeModel.root
      : undefined;
    if (!rootNode) {
      return undefined;
    }

    const newSketchName = await this.newSketchName(rootNode, initialValue);
    if (!newSketchName) {
      return undefined;
    }
    let result: Create.Sketch | undefined | 'conflict';
    try {
      result = await this.createApi.createSketch(newSketchName);
    } catch (err) {
      if (isConflict(err)) {
        result = 'conflict';
      } else {
        throw err;
      }
    } finally {
      if (result) {
        await treeModel.refresh();
      }
    }

    if (result === 'conflict') {
      return this.createNewSketch(newSketchName);
    }

    if (result) {
      return this.open(treeModel, result);
    }
    return undefined;
  }

  private async open(
    treeModel: CloudSketchbookTreeModel,
    newSketch: Create.Sketch
  ): Promise<URI | undefined> {
    const id = CreateUri.toUri(newSketch).path.toString();
    const node = treeModel.getNode(id);
    if (!node) {
      throw new Error(
        `Could not find remote sketchbook tree node with Tree node ID: ${id}.`
      );
    }
    if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
      throw new Error(
        `Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
      );
    }
    try {
      await treeModel.sketchbookTree().pull({ node });
    } catch (err) {
      if (isNotFound(err)) {
        await treeModel.refresh();
        this.messageService.error(
          nls.localize(
            'arduino/newCloudSketch/notFound',
            "Could not pull the remote sketch '{0}'. It does not exist.",
            newSketch.name
          )
        );
        return undefined;
      }
      throw err;
    }
    return this.commandService.executeCommand(
      SketchbookCommands.OPEN_NEW_WINDOW.id,
      { node }
    );
  }

  private treeModelFrom(
    widget: SketchbookWidget
  ): CloudSketchbookTreeModel | undefined {
    const treeWidget = widget.getTreeWidget();
    if (treeWidget instanceof CloudSketchbookTreeWidget) {
      const model = treeWidget.model;
      if (model instanceof CloudSketchbookTreeModel) {
        return model;
      }
    }
    return undefined;
  }

  private async newSketchName(
    rootNode: CompositeTreeNode,
    initialValue?: string | undefined
  ): Promise<string | undefined> {
    const existingNames = rootNode.children
      .filter(CloudSketchbookTree.CloudSketchDirNode.is)
      .map(({ fileStat }) => fileStat.name);
    return new WorkspaceInputDialog(
      {
        title: nls.localize(
          'arduino/newCloudSketch/newSketchTitle',
          'Name of a new Remote Sketch'
        ),
        parentUri: CreateUri.root,
        initialValue,
        validate: (input) => {
          if (existingNames.includes(input)) {
            return nls.localize(
              'arduino/newCloudSketch/sketchAlreadyExists',
              "Remote sketch '{0}' already exists.",
              input
            );
          }
          // This is how https://create.arduino.cc/editor/ works when renaming a sketch.
          if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
            return '';
          }
          return nls.localize(
            'arduino/newCloudSketch/invalidSketchName',
            'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
          );
        },
      },
      this.labelProvider
    ).open();
  }
}
export namespace NewCloudSketch {
  export namespace Commands {
    export const NEW_CLOUD_SKETCH: Command = {
      id: 'arduino-new-cloud-sketch',
    };
  }
}

function isConflict(err: unknown): boolean {
  return isErrorWithStatusOf(err, 409);
}
function isNotFound(err: unknown): boolean {
  return isErrorWithStatusOf(err, 404);
}
function isErrorWithStatusOf(
  err: unknown,
  status: number
): err is Error & { status: number } {
  if (err instanceof Error) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const object = err as any;
    return 'status' in object && object.status === status;
  }
  return false;
}