// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

import {
  User,
  Repository,
  Release,
  UploadSession,
  UploadResult,
  APIErrorResponse,
} from './types';
import {
  APIError,
  AuthenticationError,
  NotFoundError,
} from './errors';
import { ChunkedUpload, ChunkedUploadOptions } from './upload';

/**
 * Options for creating a GiteaClient.
 */
export interface GiteaClientOptions {
  /** Base URL of the Gitea instance */
  baseUrl: string;
  /** API token for authentication */
  token?: string;
  /** Request timeout in milliseconds (default: 30000) */
  timeout?: number;
}

/**
 * Client for the Gitea API.
 *
 * @example
 * ```typescript
 * const client = new GiteaClient({
 *   baseUrl: 'https://gitea.example.com',
 *   token: 'your_token'
 * });
 *
 * const user = await client.getCurrentUser();
 * console.log(`Logged in as ${user.login}`);
 * ```
 */
export class GiteaClient {
  private baseUrl: string;
  private token?: string;
  private timeout: number;

  constructor(options: GiteaClientOptions) {
    this.baseUrl = options.baseUrl.replace(/\/$/, '');
    this.token = options.token;
    this.timeout = options.timeout ?? 30000;
  }

  private async request<T>(
    method: string,
    path: string,
    body?: unknown
  ): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const headers: Record<string, string> = {
      'User-Agent': 'gitea-sdk-typescript/1.0',
    };

    if (this.token) {
      headers['Authorization'] = `token ${this.token}`;
    }

    if (body) {
      headers['Content-Type'] = 'application/json';
    }

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (response.status === 401) {
        throw new AuthenticationError();
      }

      if (response.status === 404) {
        throw new NotFoundError('Resource');
      }

      if (!response.ok) {
        let errorData: APIErrorResponse;
        try {
          errorData = await response.json();
        } catch {
          throw new APIError(await response.text(), undefined, response.status);
        }
        throw new APIError(
          errorData.message,
          errorData.code,
          response.status,
          errorData.details
        );
      }

      if (response.status === 204) {
        return {} as T;
      }

      return response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      if (error instanceof Error && error.name === 'AbortError') {
        throw new APIError('Request timeout', 'TIMEOUT', 0);
      }
      throw error;
    }
  }

  // User methods

  /**
   * Get the currently authenticated user.
   */
  async getCurrentUser(): Promise<User> {
    return this.request<User>('GET', '/api/v1/user');
  }

  /**
   * Get a user by username.
   */
  async getUser(username: string): Promise<User> {
    return this.request<User>(
      'GET',
      `/api/v1/users/${encodeURIComponent(username)}`
    );
  }

  // Repository methods

  /**
   * Get a repository by owner and name.
   */
  async getRepository(owner: string, repo: string): Promise<Repository> {
    return this.request<Repository>(
      'GET',
      `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
    );
  }

  /**
   * List repositories for a user.
   */
  async listUserRepos(username: string): Promise<Repository[]> {
    return this.request<Repository[]>(
      'GET',
      `/api/v1/users/${encodeURIComponent(username)}/repos`
    );
  }

  // Release methods

  /**
   * Get a release by ID.
   */
  async getRelease(
    owner: string,
    repo: string,
    releaseId: number
  ): Promise<Release> {
    return this.request<Release>(
      'GET',
      `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
        repo
      )}/releases/${releaseId}`
    );
  }

  /**
   * Get a release by tag name.
   */
  async getReleaseByTag(
    owner: string,
    repo: string,
    tag: string
  ): Promise<Release> {
    return this.request<Release>(
      'GET',
      `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
        repo
      )}/releases/tags/${encodeURIComponent(tag)}`
    );
  }

  /**
   * List all releases for a repository.
   */
  async listReleases(owner: string, repo: string): Promise<Release[]> {
    return this.request<Release[]>(
      'GET',
      `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(
        repo
      )}/releases`
    );
  }

  // Upload methods

  /**
   * Create a chunked upload handler.
   */
  createChunkedUpload(
    owner: string,
    repo: string,
    releaseId: number,
    options?: ChunkedUploadOptions
  ): ChunkedUpload {
    return new ChunkedUpload(
      this.baseUrl,
      this.token ?? '',
      owner,
      repo,
      releaseId,
      options
    );
  }

  /**
   * Upload a release asset using chunked upload.
   *
   * @example
   * ```typescript
   * const file = new File([...], 'app.tar.gz');
   * const result = await client.uploadReleaseAsset({
   *   owner: 'myorg',
   *   repo: 'myrepo',
   *   releaseId: 123,
   *   file,
   *   onProgress: (p) => console.log(`${p.percent.toFixed(1)}%`)
   * });
   * console.log(`Uploaded: ${result.browser_download_url}`);
   * ```
   */
  async uploadReleaseAsset(options: {
    owner: string;
    repo: string;
    releaseId: number;
    file: File | Blob;
    filename?: string;
    chunkSize?: number;
    parallel?: number;
    verifyChecksum?: boolean;
    onProgress?: (progress: import('./types').Progress) => void;
  }): Promise<UploadResult> {
    const filename = options.filename ?? (options.file as File).name ?? 'file';
    const upload = this.createChunkedUpload(
      options.owner,
      options.repo,
      options.releaseId,
      {
        chunkSize: options.chunkSize,
        parallel: options.parallel,
        verifyChecksum: options.verifyChecksum,
        onProgress: options.onProgress,
      }
    );

    return upload.upload(options.file, filename);
  }

  /**
   * Get the status of an upload session.
   */
  async getUploadSession(sessionId: string): Promise<UploadSession> {
    return this.request<UploadSession>(
      'GET',
      `/api/v1/repos/uploads/${sessionId}`
    );
  }

  /**
   * Cancel an upload session.
   */
  async cancelUpload(sessionId: string): Promise<void> {
    await this.request<void>('DELETE', `/api/v1/repos/uploads/${sessionId}`);
  }
}
