/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { action, computed, makeAutoObservable } from 'mobx';
import {
  type ServiceEditorState,
  MINIMUM_SERVICE_OWNERS,
} from '../../../editor-state/element-editor-state/service/ServiceEditorState.js';
import type { EditorStore } from '../../../EditorStore.js';
import {
  type GeneratorFn,
  assertErrorThrown,
  LogEvent,
  ActionState,
  prettyCONSTName,
  assertNonEmptyString,
  guaranteeNonNullable,
  UnsupportedOperationError,
  getNullableFirstElement,
  assertTrue,
} from '@finos/legend-shared';
import { LEGEND_STUDIO_APP_EVENT } from '../../../LegendStudioAppEvent.js';
import { Version } from '@finos/legend-server-sdlc';
import {
  type ServiceRegistrationResult,
  ServiceExecutionMode,
} from '@finos/legend-graph';
import { ServiceRegistrationEnvInfo } from '../../../../application/LegendStudioApplicationConfig.js';
import {
  ActionAlertActionType,
  ActionAlertType,
} from '@finos/legend-application';

export const LATEST_PROJECT_REVISION = 'Latest Project Revision';

const getServiceExecutionMode = (mode: string): ServiceExecutionMode => {
  switch (mode) {
    case ServiceExecutionMode.FULL_INTERACTIVE:
      return ServiceExecutionMode.FULL_INTERACTIVE;
    case ServiceExecutionMode.SEMI_INTERACTIVE:
      return ServiceExecutionMode.SEMI_INTERACTIVE;
    case ServiceExecutionMode.PROD:
      return ServiceExecutionMode.PROD;
    default:
      throw new UnsupportedOperationError(
        `Encountered unsupported service execution mode '${mode}'`,
      );
  }
};

const URL_SEPARATOR = '/';

interface ServiceVersionOption {
  label: string;
  value: Version | string;
}

export enum SERVICE_REGISTRATION_PHASE {
  REGISTERING_SERVICE = 'REGISTERING_SERVICE',
  ACTIVATING_SERVICE = 'ACTIVATING_SERVICE',
}

export class ServiceRegistrationState {
  editorStore: EditorStore;
  serviceEditorState: ServiceEditorState;
  registrationState = ActionState.create();
  serviceEnv?: string | undefined;
  serviceExecutionMode?: ServiceExecutionMode | undefined;
  projectVersion?: Version | string | undefined;
  activatePostRegistration = true;

  constructor(
    editorStore: EditorStore,
    serviceEditorState: ServiceEditorState,
  ) {
    makeAutoObservable(this, {
      editorStore: false,
      executionModes: computed,
      updateVersion: action,
      setProjectVersion: action,
      initialize: action,
      updateType: action,
      updateEnv: action,
      setActivatePostRegistration: action,
    });

    this.editorStore = editorStore;
    this.serviceEditorState = serviceEditorState;
    this.initialize();
    this.registrationState.setMessageFormatter(prettyCONSTName);
  }

  setServiceEnv(val: string | undefined): void {
    this.serviceEnv = val;
  }
  setServiceExecutionMode(val: ServiceExecutionMode | undefined): void {
    this.serviceExecutionMode = val;
  }
  setProjectVersion(val: Version | string | undefined): void {
    this.projectVersion = val;
  }

  setActivatePostRegistration(val: boolean): void {
    this.activatePostRegistration = val;
  }

  initialize(): void {
    this.serviceEnv = getNullableFirstElement(
      this.editorStore.applicationStore.config.options
        .TEMPORARY__serviceRegistrationConfig,
    )?.env;
    this.serviceExecutionMode = this.executionModes[0];
    this.updateVersion();
  }

  updateVersion(): void {
    if (this.serviceExecutionMode === ServiceExecutionMode.SEMI_INTERACTIVE) {
      this.projectVersion = LATEST_PROJECT_REVISION;
    } else if (this.serviceExecutionMode === ServiceExecutionMode.PROD) {
      this.projectVersion = this.editorStore.sdlcState.projectVersions[0];
    } else {
      this.projectVersion = undefined;
    }
  }

  updateType(val: ServiceExecutionMode | undefined): void {
    this.setServiceExecutionMode(val);
    this.updateVersion();
  }

  updateEnv(val: string | undefined): void {
    this.setServiceEnv(val);
    this.setServiceExecutionMode(this.executionModes[0]);
  }

  get options(): ServiceRegistrationEnvInfo[] {
    if (this.editorStore.sdlcServerClient.features.canCreateVersion) {
      return this.editorStore.applicationStore.config.options
        .TEMPORARY__serviceRegistrationConfig;
    }
    return this.editorStore.applicationStore.config.options.TEMPORARY__serviceRegistrationConfig.map(
      (_envConfig) => {
        const envConfig = new ServiceRegistrationEnvInfo();
        envConfig.env = _envConfig.env;
        envConfig.executionUrl = _envConfig.executionUrl;
        envConfig.managementUrl = _envConfig.managementUrl;
        // NOTE: For projects that we cannot create a version for, only fully-interactive mode is supported
        envConfig.modes = _envConfig.modes.filter(
          (mode) => mode === ServiceExecutionMode.FULL_INTERACTIVE,
        );
        return envConfig;
      },
    ).filter((envConfig) => envConfig.modes.length);
  }

  get executionModes(): ServiceExecutionMode[] {
    return (
      this.options.find((e) => e.env === this.serviceEnv)?.modes ?? []
    ).map(getServiceExecutionMode);
  }

  get versionOptions(): ServiceVersionOption[] | undefined {
    if (
      this.editorStore.sdlcServerClient.features.canCreateVersion &&
      this.serviceExecutionMode !== ServiceExecutionMode.FULL_INTERACTIVE
    ) {
      const options: ServiceVersionOption[] =
        this.editorStore.sdlcState.projectVersions.map((version) => ({
          label: version.id.id,
          value: version,
        }));
      if (this.serviceExecutionMode !== ServiceExecutionMode.PROD) {
        return [
          {
            label: prettyCONSTName(LATEST_PROJECT_REVISION),
            value: LATEST_PROJECT_REVISION,
          },
          ...options,
        ];
      }
      return options;
    }
    return undefined;
  }

  *registerService(): GeneratorFn<void> {
    try {
      this.registrationState.inProgress();
      this.validateServiceForRegistration();
      const config = guaranteeNonNullable(
        this.options.find((info) => info.env === this.serviceEnv),
      );
      const serverUrl = config.executionUrl;
      const versionInput =
        this.projectVersion instanceof Version
          ? this.projectVersion.id.id
          : undefined;
      const projectConfig = guaranteeNonNullable(
        this.editorStore.projectConfigurationEditorState.projectConfiguration,
      );
      this.registrationState.setMessage(
        SERVICE_REGISTRATION_PHASE.REGISTERING_SERVICE,
      );
      const serviceRegistrationResult =
        (yield this.editorStore.graphManagerState.graphManager.registerService(
          this.serviceEditorState.service,
          this.editorStore.graphManagerState.graph,
          projectConfig.groupId,
          projectConfig.artifactId,
          versionInput,
          serverUrl,
          guaranteeNonNullable(this.serviceExecutionMode),
        )) as ServiceRegistrationResult;
      if (this.activatePostRegistration) {
        this.registrationState.setMessage(
          SERVICE_REGISTRATION_PHASE.ACTIVATING_SERVICE,
        );
        yield this.editorStore.graphManagerState.graphManager.activateService(
          serverUrl,
          serviceRegistrationResult.serviceInstanceId,
        );
      }
      assertNonEmptyString(
        serviceRegistrationResult.pattern,
        'Service registration pattern is missing',
      );
      const message = `Service with patten ${
        serviceRegistrationResult.pattern
      } registered ${this.activatePostRegistration ? 'and activated ' : ''}`;
      const encodedServicePattern =
        URL_SEPARATOR +
        encodeURIComponent(
          serviceRegistrationResult.pattern[0] === URL_SEPARATOR
            ? serviceRegistrationResult.pattern.substring(1)
            : serviceRegistrationResult.pattern,
        );
      this.editorStore.setActionAlertInfo({
        message,
        prompt: 'You can now launch and monitor the operation of your service',
        type: ActionAlertType.STANDARD,
        onEnter: (): void => this.editorStore.setBlockGlobalHotkeys(true),
        onClose: (): void => this.editorStore.setBlockGlobalHotkeys(false),
        actions: [
          {
            label: 'Launch Service',
            type: ActionAlertActionType.PROCEED,
            handler: (): void => {
              this.editorStore.applicationStore.navigator.openNewWindow(
                `${config.managementUrl}${encodedServicePattern}`,
              );
            },
            default: true,
          },
          {
            label: 'Close',
            type: ActionAlertActionType.PROCEED_WITH_CAUTION,
          },
        ],
      });
    } catch (error) {
      assertErrorThrown(error);
      this.editorStore.applicationStore.log.error(
        LogEvent.create(LEGEND_STUDIO_APP_EVENT.SERVICE_REGISTRATION_FAILURE),
        error,
      );
      this.editorStore.applicationStore.notifyError(error);
    } finally {
      this.registrationState.reset();
      this.registrationState.setMessage(undefined);
    }
  }

  validateServiceForRegistration(): void {
    this.serviceEditorState.service.owners.forEach((owner) =>
      assertNonEmptyString(owner, `Service can't have an empty owner name`),
    );
    assertTrue(
      this.serviceEditorState.service.owners.length >= MINIMUM_SERVICE_OWNERS,
      `Service needs to have at least 2 owners in order to be registered`,
    );
    guaranteeNonNullable(
      this.serviceEnv,
      'Service registration environment can not be empty',
    );
    guaranteeNonNullable(
      this.serviceExecutionMode,
      'Service type can not be empty',
    );
    if (
      this.serviceExecutionMode === ServiceExecutionMode.PROD ||
      this.serviceExecutionMode === ServiceExecutionMode.SEMI_INTERACTIVE
    ) {
      guaranteeNonNullable(
        this.projectVersion,
        'Service version can not be empty in Semi-interactive and Prod service type',
      );
    }
  }
}
