package gitlab

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
)

const dateLayout = "2006-01-02"

// Assets describes the assets as Links associated to a release.
type Assets struct {
	Count   int `json:"count,omitempty"`
	Sources []struct {
		Format string `json:"format"`
		URL    string `json:"url"`
	} `json:"sources,omitempty"`
	Links []*Link `json:"links"`
}

// Link describes the Link request/response body.
type Link struct {
	ID              int64  `json:"id,omitempty"`
	Name            string `json:"name"`
	URL             string `json:"url"`
	LinkType        string `json:"link_type,omitempty"`
	DirectAssetPath string `json:"direct_asset_path,omitempty"`
	// Deprecated Filepath redirects to `direct_asset_path` and will be removed in %18.0.
	Filepath string `json:"filepath,omitempty"`
}

// Milestone response body when creating a release. Only uses a subset of all the fields.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#create-a-release
type Milestone struct {
	ID          int       `json:"id"`
	Iid         int       `json:"iid"`
	ProjectID   int       `json:"project_id"`
	Title       string    `json:"title"`
	Description string    `json:"description"`
	State       string    `json:"state"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
	DueDate     *Date     `json:"due_date"`
	StartDate   *Date     `json:"start_date"`
	WebURL      string    `json:"web_url"`
	IssueStats  struct {
		Total  int `json:"total"`
		Closed int `json:"closed"`
	} `json:"issue_stats"`
}

// Date is a custom time.Time wrapper that can parse a date without a timestamp
// See https://gitlab.com/gitlab-org/release-cli/-/issues/121.
type Date time.Time

// UnmarshalJSON implements the json.Unmarshaler interface
func (d *Date) UnmarshalJSON(b []byte) error {
	s := strings.Trim(string(b), "\"")

	t, err := time.Parse(dateLayout, s)
	*d = Date(t)

	return err
}

// MarshalJSON implements the json.Marshaler interface
func (d Date) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("\"%s\"", time.Time(d).Format(dateLayout))), nil
}

// CreateReleaseRequest body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#create-a-release
type CreateReleaseRequest struct {
	ID                   string     `json:"id"`
	Name                 string     `json:"name,omitempty"`
	Description          string     `json:"description,omitempty"`
	TagName              string     `json:"tag_name"`
	TagMessage           string     `json:"tag_message"`
	Ref                  string     `json:"ref,omitempty"`
	Assets               *Assets    `json:"assets,omitempty"`
	Milestones           []string   `json:"milestones,omitempty"`
	ReleasedAt           *time.Time `json:"released_at,omitempty"`
	LegacyCatalogPublish *bool      `json:"legacy_catalog_publish,omitempty"`
}

// UpdateReleaseRequest body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html#update-a-release
type UpdateReleaseRequest struct {
	ID          string     `json:"id"`
	TagName     string     `json:"tag_name"`
	Name        string     `json:"name,omitempty"`
	Description string     `json:"description,omitempty"`
	Milestones  []string   `json:"milestones,omitempty"`
	ReleasedAt  *time.Time `json:"released_at,omitempty"`
}

// ReleaseResponse body.
// The full documentation can be found at https://docs.gitlab.com/ee/api/releases/index.html
type ReleaseResponse struct {
	Name            string       `json:"name"`
	Description     string       `json:"description"`
	DescriptionHTML string       `json:"description_html"`
	TagName         string       `json:"tag_name"`
	CreatedAt       time.Time    `json:"created_at"`
	ReleasedAt      time.Time    `json:"released_at"`
	Assets          *Assets      `json:"assets"`
	Milestones      []*Milestone `json:"milestones"`
	Author          *Author      `json:"author"`
	Commit          *Commit      `json:"commit"`
	CommitPath      string       `json:"commit_path"`
	TagPath         string       `json:"tag_path"`
	Evidences       []*Evidence  `json:"evidences"`
}

// Author body
type Author struct {
	ID        int    `json:"id"`
	Name      string `json:"name"`
	Username  string `json:"username"`
	State     string `json:"state"`
	AvatarURL string `json:"avatar_url"`
	WebURL    string `json:"web_url"`
}

// Commit  body
type Commit struct {
	ID             string    `json:"id"`
	ShortID        string    `json:"short_id"`
	Title          string    `json:"title"`
	CreatedAt      time.Time `json:"created_at"`
	ParentIds      []string  `json:"parent_ids"`
	Message        string    `json:"message"`
	AuthorName     string    `json:"author_name"`
	AuthorEmail    string    `json:"author_email"`
	AuthoredDate   time.Time `json:"authored_date"`
	CommitterName  string    `json:"committer_name"`
	CommitterEmail string    `json:"committer_email"`
	CommittedDate  time.Time `json:"committed_date"`
}

// Evidence body
type Evidence struct {
	Sha         string    `json:"sha"`
	Filepath    string    `json:"filepath"`
	CollectedAt time.Time `json:"collected_at"`
}

// CreateRelease will try to create a release via GitLab's Releases API
func (gc *Client) CreateRelease(ctx context.Context, createReleaseReq *CreateReleaseRequest) (*ReleaseResponse, error) {
	body, err := json.Marshal(createReleaseReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request body: %w", err)
	}

	req, err := gc.request(ctx, http.MethodPost, fmt.Sprintf("/projects/%s/releases", gc.projectID), bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	var response ReleaseResponse
	if err := gc.makeRequest(req, &response); err != nil {
		return nil, err
	}

	return &response, nil
}

// GetRelease by tagName
func (gc *Client) GetRelease(ctx context.Context, tagName string, includeHTML bool) (*ReleaseResponse, error) {
	q := url.Values{}
	q.Set("include_html_description", strconv.FormatBool(includeHTML))

	req, err := gc.request(ctx, http.MethodGet,
		fmt.Sprintf("/projects/%s/releases/%s", gc.projectID, url.QueryEscape(tagName)),
		nil)

	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.URL.RawQuery = q.Encode()

	var response ReleaseResponse
	if err := gc.makeRequest(req, &response); err != nil {
		return nil, err
	}

	return &response, nil
}

// UpdateRelease will try to update a release via GitLab's Releases API
func (gc *Client) UpdateRelease(ctx context.Context, updateReleaseRequest *UpdateReleaseRequest) (*ReleaseResponse, error) {
	body, err := json.Marshal(updateReleaseRequest)
	if err != nil {
		return nil, fmt.Errorf("marshal request body: %w", err)
	}

	req, err := gc.request(ctx, http.MethodPut, fmt.Sprintf("/projects/%s/releases/%s", gc.projectID, url.QueryEscape(updateReleaseRequest.TagName)), bytes.NewBuffer(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	var response ReleaseResponse
	if err := gc.makeRequest(req, &response); err != nil {
		return nil, err
	}

	return &response, nil
}

func (gc *Client) makeRequest(req *http.Request, response interface{}) error {
	res, err := gc.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to do request: %w", err)
	}

	defer checkClosed(res.Body)

	if gc.logger.(*logrus.Entry).Logger.Level == logrus.DebugLevel {
		dres, err := httputil.DumpResponse(res, true)
		printResponse(gc.logger, dres, err)
	}

	if res.StatusCode >= http.StatusBadRequest {
		errResponse := ErrorResponse{
			statusCode: res.StatusCode,
		}

		err := json.NewDecoder(res.Body).Decode(&errResponse)
		if err != nil {
			return fmt.Errorf("failed to decode error response: %w", err)
		}

		return &errResponse
	}

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

	return nil
}

func printResponse(log logrus.FieldLogger, dres []byte, e error) {
	if e != nil {
		log.WithError(e).Debug("Error printing the response")
	} else {
		log.Debug("Received response:")
		fmt.Println(string(dres))
	}
}
