package onescli

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/doug-martin/goqu/v9"
	"github.com/go-logr/logr"
	"github.com/go-resty/resty/v2"
	"go.uber.org/dig"

	onesapi "github.com/tidbcloud/devops/pkg/infra/clients/onescli/api"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
)

type FieldID string

func (f FieldID) Field(name string) FieldID {
	return FieldID(fmt.Sprintf("%s.%s", f, name))
}

const (
	Field_DisplayIDPath     FieldID = "field903"
	Field_Title             FieldID = "field001"
	Field_Status            FieldID = "field005"
	Field_Project           FieldID = "field006"
	Field_Creator           FieldID = "field003"
	Field_Assignee          FieldID = "field004"
	Field_IssueType         FieldID = "field007"
	Field_CreateTime        FieldID = "field009"
	Field_ServerUpdateStamp FieldID = "field010"
	Field_PublishVersion    FieldID = "field037"
)

var (
	defSelectFields = []FieldID{
		"uuid",
		Field_DisplayIDPath,
		Field_Title,
		Field_Status.Field("uuid"), Field_Status.Field("name"),
		Field_Project.Field("uuid"), Field_Project.Field("name"),
		Field_Creator.Field("email"), Field_Creator.Field("uuid"), Field_Creator.Field("name"),
		Field_Assignee.Field("email"), Field_Assignee.Field("uuid"), Field_Assignee.Field("name"),
		Field_IssueType.Field("uuid"), Field_IssueType.Field("name"),
		Field_CreateTime,
		Field_ServerUpdateStamp,
		Field_PublishVersion.Field("uuid"), Field_PublishVersion.Field("name"),
	}
)

type ClientParam struct {
	dig.In

	Conf   *config.RootConfig
	Logger logr.Logger
}

type SearchIssuesParam struct {
	DisplayIDIs    *string
	IssueTypeIn    *[]string
	IssueTypeNotIn *[]string
	ProjectIs      *string
	UpdatedAtGTE   *time.Time
	CreatedAtGTE   *time.Time

	PageCursor string
	PageSize   int
}

type OnesClient interface {
	GetIssue(ctx context.Context, name string) (*onesapi.SearchIssue, error)
	CreateIssue(ctx context.Context, req onesapi.CreateIssueRequest) (string /*dispaly id*/, error)
	SearchIssues(ctx context.Context, param SearchIssuesParam) ([]*onesapi.SearchIssue, error)
	UpdateIssueStatus(ctx context.Context, name string, status string) error
	ListIssueStatuses(ctx context.Context) ([]*onesapi.IssueStatus, error)
	ListIssueTypes(ctx context.Context) ([]*onesapi.IssueType, error)
	ListIssueFields(ctx context.Context) ([]*onesapi.IssueField, error)

	ListProjects(ctx context.Context) ([]*onesapi.Project, error)

	SearchUser(ctx context.Context, keyword string /*email, name, staffID*/) ([]*onesapi.User, error)
}

type client struct {
	raw              *resty.Client
	token            string
	baseURL          string
	issueTypeCache   *IssueTypeCache
	projectCache     *ProjectCache
	issueStatusCache *IssueStatusCache
}

func NewOnesClient(param ClientParam) (OnesClient, error) {
	domain := param.Conf.Clients.Ones.Domain
	teamID := param.Conf.Clients.Ones.TeamID
	token := param.Conf.Clients.Ones.Token

	return newOnesClient(domain, token, teamID)
}

func newOnesClient(addr string, token string, teamID string) (OnesClient, error) {
	baseURL := strings.TrimRight(addr, "/")
	raw := resty.New().
		SetTransport(http.DefaultTransport).
		SetTimeout(10 * time.Second).
		SetScheme("https").
		SetBaseURL(baseURL).
		SetAuthToken(token).
		OnBeforeRequest(func(c *resty.Client, r *resty.Request) error {
			r.QueryParam.Add("teamID", teamID)
			return nil
		}).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("call ones failed, url: %s, status: %s, body: %s", r.Request.URL, r.Status(), string(r.Body()))
			}

			// Some APIs return 200 status code even if the request is failed.
			// We need to check the response body to determine whether the request is successful or not.
			baseResp := &onesapi.BaseResponse{}
			if err := json.Unmarshal(r.Body(), baseResp); err != nil {
				return fmt.Errorf("unmarshal ones response failed: %w", err)
			}
			if baseResp.Result == onesapi.ResultFail {
				return fmt.Errorf("call ones failed, url: %s, status: %s, body: %s", r.Request.URL, r.Status(), string(r.Body()))
			}

			return nil
		})

	c := &client{
		raw:     raw,
		token:   token,
		baseURL: baseURL,
	}
	c.issueTypeCache = &IssueTypeCache{
		client: c,
	}
	c.projectCache = &ProjectCache{
		client: c,
	}
	c.issueStatusCache = &IssueStatusCache{
		client: c,
	}

	return c, nil
}

func (c *client) SearchIssues(ctx context.Context, param SearchIssuesParam) ([]*onesapi.SearchIssue, error) {
	sqlStr, err := c.buildSearchIssuesSQL(ctx, param)
	if err != nil {
		return nil, fmt.Errorf("build sql failed: %w", err)
	}

	log.FromContext(ctx).Info("generated ones sql", "sql", sqlStr)

	result := &onesapi.SearchIssuesResponse{}
	err = c.searchWithSQL(ctx, sqlStr, result)
	if err != nil {
		return nil, fmt.Errorf("search issues failed: %w", err)
	}

	issues := container.TransformSlice(result.Data.Data, func(item onesapi.SearchIssueItem) *onesapi.SearchIssue {
		return item.Issue
	})
	return issues, nil
}

func (c *client) GetIssue(ctx context.Context, name string) (*onesapi.SearchIssue, error) {
	param := SearchIssuesParam{
		DisplayIDIs: &name,
		PageSize:    1,
	}

	issues, err := c.SearchIssues(ctx, param)
	if err != nil {
		return nil, fmt.Errorf("search issue %s failed: %w", name, err)
	}
	if len(issues) == 0 {
		return nil, fmt.Errorf("issue %s not found", name)
	}

	return issues[0], nil
}

func (c *client) CreateIssue(ctx context.Context, req onesapi.CreateIssueRequest) (string /*display id*/, error) {
	apiPath := "/openapi/v2/project/issues"

	project, err := c.projectCache.GetByName(ctx, req.Project)
	if err != nil {
		return "", fmt.Errorf("get project %s failed: %w", req.Project, err)
	}
	issueType, err := c.issueTypeCache.GetByName(ctx, req.IssueType)
	if err != nil {
		return "", fmt.Errorf("get issue type %s failed: %w", req.IssueType, err)
	}
	assignees, err := c.SearchUser(ctx, req.Assignee)
	if err != nil {
		return "", fmt.Errorf("search assignee %s failed: %w", req.Assignee, err)
	}
	if len(assignees) == 0 {
		return "", fmt.Errorf("assignee %s not found", req.Assignee)
	}
	if len(assignees) > 1 {
		return "", fmt.Errorf("multiple assignees found for %s", req.Assignee)
	}
	assignee := assignees[0]

	type requestBody struct {
		Title       string                    `json:"title"`
		Assignee    string                    `json:"assignee"`
		ProjectID   string                    `json:"projectID"`
		IssueTypeID string                    `json:"issueTypeID"`
		FieldValues []onesapi.IssueFieldValue `json:"fieldValues,omitempty"`
		Watchers    []string                  `json:"watchers"`
	}
	type responseBody struct {
		*onesapi.BaseResponse

		Data struct {
			ID     string `json:"id"`
			Number int    `json:"number"`
		} `json:"data"`
	}

	request := &requestBody{
		Title:       req.Title,
		Assignee:    assignee.ID,
		ProjectID:   project.ID,
		IssueTypeID: issueType.ID,
		FieldValues: req.FieldValues,
		Watchers: []string{
			assignee.ID,
		},
	}
	response := &responseBody{}
	_, err = c.raw.R().
		SetContext(ctx).
		SetBody(request).
		SetResult(response).
		Post(apiPath)
	if err != nil {
		return "", fmt.Errorf("create issue failed: %w", err)
	}

	return fmt.Sprintf("%s-%d", req.Project, response.Data.Number), nil
}

func (c *client) ListIssueStatuses(ctx context.Context) ([]*onesapi.IssueStatus, error) {
	apiPath := "/openapi/v2/project/issueStatuses"

	result := &onesapi.ListIssueStatusesResponse{}
	_, err := c.raw.R().
		SetContext(ctx).
		SetResult(result).
		Get(apiPath)
	if err != nil {
		return nil, fmt.Errorf("list issue statuses failed: %w", err)
	}

	return result.Data.Lists, nil
}

func (c *client) UpdateIssueStatus(ctx context.Context, name string, status string) error {
	apiPath := "/openapi/v2/project/issues/{id}"

	issue, err := c.GetIssue(ctx, name)
	if err != nil {
		return fmt.Errorf("get issue %s failed: %w", name, err)
	}

	issueStatus, err := c.issueStatusCache.GetByName(ctx, status)
	if err != nil {
		return fmt.Errorf("get issue status %s failed: %w", status, err)
	}

	req := onesapi.UpdateIssueRequest{
		FieldValues: []onesapi.IssueFieldValue{
			{
				FieldID: string(Field_Status),
				Value:   issueStatus.ID,
			},
		},
	}
	_, err = c.raw.R().
		SetContext(ctx).
		SetPathParam("id", issue.UUID).
		SetBody(req).
		Put(apiPath)
	if err != nil {
		return fmt.Errorf("update issue status failed: %w", err)
	}

	return nil
}

func (c *client) ListIssueFields(ctx context.Context) ([]*onesapi.IssueField, error) {
	apiPath := "/openapi/v2/project/issueFields"

	result := &onesapi.ListIssueFieldsResponse{}
	_, err := c.raw.R().
		SetContext(ctx).
		SetResult(result).
		Get(apiPath)
	if err != nil {
		return nil, fmt.Errorf("list issue fields failed: %w", err)
	}

	return result.Data.Lists, nil
}

func (c *client) ListIssueTypes(ctx context.Context) ([]*onesapi.IssueType, error) {
	apiPath := "/openapi/v2/project/issueTypes"

	result := &onesapi.ListIssueTypesResponse{}
	_, err := c.raw.R().
		SetContext(ctx).
		SetResult(result).
		Get(apiPath)
	if err != nil {
		return nil, fmt.Errorf("list issue types failed: %w", err)
	}

	return result.Data.Lists, nil
}

func (c *client) ListProjects(ctx context.Context) ([]*onesapi.Project, error) {
	apiPath := "/openapi/v2/project/projects"

	result := &onesapi.ListProjectsResponse{}
	_, err := c.raw.R().
		SetContext(ctx).
		SetResult(result).
		Get(apiPath)
	if err != nil {
		return nil, fmt.Errorf("list projects failed: %w", err)
	}

	return result.Data.Lists, nil
}

func (c *client) SearchUser(ctx context.Context, keyword string /*email, name, staffID*/) ([]*onesapi.User, error) {
	apiPath := "/openapi/v2/account/users/search"

	result := &onesapi.SearchUsersResponse{}
	_, err := c.raw.R().
		SetContext(ctx).
		SetQueryParam("keyword", keyword).
		SetResult(result).
		Get(apiPath)
	if err != nil {
		return nil, fmt.Errorf("search user failed: %w", err)
	}

	return result.Data.List, nil
}

func (c *client) searchWithSQL(ctx context.Context, sql string, result any) error {
	apiPath := "/openapi/v3alpha/onesql/query"

	_, err := c.raw.R().
		SetContext(ctx).
		SetBody(map[string]string{
			"query": sql,
		}).
		SetResult(result).
		Post(apiPath)
	return err
}

func (c *client) buildSearchIssuesSQL(ctx context.Context, param SearchIssuesParam) (string, error) {
	selectColumns := container.TransformSlice(defSelectFields, container.ToString)
	selectExpr := goqu.L(fmt.Sprintf("uid(%s)", strings.Join(selectColumns, ",")))

	query := goqu.From(goqu.L("issue")).Select(selectExpr)

	if param.DisplayIDIs != nil {
		colName := fmt.Sprintf("uid(%s)", Field_DisplayIDPath)
		value := fmt.Sprintf("'%s'", *param.DisplayIDIs)
		query = query.Where(goqu.L(colName).Eq(goqu.L(value)))
	}

	if param.IssueTypeIn != nil {
		colName := fmt.Sprintf("uid(%s)", Field_IssueType)
		conds := []string{}
		for _, typ := range *param.IssueTypeIn {
			issueType, err := c.issueTypeCache.GetByName(ctx, typ)
			if err != nil {
				return "", fmt.Errorf("get issue type %s failed: %w", typ, err)
			}
			conds = append(conds, fmt.Sprintf("uid('%s')", issueType.ID))
		}
		value := strings.Join(conds, ",")
		query = query.Where(goqu.L(colName).In(goqu.L(value)))
	}

	if param.IssueTypeNotIn != nil {
		colName := fmt.Sprintf("uid(%s)", Field_IssueType)
		conds := []string{}
		for _, typ := range *param.IssueTypeNotIn {
			issueType, err := c.issueTypeCache.GetByName(ctx, typ)
			if err != nil {
				return "", fmt.Errorf("get issue type %s failed: %w", typ, err)
			}
			conds = append(conds, fmt.Sprintf("uid('%s')", issueType.ID))
		}
		value := strings.Join(conds, ",")
		query = query.Where(goqu.L(colName).NotIn(goqu.L(value)))
	}

	if param.ProjectIs != nil {
		colName := fmt.Sprintf("uid(%s)", Field_Project)
		project, err := c.projectCache.GetByName(ctx, *param.ProjectIs)
		if err != nil {
			return "", fmt.Errorf("get project %s failed: %w", *param.ProjectIs, err)
		}
		cond := fmt.Sprintf("uid('%s')", project.ID)
		query = query.Where(goqu.L(colName).Eq(goqu.L(cond)))
	}

	if param.UpdatedAtGTE != nil {
		colName := fmt.Sprintf("uid(%s)", Field_ServerUpdateStamp)
		cond := param.UpdatedAtGTE.Format("2006-01-02 15:04:05")
		query = query.Where(goqu.L(colName).Gte(cond))
	}

	if param.CreatedAtGTE != nil {
		colName := fmt.Sprintf("uid(%s)", Field_CreateTime)
		cond := param.CreatedAtGTE.Format("2006-01-02 15:04:05")
		query = query.Where(goqu.L(colName).Gte(cond))
	}

	query = query.Order(goqu.L(string(Field_ServerUpdateStamp)).Desc())

	sqlStr, _, err := query.ToSQL()
	if err != nil {
		return "", err
	}

	return sqlStr, nil
}
