// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package gitlab

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	gitlab_options "code.forgejo.org/f3/gof3/v3/forges/gitlab/options"
	"code.forgejo.org/f3/gof3/v3/kind"
	f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
	"code.forgejo.org/f3/gof3/v3/tree/generic"

	"github.com/hashicorp/go-version"
	"gitlab.com/gitlab-org/api/client-go"
)

type treeDriver struct {
	generic.NullTreeDriver

	user    *gitlab.User
	client  *gitlab.Client
	options *gitlab_options.Options
	version *version.Version
}

func (o *treeDriver) GetClient() *gitlab.Client {
	return o.client
}

func (o *treeDriver) GetIsAdmin() bool {
	return o.user.IsAdmin
}

func (o *treeDriver) SetIsAdmin() {
	user, _, err := o.client.Users.CurrentUser()
	if err != nil {
		panic(fmt.Errorf("Failed to get information about the user for: %s. Error: %v", o.options.GetURL(), err))
	}
	o.user = user
	o.options.SetUsername(user.Username)
}

var (
	ForgejoVersion700 = version.Must(version.NewVersion("7.0.0")) // 1.22
	ForgejoVersion600 = version.Must(version.NewVersion("6.0.0")) // 1.21
	ForgejoVersion500 = version.Must(version.NewVersion("5.0.0")) // 1.20.1
	ForgejoVersion501 = version.Must(version.NewVersion("5.0.1")) // 1.20.2
	ForgejoVersion502 = version.Must(version.NewVersion("5.0.2")) // 1.20.3
	ForgejoVersion503 = version.Must(version.NewVersion("5.0.3")) // 1.20.4
	ForgejoVersion504 = version.Must(version.NewVersion("5.0.4")) // 1.20.5
	ForgejoVersion4   = version.Must(version.NewVersion("4.0.0")) // 1.19

	ForgejoVersionNotFound = version.Must(version.NewVersion("1.0.0"))
)

func (o *treeDriver) GetVersion() *version.Version {
	o.SetVersion()
	return o.version
}

func (o *treeDriver) SetVersion() {
	if o.version != nil {
		return
	}
	client := &http.Client{}
	url := fmt.Sprintf("%s/api/forgejo/v1/version", o.options.GetURL())
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		panic(err)
	}
	resp, err := client.Do(req)
	if err != nil {
		panic(fmt.Errorf("while getting %s %w", url, err))
	}

	switch resp.StatusCode {
	case http.StatusNotFound:
		o.version = ForgejoVersionNotFound
	case http.StatusOK:
		v := struct{ Version string }{}
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			panic(fmt.Errorf("reading response body %+v %w", resp, err))
		}
		if err := json.Unmarshal(body, &v); err != nil {
			panic(fmt.Errorf("decoding JSON response from %s %s %w", url, string(body), err))
		}
		o.version = version.Must(version.NewVersion(v.Version))
	default:
		panic(fmt.Errorf("unexpected status code fetching %s %d %v", url, resp.StatusCode, resp))
	}
}

func (o *treeDriver) maybeSudo(uid interface{}) []gitlab.RequestOptionFunc {
	if !o.GetIsAdmin() {
		return []gitlab.RequestOptionFunc{}
	}
	if name, ok := uid.(string); ok && name == o.options.GetUsername() {
		return []gitlab.RequestOptionFunc{}
	}
	return []gitlab.RequestOptionFunc{
		gitlab.WithSudo(uid),
	}
}

func (o *treeDriver) Init() {
	o.NullTreeDriver.Init()

	var err error
	var client *gitlab.Client
	if o.options.GetToken() != "" {
		o.Debug("Connecting to %s with token", o.options.GetURL())
		client, err = gitlab.NewClient(o.options.GetToken(), gitlab.WithBaseURL(o.options.GetURL()), gitlab.WithHTTPClient(o.options.GetNewMigrationHTTPClient()()))
	} else {
		o.Debug("Connecting to %s as user %s", o.options.GetURL(), o.options.GetUsername())
		client, err = gitlab.NewBasicAuthClient(o.options.GetUsername(), o.options.GetPassword(), gitlab.WithBaseURL(o.options.GetURL()), gitlab.WithHTTPClient(o.options.GetNewMigrationHTTPClient()()))
	}
	if err != nil {
		panic(fmt.Errorf("Failed to create Forgejo client for: %s. Error: %v", o.options.GetURL(), err))
	}
	o.client = client

	o.SetIsAdmin()
	if o.GetIsAdmin() {
		o.Debug("Connected as admin")
	} else {
		o.Debug("Connected as regular user")
	}
}

func newTreeDriver(tree generic.TreeInterface, anyOptions any) generic.TreeDriverInterface {
	driver := &treeDriver{
		options: anyOptions.(*gitlab_options.Options),
	}
	driver.SetTree(tree)
	driver.Init()
	return driver
}

func (o *treeDriver) Factory(ctx context.Context, k kind.Kind) generic.NodeDriverInterface {
	switch k {
	case f3_tree.KindForge:
		return newForge()
	case f3_tree.KindOrganizations:
		return newOrganizations()
	case f3_tree.KindOrganization:
		return newOrganization()
	case f3_tree.KindUsers:
		return newUsers()
	case f3_tree.KindUser:
		return newUser()
	case f3_tree.KindProjects:
		return newProjects()
	// case f3_tree.KindProject:
	// 	return newProject()
	// case f3_tree.KindIssues:
	// 	return newIssues()
	// case f3_tree.KindIssue:
	// 	return newIssue()
	// case f3_tree.KindComments:
	// 	return newComments()
	// case f3_tree.KindComment:
	// 	return newComment()
	// case f3_tree.KindAttachments:
	// 	return newAttachments()
	// case f3_tree.KindAttachment:
	// 	return newAttachment()
	// case f3_tree.KindLabels:
	// 	return newLabels()
	// case f3_tree.KindLabel:
	// 	return newLabel()
	// case f3_tree.KindReactions:
	// 	return newReactions()
	// case f3_tree.KindReaction:
	// 	return newReaction()
	// case f3_tree.KindReviews:
	// 	return newReviews()
	// case f3_tree.KindReview:
	// 	return newReview()
	// case f3_tree.KindReviewComments:
	// 	return newReviewComments()
	// case f3_tree.KindReviewComment:
	// 	return newReviewComment()
	// case f3_tree.KindMilestones:
	// 	return newMilestones()
	// case f3_tree.KindMilestone:
	// 	return newMilestone()
	// case f3_tree.KindPullRequests:
	// 	return newPullRequests()
	// case f3_tree.KindPullRequest:
	// 	return newPullRequest()
	// case f3_tree.KindReleases:
	// 	return newReleases()
	// case f3_tree.KindRelease:
	// 	return newRelease()
	case f3_tree.KindTopics:
		return newTopics()
	// case f3_tree.KindRepositories:
	// 	return newRepositories()
	// case f3_tree.KindRepository:
	// 	return newRepository(ctx)
	case kind.KindRoot:
		return newRoot(o.GetTree().(f3_tree.TreeInterface).NewFormat(k))
	default:
		panic(fmt.Errorf("unexpected kind %s", k))
	}
}
