// Copyright 2016 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package gitea

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

// Milestone milestone is a collection of issues on one repository
type Milestone struct {
	ID           int64      `json:"id"`
	Title        string     `json:"title"`
	Description  string     `json:"description"`
	State        StateType  `json:"state"`
	OpenIssues   int        `json:"open_issues"`
	ClosedIssues int        `json:"closed_issues"`
	Created      time.Time  `json:"created_at"`
	Updated      *time.Time `json:"updated_at"`
	Closed       *time.Time `json:"closed_at"`
	Deadline     *time.Time `json:"due_on"`
}

// ListMilestoneOption list milestone options
type ListMilestoneOption struct {
	ListOptions
	// open, closed, all
	State StateType
	Name  string
}

// QueryEncode turns options into querystring argument
func (opt *ListMilestoneOption) QueryEncode() string {
	query := opt.getURLQuery()
	if opt.State != "" {
		query.Add("state", string(opt.State))
	}
	if len(opt.Name) != 0 {
		query.Add("name", opt.Name)
	}
	return query.Encode()
}

// ListMilestones list all the milestones of one repository
func (c *RepositoriesService) ListMilestones(ctx context.Context, owner, repo string, opt ListMilestoneOption) ([]*Milestone, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	opt.setDefaults()
	milestones := make([]*Milestone, 0, opt.PageSize)

	link, _ := url.Parse(fmt.Sprintf("/repos/%s/%s/milestones", owner, repo))
	link.RawQuery = opt.QueryEncode()
	resp, err := c.getParsedResponse(ctx, "GET", link.String(), nil, nil, &milestones)
	return milestones, resp, err
}

// GetMilestone get one milestone by repo name and milestone id
func (c *RepositoriesService) GetMilestone(ctx context.Context, owner, repo string, id int64) (*Milestone, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	milestone := new(Milestone)
	resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/repos/%s/%s/milestones/%d", owner, repo, id), nil, nil, milestone)
	return milestone, resp, err
}

// GetMilestoneByName get one milestone by repo and milestone name
func (c *RepositoriesService) GetMilestoneByName(ctx context.Context, owner, repo, name string) (*Milestone, *Response, error) {
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_13_0) != nil {
		// backwards compatibility mode
		m, resp, err := c.resolveMilestoneByName(ctx, owner, repo, name)
		return m, resp, err
	}
	if err := escapeValidatePathSegments(&owner, &repo, &name); err != nil {
		return nil, nil, err
	}
	milestone := new(Milestone)
	resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/repos/%s/%s/milestones/%s", owner, repo, name), nil, nil, milestone)
	return milestone, resp, err
}

// CreateMilestoneOption options for creating a milestone
type CreateMilestoneOption struct {
	Title       string     `json:"title"`
	Description string     `json:"description"`
	State       StateType  `json:"state"`
	Deadline    *time.Time `json:"due_on"`
}

// Validate the CreateMilestoneOption struct
func (opt CreateMilestoneOption) Validate() error {
	if len(strings.TrimSpace(opt.Title)) == 0 {
		return fmt.Errorf("title is empty")
	}
	return nil
}

// CreateMilestone create one milestone with options
func (c *RepositoriesService) CreateMilestone(ctx context.Context, owner, repo string, opt CreateMilestoneOption) (*Milestone, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	if err := opt.Validate(); err != nil {
		return nil, nil, err
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return nil, nil, err
	}
	milestone := new(Milestone)
	resp, err := c.getParsedResponse(ctx, "POST", fmt.Sprintf("/repos/%s/%s/milestones", owner, repo), jsonHeader, bytes.NewReader(body), milestone)

	// make creating closed milestones need gitea >= v1.13.0
	// this make it backwards compatible
	if err == nil && opt.State == StateClosed && milestone.State != StateClosed {
		closed := StateClosed
		return c.EditMilestone(ctx, owner, repo, milestone.ID, EditMilestoneOption{
			State: &closed,
		})
	}

	return milestone, resp, err
}

// EditMilestoneOption options for editing a milestone
type EditMilestoneOption struct {
	Title       string     `json:"title"`
	Description *string    `json:"description"`
	State       *StateType `json:"state"`
	Deadline    *time.Time `json:"due_on"`
}

// Validate the EditMilestoneOption struct
func (opt EditMilestoneOption) Validate() error {
	if len(opt.Title) != 0 && len(strings.TrimSpace(opt.Title)) == 0 {
		return fmt.Errorf("title is empty")
	}
	return nil
}

// EditMilestone modify milestone with options
func (c *RepositoriesService) EditMilestone(ctx context.Context, owner, repo string, id int64, opt EditMilestoneOption) (*Milestone, *Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, nil, err
	}
	if err := opt.Validate(); err != nil {
		return nil, nil, err
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return nil, nil, err
	}
	milestone := new(Milestone)
	resp, err := c.getParsedResponse(ctx, "PATCH", fmt.Sprintf("/repos/%s/%s/milestones/%d", owner, repo, id), jsonHeader, bytes.NewReader(body), milestone)
	return milestone, resp, err
}

// EditMilestoneByName modify milestone with options
func (c *RepositoriesService) EditMilestoneByName(ctx context.Context, owner, repo, name string, opt EditMilestoneOption) (*Milestone, *Response, error) {
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_13_0) != nil {
		// backwards compatibility mode
		m, _, err := c.resolveMilestoneByName(ctx, owner, repo, name)
		if err != nil {
			return nil, nil, err
		}
		return c.EditMilestone(ctx, owner, repo, m.ID, opt)
	}
	if err := escapeValidatePathSegments(&owner, &repo, &name); err != nil {
		return nil, nil, err
	}
	if err := opt.Validate(); err != nil {
		return nil, nil, err
	}
	body, err := json.Marshal(&opt)
	if err != nil {
		return nil, nil, err
	}
	milestone := new(Milestone)
	resp, err := c.getParsedResponse(ctx, "PATCH", fmt.Sprintf("/repos/%s/%s/milestones/%s", owner, repo, name), jsonHeader, bytes.NewReader(body), milestone)
	return milestone, resp, err
}

// DeleteMilestone delete one milestone by id
func (c *RepositoriesService) DeleteMilestone(ctx context.Context, owner, repo string, id int64) (*Response, error) {
	if err := escapeValidatePathSegments(&owner, &repo); err != nil {
		return nil, err
	}
	return c.doRequestWithStatusHandle(ctx, "DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%d", owner, repo, id), nil, nil)
}

// DeleteMilestoneByName delete one milestone by name
func (c *RepositoriesService) DeleteMilestoneByName(ctx context.Context, owner, repo, name string) (*Response, error) {
	if c.checkServerVersionGreaterThanOrEqual(ctx, version1_13_0) != nil {
		// backwards compatibility mode
		m, _, err := c.resolveMilestoneByName(ctx, owner, repo, name)
		if err != nil {
			return nil, err
		}
		return c.DeleteMilestone(ctx, owner, repo, m.ID)
	}
	if err := escapeValidatePathSegments(&owner, &repo, &name); err != nil {
		return nil, err
	}
	return c.doRequestWithStatusHandle(ctx, "DELETE", fmt.Sprintf("/repos/%s/%s/milestones/%s", owner, repo, name), nil, nil)
}

// resolveMilestoneByName is a fallback method to find milestone id by name
func (c *RepositoriesService) resolveMilestoneByName(ctx context.Context, owner, repo, name string) (*Milestone, *Response, error) {
	for i := 1; ; i++ {
		miles, resp, err := c.ListMilestones(ctx, owner, repo, ListMilestoneOption{
			ListOptions: ListOptions{
				Page: i,
			},
			State: "all",
		})
		if err != nil {
			return nil, nil, err
		}
		if len(miles) == 0 {
			return nil, nil, fmt.Errorf("milestone '%s' do not exist", name)
		}
		for _, m := range miles {
			if strings.EqualFold(strings.TrimSpace(m.Title), strings.TrimSpace(name)) {
				return m, resp, nil
			}
		}
	}
}
