// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT

// Package gitea provides a Go SDK for the Gitea API.
package gitea

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// Client is a Gitea API client
type Client struct {
	baseURL    string
	token      string
	httpClient *http.Client
	userAgent  string
}

// ClientOption is a function that configures the client
type ClientOption func(*Client)

// NewClient creates a new Gitea API client
func NewClient(baseURL string, opts ...ClientOption) (*Client, error) {
	// Normalize URL
	baseURL = strings.TrimSuffix(baseURL, "/")
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		baseURL = "https://" + baseURL
	}

	c := &Client{
		baseURL:    baseURL,
		httpClient: &http.Client{Timeout: 30 * time.Second},
		userAgent:  "gitea-sdk-go/1.0",
	}

	for _, opt := range opts {
		opt(c)
	}

	return c, nil
}

// SetToken sets the API token
func SetToken(token string) ClientOption {
	return func(c *Client) {
		c.token = token
	}
}

// SetHTTPClient sets a custom HTTP client
func SetHTTPClient(client *http.Client) ClientOption {
	return func(c *Client) {
		c.httpClient = client
	}
}

// SetUserAgent sets a custom user agent
func SetUserAgent(ua string) ClientOption {
	return func(c *Client) {
		c.userAgent = ua
	}
}

// APIError represents an API error response
type APIError struct {
	Code    string         `json:"code"`
	Message string         `json:"message"`
	Status  int            `json:"status"`
	Details map[string]any `json:"details,omitempty"`
}

func (e *APIError) Error() string {
	if e.Code != "" {
		return fmt.Sprintf("%s: %s", e.Code, e.Message)
	}
	return e.Message
}

// doRequest performs an HTTP request
func (c *Client) doRequest(ctx context.Context, method, path string, body any, result any) error {
	fullURL := c.baseURL + path

	var bodyReader io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("failed to marshal body: %w", err)
		}
		bodyReader = bytes.NewReader(jsonBody)
	}

	req, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("User-Agent", c.userAgent)
	if c.token != "" {
		req.Header.Set("Authorization", "token "+c.token)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		var apiErr APIError
		if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
			body, _ := io.ReadAll(resp.Body)
			return &APIError{
				Status:  resp.StatusCode,
				Message: string(body),
			}
		}
		apiErr.Status = resp.StatusCode
		return &apiErr
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// doRequestRaw performs an HTTP request with raw body
func (c *Client) doRequestRaw(ctx context.Context, method, path string, body io.Reader, contentType string, result any) error {
	fullURL := c.baseURL + path

	req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("User-Agent", c.userAgent)
	if c.token != "" {
		req.Header.Set("Authorization", "token "+c.token)
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		var apiErr APIError
		if err := json.NewDecoder(resp.Body).Decode(&apiErr); err != nil {
			body, _ := io.ReadAll(resp.Body)
			return &APIError{
				Status:  resp.StatusCode,
				Message: string(body),
			}
		}
		apiErr.Status = resp.StatusCode
		return &apiErr
	}

	if result != nil {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			return fmt.Errorf("failed to decode response: %w", err)
		}
	}

	return nil
}

// GetVersion returns the Gitea server version
func (c *Client) GetVersion(ctx context.Context) (string, error) {
	var result struct {
		Version string `json:"version"`
	}
	if err := c.doRequest(ctx, "GET", "/api/v1/version", nil, &result); err != nil {
		return "", err
	}
	return result.Version, nil
}

// User represents a Gitea user
type User struct {
	ID        int64  `json:"id"`
	Login     string `json:"login"`
	FullName  string `json:"full_name"`
	Email     string `json:"email"`
	AvatarURL string `json:"avatar_url"`
	IsAdmin   bool   `json:"is_admin"`
}

// GetCurrentUser returns the authenticated user
func (c *Client) GetCurrentUser(ctx context.Context) (*User, error) {
	var user User
	if err := c.doRequest(ctx, "GET", "/api/v1/user", nil, &user); err != nil {
		return nil, err
	}
	return &user, nil
}

// Repository represents a Gitea repository
type Repository struct {
	ID            int64  `json:"id"`
	Owner         *User  `json:"owner"`
	Name          string `json:"name"`
	FullName      string `json:"full_name"`
	Description   string `json:"description"`
	Private       bool   `json:"private"`
	Fork          bool   `json:"fork"`
	DefaultBranch string `json:"default_branch"`
	Stars         int    `json:"stars_count"`
	Forks         int    `json:"forks_count"`
	CloneURL      string `json:"clone_url"`
	HTMLURL       string `json:"html_url"`
}

// GetRepository returns a repository by owner and name
func (c *Client) GetRepository(ctx context.Context, owner, repo string) (*Repository, error) {
	var repository Repository
	path := fmt.Sprintf("/api/v1/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repo))
	if err := c.doRequest(ctx, "GET", path, nil, &repository); err != nil {
		return nil, err
	}
	return &repository, nil
}

// Release represents a Gitea release
type Release struct {
	ID          int64        `json:"id"`
	TagName     string       `json:"tag_name"`
	Name        string       `json:"name"`
	Body        string       `json:"body"`
	Draft       bool         `json:"draft"`
	Prerelease  bool         `json:"prerelease"`
	PublishedAt time.Time    `json:"published_at"`
	Assets      []Attachment `json:"assets"`
}

// Attachment represents a release asset
type Attachment struct {
	ID            int64     `json:"id"`
	Name          string    `json:"name"`
	Size          int64     `json:"size"`
	DownloadCount int64     `json:"download_count"`
	DownloadURL   string    `json:"browser_download_url"`
	CreatedAt     time.Time `json:"created_at"`
}

// GetRelease returns a release by tag name
func (c *Client) GetRelease(ctx context.Context, owner, repo, tag string) (*Release, error) {
	var release Release
	path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s",
		url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(tag))
	if err := c.doRequest(ctx, "GET", path, nil, &release); err != nil {
		return nil, err
	}
	return &release, nil
}

// ListReleases returns all releases for a repository
func (c *Client) ListReleases(ctx context.Context, owner, repo string) ([]*Release, error) {
	var releases []*Release
	path := fmt.Sprintf("/api/v1/repos/%s/%s/releases", url.PathEscape(owner), url.PathEscape(repo))
	if err := c.doRequest(ctx, "GET", path, nil, &releases); err != nil {
		return nil, err
	}
	return releases, nil
}
