package builder

import (
	"bytes"
	"encoding/xml"
	"io"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	"go.alanpearce.eu/website/internal/atom"
	"go.alanpearce.eu/website/internal/config"
	"go.alanpearce.eu/website/internal/content"

	"github.com/PuerkitoBio/goquery"
	"github.com/antchfx/xmlquery"
	"github.com/antchfx/xpath"
	"gitlab.com/tozd/go/errors"
)

var (
	css   string
	nsMap = map[string]string{
		"xsl":   "http://www.w3.org/1999/XSL/Transform",
		"atom":  "http://www.w3.org/2005/Atom",
		"xhtml": "http://www.w3.org/1999/xhtml",
	}
)

func loadCSS(source string) {
	bytes, err := os.ReadFile(filepath.Join(source, "templates/style.css"))
	if err != nil {
		panic(err)
	}
	css = string(bytes)
}

type QuerySelection struct {
	*goquery.Selection
}

type QueryDocument struct {
	*goquery.Document
}

func NewDocumentFromReader(r io.Reader) (*QueryDocument, error) {
	doc, err := goquery.NewDocumentFromReader(r)

	return &QueryDocument{doc}, errors.Wrap(err, "could not create query document")
}

func (q *QueryDocument) Find(selector string) *QuerySelection {
	return &QuerySelection{q.Document.Find(selector)}
}

func renderRobotsTXT(source string, config *config.Config) (io.Reader, error) {
	r, w := io.Pipe()
	tpl, err := template.ParseFiles(filepath.Join(source, "templates/robots.tmpl"))
	if err != nil {
		return nil, err
	}
	go func() {
		err = tpl.Execute(w, map[string]interface{}{
			"BaseURL": config.BaseURL,
		})
		if err != nil {
			w.CloseWithError(err)
		}
		w.Close()
	}()

	return r, nil
}

func renderFeed(
	title string,
	config *config.Config,
	posts []content.Post,
	specific string,
) (io.WriterTo, error) {
	buf := &bytes.Buffer{}
	datetime := posts[0].Date.UTC()

	buf.WriteString(xml.Header)
	err := atom.LinkXSL(buf, "/feed-styles.xsl")
	if err != nil {
		return nil, err
	}
	feed := &atom.Feed{
		Title:   title,
		Link:    atom.MakeLink(config.BaseURL.URL),
		ID:      atom.MakeTagURI(config, specific),
		Updated: datetime,
		Entries: make([]*atom.FeedEntry, len(posts)),
	}

	for i, post := range posts {
		feed.Entries[i] = &atom.FeedEntry{
			Title:   post.Title,
			Link:    atom.MakeLink(config.BaseURL.JoinPath(post.URL)),
			ID:      atom.MakeTagURI(config, post.Basename),
			Updated: post.Date.UTC(),
			Summary: post.Description,
			Author:  config.Title,
			Content: atom.FeedContent{
				Content: post.Content,
				Type:    "html",
			},
		}
	}
	enc := xml.NewEncoder(buf)
	err = enc.Encode(feed)
	if err != nil {
		return nil, err
	}

	return buf, nil
}

func renderFeedStyles(source string) (*strings.Reader, error) {
	tpl, err := template.ParseFiles(filepath.Join(source, "templates/feed-styles.xsl"))
	if err != nil {
		return nil, err
	}

	esc := &strings.Builder{}
	err = xml.EscapeText(esc, []byte(css))
	if err != nil {
		return nil, err
	}

	w := &strings.Builder{}
	err = tpl.Execute(w, map[string]interface{}{
		"css": esc.String(),
	})
	if err != nil {
		return nil, err
	}

	return strings.NewReader(w.String()), nil
}

func getFeedStylesHash(r io.Reader) (string, error) {
	doc, err := xmlquery.Parse(r)
	if err != nil {
		return "", err
	}
	expr, err := xpath.CompileWithNS("//xhtml:style", nsMap)
	if err != nil {
		return "", errors.Wrap(err, "could not parse XPath")
	}
	style := xmlquery.QuerySelector(doc, expr)

	return hash(style.InnerText()), nil
}

func getHTMLStyleHash(filenames ...string) (string, error) {
	fn := filepath.Join(filenames...)
	f, err := os.Open(fn)
	if err != nil {
		return "", err
	}
	defer f.Close()
	doc, err := NewDocumentFromReader(f)
	if err != nil {
		return "", err
	}
	html := doc.Find("head > style").Text()

	return hash(html), nil
}