package api

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"golang.org/x/oauth2"

	adminapi "srs.epita.fr/fic-server/admin/api"
	"srs.epita.fr/fic-server/libfic"
)

var (
	gitlabBaseURL      = "https://gitlab.cri.epita.fr"
	gitlabClientID     = ""
	gitlabSecret       = ""
	gitlaboauth2Config *oauth2.Config
	oidcRedirectURL    = "https://fic.srs.epita.fr"
)

func init() {
	if v, ok := os.LookupEnv("FIC_OIDC_REDIRECT"); ok {
		oidcRedirectURL = v
	}

	flag.StringVar(&oidcRedirectURL, "oidc-redirect", oidcRedirectURL, "Base URL for the redirect after connection")
	flag.StringVar(&gitlabBaseURL, "gitlab-baseurl", gitlabBaseURL, "Base URL of the Gitlab instance")
	flag.StringVar(&gitlabClientID, "gitlab-clientid", os.Getenv("FIC_GITLAB_CLIENT_ID"), "ClientID for GitLab's OIDC")
	flag.StringVar(&gitlabSecret, "gitlab-secret", os.Getenv("FIC_GITLAB_CLIENT_SECRET"), "Secret for GitLab's OIDC")
}

func InitializeGitLabOIDC(baseURL string) {
	if gitlabClientID != "" && gitlabSecret != "" {
		gitlaboauth2Config = &oauth2.Config{
			ClientID:     gitlabClientID,
			ClientSecret: gitlabSecret,
			RedirectURL:  oidcRedirectURL + baseURL + "/callback/gitlab/complete",

			// Discovery returns the OAuth2 endpoints.
			Endpoint: oauth2.Endpoint{
				AuthURL:  gitlabBaseURL + "/oauth/authorize",
				TokenURL: gitlabBaseURL + "/oauth/token",
			},

			// "openid" is a required scope for OpenID Connect flows.
			Scopes: []string{"api", "email"},
		}
	}
}

func declareGitlabRoutes(router *gin.RouterGroup, authrouter *gin.RouterGroup) {
	if gitlaboauth2Config != nil {
		router.GET("/auth/gitlab", redirectOAuth_GitLab)
		router.GET("/callback/gitlab/complete", GitLab_OAuth_complete)

		tokenRoutes := authrouter.Group("")
		tokenRoutes.Use(gitlabHandler)

		tokenRoutes.GET("/gitlab/token", GitLab_GetMyToken)
	}
}

func declareGitlabExportRoutes(router *gin.RouterGroup) {
	if gitlaboauth2Config != nil {
		tokenRoutes := router.Group("")
		tokenRoutes.Use(gitlabHandler)

		if adminLink == "" {
			log.Println("-admin-link should be defined to be able to export to GitLab!")
		} else {
			tokenRoutes.POST("/gitlab_export", GitLab_ExportQA)
		}
	}
}

func gitlabHandler(c *gin.Context) {
	session := sessions.Default(c)

	var oauth2Token *oauth2.Token
	if tok, ok := session.Get("gitlab-token").([]byte); ok {
		err := json.Unmarshal(tok, &oauth2Token)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}
	} else {
		c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "No token defined"})
		return
	}

	err := prepareOAuth2Token(c.Request.Context(), oauth2Token)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	c.Set("gitlab-token", oauth2Token)

	c.Next()
}

func prepareOAuth2Token(ctx context.Context, tk *oauth2.Token) error {
	tsource := oauth2.ReuseTokenSource(tk, gitlaboauth2Config.TokenSource(ctx, tk))

	_, err := tsource.Token()
	if err != nil {
		log.Println("Unable to regenerate GitLab token:", err)
	}
	return err
}

func redirectOAuth_GitLab(c *gin.Context) {
	session := sessions.Default(c)

	// Save next parameter
	if len(c.Request.URL.Query().Get("next")) > 0 {
		session.Set("gitlab-oidc-source", c.Request.URL.Query().Get("next"))
	}

	b := make([]byte, 32)
	_, err := rand.Read(b)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	session.Set("oidc-state", b)
	session.Save()

	c.Redirect(http.StatusFound, gitlaboauth2Config.AuthCodeURL(hex.EncodeToString(b)))
}

func GitLab_OAuth_complete(c *gin.Context) {
	session := sessions.Default(c)

	state, err := hex.DecodeString(c.Request.URL.Query().Get("state"))
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
		return
	}

	if session_state, ok := session.Get("oidc-state").([]byte); !ok {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "No OIDC session in progress."})
		return
	} else if !bytes.Equal(state, session_state) {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad session state."})
		return
	}

	oauth2Token, err := gitlaboauth2Config.Exchange(c.Request.Context(), c.Request.URL.Query().Get("code"))
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to exchange token: " + err.Error()})
		return
	}

	jsonToken, err := json.Marshal(oauth2Token)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
	}

	session.Set("gitlab-token", jsonToken)
	prepareOAuth2Token(context.Background(), oauth2Token)
	session.Save()
	log.Println("New GitLab OAuth2 session opened")

	baseURL := c.MustGet("baseurl").(string)

	if source, ok := session.Get("gitlab-oidc-source").(string); ok {
		session.Delete("gitlab-oidc-source")
		c.Redirect(http.StatusFound, baseURL+source)
	} else {
		c.Redirect(http.StatusFound, baseURL)
	}
}

func GitLab_GetMyToken(c *gin.Context) {
	oauth2Token := c.MustGet("gitlab-token").(*oauth2.Token)

	c.JSON(http.StatusOK, oauth2Token)
}

type GitLabIssue struct {
	Id          int64  `json:"id,omitempty"`
	IId         int64  `json:"iid,omitempty"`
	ProjectId   int64  `json:"project_id,omitempty"`
	Title       string `json:"title"`
	Description string `json:"description"`
	WebURL      string `json:"web_url"`
}

func gitlab_newIssue(ctx context.Context, token *oauth2.Token, exerciceid string, issue *GitLabIssue) (*GitLabIssue, error) {
	client := gitlaboauth2Config.Client(ctx, token)

	enc, err := json.Marshal(issue)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", gitlabBaseURL+"/api/v4/projects/"+url.QueryEscape(exerciceid)+"/issues", bytes.NewBuffer(enc))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		str, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("Bad status code from the API: %d %s", resp.StatusCode, string(str))
	}

	var issueCreated GitLabIssue
	dec := json.NewDecoder(resp.Body)
	err = dec.Decode(&issueCreated)
	if err != nil {
		return nil, fmt.Errorf("Unable to decode issue JSON: %s", err.Error())
	}

	return &issueCreated, nil
}

func GitLab_getExerciceId(exercice *fic.Exercice) (string, error) {
	// Get the forge link
	var adminEx adminapi.Exercice
	err := fwdAdmin("GET", fmt.Sprintf("/api/exercices/%d", exercice.Id), nil, &adminEx)
	if err != nil {
		return "", err
	}

	forgelink, err := url.Parse(adminEx.ForgeLink)
	if err != nil {
		return "", fmt.Errorf("forge_link not found")
	}
	forgelink.Path = forgelink.Path[:strings.Index(forgelink.Path, "/-/tree/")]
	return strings.TrimPrefix(forgelink.Path, "/"), nil
}

func GitLab_ExportQA(c *gin.Context) {
	query := c.MustGet("qa").(*fic.QAQuery)
	exercice, err := fic.GetExercice(query.IdExercice)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	gitlabid, err := GitLab_getExerciceId(exercice)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	comments, err := query.GetComments()
	if err != nil {
		log.Println("Unable to GetComments: ", err.Error())
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list comments: %s", err.Error())})
		return
	}

	description := "<" + oidcRedirectURL + path.Join(path.Clean("/"+c.MustGet("baseurl").(string)), "exercices", fmt.Sprintf("%d", exercice.Id), fmt.Sprintf("%d", query.Id)) + ">"

	if len(comments) > 0 {
		for i, comment := range comments {
			if i > 0 {
				description += "\n\n---"
			}
			description += fmt.Sprintf("\n\n%s\n\nPar %s, le %s", comment.Content, comment.User, comment.Date.Format(time.UnixDate))
		}
	}

	// Format the issue
	issue := GitLabIssue{
		Title:       "[QA] " + query.Subject,
		Description: description,
	}

	// Create the issue on GitLab
	oauth2Token := c.MustGet("gitlab-token").(*oauth2.Token)
	iid, err := gitlab_newIssue(c.Request.Context(), oauth2Token, gitlabid, &issue)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
		return
	}

	query.Exported = &iid.IId
	_, err = query.Update()
	if err != nil {
		log.Println("Unable to update QAquery in GitLab_ExportQA:", err.Error())
	}

	c.JSON(http.StatusOK, iid)
}
