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

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Gitea.SDK;

/// <summary>
/// Client for the Gitea API.
/// </summary>
public class GiteaClient : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    private readonly JsonSerializerOptions _jsonOptions;
    private bool _disposed;

    /// <summary>
    /// Creates a new Gitea API client.
    /// </summary>
    /// <param name="baseUrl">Base URL of the Gitea instance</param>
    /// <param name="token">API token for authentication</param>
    /// <param name="httpClient">Optional HttpClient instance</param>
    public GiteaClient(string baseUrl, string? token = null, HttpClient? httpClient = null)
    {
        _baseUrl = baseUrl.TrimEnd('/');
        _httpClient = httpClient ?? new HttpClient();
        _httpClient.DefaultRequestHeaders.UserAgent.Add(
            new ProductInfoHeaderValue("Gitea.SDK", "1.0.0"));

        if (!string.IsNullOrEmpty(token))
        {
            _httpClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("token", token);
        }

        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };
    }

    #region User Methods

    /// <summary>
    /// Gets the currently authenticated user.
    /// </summary>
    public async Task<User> GetCurrentUserAsync(CancellationToken cancellationToken = default)
    {
        return await GetAsync<User>("/api/v1/user", cancellationToken);
    }

    /// <summary>
    /// Gets a user by username.
    /// </summary>
    public async Task<User> GetUserAsync(string username, CancellationToken cancellationToken = default)
    {
        return await GetAsync<User>($"/api/v1/users/{Uri.EscapeDataString(username)}", cancellationToken);
    }

    #endregion

    #region Repository Methods

    /// <summary>
    /// Gets a repository by owner and name.
    /// </summary>
    public async Task<Repository> GetRepositoryAsync(string owner, string repo, CancellationToken cancellationToken = default)
    {
        return await GetAsync<Repository>(
            $"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}",
            cancellationToken);
    }

    /// <summary>
    /// Lists repositories for a user.
    /// </summary>
    public async Task<List<Repository>> ListUserRepositoriesAsync(string username, CancellationToken cancellationToken = default)
    {
        return await GetAsync<List<Repository>>(
            $"/api/v1/users/{Uri.EscapeDataString(username)}/repos",
            cancellationToken);
    }

    #endregion

    #region Release Methods

    /// <summary>
    /// Gets a release by ID.
    /// </summary>
    public async Task<Release> GetReleaseAsync(string owner, string repo, long releaseId, CancellationToken cancellationToken = default)
    {
        return await GetAsync<Release>(
            $"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases/{releaseId}",
            cancellationToken);
    }

    /// <summary>
    /// Gets a release by tag name.
    /// </summary>
    public async Task<Release> GetReleaseByTagAsync(string owner, string repo, string tag, CancellationToken cancellationToken = default)
    {
        return await GetAsync<Release>(
            $"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases/tags/{Uri.EscapeDataString(tag)}",
            cancellationToken);
    }

    /// <summary>
    /// Lists all releases for a repository.
    /// </summary>
    public async Task<List<Release>> ListReleasesAsync(string owner, string repo, CancellationToken cancellationToken = default)
    {
        return await GetAsync<List<Release>>(
            $"/api/v1/repos/{Uri.EscapeDataString(owner)}/{Uri.EscapeDataString(repo)}/releases",
            cancellationToken);
    }

    #endregion

    #region Upload Methods

    /// <summary>
    /// Uploads a release asset using chunked upload.
    /// </summary>
    public async Task<UploadResult> UploadReleaseAssetAsync(
        string owner,
        string repo,
        long releaseId,
        Stream fileStream,
        string filename,
        ChunkedUploadOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        var upload = new ChunkedUpload(this, owner, repo, releaseId, options ?? new ChunkedUploadOptions());
        return await upload.UploadAsync(fileStream, filename, cancellationToken);
    }

    /// <summary>
    /// Creates a chunked upload handler.
    /// </summary>
    public ChunkedUpload CreateChunkedUpload(
        string owner,
        string repo,
        long releaseId,
        ChunkedUploadOptions? options = null)
    {
        return new ChunkedUpload(this, owner, repo, releaseId, options ?? new ChunkedUploadOptions());
    }

    /// <summary>
    /// Gets the status of an upload session.
    /// </summary>
    public async Task<UploadSession> GetUploadSessionAsync(string sessionId, CancellationToken cancellationToken = default)
    {
        return await GetAsync<UploadSession>($"/api/v1/repos/uploads/{sessionId}", cancellationToken);
    }

    /// <summary>
    /// Cancels an upload session.
    /// </summary>
    public async Task CancelUploadAsync(string sessionId, CancellationToken cancellationToken = default)
    {
        await DeleteAsync($"/api/v1/repos/uploads/{sessionId}", cancellationToken);
    }

    #endregion

    #region Internal HTTP Methods

    internal async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken)
    {
        var response = await _httpClient.GetAsync(_baseUrl + path, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
        return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
            ?? throw new GiteaException("Empty response");
    }

    internal async Task<T> PostAsync<T>(string path, object? body, CancellationToken cancellationToken)
    {
        var response = await _httpClient.PostAsJsonAsync(_baseUrl + path, body, _jsonOptions, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
        return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
            ?? throw new GiteaException("Empty response");
    }

    internal async Task PostAsync(string path, object? body, CancellationToken cancellationToken)
    {
        var response = await _httpClient.PostAsJsonAsync(_baseUrl + path, body, _jsonOptions, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
    }

    internal async Task<T> PutBinaryAsync<T>(string path, byte[] data, CancellationToken cancellationToken)
    {
        var content = new ByteArrayContent(data);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        var response = await _httpClient.PutAsync(_baseUrl + path, content, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
        return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, cancellationToken)
            ?? throw new GiteaException("Empty response");
    }

    internal async Task PutBinaryAsync(string path, byte[] data, CancellationToken cancellationToken)
    {
        var content = new ByteArrayContent(data);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        var response = await _httpClient.PutAsync(_baseUrl + path, content, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
    }

    internal async Task DeleteAsync(string path, CancellationToken cancellationToken)
    {
        var response = await _httpClient.DeleteAsync(_baseUrl + path, cancellationToken);
        await EnsureSuccessAsync(response, cancellationToken);
    }

    private async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        if (response.IsSuccessStatusCode)
            return;

        var content = await response.Content.ReadAsStringAsync(cancellationToken);

        switch ((int)response.StatusCode)
        {
            case 401:
                throw new AuthenticationException("Authentication failed");
            case 404:
                throw new NotFoundException("Resource not found");
            default:
                try
                {
                    var error = JsonSerializer.Deserialize<ApiErrorResponse>(content, _jsonOptions);
                    throw new ApiException(
                        error?.Message ?? content,
                        error?.Code,
                        (int)response.StatusCode);
                }
                catch (JsonException)
                {
                    throw new ApiException(content, null, (int)response.StatusCode);
                }
        }
    }

    #endregion

    public void Dispose()
    {
        if (!_disposed)
        {
            _httpClient.Dispose();
            _disposed = true;
        }
        GC.SuppressFinalize(this);
    }
}

internal class ApiErrorResponse
{
    public string? Code { get; set; }
    public string? Message { get; set; }
    public int? Status { get; set; }
}
