// Package github provides a client for the Github REST API.
package github

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
)

// Client for the Github REST API.
type Client struct {
	client *http.Client
	base   string
	token  string
}

// Option configures a Github client.
type Option func(*Client)

// WithHTTPClient configures the HTTP client that should be used for Github API
// requests.
func WithHTTPClient(h *http.Client) Option {
	return func(c *Client) { c.client = h }
}

// WithToken configures a Client with an authentication token for Github API
// requests.
func WithToken(token string) Option {
	return func(c *Client) { c.token = token }
}

// WithTokenFromEnvironment configures a Client using the GITHUB_TOKEN
// environment variable.
func WithTokenFromEnvironment() Option {
	return WithToken(os.Getenv("GITHUB_TOKEN"))
}

// NewClient initializes a client using the given HTTP client.
func NewClient(opts ...Option) *Client {
	c := &Client{
		client: http.DefaultClient,
		base:   "https://api.github.com",
	}
	for _, opt := range opts {
		opt(c)
	}
	return c
}

// Repository gets information about the given Github repository.
func (c *Client) Repository(ctx context.Context, owner, name string) (*Repository, error) {
	// Build request.
	u := c.base + "/repos/" + owner + "/" + name
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}

	// Execute.
	repo := &Repository{}
	if err := c.request(req, repo); err != nil {
		return nil, err
	}

	return repo, nil
}

// Issue gets information about the given Github issue.
func (c *Client) Issue(ctx context.Context, owner, name string, number int) (*Issue, error) {
	// Build request.
	u := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base, owner, name, number)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}

	// Execute.
	issue := &Issue{}
	if err := c.request(req, issue); err != nil {
		return nil, err
	}

	return issue, nil
}

func (c *Client) request(req *http.Request, payload interface{}) (err error) {
	// Add common headers.
	if c.token != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}
	req.Header.Set("Accept", "application/vnd.github.v3+json")

	// Execute the request.
	res, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer func() {
		if errc := res.Body.Close(); errc != nil && err == nil {
			err = errc
		}
	}()

	// Check status.
	if res.StatusCode != http.StatusOK {
		return fmt.Errorf("http status %d: %s", res.StatusCode, http.StatusText(res.StatusCode))
	}

	// Parse response body.
	d := json.NewDecoder(res.Body)

	if err := d.Decode(payload); err != nil {
		return err
	}

	// Should not have trailing data.
	if d.More() {
		return errors.New("unexpected extra data after JSON")
	}

	return nil
}
