/**
 * @license
 * Copyright 2024 Google Inc.
 * SPDX-License-Identifier: Apache-2.0
 */
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js';

import type {CommandOptions} from '../api/CDPSession.js';
import {CDPSession} from '../api/CDPSession.js';
import type {Connection as CdpConnection} from '../cdp/Connection.js';
import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js';
import {Deferred} from '../util/Deferred.js';

import type {BidiConnection} from './Connection.js';
import type {BidiFrame} from './Frame.js';

/**
 * @internal
 */
export class BidiCdpSession extends CDPSession {
  static sessions = new Map<string, BidiCdpSession>();

  #detached = false;
  readonly #connection?: BidiConnection;
  readonly #sessionId = Deferred.create<string>();
  readonly frame: BidiFrame;

  constructor(frame: BidiFrame, sessionId?: string) {
    super();
    this.frame = frame;
    if (!this.frame.page().browser().cdpSupported) {
      return;
    }

    const connection = this.frame.page().browser().connection;
    this.#connection = connection;

    if (sessionId) {
      this.#sessionId.resolve(sessionId);
      BidiCdpSession.sessions.set(sessionId, this);
    } else {
      (async () => {
        try {
          const {result} = await connection.send('cdp.getSession', {
            context: frame._id,
          });
          this.#sessionId.resolve(result.session!);
          BidiCdpSession.sessions.set(result.session!, this);
        } catch (error) {
          this.#sessionId.reject(error as Error);
        }
      })();
    }

    // SAFETY: We never throw #sessionId.
    BidiCdpSession.sessions.set(this.#sessionId.value() as string, this);
  }

  override connection(): CdpConnection | undefined {
    return undefined;
  }

  override async send<T extends keyof ProtocolMapping.Commands>(
    method: T,
    params?: ProtocolMapping.Commands[T]['paramsType'][0],
    options?: CommandOptions
  ): Promise<ProtocolMapping.Commands[T]['returnType']> {
    if (this.#connection === undefined) {
      throw new UnsupportedOperation(
        'CDP support is required for this feature. The current browser does not support CDP.'
      );
    }
    if (this.#detached) {
      throw new TargetCloseError(
        `Protocol error (${method}): Session closed. Most likely the page has been closed.`
      );
    }
    const session = await this.#sessionId.valueOrThrow();
    const {result} = await this.#connection.send(
      'cdp.sendCommand',
      {
        method: method,
        params: params,
        session,
      },
      options?.timeout
    );
    return result.result;
  }

  override async detach(): Promise<void> {
    if (
      this.#connection === undefined ||
      this.#connection.closed ||
      this.#detached
    ) {
      return;
    }
    try {
      await this.frame.client.send('Target.detachFromTarget', {
        sessionId: this.id(),
      });
    } finally {
      this.onClose();
    }
  }

  /**
   * @internal
   */
  onClose = (): void => {
    BidiCdpSession.sessions.delete(this.id());
    this.#detached = true;
  };

  override id(): string {
    const value = this.#sessionId.value();
    return typeof value === 'string' ? value : '';
  }
}
