// src/forgejo.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
)

// ForgejoLabel represents a Forgejo label
type ForgejoLabel struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Color       string `json:"color"`
	Exclusive   bool   `json:"exclusive,omitempty"`
	IsArchived  bool   `json:"is_archived,omitempty"`
	URL         string `json:"url,omitempty"`
}

// CreateLabelOption represents options for creating a label
type CreateLabelOption struct {
	Name        string `json:"name"`
	Color       string `json:"color"`
	Description string `json:"description,omitempty"`
	Exclusive   bool   `json:"exclusive,omitempty"`
	IsArchived  bool   `json:"is_archived,omitempty"`
}

// EditLabelOption represents options for editing a label
type EditLabelOption struct {
	Name        string `json:"name,omitempty"`
	Color       string `json:"color,omitempty"`
	Description string `json:"description,omitempty"`
	Exclusive   *bool  `json:"exclusive,omitempty"`
	IsArchived  *bool  `json:"is_archived,omitempty"`
}

// ForgejoClient is a client for interacting with the Forgejo API
type ForgejoClient struct {
	BaseURL    string
	HTTPClient *http.Client
	Token      string
}

// NewForgejoClient creates a new ForgejoClient
func NewForgejoClient(baseURL, token string) *ForgejoClient {
	return &ForgejoClient{
		BaseURL:    baseURL,
		HTTPClient: &http.Client{},
		Token:      token,
	}
}

// GetLabels retrieves all labels in a repository
func (c *ForgejoClient) GetLabels(owner, repo string, page, limit int) ([]ForgejoLabel, error) {
	url := fmt.Sprintf("%s/repos/%s/%s/labels?page=%d&limit=%d", c.BaseURL, owner, repo, page, limit)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	c.addAuthHeader(req)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get labels: %s", resp.Status)
	}

	var labels []ForgejoLabel
	if err := json.NewDecoder(resp.Body).Decode(&labels); err != nil {
		return nil, err
	}
	return labels, nil
}

// CreateLabel creates a new label in a repository
func (c *ForgejoClient) CreateLabel(owner, repo string, option CreateLabelOption) (*ForgejoLabel, error) {
	url := fmt.Sprintf("%s/repos/%s/%s/labels", c.BaseURL, owner, repo)
	body, err := json.Marshal(option)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
	if err != nil {
		return nil, err
	}
	c.addAuthHeader(req)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		bodyBytes, _ := ioutil.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to create label: %s - %s", resp.Status, string(bodyBytes))
	}

	var label ForgejoLabel
	if err := json.NewDecoder(resp.Body).Decode(&label); err != nil {
		return nil, err
	}
	return &label, nil
}

// GetLabel retrieves a single label by ID
func (c *ForgejoClient) GetLabel(owner, repo string, id int64) (*ForgejoLabel, error) {
	url := fmt.Sprintf("%s/repos/%s/%s/labels/%d", c.BaseURL, owner, repo, id)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	c.addAuthHeader(req)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get label: %s", resp.Status)
	}

	var label ForgejoLabel
	if err := json.NewDecoder(resp.Body).Decode(&label); err != nil {
		return nil, err
	}
	return &label, nil
}

// UpdateLabel updates an existing label
func (c *ForgejoClient) UpdateLabel(owner, repo string, id int64, option EditLabelOption) error {
	url := fmt.Sprintf("%s/repos/%s/%s/labels/%d", c.BaseURL, owner, repo, id)
	body, err := json.Marshal(option)
	if err != nil {
		return err
	}
	req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(body))
	if err != nil {
		return err
	}
	c.addAuthHeader(req)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("failed to update label: %s - %s", resp.Status, string(bodyBytes))
	}

	return nil
}

// DeleteLabel deletes a label by ID
func (c *ForgejoClient) DeleteLabel(owner, repo string, id int64) error {
	url := fmt.Sprintf("%s/repos/%s/%s/labels/%d", c.BaseURL, owner, repo, id)
	req, err := http.NewRequest("DELETE", url, nil)
	if err != nil {
		return err
	}
	c.addAuthHeader(req)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("failed to delete label: %s - %s", resp.Status, string(body))
	}
	return nil
}

// SetLabelsOnIssue sets the labels on a specific issue or pull request.
// It replaces existing labels with the provided list.
func (c *ForgejoClient) SetLabelsOnIssue(owner, repo string, issueID int64, labels []string) error {
	url := fmt.Sprintf("%s/repos/%s/%s/issues/%d/labels", c.BaseURL, owner, repo, issueID)
	body, err := json.Marshal(labels)
	if err != nil {
		return fmt.Errorf("failed to marshal labels: %w", err)
	}
	req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(body))
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}
	c.addAuthHeader(req)
	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 != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("failed to set labels: %s - %s", resp.Status, string(bodyBytes))
	}

	return nil
}

// GetLabelsOnIssue fetches the current labels on a specific issue or pull request.
func (c *ForgejoClient) GetLabelsOnIssue(owner, repo string, issueID int64) ([]ForgejoLabel, error) {
	url := fmt.Sprintf("%s/repos/%s/%s/issues/%d/labels", c.BaseURL, owner, repo, issueID)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	c.addAuthHeader(req)

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

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to get labels on issue: %s - %s", resp.Status, string(bodyBytes))
	}

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

	return labels, nil
}

// addAuthHeader adds the Authorization header to the request
func (c *ForgejoClient) addAuthHeader(req *http.Request) {
	if c.Token != "" {
		req.Header.Set("Authorization", "token "+c.Token)
	}
}

// EnsureLabelsExist ensures that all labels defined in labels.yaml exist in the repository.
// If deleteOtherLabels is true, it deletes any labels in the repository that are not in the config.
// If dryRun is true, it only prints the actions without executing them.
func EnsureLabelsExist(client *ForgejoClient, owner, repo string, labels []ForgejoLabel, deleteOtherLabels bool, dryRun bool) error {
	// Fetch existing labels from the repository
	existingLabels, err := client.GetLabels(owner, repo, 1, 100)
	if err != nil {
		return fmt.Errorf("failed to fetch existing labels: %w", err)
	}

	// Create a map for existing labels for quick lookup
	existingLabelMap := make(map[string]ForgejoLabel)
	for _, label := range existingLabels {
		existingLabelMap[label.Name] = label
	}

	// Create a map of desired labels from the config
	desiredLabelMap := make(map[string]ForgejoLabel)
	for _, label := range labels {
		desiredLabelMap[label.Name] = label
	}

	// Create or Update labels
	for _, label := range labels {
		existingLabel, exists := existingLabelMap[label.Name]
		if !exists {
			// Label does not exist; create it.
			if dryRun {
				fmt.Printf("[Dry-Run] Would create missing label: %s\n", label.Name)
			} else {
				fmt.Printf("Creating missing label: %s\n", label.Name)
				createOption := CreateLabelOption{
					Name:        label.Name,
					Color:       label.Color,
					Description: fmt.Sprintf("Automatically created label: %s", label.Name),
				}
				_, err := client.CreateLabel(owner, repo, createOption)
				if err != nil {
					return fmt.Errorf("failed to create label '%s': %w", label.Name, err)
				}
			}
		} else {
			// Label exists; check if it needs to be updated.
			needsUpdate := false
			updateOption := EditLabelOption{}

			if existingLabel.Color != label.Color {
				updateOption.Color = label.Color
				needsUpdate = true
			}

			if existingLabel.Description != label.Description {
				updateOption.Description = label.Description
				needsUpdate = true
			}

			if needsUpdate {
				if dryRun {
					fmt.Printf("[Dry-Run] Would update label: %s\n", label.Name)
				} else {
					fmt.Printf("Updating label: %s\n", label.Name)
					err := client.UpdateLabel(owner, repo, existingLabel.ID, updateOption)
					if err != nil {
						return fmt.Errorf("failed to update label '%s': %w", label.Name, err)
					}
				}
			}
		}
	}

	// Delete labels not in config if flag is set
	if deleteOtherLabels {
		for _, label := range existingLabels {
			if _, exists := desiredLabelMap[label.Name]; !exists {
				if dryRun {
					fmt.Printf("[Dry-Run] Would delete label not in config: %s\n", label.Name)
				} else {
					fmt.Printf("Deleting label not in config: %s\n", label.Name)
					err := client.DeleteLabel(owner, repo, label.ID)
					if err != nil {
						return fmt.Errorf("failed to delete label '%s': %w", label.Name, err)
					}
				}
			}
		}
	}

	return nil
}
