package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"homepage/antispam"
	"log/slog"
	"mime/multipart"
	"net/http"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/AlessandroSechi/zammad-go"
)

const formMarker = "<!-- FORM -->"
var namesRegex = regexp.MustCompile("^ *(.*) *([^ ]+) *$")
var multiSpaceRegex = regexp.MustCompile("\\s+")

type submission struct {
	group       string
	category    string
	name        string
	email       string
	message     string
	attachments []submissionAttachment
	information []struct {
		key   string
		value string
	}
}

type submissionAttachment struct {
	multipart.File
	*multipart.FileHeader
}

type toSubmission func(r *http.Request) (submission, error)
type form struct {
	path         string
	toSubmission toSubmission
}

// CleanupInput removes unprintable characters like newlines.
func CleanupInput(s string) string {
	s = strings.TrimSpace(s)
	s = multiSpaceRegex.ReplaceAllString(s, " ")
	return strings.Map(func(r rune) rune {
		if unicode.IsPrint(r) {
			return r
		}
		return -1
	}, s)
}

func (form form) handleGet(w http.ResponseWriter, r *http.Request) {
	logger := slog.Default().With("formPath", form.path)
	ctx := r.Context()

	templateBytes, err := os.ReadFile(path.Join(getRootPath(r), form.path, "index.html"))
	if err != nil {
		logger.ErrorContext(ctx, "Failed to read form template", "error", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	output := string(templateBytes)

	markerIndex := strings.Index(output, formMarker)
	if markerIndex != -1 {
		csrfField := CsrfTemplateField(r)
		antispamInstance := antispam.GetAntispam(r)
		antispamField := string(antispamInstance.Script() + antispamInstance.FormField())
		output = output[:markerIndex] + string(csrfField) + antispamField + output[markerIndex+len(formMarker):]
	}

	http.ServeContent(w, r, "form.html", time.Now(), strings.NewReader(output))
}

// getCustomerId uses the Zammad API to find a user ID to use as the
// submission's customer.
//
// Note the API also supports using `guess:{email}` as the custom ID when creating
// a ticket, according to the documentation [1]. However, that doesn't seem to
// work very reliably [2].
// [1]: https://docs.zammad.org/en/latest/api/ticket/index.html#create
// [2]: https://codeberg.org/angestoepselt/homepage/issues/141
func getCustomerId(submission *submission, zClient *zammad.Client) (int, error) {
	nameParts := strings.Split(submission.name, " ")
	if len(nameParts) == 1 {
		nameParts = append([]string{"?"}, nameParts...)
	}

	// Create a new user. This will fail if it already exists.
	user, err := zClient.UserCreate(zammad.User{
		Firstname: strings.Join(nameParts[:len(nameParts)-1], " "),
		Lastname:  nameParts[len(nameParts)-1],
		Email:     submission.email,
	})
	if err == nil {
		return user.ID, nil
	}

	var errResp *zammad.ErrorResponse
	if !errors.As(err, &errResp) || !strings.Contains(errResp.Description, "already used") {
		return 0, fmt.Errorf("creating new Zammad user failed: %w", err)
	}

	// Email is already associated with an existing user account. Find that one
	// instead and use its ID.
	userCandidates, err := zClient.UserSearch("email:"+submission.email, 1)
	if err != nil {
		return 0, fmt.Errorf("search for existing Zammad user failed: %w", err)
	}
	if len(userCandidates) == 0 {
		return 0, errors.New("search for existing Zammad user yielded no results")
	}
	if len(userCandidates) > 1 {
		return 0, errors.New("search for existing Zammad user yielded more than one result")
	}
	return userCandidates[0].ID, nil
}

func (form form) handlePost(w http.ResponseWriter, r *http.Request) {
	logger := slog.Default().With("formPath", form.path)
	ctx := r.Context()

	if !GetFetchMetadata(r).IsLocalNavigation(false) {
		AbortFetchMetadata(w, r)
		return
	}

	err := r.ParseMultipartForm(128 << 20) // 128 KiB
	if err != nil {
		logger.DebugContext(ctx, "Failed to parse form template", "error", err)
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	submission, err := form.toSubmission(r)
	if err != nil {
		logger.DebugContext(ctx, "Could not convert form content to submission", "error", err)
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
	}

	contactNames := strings.Split(submission.name, " ")

	type zammadUsersBody struct {
		FirstName string `json:"firstname"`
		LastName  string `json:"lastname"`
		Email     string `json:"email"`
	}
	body := zammadUsersBody{
		FirstName: "?",
		LastName:  submission.,
	}
	body, _ := json.Marshal(zammadUsersBody{
		FirstName: "?",
		LastName:  contactNames[len(contactNames)-1],
		Email:     "",
	})
	resp = http.Post("TODO", "application/json", bytes.NewReader(body))

	zClient, ok := r.Context().Value(zammadKey{}).(*zammad.Client)
	if !ok {
		logger.DebugContext(ctx, "Zammad integration unavailable")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	customerId, err := getCustomerId(&submission, zClient)
	if err != nil {
		logger.DebugContext(ctx, "Failed to match Zammad user as customer for submission", "error", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	zClientFrom := zammad.Client{
		Client: zClient.Client,
		Token:  zClient.Token,
		Url:    zClient.Url,
		FromFunc: func() string {
			return strconv.Itoa(customerId)
		},
	}
	zClientFrom.TicketCreate(zammad.Ticket{
		Title:      fmt.Sprintf("Kontaktformular %s – %s", submission.name, form.path), // TODO form.path
		Group:      "",                                                                 // TODO group
		CustomerID: customerId,
		Article: zammad.TicketArticle{
			Type:        "web",
			Internal:    false, // TODO this was true
			ContentType: "text/plain",
			Subject:     "Kontaktformular-Anfrage " + submission.name,
			Body:        submission.message,
			Attachments: []zammad.TicketArticleAttachment{},
		},
	})

	http.ServeFile(w, r, path.Join(getRootPath(r), "kontakt/fertig/index.html"))
}

func (form form) setup(mux *http.ServeMux) {
	mux.HandleFunc(http.MethodGet+" "+form.path+"/{$}", form.handleGet)
	mux.HandleFunc(http.MethodPost+" "+form.path+"/{$}", form.handlePost)
}
