package format

import (
	"encoding/json"
	"strings"

	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
)

// JSONProject serializes a Project to JSON.
func JSONProject(project queries.Project) ([]byte, error) {
	return json.Marshal(projectJSON{
		Number:           project.Number,
		URL:              project.URL,
		ShortDescription: project.ShortDescription,
		Public:           project.Public,
		Closed:           project.Closed,
		Template:         project.Template,
		Title:            project.Title,
		ID:               project.ID,
		Readme:           project.Readme,
		Items: struct {
			TotalCount int `json:"totalCount"`
		}{
			TotalCount: project.Items.TotalCount,
		},
		Fields: struct {
			TotalCount int `json:"totalCount"`
		}{
			TotalCount: project.Fields.TotalCount,
		},
		Owner: struct {
			Type  string `json:"type"`
			Login string `json:"login"`
		}{
			Type:  project.OwnerType(),
			Login: project.OwnerLogin(),
		},
	})
}

// JSONProjects serializes a slice of Projects to JSON.
// JSON fields are `totalCount` and `projects`.
func JSONProjects(projects []queries.Project, totalCount int) ([]byte, error) {
	var result []projectJSON
	for _, p := range projects {
		result = append(result, projectJSON{
			Number:           p.Number,
			URL:              p.URL,
			ShortDescription: p.ShortDescription,
			Public:           p.Public,
			Closed:           p.Closed,
			Template:         p.Template,
			Title:            p.Title,
			ID:               p.ID,
			Readme:           p.Readme,
			Items: struct {
				TotalCount int `json:"totalCount"`
			}{
				TotalCount: p.Items.TotalCount,
			},
			Fields: struct {
				TotalCount int `json:"totalCount"`
			}{
				TotalCount: p.Fields.TotalCount,
			},
			Owner: struct {
				Type  string `json:"type"`
				Login string `json:"login"`
			}{
				Type:  p.OwnerType(),
				Login: p.OwnerLogin(),
			},
		})
	}

	return json.Marshal(struct {
		Projects   []projectJSON `json:"projects"`
		TotalCount int           `json:"totalCount"`
	}{
		Projects:   result,
		TotalCount: totalCount,
	})
}

type projectJSON struct {
	Number           int32  `json:"number"`
	URL              string `json:"url"`
	ShortDescription string `json:"shortDescription"`
	Public           bool   `json:"public"`
	Closed           bool   `json:"closed"`
	Template         bool   `json:"template"`
	Title            string `json:"title"`
	ID               string `json:"id"`
	Readme           string `json:"readme"`
	Items            struct {
		TotalCount int `json:"totalCount"`
	} `graphql:"items(first: 100)" json:"items"`
	Fields struct {
		TotalCount int `json:"totalCount"`
	} `graphql:"fields(first:100)" json:"fields"`
	Owner struct {
		Type  string `json:"type"`
		Login string `json:"login"`
	} `json:"owner"`
}

// JSONProjectField serializes a ProjectField to JSON.
func JSONProjectField(field queries.ProjectField) ([]byte, error) {
	val := projectFieldJSON{
		ID:   field.ID(),
		Name: field.Name(),
		Type: field.Type(),
	}
	for _, o := range field.Options() {
		val.Options = append(val.Options, singleSelectOptionJSON{
			Name: o.Name,
			ID:   o.ID,
		})
	}

	return json.Marshal(val)
}

// JSONProjectFields serializes a slice of ProjectFields to JSON.
// JSON fields are `totalCount` and `fields`.
func JSONProjectFields(project *queries.Project) ([]byte, error) {
	var result []projectFieldJSON
	for _, f := range project.Fields.Nodes {
		val := projectFieldJSON{
			ID:   f.ID(),
			Name: f.Name(),
			Type: f.Type(),
		}
		for _, o := range f.Options() {
			val.Options = append(val.Options, singleSelectOptionJSON{
				Name: o.Name,
				ID:   o.ID,
			})
		}

		result = append(result, val)
	}

	return json.Marshal(struct {
		Fields     []projectFieldJSON `json:"fields"`
		TotalCount int                `json:"totalCount"`
	}{
		Fields:     result,
		TotalCount: project.Fields.TotalCount,
	})
}

type projectFieldJSON struct {
	ID      string                   `json:"id"`
	Name    string                   `json:"name"`
	Type    string                   `json:"type"`
	Options []singleSelectOptionJSON `json:"options,omitempty"`
}

type singleSelectOptionJSON struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// JSONProjectItem serializes a ProjectItem to JSON.
func JSONProjectItem(item queries.ProjectItem) ([]byte, error) {
	return json.Marshal(projectItemJSON{
		ID:    item.ID(),
		Title: item.Title(),
		Body:  item.Body(),
		Type:  item.Type(),
		URL:   item.URL(),
	})
}

type projectItemJSON struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	Body  string `json:"body"`
	Type  string `json:"type"`
	URL   string `json:"url,omitempty"`
}

// JSONProjectDraftIssue serializes a DraftIssue to JSON.
// This is needed because the field for
// https://docs.github.com/en/graphql/reference/mutations#updateprojectv2draftissue
// is a DraftIssue https://docs.github.com/en/graphql/reference/objects#draftissue
// and not a ProjectV2Item https://docs.github.com/en/graphql/reference/objects#projectv2item
func JSONProjectDraftIssue(item queries.DraftIssue) ([]byte, error) {

	return json.Marshal(draftIssueJSON{
		ID:    item.ID,
		Title: item.Title,
		Body:  item.Body,
		Type:  "DraftIssue",
	})
}

type draftIssueJSON struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	Body  string `json:"body"`
	Type  string `json:"type"`
}

func projectItemContent(p queries.ProjectItem) any {
	switch p.Content.TypeName {
	case "DraftIssue":
		return struct {
			Type  string `json:"type"`
			Body  string `json:"body"`
			Title string `json:"title"`
		}{
			Type:  p.Type(),
			Body:  p.Body(),
			Title: p.Title(),
		}
	case "Issue":
		return struct {
			Type       string `json:"type"`
			Body       string `json:"body"`
			Title      string `json:"title"`
			Number     int    `json:"number"`
			Repository string `json:"repository"`
			URL        string `json:"url"`
		}{
			Type:       p.Type(),
			Body:       p.Body(),
			Title:      p.Title(),
			Number:     p.Number(),
			Repository: p.Repo(),
			URL:        p.URL(),
		}
	case "PullRequest":
		return struct {
			Type       string `json:"type"`
			Body       string `json:"body"`
			Title      string `json:"title"`
			Number     int    `json:"number"`
			Repository string `json:"repository"`
			URL        string `json:"url"`
		}{
			Type:       p.Type(),
			Body:       p.Body(),
			Title:      p.Title(),
			Number:     p.Number(),
			Repository: p.Repo(),
			URL:        p.URL(),
		}
	}

	return nil
}

func projectFieldValueData(v queries.FieldValueNodes) any {
	switch v.Type {
	case "ProjectV2ItemFieldDateValue":
		return v.ProjectV2ItemFieldDateValue.Date
	case "ProjectV2ItemFieldIterationValue":
		return struct {
			Title     string `json:"title"`
			StartDate string `json:"startDate"`
			Duration  int    `json:"duration"`
		}{
			Title:     v.ProjectV2ItemFieldIterationValue.Title,
			StartDate: v.ProjectV2ItemFieldIterationValue.StartDate,
			Duration:  v.ProjectV2ItemFieldIterationValue.Duration,
		}
	case "ProjectV2ItemFieldNumberValue":
		return v.ProjectV2ItemFieldNumberValue.Number
	case "ProjectV2ItemFieldSingleSelectValue":
		return v.ProjectV2ItemFieldSingleSelectValue.Name
	case "ProjectV2ItemFieldTextValue":
		return v.ProjectV2ItemFieldTextValue.Text
	case "ProjectV2ItemFieldMilestoneValue":
		return struct {
			Title       string `json:"title"`
			Description string `json:"description"`
			DueOn       string `json:"dueOn"`
		}{
			Title:       v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
			Description: v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
			DueOn:       v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
		}
	case "ProjectV2ItemFieldLabelValue":
		names := make([]string, 0)
		for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
			names = append(names, p.Name)
		}
		return names
	case "ProjectV2ItemFieldPullRequestValue":
		urls := make([]string, 0)
		for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
			urls = append(urls, p.Url)
		}
		return urls
	case "ProjectV2ItemFieldRepositoryValue":
		return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
	case "ProjectV2ItemFieldUserValue":
		logins := make([]string, 0)
		for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
			logins = append(logins, p.Login)
		}
		return logins
	case "ProjectV2ItemFieldReviewerValue":
		names := make([]string, 0)
		for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
			if p.Type == "Team" {
				names = append(names, p.Team.Name)
			} else if p.Type == "User" {
				names = append(names, p.User.Login)
			}
		}
		return names

	}

	return nil
}

// serialize creates a map from field to field values
func serializeProjectWithItems(project *queries.Project) []map[string]any {
	fields := make(map[string]string)

	// make a map of fields by ID
	for _, f := range project.Fields.Nodes {
		fields[f.ID()] = CamelCase(f.Name())
	}
	itemsSlice := make([]map[string]any, 0)

	// for each value, look up the name by ID
	// and set the value to the field value
	for _, i := range project.Items.Nodes {
		o := make(map[string]any)
		o["id"] = i.Id
		o["content"] = projectItemContent(i)
		for _, v := range i.FieldValues.Nodes {
			id := v.ID()
			value := projectFieldValueData(v)

			o[fields[id]] = value
		}
		itemsSlice = append(itemsSlice, o)
	}
	return itemsSlice
}

// JSONProjectWithItems returns a detailed JSON representation of project items.
// JSON fields are `totalCount` and `items`.
func JSONProjectDetailedItems(project *queries.Project) ([]byte, error) {
	items := serializeProjectWithItems(project)
	return json.Marshal(struct {
		Items      []map[string]any `json:"items"`
		TotalCount int              `json:"totalCount"`
	}{
		Items:      items,
		TotalCount: project.Items.TotalCount,
	})
}

// CamelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
func CamelCase(s string) string {
	if len(s) == 0 {
		return ""
	}

	if len(s) == 1 {
		return strings.ToLower(s)
	}
	return strings.ToLower(s[0:1]) + s[1:]
}
