package main

/*
 * This file is part of Autogits.
 *
 * Copyright © 2024 SUSE LLC
 *
 * Autogits is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 2 of the License, or (at your option) any later
 * version.
 *
 * Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * Foobar. If not, see <https://www.gnu.org/licenses/>.
 */

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"src.opensuse.org/autogits/common"
)

const (
	ListenAddrDef  = "[::1]:8002"
	AppName        = "rabbitmq-forwarder"
	DefTopicDomain = "opensuse"
)

var DebugMode bool
var topicScope string

func connectToRabbitMQ() {
	host := os.Getenv("RABBITMQ_HOST")
	username := os.Getenv("RABBITMQ_USERNAME")
	password := os.Getenv("RABBITMQ_PASSWORD")

	if len(host) == 0 || len(username) == 0 || len(password) == 0 {
		fmt.Println("Missing RABBITMQ_HOST, RABBITMQ_USERNAME, RABBITMQ_PASSWORD")
		os.Exit(1)
	}

	go ConnectToExchangeForPublish(host, username, password)
}

var id int

func dumpUnhandledData(reqType string, data []byte) {
	id++
	if err := os.WriteFile(fmt.Sprintf("/tmp/unhandled-json-%s-%d.json", reqType, id), data, 0600); err != nil {
		log.Printf("Cannot dump json. err: %v\n", err)
		log.Println(string(data))
	}
}

func parseRequestJSONOrg(reqType string, data []byte) (org *common.Organization, extraAction string, err error) {
	extraAction = ""

	switch reqType {
	case common.RequestType_CreateBrachTag, common.RequestType_DeleteBranchTag:
		create := common.CreateWebhookEvent{}
		if err = json.Unmarshal(data, &create); err != nil {
			return
		}
		org = create.Repository.Owner

	case common.RequestType_Fork:
		fork := common.ForkWebhookEvent{}
		if err = json.Unmarshal(data, &fork); err != nil {
			return
		}
		org = fork.Forkee.Owner

	case common.RequestType_Push:
		push := common.PushWebhookEvent{}
		if err = json.Unmarshal(data, &push); err != nil {
			return
		}
		org = push.Repository.Owner

	case common.RequestType_Repository:
		repoAction := common.RepositoryWebhookEvent{}
		if err = json.Unmarshal(data, &repoAction); err != nil {
			return
		}
		switch repoAction.Action {
		case "created", "deleted":
			break
		default:
			err = fmt.Errorf("Unknown repository webhook action type: %s", repoAction.Action)
			return
		}
		org = repoAction.Organization
		extraAction = repoAction.Action

	case common.RequestType_Release:
		release := common.ReleaseWebhookEvent{}
		if err = json.Unmarshal(data, &release); err != nil {
			return
		}
		switch release.Action {
		case "published", "updated", "deleted":
			break
		default:
			err = fmt.Errorf("Unknwon Release webhook action type: %s", release.Action)
			return
		}

		org = release.Repository.Owner
		extraAction = release.Action

	case common.RequestType_Issue:
		issue := common.IssueWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "opened", "closed", "reopened", "edited":
			break
		default:
			err = fmt.Errorf("Unknown Issue webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_IssueAssign:
		issue := common.IssueWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "assigned", "unassigned":
			break
		default:
			err = fmt.Errorf("Unknown Issue Assign webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_IssueComment, common.RequestType_PRComment:
		issue := common.IssueCommentWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "edited", "created", "deleted":
			break
		default:
			err = fmt.Errorf("Unknown Issue/PR Comment webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_IssueLabel:
		issue := common.IssueWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "label_updated", "label_cleared":
			break
		default:
			err = fmt.Errorf("Unknown Issue Assign webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_IssueMilestone:
		issue := common.IssueWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "milestoned", "demilestoned":
			break
		default:
			err = fmt.Errorf("Unknown Issue Assign webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_PR:
		pr := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &pr); err != nil {
			return
		}
		switch pr.Action {
		case "opened", "closed", "reopened", "edited":
			break
		default:
			err = fmt.Errorf("Unknown PR webhook action type: %s", pr.Action)
			return
		}
		org = pr.Repository.Owner
		extraAction = pr.Action

	case common.RequestType_PRLabel:
		pr := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &pr); err != nil {
			return
		}
		switch pr.Action {
		case "label_updated", "label_cleared":
			break
		default:
			err = fmt.Errorf("Unknown PR Label webhook action type: %s", pr.Action)
			return
		}
		org = pr.Repository.Owner
		extraAction = pr.Action

	case common.RequestType_PRMilestone:
		pr := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &pr); err != nil {
			return
		}
		switch pr.Action {
		case "milestoned", "demilestoned":
			break
		default:
			err = fmt.Errorf("Unknown PR Milestone webhook action type: %s", pr.Action)
			return
		}
		org = pr.Repository.Owner
		extraAction = pr.Action

	case common.RequestType_PRAssign:
		issue := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "assigned", "unassigned":
			break
		default:
			err = fmt.Errorf("Unknown PR Assign webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_PRReviewRequest:
		issue := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &issue); err != nil {
			return
		}
		switch issue.Action {
		case "review_requested", "review_request_removed":
			break
		default:
			err = fmt.Errorf("Unknown PR Review Request webhook action type: %s", issue.Action)
			return
		}
		org = issue.Repository.Owner
		extraAction = issue.Action

	case common.RequestType_PRReviewAccepted, common.RequestType_PRReviewRejected, common.RequestType_PRReviewComment:
		pr := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &pr); err != nil {
			return
		}
		switch pr.Action {
		case "reviewed":
			break
		default:
			err = fmt.Errorf("Unknown PR Review webhook action type: %s", pr.Action)
			return
		}
		org = pr.Repository.Owner
		extraAction = ""

	case common.RequestType_PRSync:
		pr := common.PullRequestWebhookEvent{}
		if err = json.Unmarshal(data, &pr); err != nil {
			return
		}
		switch pr.Action {
		case "synchronized":
			break
		default:
			err = fmt.Errorf("Unknown PR Sync webhook action type: %s", pr.Action)
			return
		}
		org = pr.Repository.Owner
		extraAction = ""

	case common.RequestType_Wiki:
		wiki := common.WikiWebhookEvent{}
		if err = json.Unmarshal(data, &wiki); err != nil {
			return
		}

		switch wiki.Action {
		case "created", "edited", "renamed", "deleted":
			break
		default:
			err = fmt.Errorf("Unknown Wiki webhook action type: %s", wiki.Action)
			return
		}
		org = wiki.Repository.Owner
		extraAction = wiki.Action

	default:
		// TODO: package webhook
		err = fmt.Errorf("Unknown webhook request type: %s", reqType)
	}

	return
}

func main() {
	var listenAddr string
	var reqBearerToken string
	var doPublish bool

	flag.BoolVar(&DebugMode, "debug", false, "enables debugging messages")
	flag.StringVar(&listenAddr, "listen", ListenAddrDef, "HTTP listen socket address for webhook events")
	flag.StringVar(&topicScope, "topic-domain", DefTopicDomain, "Default domain for RabbitMQ topics")
	flag.StringVar(&reqBearerToken, "token", "", "HTTP Bearer token to match")
	flag.BoolVar(&doPublish, "publish", true, "Publish to RabbitMQ. Disable for testing.")
	flag.Parse()

	log.Println("Starting....")
	log.Printf("  * Debugging: %t\n", DebugMode)
	log.Printf("  * Listening: %s\n", listenAddr)
	log.Printf("  * Bearer token: %t\n", len(reqBearerToken) > 0)
	log.Printf("  * Publish to RabbitMQ: %t\n", doPublish)

	if doPublish {
		connectToRabbitMQ()
	}

	http.HandleFunc("POST /rabbitmq-forwarder", func(res http.ResponseWriter, req *http.Request) {
		if len(req.Header.Get("Content-Type")) == 0 ||
			req.Header["Content-Type"][0] != "application/json" ||
			req.Method != "POST" {

			res.WriteHeader(http.StatusInternalServerError)
			return
		}

		if len(reqBearerToken) > 0 {
			authToken := req.Header.Get("Authorization")
			if len(authToken) != len(reqBearerToken)+7 || authToken[0:7] != "Bearer " || authToken[7:] != reqBearerToken {
				log.Println("Invalid Authorization request...", authToken)
				res.WriteHeader(http.StatusNetworkAuthenticationRequired)
			}
		}

		hdr := req.Header[common.GiteaRequestHeader]
		if len(hdr) != 1 {
			res.WriteHeader(http.StatusInternalServerError)
			log.Printf("Multiple Gitea headers received. %#v\n", hdr)
			if DebugMode {
				log.Println(req.Header)
			}
			return
		}
		reqType := hdr[0]
		data, err := io.ReadAll(req.Body)
		if err != nil {
			errorStr := fmt.Sprintf("error reading hook info: %v", err)
			res.Header().Add("Content-Type", "plain/text")
			res.Write([]byte(errorStr))
			res.WriteHeader(http.StatusBadRequest)

			if DebugMode {
				log.Printf(errorStr)
			}
			return
		}

		if !json.Valid(data) {
			if DebugMode {
				log.Println("send invalid json request")
			}
			res.WriteHeader(http.StatusBadRequest)
			return
		}

		org, extraAction, err := parseRequestJSONOrg(reqType, data)
		if err != nil {
			res.WriteHeader(http.StatusBadRequest)
			log.Printf("error parsing webhook %s JSON. err: %v", reqType, err)
			if DebugMode {
				dumpUnhandledData(reqType, data)
			}
			return
		}

		if org == nil {
			res.WriteHeader(http.StatusBadRequest)
			log.Printf("no `org` for message... type: %s", reqType)
			if DebugMode {
				dumpUnhandledData(reqType, data)
			}
			return
		}

		if doPublish {
			err = PublishMessage(org.Username, reqType, extraAction, data)
			if err != nil {
				errorStr := fmt.Sprintf("hook (%s) processing error: %v\n", reqType, err)
				res.Header().Add("Content-Type", "plain/text")
				res.Write([]byte(errorStr))
				res.WriteHeader(http.StatusBadRequest)

				if DebugMode {
					log.Println(errorStr)
				}
				return
			}
		}
		res.WriteHeader(http.StatusOK)
	})

	log.Fatal(http.ListenAndServe(listenAddr, nil))
}
