// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type * as Common from '../../../core/common/common.js';
import * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as ProtocolClient from '../../../core/protocol_client/protocol_client.js';
import * as SDK from '../../../core/sdk/sdk.js';
import type * as ProtocolProxyApi from '../../../generated/protocol-proxy-api.js';
import type * as Protocol from '../../../generated/protocol.js';
import * as Components from '../../../ui/legacy/components/utils/utils.js';

const UIStrings = {
  /**
   * @description Text that refers to the main target
   */
  main: 'Main',
  /**
   * @description Text in Node Main of the Sources panel when debugging a Node.js app
   * @example {example.com} PH1
   */
  nodejsS: 'Node.js: {PH1}',
  /**
   * @description Text in DevTools window title when debugging a Node.js app
   * @example {example.com} PH1
   */
  NodejsTitleS: 'DevTools - Node.js: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('entrypoints/node_app/app/NodeMain.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let nodeMainImplInstance: NodeMainImpl;

export class NodeMainImpl implements Common.Runnable.Runnable {
  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): NodeMainImpl {
    const {forceNew} = opts;
    if (!nodeMainImplInstance || forceNew) {
      nodeMainImplInstance = new NodeMainImpl();
    }
    return nodeMainImplInstance;
  }
  async run(): Promise<void> {
    Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConnectToNodeJSFromFrontend);
    void SDK.Connections.initMainConnection(async () => {
      const target = SDK.TargetManager.TargetManager.instance().createTarget(
          // TODO: Use SDK.Target.Type.NODE rather thatn BROWSER once DevTools is loaded appropriately in that case.
          'main', i18nString(UIStrings.main), SDK.Target.Type.BROWSER, null);
      target.setInspectedURL('Node.js' as Platform.DevToolsPath.UrlString);
    }, Components.TargetDetachedDialog.TargetDetachedDialog.connectionLost);
  }
}

export class NodeChildTargetManager extends SDK.SDKModel.SDKModel<void> implements ProtocolProxyApi.TargetDispatcher {
  readonly #targetManager: SDK.TargetManager.TargetManager;
  readonly #parentTarget: SDK.Target.Target;
  readonly #targetAgent: ProtocolProxyApi.TargetApi;
  readonly #childTargets = new Map<Protocol.Target.SessionID, SDK.Target.Target>();
  readonly #childConnections = new Map<string, NodeConnection>();
  constructor(parentTarget: SDK.Target.Target) {
    super(parentTarget);
    this.#targetManager = parentTarget.targetManager();
    this.#parentTarget = parentTarget;
    this.#targetAgent = parentTarget.targetAgent();

    parentTarget.registerTargetDispatcher(this);
    void this.#targetAgent.invoke_setDiscoverTargets({discover: true});

    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener(
        Host.InspectorFrontendHostAPI.Events.DevicesDiscoveryConfigChanged, this.#devicesDiscoveryConfigChanged, this);
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.setDevicesUpdatesEnabled(false);
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.setDevicesUpdatesEnabled(true);
  }

  #devicesDiscoveryConfigChanged({data: config}: Common.EventTarget.EventTargetEvent<Adb.Config>): void {
    const locations = [];
    for (const address of config.networkDiscoveryConfig) {
      const parts = address.split(':');
      const port = parseInt(parts[1], 10);
      if (parts[0] && port) {
        locations.push({host: parts[0], port});
      }
    }
    void this.#targetAgent.invoke_setRemoteLocations({locations});
  }

  override dispose(): void {
    Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener(
        Host.InspectorFrontendHostAPI.Events.DevicesDiscoveryConfigChanged, this.#devicesDiscoveryConfigChanged, this);

    for (const sessionId of this.#childTargets.keys()) {
      this.detachedFromTarget({sessionId});
    }
  }

  targetCreated({targetInfo}: Protocol.Target.TargetCreatedEvent): void {
    if (targetInfo.type === 'node' && !targetInfo.attached) {
      void this.#targetAgent.invoke_attachToTarget({targetId: targetInfo.targetId, flatten: false});
    } else if (targetInfo.type === 'node_worker') {
      void this.#targetAgent.invoke_setAutoAttach({autoAttach: true, waitForDebuggerOnStart: false});
    }
  }

  targetInfoChanged(_event: Protocol.Target.TargetInfoChangedEvent): void {
  }

  targetDestroyed(_event: Protocol.Target.TargetDestroyedEvent): void {
  }

  async attachedToTarget({sessionId, targetInfo}: Protocol.Target.AttachedToTargetEvent): Promise<void> {
    let target: SDK.Target.Target;
    if (targetInfo.type === 'node_worker') {
      target = this.#targetManager.createTarget(
          targetInfo.targetId, targetInfo.title, SDK.Target.Type.NODE_WORKER, this.#parentTarget, sessionId, true,
          undefined, targetInfo);
    } else {
      const name = i18nString(UIStrings.nodejsS, {PH1: targetInfo.url});
      document.title = i18nString(UIStrings.NodejsTitleS, {PH1: targetInfo.url});
      const connection = new NodeConnection(this.#targetAgent, sessionId);
      this.#childConnections.set(sessionId, connection);
      target = this.#targetManager.createTarget(
          targetInfo.targetId, name, SDK.Target.Type.NODE, null, undefined, undefined,
          new ProtocolClient.DevToolsCDPConnection.DevToolsCDPConnection(connection));
    }
    this.#childTargets.set(sessionId, target);
    void target.runtimeAgent().invoke_runIfWaitingForDebugger();
    await this.#initializeStorage(target);
  }

  async #initializeStorage(target: SDK.Target.Target): Promise<void> {
    const storageAgent = target.storageAgent();
    const response = await storageAgent.invoke_getStorageKey({});

    const storageKey = response.storageKey;
    if (response.getError() || !storageKey) {
      console.error(`Failed to get storage key for target ${target.id()}: ${response.getError()}`);
      return;
    }

    const storageKeyManager = target.model(SDK.StorageKeyManager.StorageKeyManager);
    if (storageKeyManager) {
      storageKeyManager.setMainStorageKey(storageKey);
      storageKeyManager.updateStorageKeys(new Set([storageKey]));
    }
  }

  detachedFromTarget({sessionId}: Protocol.Target.DetachedFromTargetEvent): void {
    const childTarget = this.#childTargets.get(sessionId);
    if (childTarget) {
      childTarget.dispose('target terminated');
    }
    this.#childTargets.delete(sessionId);
    this.#childConnections.delete(sessionId);
  }

  receivedMessageFromTarget({sessionId, message}: Protocol.Target.ReceivedMessageFromTargetEvent): void {
    const connection = this.#childConnections.get(sessionId);
    const onMessage = connection ? connection.onMessage : null;
    if (onMessage) {
      onMessage.call(null, message);
    }
  }

  targetCrashed(_event: Protocol.Target.TargetCrashedEvent): void {
  }
}

export class NodeConnection implements ProtocolClient.ConnectionTransport.ConnectionTransport {
  readonly #targetAgent: ProtocolProxyApi.TargetApi;
  readonly #sessionId: Protocol.Target.SessionID;
  onMessage: ((arg0: Object|string) => void)|null;
  #onDisconnect: ((arg0: string) => void)|null;
  constructor(targetAgent: ProtocolProxyApi.TargetApi, sessionId: Protocol.Target.SessionID) {
    this.#targetAgent = targetAgent;
    this.#sessionId = sessionId;
    this.onMessage = null;
    this.#onDisconnect = null;
  }

  setOnMessage(onMessage: (arg0: Object|string) => void): void {
    this.onMessage = onMessage;
  }

  setOnDisconnect(onDisconnect: (arg0: string) => void): void {
    this.#onDisconnect = onDisconnect;
  }

  sendRawMessage(message: string): void {
    void this.#targetAgent.invoke_sendMessageToTarget({message, sessionId: this.#sessionId});
  }

  async disconnect(): Promise<void> {
    if (this.#onDisconnect) {
      this.#onDisconnect.call(null, 'force disconnect');
    }
    this.#onDisconnect = null;
    this.onMessage = null;
    await this.#targetAgent.invoke_detachFromTarget({sessionId: this.#sessionId});
  }
}

SDK.SDKModel.SDKModel.register(NodeChildTargetManager, {capabilities: SDK.Target.Capability.TARGET, autostart: true});
