package main

import (
	"errors"
	"fmt"
	"net/url"

	"path"

	"gopkg.in/yaml.v3"

	"github.com/go-git/go-git/v5"
	"github.com/xanzy/go-gitlab"
)

type gitlabContext struct {
	rootPath string
	repos    *git.Repository
	apiUrl   url.URL

	group string
	name  string

	token  string
	client *gitlab.Client

	project *gitlab.Project
}

func getGitlabContext() *gitlabContext {

	context := &gitlabContext{}
	workingPath, err := getGitDirectory()
	if err != nil {
		printErrorAndExit(2, "Failed to get git directory", err.Error())
	}

	context.rootPath = workingPath

	repos, err := git.PlainOpen(workingPath)
	if err != nil {
		printErrorAndExit(2, "Failed to open repository", err.Error())
	}

	context.repos = repos
	if err != nil {
		printErrorAndExit(2, "Failed to get remotes", err.Error())
	}

	remotes, err := repos.Remotes()
	if err != nil {
		printErrorAndExit(2, "Failed to get remotes", err.Error())
	}

	remote := remotes[0]
	if remote == nil {
		printErrorAndExit(2, "Failed to get remote")
	}

	urls := remote.Config().URLs
	if len(urls) == 0 {
		printErrorAndExit(2, "Failed to get remote url")
	}

	u, err := url.Parse(urls[0])
	if err != nil {
		printErrorAndExit(2, "Failed to parse remote url", err.Error())
	}

	context.apiUrl = *u
	context.apiUrl.Path = "" //path.Join("api", "v4")

	p := u.Path
	if p == "" {
		printErrorAndExit(2, "Failed to get remote path")
	}

	s := p[1 : len(p)-4]
	context.name = path.Base(s)
	context.group = path.Dir(s)

	options := gitlab.WithBaseURL(context.apiUrl.String())

	context.token = config.Gitlab.Token
	c, err := gitlab.NewClient(config.Gitlab.Token, options)
	if err != nil {
		printErrorAndExit(2, "Failed to create client", err.Error())
	}

	context.client = c

	return context
}

func searchProject(context *gitlabContext) {
	//
	options := gitlab.ListProjectsOptions{
		Search:           gitlab.String(context.group + "/" + context.name),
		SearchNamespaces: gitlab.Bool(true),
	}

	projects, response, err := context.client.Projects.ListProjects(&options)

	if response.StatusCode == 404 {
		printErrorAndExit(2, "Project not found %s", err.Error())
	}

	if err != nil {
		printErrorAndExit(2, "Failed to get projects %s", err.Error())
	}

	if response.StatusCode != 200 {
		printErrorAndExit(2, "Failed to get projects %s", err.Error())
	}

	if len(projects) == 0 {
		printErrorAndExit(2, "Project not found")
	}

	context.project = projects[0]

}

func enrichIssuesWithGitlab(pageMap map[string]*requirement) error {

	context := getGitlabContext()
	searchProject(context)

	for _, requirement := range pageMap {

		for k, issue := range requirement.Issues {

			if issue.GitlabIntern == nil {
				continue
			}

			if issue.GitlabIntern.ID == 0 {
				continue
			}

			i, err := loadIssuesFromGitlab(context, issue.GitlabIntern.ID)
			if err != nil {
				return err
			}

			requirement.Issues[k].GitlabRemote = i
		}
	}

	return nil
}

func syncFilesWithGitlab() error {

	err, pageData := collectStructureFromFiles(config.Path)
	if err != nil {
		return err
	}

	issuesList := []Issue{}

	for _, pageData := range pageData {
		for _, info := range pageData.Issues {
			issuesList = append(issuesList, info)
		}
	}

	context := getGitlabContext()
	searchProject(context)

	syncIssuesWithGitlab(pageData)
	return rewriteFiles(pageData)

}

func nodesEqual(l, r *yaml.Node) bool {
	if l.Kind == yaml.ScalarNode && r.Kind == yaml.ScalarNode {
		return l.Value == r.Value
	}
	panic("equals on non-scalars not implemented!")
}

func recursiveRemove(nodes *yaml.Node) error {

	if nodes.Kind == yaml.DocumentNode {
		recursiveRemove(nodes.Content[0])
	}

	if nodes.Kind != yaml.MappingNode {
		return nil
	}

	var found int

	for i := 0; i < len(nodes.Content); i += 2 {

		if nodes.Content[i].Value == "Issues" {
			found = i
			break
		}

		if err := recursiveRemove(nodes.Content[i+1]); err != nil {
			return errors.New("at key " + nodes.Content[i].Value + ": " + err.Error())
		}

	}

	if found > 0 {

		copy(nodes.Content[found:], nodes.Content[found+2:]) // Shift a[i+1:] left one index.
		nodes.Content[len(nodes.Content)-1] = nil            // Erase last element (write zero value).
		nodes.Content[len(nodes.Content)-2] = nil            // Erase last element (write zero value).
		nodes.Content = nodes.Content[:len(nodes.Content)-2]
	}

	return nil
}

func recursiveMerge(from, into *yaml.Node) error {
	if from.Kind != into.Kind {
		return errors.New("cannot merge nodes of different kinds")
	}
	switch from.Kind {
	case yaml.MappingNode:
		for i := 0; i < len(from.Content); i += 2 {
			found := false
			for j := 0; j < len(into.Content); j += 2 {
				if nodesEqual(from.Content[i], into.Content[j]) {
					found = true

					if err := recursiveMerge(from.Content[i+1], into.Content[j+1]); err != nil {
						return errors.New("at key " + from.Content[i].Value + ": " + err.Error())
					}
					break
				}
			}
			if !found {
				into.Content = append(into.Content, from.Content[i:i+2]...)
			}
		}
	case yaml.SequenceNode:
		into.Content = append(into.Content, from.Content...)
	case yaml.DocumentNode:
		recursiveMerge(from.Content[0], into.Content[0])
	default:
		return errors.New("can only merge mapping and sequence nodes")
	}
	return nil
}

func syncIssuesWithGitlab(pageData map[string]*requirement) {

	for _, pageData := range pageData {
		for k, info := range pageData.Issues {
			if info.GitlabRemote != nil {
				continue
			}
			if info.GitlabIntern == nil {
				continue
			}
			if info.GitlabIntern.ID == 0 {
				issue, err := createIssue(info)
				if err != nil {
					printErrorAndExit(2, "Failed to create issue %s", err.Error())
				}
				pageData.Issues[k].GitlabRemote = issue
				pageData.Issues[k].GitlabIntern = new(GitlabInternalIssueStruct)
				pageData.Issues[k].GitlabIntern.ID = issue.IID

				var change yaml.Node
				bytes, err := yaml.Marshal(&pageData)
				if err != nil {
					fmt.Println(err)
				}
				yaml.Unmarshal(bytes, &change)
				recursiveRemove(pageData.OriginNode)
				recursiveMerge(&change, pageData.OriginNode)
			}

		}
	}
}

func createIssue(issue Issue) (*gitlab.Issue, error) {
	context := getGitlabContext()
	searchProject(context)

	//title := *string(issue.GitlabIntern.Title)
	labels := gitlab.Labels{}
	for _, label := range issue.GitlabIntern.Labels {
		labels = append(labels, label)
	}

	createionOptions := gitlab.CreateIssueOptions{
		Title:       gitlab.String(issue.GitlabIntern.Title),
		Description: gitlab.String(issue.GitlabIntern.Description),
		Labels:      &labels,
	}

	ptr := &createionOptions

	gitlabIssue, response, err := context.client.Issues.CreateIssue(context.project.ID, ptr)

	if response.StatusCode == 401 {
		return gitlabIssue, err
	}

	if response.StatusCode > 299 {
		return gitlabIssue, err
	}

	return gitlabIssue, nil
}

//
//	context := getGitlabContext(config)
//	searchProject(context)
//
//	options := &gitlab.CreateIssueOptions{
//		Title:  gitlab.String(issue.Title),
//		Status: gitlab.String(issue.Status),
//		Labels: gitlab.StringSlice(issue.Labels),
//	}
//
//	i, _, err := context.client.Issues.CreateIssue(context.project.ID, options)
//	if err != nil {
//		return nil, err
//	}
//
//	return i, nil
//
//}
//
//
//func getProject(config *Configuration) *gitlab.Project {
//
//	client := getGitlabClient(config)
//
//	pattern := "test"
//
//	options := gitlab.ListProjectsOptions{
//		Search: &pattern,
//	}
//
//	projects, _, err := client.Projects.ListProjects(&options)
//	if err != nil {
//		printErrorAndExit(2, "Failed to get projects", err.Error())
//	}
//
//	//project, _, err := client.Projects.GetProject(config.GitlabProjectID, nil)
//	//if err != nil {
//	//	printErrorAndExit(2, "Failed to get project", err.Error())
//	//}
//
//	return projects[0]
//}