package gmi

import (
	"bytes"
	"context"
	"embed"
	"fmt"
	"io"
	"io/fs"
	"mime"
	"net/url"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"text/template"

	"git.sr.ht/~adnano/go-gemini"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
	"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
	gmnhg "github.com/tdemin/gmnhg"
)

//go:embed tpl
var tplFS embed.FS

type rendererGetPostsRes struct {
	Posts   []post.StoredPost
	HasMore bool
}

type rendererGetPostSeriesNextPreviousRes struct {
	Next     *post.StoredPost
	Previous *post.StoredPost
}

type renderer struct {
	url             *url.URL
	publicURL       *url.URL
	postStore       post.Store
	preprocessFuncs post.PreprocessFunctions
}

func (r renderer) GetPosts(page, count int) (rendererGetPostsRes, error) {
	posts, hasMore, err := r.postStore.Get(page, count)
	return rendererGetPostsRes{posts, hasMore}, err
}

func (r renderer) GetPostByID(id string) (post.StoredPost, error) {
	p, err := r.postStore.GetByID(id)
	if err != nil {
		return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err)
	}
	return p, nil
}

func (r renderer) GetPostSeriesNextPrevious(p post.StoredPost) (rendererGetPostSeriesNextPreviousRes, error) {

	seriesPosts, err := r.postStore.GetBySeries(p.Series)
	if err != nil {
		return rendererGetPostSeriesNextPreviousRes{}, fmt.Errorf(
			"fetching posts for series %q: %w", p.Series, err,
		)
	}

	var (
		res       rendererGetPostSeriesNextPreviousRes
		foundThis bool
	)

	for i := range seriesPosts {

		seriesPost := seriesPosts[i]

		if seriesPost.ID == p.ID {
			foundThis = true
			continue
		}

		if !foundThis {
			res.Next = &seriesPost
			continue
		}

		res.Previous = &seriesPost
		break
	}

	return res, nil
}

func (r renderer) PostBody(p post.StoredPost) (string, error) {

	buf := new(bytes.Buffer)

	if err := p.PreprocessBody(buf, r.preprocessFuncs); err != nil {
		return "", fmt.Errorf("preprocessing post body: %w", err)
	}

	bodyBytes := buf.Bytes()

	if p.Format == post.FormatMarkdown {

		gemtextBodyBytes, err := gmnhg.RenderMarkdown(bodyBytes, 0)
		if err != nil {
			return "", fmt.Errorf("converting from markdown: %w", err)
		}

		bodyBytes = gemtextBodyBytes
	}

	return string(bodyBytes), nil
}

func (r renderer) GetQueryValue(key, def string) string {
	v := r.url.Query().Get(key)
	if v == "" {
		v = def
	}
	return v
}

func (r renderer) GetQueryIntValue(key string, def int) (int, error) {
	vStr := r.GetQueryValue(key, strconv.Itoa(def))
	return strconv.Atoi(vStr)
}

func (r renderer) GetPath() (string, error) {
	basePath := filepath.Join("/", r.publicURL.Path) // in case it's empty
	return filepath.Rel(basePath, r.url.Path)
}

func (r renderer) Add(a, b int) int { return a + b }

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

	blogURL := func(base *url.URL, path string, abs bool) string {

		// filepath.Join strips trailing slash, but we want to keep it
		trailingSlash := strings.HasSuffix(path, "/")

		path = filepath.Join("/", base.Path, path)

		if trailingSlash && path != "/" {
			path += "/"
		}

		if !abs {
			return path
		}

		u := *base
		u.Path = path
		return u.String()
	}

	preprocessFuncs := post.PreprocessFunctions{
		BlogURL: func(path string) string {
			return blogURL(a.params.PublicURL, path, false)
		},
		BlogHTTPURL: func(path string) string {
			return blogURL(a.params.HTTPPublicURL, path, true)
		},
		BlogGeminiURL: func(path string) string {
			return blogURL(a.params.PublicURL, path, true)
		},
		AssetURL: func(id string) string {
			path := filepath.Join("assets", id)
			return blogURL(a.params.PublicURL, path, false)
		},
		PostURL: func(id string) string {
			path := filepath.Join("posts", id) + ".gmi"
			return blogURL(a.params.PublicURL, path, false)
		},
		StaticURL: func(path string) string {
			path = filepath.Join("static", path)
			return blogURL(a.params.HTTPPublicURL, path, true)
		},
		Image: func(args ...string) (string, error) {

			var (
				id    = args[0]
				descr = "Image"
			)

			if len(args) > 1 {
				descr = args[1]
			}

			path := filepath.Join("assets", id)
			path = blogURL(a.params.PublicURL, path, false)

			return fmt.Sprintf("\n=> %s %s", path, descr), nil
		},
	}

	allTpls := template.New("")

	allTpls.Funcs(preprocessFuncs.ToFuncMap())

	allTpls.Funcs(template.FuncMap{
		"PostURLAbs": func(id string) string {
			path := filepath.Join("posts", id) + ".gmi"
			return blogURL(a.params.PublicURL, path, true)
		},
		"PostHTTPURL": func(id string) string {
			path := filepath.Join("posts", id)
			return preprocessFuncs.BlogHTTPURL(path)
		},
	})

	err := fs.WalkDir(tplFS, "tpl", func(path string, d fs.DirEntry, err error) error {

		if err != nil {
			return err
		}

		if d.IsDir() {
			return nil
		}

		body, err := fs.ReadFile(tplFS, path)
		if err != nil {
			panic(err)
		}

		name := strings.TrimPrefix(path, "tpl/")

		allTpls, err = allTpls.New(name).Parse(string(body))
		if err != nil {
			return fmt.Errorf("parsing %q as template: %w", path, err)
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("parsing templates: %w", err)
	}

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

		tplPath := strings.TrimPrefix(r.URL.Path, "/")
		mimeType := mime.TypeByExtension(path.Ext(r.URL.Path))

		ctx = mctx.Annotate(ctx,
			"url", r.URL,
			"tplPath", tplPath,
			"mimeType", mimeType,
		)

		tpl := allTpls.Lookup(tplPath)

		if tpl == nil {
			rw.WriteHeader(gemini.StatusNotFound, "Page not found, sorry!")
			return
		}

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

		buf := new(bytes.Buffer)

		err := tpl.Execute(buf, renderer{
			url:             r.URL,
			publicURL:       a.params.PublicURL,
			postStore:       a.params.PostStore,
			preprocessFuncs: preprocessFuncs,
		})

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

		io.Copy(rw, buf)
	}), nil
}
