// Package gmi implements the gemini-based api for the mediocre-blog.
package gmi

import (
	"context"
	"errors"
	"fmt"
	"mime"
	"net/url"
	"os"
	"path"
	"strings"

	"git.sr.ht/~adnano/go-gemini"
	"git.sr.ht/~adnano/go-gemini/certificate"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
	"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
	"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)

// Params are used to instantiate a new API instance. All fields are required
// unless otherwise noted.
type Params struct {
	Logger *mlog.Logger
	Cache  cache.Cache

	PostStore      post.Store
	PostAssetStore post.AssetStore

	PublicURL        *url.URL
	ListenAddr       string
	CertificatesPath string

	HTTPPublicURL *url.URL
}

// SetupCfg implement the cfg.Cfger interface.
func (p *Params) SetupCfg(cfg *cfg.Cfg) {

	publicURLStr := cfg.String("gemini-public-url", "gemini://localhost:2065", "URL this service is accessible at")

	cfg.StringVar(&p.ListenAddr, "gemini-listen-addr", ":2065", "Address to listen for HTTP requests on")

	cfg.StringVar(&p.CertificatesPath, "gemini-certificates-path", "", "Path to directory where gemini certs should be created/stored")

	cfg.OnInit(func(context.Context) error {

		if p.CertificatesPath == "" {
			return errors.New("-gemini-certificates-path is required")
		}

		var err error

		*publicURLStr = strings.TrimSuffix(*publicURLStr, "/")

		if p.PublicURL, err = url.Parse(*publicURLStr); err != nil {
			return fmt.Errorf("parsing -gemini-public-url: %w", err)
		}

		return nil
	})
}

// Annotate implements mctx.Annotator interface.
func (p *Params) Annotate(a mctx.Annotations) {
	a["geminiPublicURL"] = p.PublicURL
	a["geminiListenAddr"] = p.ListenAddr
	a["geminiCertificatesPath"] = p.CertificatesPath
}

// API will listen on the port configured for it, and serve gemini requests for
// the mediocre-blog.
type API interface {
	Shutdown(ctx context.Context) error
}

type api struct {
	params Params
	srv    *gemini.Server
}

// New initializes and returns a new API instance, including setting up all
// listening ports.
func New(params Params) (API, error) {

	if err := os.MkdirAll(params.CertificatesPath, 0700); err != nil {
		return nil, fmt.Errorf("creating certificate directory %q: %w", params.CertificatesPath, err)
	}

	certStore := new(certificate.Store)
	certStore.Load(params.CertificatesPath)
	certStore.Register(params.PublicURL.Hostname())

	a := &api{
		params: params,
	}

	handler, err := a.handler()
	if err != nil {
		return nil, fmt.Errorf("constructing handler: %w", err)
	}

	a.srv = &gemini.Server{
		Addr:           params.ListenAddr,
		Handler:        handler,
		GetCertificate: certStore.Get,
	}

	go func() {

		err := a.srv.ListenAndServe(context.Background())

		if err != nil && !errors.Is(err, context.Canceled) {

			ctx := mctx.WithAnnotator(context.Background(), &a.params)
			a.params.Logger.Fatal(ctx, "serving gemini server", err)
		}
	}()

	return a, nil
}

func (a *api) Shutdown(ctx context.Context) error {
	return a.srv.Shutdown(ctx)
}

func (a *api) logReqMiddleware(h gemini.Handler) gemini.Handler {

	type logCtxKey string

	return gemini.HandlerFunc(func(
		ctx context.Context,
		rw gemini.ResponseWriter,
		r *gemini.Request,
	) {

		ctx = mctx.Annotate(ctx,
			logCtxKey("url"), r.URL.String(),
		)

		h.ServeGemini(ctx, rw, r)

		a.params.Logger.Info(ctx, "handled gemini request")
	})
}

func indexMiddleware(h gemini.Handler) gemini.Handler {

	return gemini.HandlerFunc(func(
		ctx context.Context,
		rw gemini.ResponseWriter,
		r *gemini.Request,
	) {
		if strings.HasSuffix(r.URL.Path, "/") {
			r.URL.Path += "index.gmi"
		}

		h.ServeGemini(ctx, rw, r)
	})
}

func postsMiddleware(tplHandler gemini.Handler) gemini.Handler {

	return gemini.HandlerFunc(func(
		ctx context.Context,
		rw gemini.ResponseWriter,
		r *gemini.Request,
	) {

		id := path.Base(r.URL.Path)
		id = strings.TrimSuffix(id, ".gmi")

		if id == "index" {
			tplHandler.ServeGemini(ctx, rw, r)
			return
		}

		query := r.URL.Query()
		query.Set("id", id)
		r.URL.RawQuery = query.Encode()

		r.URL.Path = "/posts/post.gmi"

		tplHandler.ServeGemini(ctx, rw, r)
	})
}

func (a *api) assetsMiddleware() gemini.Handler {

	return gemini.HandlerFunc(func(
		ctx context.Context,
		rw gemini.ResponseWriter,
		r *gemini.Request,
	) {

		id := path.Base(r.URL.Path)
		mimeType := mime.TypeByExtension(path.Ext(id))

		ctx = mctx.Annotate(ctx, "assetID", id, "mimeType", mimeType)

		if mimeType != "" {
			rw.SetMediaType(mimeType)
		}

		err := a.params.PostAssetStore.Get(id, rw)

		if errors.Is(err, post.ErrAssetNotFound) {
			rw.WriteHeader(gemini.StatusNotFound, "Asset not found, sorry!")
			return

		} else if err != nil {
			a.params.Logger.Error(ctx, "error fetching asset", err)
			rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
			return
		}
	})
}

func (a *api) handler() (gemini.Handler, error) {

	tplHandler, err := a.tplHandler()
	if err != nil {
		return nil, fmt.Errorf("generating tpl handler: %w", err)
	}

	mux := new(gemini.Mux)
	mux.Handle("/posts/", postsMiddleware(tplHandler))
	mux.Handle("/assets/", a.assetsMiddleware())
	mux.Handle("/", tplHandler)

	var h gemini.Handler

	h = mux
	h = indexMiddleware(h)

	h = a.logReqMiddleware(h)
	h = cacheMiddleware(a.params.Cache)(h)

	return h, nil
}
