import { type BaseAnthropic } from '../client';

import { type PromiseOrValue } from '../internal/types';
import {
  type APIResponseProps,
  type WithRequestID,
  defaultParseResponse,
  addResponseIDs,
} from '../internal/parse';
import { endApiCallSpan, guardTracing, setCreatedIDAttribute, type ApiCallResult } from '../internal/tracing';
import { isAsyncIterable } from '../internal/uploads';
import { endAnthropicApiCallSpanWhenStreamEnds, setAnthropicSpanAttributes } from '../lib/internal/tracing';

/**
 * A subclass of `Promise` providing additional helper methods
 * for interacting with the SDK.
 */
export class APIPromise<T> extends Promise<WithRequestID<T>> {
  private parsedPromise: Promise<WithRequestID<T>> | undefined;
  #client: BaseAnthropic;

  constructor(
    client: BaseAnthropic,
    private responsePromise: Promise<APIResponseProps>,
    private parseResponse: (
      client: BaseAnthropic,
      props: APIResponseProps,
    ) => PromiseOrValue<WithRequestID<T>> = defaultParseResponse,
  ) {
    super((resolve) => {
      // this is maybe a bit weird but this has to be a no-op to not implicitly
      // parse the response body; instead .then, .catch, .finally are overridden
      // to parse the response
      resolve(null as any);
    });
    this.#client = client;
  }

  _thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
    return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
      addResponseIDs(transform(await this.parseResponse(client, props), props), props.response),
    );
  }

  /**
   * Gets the raw `Response` instance instead of parsing the response
   * data.
   *
   * If you want to parse the response body but still get the `Response`
   * instance, you can use {@link withResponse()}.
   *
   * 👋 Getting the wrong TypeScript type for `Response`?
   * Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
   * to your `tsconfig.json`.
   */
  asResponse(): Promise<Response> {
    return this.responsePromise.then((props) => {
      // A traced API call's span ends with the body. A response read this way, before any parse
      // of its body has begun, may never be parsed, so its span ends now, without the body.
      const span = props.options.__span?.span;
      if (span && !this.parsedPromise) {
        const result: ApiCallResult = { status: 'success', ...props, data: undefined };
        setAnthropicSpanAttributes(this.#client, span, result);
        guardTracing(this.#client, () => endApiCallSpan(span, result));
      }
      return props.response;
    });
  }

  /**
   * Gets the parsed response data, the raw `Response` instance and the ID of the request,
   * returned via the `request-id` header which is useful for debugging requests and resporting
   * issues to Anthropic.
   *
   * If you just want to get the raw `Response` instance without parsing it,
   * you can use {@link asResponse()}.
   *
   * 👋 Getting the wrong TypeScript type for `Response`?
   * Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
   * to your `tsconfig.json`.
   */
  async withResponse(): Promise<{
    data: T;
    response: Response;
    request_id: string | null | undefined;
    workspace_id: string | null | undefined;
  }> {
    const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
    return {
      data,
      response,
      request_id: response.headers.get('request-id'),
      workspace_id: response.headers.get('anthropic-workspace-id'),
    };
  }

  private parse(): Promise<WithRequestID<T>> {
    if (!this.parsedPromise) {
      this.parsedPromise = this.responsePromise.then((props) => this.parseResponseTraced(props));
    }
    return this.parsedPromise;
  }

  /**
   * Parses the body of an API call. When the call has a span, the span ends with the parse, or,
   * when the parse returns a streamed body, when the application has read that stream.
   */
  private async parseResponseTraced(props: APIResponseProps): Promise<WithRequestID<T>> {
    const call = props.options.__span;
    if (!call) {
      return this.parseResponse(this.#client, props);
    }
    const { span } = call;
    let data: WithRequestID<T>;
    try {
      data = await this.parseResponse(this.#client, props);
    } catch (error) {
      const result: ApiCallResult = { status: 'error', ...props, error };
      setAnthropicSpanAttributes(this.#client, span, result);
      guardTracing(this.#client, () => endApiCallSpan(span, result));
      throw error;
    }
    const result: ApiCallResult = { status: 'success', ...props, data };
    guardTracing(this.#client, () => setCreatedIDAttribute(call, data));
    setAnthropicSpanAttributes(this.#client, span, result);
    if (props.options.stream && isAsyncIterable(data)) {
      guardTracing(this.#client, () =>
        endAnthropicApiCallSpanWhenStreamEnds(this.#client, span, data, props),
      );
    } else {
      guardTracing(this.#client, () => endApiCallSpan(span, result));
    }
    return data;
  }

  override then<TResult1 = WithRequestID<T>, TResult2 = never>(
    onfulfilled?: ((value: WithRequestID<T>) => TResult1 | PromiseLike<TResult1>) | undefined | null,
    onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
  ): Promise<TResult1 | TResult2> {
    return this.parse().then(onfulfilled, onrejected);
  }

  override catch<TResult = never>(
    onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
  ): Promise<WithRequestID<T> | TResult> {
    return this.parse().catch(onrejected);
  }

  override finally(onfinally?: (() => void) | undefined | null): Promise<WithRequestID<T>> {
    return this.parse().finally(onfinally);
  }
}
