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

package forgejo

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

	forgejo_options "code.forgejo.org/f3/gof3/v3/forges/forgejo/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"

	forgejo_sdk "code.forgejo.org/f3/gof3/v3/forges/forgejo/sdk"
	"github.com/hashicorp/go-version"
)

type treeDriver struct {
	generic.NullTreeDriver

	user    *forgejo_sdk.User
	client  *forgejo_sdk.Client
	options *forgejo_options.Options
	version *version.Version
}

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

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

func (o *treeDriver) SetIsAdmin() {
	user, _, err := o.client.GetMyUserInfo()
	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)
	}
	if o.options.GetToken() != "" {
		o.Debug("Getting version from %s with token", o.options.GetURL())
		req.Header.Add("Authorization", fmt.Sprintf("token %s", o.options.GetToken()))
	} else {
		o.Debug("Getting version from %s %s as user %s", o.options.GetURL(), o.options.GetUsername())
		req.SetBasicAuth(o.options.GetUsername(), o.options.GetPassword())
	}
	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) SetToken() {
	scopes := []forgejo_sdk.AccessTokenScope{
		forgejo_sdk.AccessTokenScopeAll,
	}
	var token int
	var name string
	existingTokens, _, err := o.client.ListAccessTokens(forgejo_sdk.ListAccessTokensOptions{})
	if err != nil {
		panic(fmt.Errorf("ListAccessTokens %w", err))
	}
	for {
		token++
		name = fmt.Sprintf("f3-token-%d", token)
		exists := false
		for _, existingToken := range existingTokens {
			if existingToken.Name == name {
				exists = true
				break
			}
		}
		if !exists {
			break
		}
	}

	t, _, err := o.client.CreateAccessToken(forgejo_sdk.CreateAccessTokenOption{
		Name:   name,
		Scopes: scopes,
	})
	if err != nil {
		panic(fmt.Errorf("Failed to create Forgejo token for: %s. Error: %v", o.options.GetURL(), err))
	}

	exists := false
	for _, delay := range []time.Duration{1 * time.Second, 1 * time.Second, 1 * time.Second, 1 * time.Second} {
		existingTokens, _, err = o.client.ListAccessTokens(forgejo_sdk.ListAccessTokensOptions{})
		if err != nil {
			panic(fmt.Errorf("ListAccessTokens %w", err))
		}
		for _, existingToken := range existingTokens {
			if existingToken.Name == name {
				exists = true
				break
			}
		}
		if exists {
			break
		}
		o.GetLogger().Error("token does not exist yet %s:%s, waiting", o.options.GetUsername(), name)
		time.Sleep(delay)
	}
	if !exists {
		panic(fmt.Errorf("token %s cannot be verified to exist", name))
	}

	o.options.SetToken(t.Token)
}

func (o *treeDriver) SetClient(options ...forgejo_sdk.ClientOption) {
	options = append(options, forgejo_sdk.SetHTTPClient(o.options.GetNewMigrationHTTPClient()()))
	c, err := forgejo_sdk.NewClient(
		o.options.GetURL(),
		options...,
	)
	if err != nil {
		panic(fmt.Errorf("Failed to create Forgejo client for: %s. Error: %v", o.options.GetURL(), err))
	}
	o.client = c
}

func (o *treeDriver) maybeSudoInternal(getUsername func() string) {
	if !o.GetIsAdmin() {
		return
	}
	username := getUsername()
	if o.options.GetUsername() != username {
		o.Debug(fmt.Sprintf("sudo %s", username))
		o.GetClient().SetSudo(username)
	}
}

func (o *treeDriver) MaybeSudoName(name string) {
	o.maybeSudoInternal(func() string { return name })
}

func (o *treeDriver) maybeSudoID(ctx context.Context, id int64) {
	o.maybeSudoInternal(func() string { return f3_tree.GetUsernameFromID(ctx, o.GetTree().(f3_tree.TreeInterface), id) })
}

func (o *treeDriver) NotSudo() {
	o.GetClient().SetSudo("")
}

func (o *treeDriver) Init() {
	o.NullTreeDriver.Init()
	if o.options.GetToken() != "" {
		o.Debug("Connecting to %s with token", o.options.GetURL())
		o.SetClient(forgejo_sdk.SetToken(o.options.GetToken()))
	} else {
		o.Debug("Connecting to %s as user %s", o.options.GetURL(), o.options.GetUsername())
		o.SetClient(forgejo_sdk.SetBasicAuth(o.options.GetUsername(), o.options.GetPassword()))
	}
	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.(*forgejo_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))
	}
}
