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

package io.gitea.sdk;

import io.gitea.sdk.exceptions.*;
import io.gitea.sdk.models.*;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;

/**
 * Client for the Gitea API.
 */
public class GiteaClient implements AutoCloseable {
    private final HttpClient httpClient;
    private final String baseUrl;
    private final String token;
    private final ObjectMapper objectMapper;

    /**
     * Creates a new Gitea API client.
     *
     * @param baseUrl Base URL of the Gitea instance
     * @param token   API token for authentication
     */
    public GiteaClient(String baseUrl, String token) {
        this(baseUrl, token, null);
    }

    /**
     * Creates a new Gitea API client with custom HttpClient.
     *
     * @param baseUrl    Base URL of the Gitea instance
     * @param token      API token for authentication
     * @param httpClient Custom HttpClient (optional)
     */
    public GiteaClient(String baseUrl, String token, HttpClient httpClient) {
        this.baseUrl = baseUrl.replaceAll("/+$", "");
        this.token = token;
        this.httpClient = httpClient != null ? httpClient : HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(30))
                .build();

        this.objectMapper = new ObjectMapper()
                .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    // User Methods

    /**
     * Gets the currently authenticated user.
     */
    public User getCurrentUser() throws GiteaException {
        return get("/api/v1/user", User.class);
    }

    /**
     * Gets a user by username.
     */
    public User getUser(String username) throws GiteaException {
        return get("/api/v1/users/" + encode(username), User.class);
    }

    // Repository Methods

    /**
     * Gets a repository by owner and name.
     */
    public Repository getRepository(String owner, String repo) throws GiteaException {
        return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo), Repository.class);
    }

    /**
     * Lists repositories for a user.
     */
    public List<Repository> listUserRepositories(String username) throws GiteaException {
        return getList("/api/v1/users/" + encode(username) + "/repos", Repository.class);
    }

    // Release Methods

    /**
     * Gets a release by ID.
     */
    public Release getRelease(String owner, String repo, long releaseId) throws GiteaException {
        return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases/" + releaseId, Release.class);
    }

    /**
     * Gets a release by tag name.
     */
    public Release getReleaseByTag(String owner, String repo, String tag) throws GiteaException {
        return get("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases/tags/" + encode(tag), Release.class);
    }

    /**
     * Lists all releases for a repository.
     */
    public List<Release> listReleases(String owner, String repo) throws GiteaException {
        return getList("/api/v1/repos/" + encode(owner) + "/" + encode(repo) + "/releases", Release.class);
    }

    // Upload Methods

    /**
     * Uploads a release asset using chunked upload.
     */
    public UploadResult uploadReleaseAsset(
            String owner,
            String repo,
            long releaseId,
            InputStream fileStream,
            String filename,
            long fileSize,
            ChunkedUploadOptions options) throws GiteaException {
        var upload = new ChunkedUpload(this, owner, repo, releaseId, options);
        return upload.upload(fileStream, filename, fileSize);
    }

    /**
     * Creates a chunked upload handler.
     */
    public ChunkedUpload createChunkedUpload(
            String owner,
            String repo,
            long releaseId,
            ChunkedUploadOptions options) {
        return new ChunkedUpload(this, owner, repo, releaseId, options);
    }

    /**
     * Gets the status of an upload session.
     */
    public UploadSession getUploadSession(String sessionId) throws GiteaException {
        return get("/api/v1/repos/uploads/" + encode(sessionId), UploadSession.class);
    }

    /**
     * Cancels an upload session.
     */
    public void cancelUpload(String sessionId) throws GiteaException {
        delete("/api/v1/repos/uploads/" + encode(sessionId));
    }

    // Internal HTTP Methods

    <T> T get(String path, Class<T> type) throws GiteaException {
        var request = buildRequest(path)
                .GET()
                .build();
        return execute(request, type);
    }

    <T> List<T> getList(String path, Class<T> elementType) throws GiteaException {
        var request = buildRequest(path)
                .GET()
                .build();
        return executeList(request, elementType);
    }

    <T> T post(String path, Object body, Class<T> type) throws GiteaException {
        try {
            var json = body != null ? objectMapper.writeValueAsString(body) : "{}";
            var request = buildRequest(path)
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(json))
                    .build();
            return execute(request, type);
        } catch (IOException e) {
            throw new GiteaException("Failed to serialize request body", e);
        }
    }

    void post(String path, Object body) throws GiteaException {
        try {
            var json = body != null ? objectMapper.writeValueAsString(body) : "{}";
            var request = buildRequest(path)
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(json))
                    .build();
            executeVoid(request);
        } catch (IOException e) {
            throw new GiteaException("Failed to serialize request body", e);
        }
    }

    <T> T putBinary(String path, byte[] data, Class<T> type) throws GiteaException {
        var request = buildRequest(path)
                .header("Content-Type", "application/octet-stream")
                .PUT(HttpRequest.BodyPublishers.ofByteArray(data))
                .build();
        return execute(request, type);
    }

    void putBinary(String path, byte[] data) throws GiteaException {
        var request = buildRequest(path)
                .header("Content-Type", "application/octet-stream")
                .PUT(HttpRequest.BodyPublishers.ofByteArray(data))
                .build();
        executeVoid(request);
    }

    void delete(String path) throws GiteaException {
        var request = buildRequest(path)
                .DELETE()
                .build();
        executeVoid(request);
    }

    private HttpRequest.Builder buildRequest(String path) {
        var builder = HttpRequest.newBuilder()
                .uri(URI.create(baseUrl + path))
                .header("User-Agent", "Gitea.SDK/1.0.0");

        if (token != null && !token.isEmpty()) {
            builder.header("Authorization", "token " + token);
        }

        return builder;
    }

    private <T> T execute(HttpRequest request, Class<T> type) throws GiteaException {
        try {
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            ensureSuccess(response);
            return objectMapper.readValue(response.body(), type);
        } catch (IOException | InterruptedException e) {
            throw new GiteaException("Request failed", e);
        }
    }

    private <T> List<T> executeList(HttpRequest request, Class<T> elementType) throws GiteaException {
        try {
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            ensureSuccess(response);
            var listType = objectMapper.getTypeFactory().constructCollectionType(List.class, elementType);
            return objectMapper.readValue(response.body(), listType);
        } catch (IOException | InterruptedException e) {
            throw new GiteaException("Request failed", e);
        }
    }

    private void executeVoid(HttpRequest request) throws GiteaException {
        try {
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            ensureSuccess(response);
        } catch (IOException | InterruptedException e) {
            throw new GiteaException("Request failed", e);
        }
    }

    private void ensureSuccess(HttpResponse<String> response) throws GiteaException {
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            return;
        }

        switch (response.statusCode()) {
            case 401:
                throw new AuthenticationException("Authentication failed");
            case 404:
                throw new NotFoundException("Resource not found");
            default:
                try {
                    var error = objectMapper.readValue(response.body(), ApiErrorResponse.class);
                    throw new ApiException(
                            error.getMessage() != null ? error.getMessage() : response.body(),
                            error.getCode(),
                            response.statusCode());
                } catch (IOException e) {
                    throw new ApiException(response.body(), null, response.statusCode());
                }
        }
    }

    private static String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }

    @Override
    public void close() {
        // HttpClient doesn't need explicit cleanup in Java 11+
    }
}
