// Package gitea provides a Gitea API adapter implementing port.GitRepository.
package gitea

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

// ChangeFileOperation represents a single file operation in a bulk change.
type ChangeFileOperation struct {
	// Operation is "create", "update", or "delete"
	Operation string `json:"operation"`
	// Path is the file path (max 500 characters)
	Path string `json:"path"`
	// Content is the base64-encoded file content (required for create/update)
	Content string `json:"content,omitempty"`
	// SHA is required for update and delete operations
	SHA string `json:"sha,omitempty"`
	// FromPath is the original path when moving/renaming files
	FromPath string `json:"from_path,omitempty"`
}

// ChangeFilesOptions options for changing multiple files in a single commit.
type ChangeFilesOptions struct {
	// Files is the list of file operations
	Files []ChangeFileOperation `json:"files"`
	// Message is the commit message
	Message string `json:"message,omitempty"`
	// BranchName is the branch to commit to (optional, defaults to repo default)
	BranchName string `json:"branch,omitempty"`
	// NewBranchName creates a new branch (optional)
	NewBranchName string `json:"new_branch,omitempty"`
}

// FilesResponse is the response from bulk file operations.
type FilesResponse struct {
	Files  []FileContentResponse `json:"files"`
	Commit *FileCommitResponse   `json:"commit"`
}

// FileContentResponse represents a file in the response.
type FileContentResponse struct {
	Name        string `json:"name"`
	Path        string `json:"path"`
	SHA         string `json:"sha"`
	Size        int64  `json:"size"`
	URL         string `json:"url"`
	HTMLURL     string `json:"html_url"`
	GitURL      string `json:"git_url"`
	DownloadURL string `json:"download_url"`
	Type        string `json:"type"`
}

// FileCommitResponse contains commit information from the response.
type FileCommitResponse struct {
	SHA       string `json:"sha"`
	URL       string `json:"url"`
	HTMLURL   string `json:"html_url"`
	Author    *User  `json:"author"`
	Committer *User  `json:"committer"`
	Message   string `json:"message"`
}

// User represents a git user in commit info.
type User struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

// BulkFileClient wraps a Gitea client with direct HTTP capabilities
// for operations not yet supported by the SDK.
type BulkFileClient struct {
	baseURL string
	token   string
	client  *http.Client
}

// NewBulkFileClient creates a new client for bulk file operations.
// baseURL is the Gitea server URL (e.g., https://git.threesix.ai)
// token is an API access token with repo permissions
func NewBulkFileClient(baseURL, token string) *BulkFileClient {
	return &BulkFileClient{
		baseURL: strings.TrimSuffix(baseURL, "/"),
		token:   token,
		client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// ChangeFiles creates, updates, or deletes multiple files in a single commit.
// This uses the Gitea API endpoint POST /repos/{owner}/{repo}/contents
// which was added in Gitea v1.20.0 (PR #24887).
// Includes retry with exponential backoff for transient failures.
func (c *BulkFileClient) ChangeFiles(ctx context.Context, owner, repo string, opts ChangeFilesOptions) (*FilesResponse, error) {
	const maxRetries = 3

	var lastErr error
	for attempt := 0; attempt < maxRetries; attempt++ {
		result, err := c.doChangeFiles(ctx, owner, repo, opts)
		if err == nil {
			return result, nil
		}
		lastErr = err

		// Don't retry client errors (4xx) except rate limiting (429)
		if !isRetryableError(err) {
			return nil, err
		}

		// Wait before retry with exponential backoff
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(time.Duration(attempt+1) * time.Second):
		}
	}
	return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}

// doChangeFiles performs the actual API request.
func (c *BulkFileClient) doChangeFiles(ctx context.Context, owner, repo string, opts ChangeFilesOptions) (*FilesResponse, error) {
	url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents", c.baseURL, owner, repo)

	body, err := json.Marshal(&opts)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

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

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "token "+c.token)

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

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, &apiError{StatusCode: resp.StatusCode, Body: string(respBody)}
	}

	var result FilesResponse
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return &result, nil
}

// apiError represents an API error with status code.
type apiError struct {
	StatusCode int
	Body       string
}

func (e *apiError) Error() string {
	return fmt.Sprintf("API error (status %d): %s", e.StatusCode, e.Body)
}

// isRetryableError checks if an error should be retried.
func isRetryableError(err error) bool {
	if apiErr, ok := err.(*apiError); ok {
		// Retry server errors (5xx) and rate limiting (429)
		return apiErr.StatusCode >= 500 || apiErr.StatusCode == 429
	}
	// Retry network errors
	return strings.Contains(err.Error(), "connection") ||
		strings.Contains(err.Error(), "timeout") ||
		strings.Contains(err.Error(), "EOF")
}

// GetFileContent retrieves the content of a file from a repository.
// Returns the decoded content and the file's SHA (needed for updates).
// Returns nil, nil if the file doesn't exist (404).
func (c *BulkFileClient) GetFileContent(ctx context.Context, owner, repo, filepath string) ([]byte, string, error) {
	url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", c.baseURL, owner, repo, filepath)

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

	req.Header.Set("Authorization", "token "+c.token)

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

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, "", fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode == 404 {
		return nil, "", nil // File doesn't exist
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, "", &apiError{StatusCode: resp.StatusCode, Body: string(respBody)}
	}

	var result struct {
		Content  string `json:"content"`
		Encoding string `json:"encoding"`
		SHA      string `json:"sha"`
	}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, "", fmt.Errorf("failed to parse response: %w", err)
	}

	// Decode base64 content
	content, err := base64.StdEncoding.DecodeString(result.Content)
	if err != nil {
		return nil, "", fmt.Errorf("failed to decode content: %w", err)
	}

	return content, result.SHA, nil
}
