package http

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"source.toby3d.me/toby3d/home/internal/common"
	"source.toby3d.me/toby3d/home/internal/domain"
	"source.toby3d.me/toby3d/home/internal/webfinger"
)

type (
	Handler struct {
		webFingers webfinger.UseCase
	}

	Request struct {
		Resource *Resource
		Rel      string // optional
	}

	Response struct {
		Properties map[string]string `json:"properties,omitempty"`
		Subject    string            `json:"subject"`
		Aliases    []string          `json:"aliases,omitempty"`
		Links      []Link            `json:"links,omitempty"`
	}

	Link struct {
		Titles     map[string]string `json:"titles,omitempty"`
		Properties map[string]string `json:"properties,omitempty"`
		Rel        string            `json:"rel"`
		Type       string            `json:"type,omitempty"`
		Href       string            `json:"href,omitempty"`
	}

	Resource struct {
		User    string
		Host    string
		IsEmail bool
	}
)

var ErrResource error = errors.New("'resource' URL must be absolute, or an email address")

func NewHandler(webFingers webfinger.UseCase) *Handler {
	return &Handler{webFingers: webFingers}
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set(common.HeaderAccessControlAllowOrigin, "*")

	if r.TLS == nil {
		http.Error(w, "webfinger usage is https only", http.StatusBadRequest)

		return
	}

	if r.Method != "" && r.Method != http.MethodGet {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)

		return
	}

	req := new(Request)
	if err := req.bind(r.URL.Query()); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)

		return
	}

	site, err := h.webFingers.Do(r.Context(), strings.TrimPrefix(req.Resource.String(), "acct:"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	resp := NewResponse(req.Resource.String())
	// TODO(toby3d): fill resp.Aliases with profile syndications links.
	// NOTE(toby3d): rel="self" link is a Mastodon requirement for
	// ActivityPub support.
	resp.Links = append(resp.Links, Link{
		Titles:     make(map[string]string),
		Properties: make(map[string]string),
		Rel:        "self",
		Type:       common.MIMEApplicationActivityJSON,
		Href:       site.BaseURL.String(),
	}, Link{
		Titles:     make(map[string]string),
		Properties: make(map[string]string),
		Rel:        "http://webfinger.net/rel/profile-page",
		Type:       "text/html",
		Href:       site.BaseURL.String(),
	})

	// TODO(toby3d): explicitly control which resource is to be used.
	if photo := site.Resources.GetType(domain.ResourceTypeImage).GetMatch("logo"); photo != nil {
		resp.Links = append(resp.Links, Link{
			Titles:     make(map[string]string),
			Properties: make(map[string]string),
			Rel:        "http://webfinger.net/rel/avatar",
			Type:       photo.MediaType().String(),
			Href:       site.BaseURL.JoinPath(photo.File.Path()).String(),
		})
	}

	w.Header().Set(common.HeaderContentType, common.MIMEApplicationJRDKJSONCharsetUTF8)
	if err := json.NewEncoder(w).Encode(resp); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (r *Request) bind(req url.Values) error {
	if !req.Has("resource") {
		return ErrResource
	}

	var err error
	if r.Resource, err = ParseResource(req.Get("resource")); err != nil {
		return fmt.Errorf("cannot parse 'resource' query as URI: %w", err)
	}

	return nil
}

func NewResponse(subject string) *Response {
	return &Response{
		Properties: make(map[string]string),
		Subject:    subject,
		Aliases:    make([]string, 0),
		Links:      make([]Link, 0),
	}
}

func ParseResource(raw string) (*Resource, error) {
	u, err := url.Parse(raw)
	if err != nil {
		return nil, fmt.Errorf("cannot parse 'resource' query value as URL or email address: %w", err)
	}

	var parts []string
	switch {
	case u.Path != "":
		parts = strings.Split(u.Path, "@")
	case u.Opaque != "":
		parts = strings.Split(u.Opaque, "@")
	}

	return &Resource{
		IsEmail: u.Scheme != "acct",
		User:    parts[0],
		Host:    parts[len(parts)-1],
	}, nil
}

func (r Resource) String() string {
	if !r.IsEmail {
		return "acct:" + r.User + "@" + r.Host
	}

	return r.User + "@" + r.Host
}
