import { requestUrl } from "obsidian";
import type { HaloSite } from "../settings";
import { HaloError, HttpError } from "./error";
import { withRetry, type RetryOptions } from "../utils/retry";

export interface HaloClientOptions {
  retry?: RetryOptions;
}

export class HaloClient {
  private readonly baseUrl: string;
  private readonly headers: Record<string, string>;
  private readonly retryOptions: RetryOptions;

  constructor(site: HaloSite, options: HaloClientOptions = {}) {
    this.baseUrl = site.url;
    this.headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${site.token}`,
    };
    this.retryOptions = options.retry || {};
  }

  private handleResponse<T>(response: { json: unknown; status: number }): T {
    const { json, status } = response;

    if (status >= 200 && status < 300) {
      return json as T;
    }

    throw HttpError.fromStatus(status, json);
  }

  private async request<T>(path: string, options: { method: string; body?: object }): Promise<T> {
    const doRequest = async () => {
      const response = await requestUrl({
        url: `${this.baseUrl}${path}`,
        method: options.method,
        headers: this.headers,
        body: options.body ? JSON.stringify(options.body) : undefined,
        contentType: options.body ? "application/json" : undefined,
      });

      return this.handleResponse<T>({ json: response.json, status: response.status });
    };

    try {
      return await withRetry(doRequest, this.retryOptions);
    } catch (error) {
      if (error instanceof HttpError) {
        throw this.mapHttpError(error);
      }
      if (error instanceof TypeError) {
        throw HaloError.network(error);
      }
      throw HaloError.unknown(error);
    }
  }

  private mapHttpError(error: HttpError): HaloError {
    switch (error.statusCode) {
      case 401:
        return HaloError.unauthorized();
      case 403:
        return HaloError.forbidden();
      case 404:
        return HaloError.notFound("资源");
      case 500:
      case 502:
      case 503:
        return HaloError.serverError();
      default:
        return new HaloError(error.message, `HTTP_${error.statusCode}`, error.statusCode, error);
    }
  }

  async get<T>(path: string): Promise<T> {
    return this.request<T>(path, { method: "GET" });
  }

  async post<T>(path: string, body: object): Promise<T> {
    return this.request<T>(path, { method: "POST", body });
  }

  async put<T>(path: string, body: object): Promise<T> {
    return this.request<T>(path, { method: "PUT", body });
  }

  async delete(path: string): Promise<void> {
    const doDelete = async () => {
      const response = await requestUrl({
        url: `${this.baseUrl}${path}`,
        method: "DELETE",
        headers: this.headers,
      });

      if (response.status >= 400) {
        throw HttpError.fromStatus(response.status, response.json);
      }
    };

    try {
      await withRetry(doDelete, this.retryOptions);
    } catch (error) {
      if (error instanceof HttpError) {
        throw this.mapHttpError(error);
      }
      if (error instanceof TypeError) {
        throw HaloError.network(error);
      }
      throw HaloError.unknown(error);
    }
  }

  async putVoid(path: string, body?: object): Promise<void> {
    const doPut = async () => {
      const response = await requestUrl({
        url: `${this.baseUrl}${path}`,
        method: "PUT",
        headers: this.headers,
        body: body ? JSON.stringify(body) : undefined,
        contentType: body ? "application/json" : undefined,
      });

      if (response.status >= 400) {
        throw HttpError.fromStatus(response.status, response.json);
      }
    };

    try {
      await withRetry(doPut, this.retryOptions);
    } catch (error) {
      if (error instanceof HttpError) {
        throw this.mapHttpError(error);
      }
      if (error instanceof TypeError) {
        throw HaloError.network(error);
      }
      throw HaloError.unknown(error);
    }
  }
}
