package builder

import (
	"encoding/xml"
	"fmt"
	"io"
	"net/url"
	"os"
	"strings"
	"sync"
	"text/template"
	"time"
	"website/internal/atom"
	"website/internal/config"
	"website/internal/log"

	"github.com/PuerkitoBio/goquery"
	"github.com/a-h/htmlformat"
	"github.com/antchfx/xmlquery"
	"github.com/antchfx/xpath"
	mapset "github.com/deckarep/golang-set/v2"
	"github.com/pkg/errors"
	"golang.org/x/net/html"
)

var (
	assetsOnce     sync.Once
	css            string
	countHTML      *goquery.Document
	liveReloadHTML *goquery.Document
	templates      = make(map[string]*os.File)
)

func loadTemplate(path string) (file *os.File, err error) {
	if templates[path] == nil {
		file, err = os.OpenFile(path, os.O_RDONLY, 0)
		if err != nil {
			return nil, errors.Wrapf(err, "could not load template at path %s", path)
		}
		templates[path] = file
	}
	file = templates[path]

	return
}

var (
	imgOnce     sync.Once
	img         *goquery.Selection
	urlTemplate *url.URL
)

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 NewDocumentNoScript(r io.Reader) (*goquery.Document, error) {
	root, err := html.ParseWithOptions(r, html.ParseOptionEnableScripting(false))

	return goquery.NewDocumentFromNode(root), errors.Wrap(err, "could not parse HTML")
}

func (root QuerySelection) setImgURL(pageURL string, pageTitle string) QuerySelection {
	clone := countHTML.Clone()
	imgOnce.Do(func() {
		var err error
		img = clone.Find("img")
		attr, _ := img.Attr("src")
		if attr == "" {
			panic("<img> does not have src attribute")
		}
		urlTemplate, err = url.Parse(attr)
		if err != nil {
			panic(err.Error())
		}
	})
	q := urlTemplate.Query()
	urlTemplate.RawQuery = ""
	q.Set("p", pageURL)
	q.Set("t", pageTitle)
	output := urlTemplate.String() + "?" + q.Encode()
	clone.Find("img").SetAttr("src", output)
	root.AppendSelection(clone.Find("body").Children())

	return root
}

func layout(
	filename string,
	config config.Config,
	pageTitle string,
	pageURL string,
) (*goquery.Document, error) {
	html, err := loadTemplate(filename)
	if err != nil {
		return nil, err
	}
	defer func() {
		_, err := html.Seek(0, io.SeekStart)
		if err != nil {
			panic("could not reset template file offset: " + err.Error())
		}
	}()
	assetsOnce.Do(func() {
		var bytes []byte
		bytes, err = os.ReadFile("templates/style.css")
		if err != nil {
			return
		}
		css = string(bytes)
		countFile, err := os.OpenFile("templates/count.html", os.O_RDONLY, 0)
		if err != nil {
			return
		}
		defer countFile.Close()
		countHTML, err = NewDocumentNoScript(countFile)
		if err != nil {
			return
		}
		if config.InjectLiveReload {
			liveReloadFile, err := os.OpenFile("templates/dev.html", os.O_RDONLY, 0)
			if err != nil {
				return
			}
			defer liveReloadFile.Close()
			liveReloadHTML, err = goquery.NewDocumentFromReader(liveReloadFile)
			if err != nil {
				return
			}
		}
	})
	if err != nil {
		return nil, errors.Wrap(err, "could not set up layout template")
	}

	doc, err := NewDocumentFromReader(html)
	if err != nil {
		return nil, err
	}
	doc.Find("html").SetAttr("lang", config.DefaultLanguage)
	doc.Find("head > link[rel=alternate]").SetAttr("title", config.Title)
	doc.Find("head > link[rel=canonical]").SetAttr("href", pageURL)
	doc.Find(".title").SetText(config.Title)
	doc.Find("title").Add(".p-name").SetText(pageTitle)
	doc.Find("head > style").SetHtml(css)
	doc.Find("body").setImgURL(pageURL, pageTitle)
	if config.InjectLiveReload {
		doc.Find("body").AppendSelection(liveReloadHTML.Find("body").Clone())
	}
	nav := doc.Find("nav")
	navLink := doc.Find("nav a")
	nav.Empty()
	for _, link := range config.Menus["main"] {
		nav.AppendSelection(navLink.Clone().SetAttr("href", link.URL).SetText(link.Name))
	}

	return doc.Document, nil
}

func renderPost(post Post, config config.Config) (r io.Reader, err error) {
	doc, err := layout("templates/post.html", config, post.PostMatter.Title, post.URL)
	if err != nil {
		return nil, err
	}
	doc.Find(".title").AddClass("p-author h-card").SetAttr("rel", "author")
	doc.Find(".h-entry .dt-published").
		SetAttr("datetime", post.PostMatter.Date.UTC().Format(time.RFC3339)).
		SetText(
			post.PostMatter.Date.Format("2006-01-02"),
		)
	doc.Find(".h-entry .e-content").SetHtml(post.Content)
	categories := doc.Find(".h-entry .p-categories")
	tpl := categories.Find(".p-category").ParentsUntilSelection(categories)
	tpl.Remove()
	for _, tag := range post.Taxonomies.Tags {
		cat := tpl.Clone()
		cat.Find(".p-category").SetAttr("href", fmt.Sprintf("/tags/%s/", tag)).SetText("#" + tag)
		categories.AppendSelection(cat)
	}

	return renderHTML(doc), nil
}

func renderTags(tags Tags, config config.Config, url string) (io.Reader, error) {
	doc, err := layout("templates/tags.html", config, config.Title, url)
	if err != nil {
		return nil, err
	}
	tagList := doc.Find(".tags")
	tpl := doc.Find(".h-feed")
	tpl.Remove()
	for _, tag := range mapset.Sorted(tags) {
		li := tpl.Clone()
		li.Find("a").SetAttr("href", fmt.Sprintf("/tags/%s/", tag)).SetText("#" + tag)
		tagList.AppendSelection(li)
	}

	return renderHTML(doc), nil
}

func renderListPage(tag string, config config.Config, posts []Post, url string) (io.Reader, error) {
	var title string
	if len(tag) > 0 {
		title = tag
	} else {
		title = config.Title
	}
	doc, err := layout("templates/list.html", config, title, url)
	if err != nil {
		return nil, err
	}
	feed := doc.Find(".h-feed")
	tpl := feed.Find(".h-entry")
	tpl.Remove()

	doc.Find(".title").AddClass("p-author h-card").SetAttr("rel", "author")
	if tag == "" {
		doc.Find(".filter").Remove()
	} else {
		doc.Find(".filter").Find("h3").SetText("#" + tag)
	}

	for _, post := range posts {
		entry := tpl.Clone()
		entry.Find(".p-name").SetText(post.Title).SetAttr("href", post.URL)
		entry.Find(".dt-published").
			SetAttr("datetime", post.PostMatter.Date.UTC().Format(time.RFC3339)).
			SetText(post.PostMatter.Date.Format("2006-01-02"))
		feed.AppendSelection(entry)
	}

	return renderHTML(doc), nil
}

func renderHomepage(config config.Config, posts []Post, url string) (io.Reader, error) {
	_, index, err := getPost("content/_index.md")
	if err != nil {
		return nil, err
	}
	doc, err := layout("templates/homepage.html", config, config.Title, url)
	if err != nil {
		return nil, err
	}
	doc.Find("body").AddClass("h-card")
	doc.Find(".title").AddClass("p-name u-url")

	html, err := renderMarkdown(index)
	if err != nil {
		return nil, err
	}
	doc.Find("#content").SetHtml(html)

	feed := doc.Find(".h-feed")
	tpl := feed.Find(".h-entry")
	tpl.Remove()

	for _, post := range posts[0:3] {
		entry := tpl.Clone()
		entry.Find(".p-name").SetText(post.Title)
		entry.Find(".u-url").SetAttr("href", post.URL)
		entry.
			Find(".dt-published").
			SetAttr("datetime", post.PostMatter.Date.UTC().Format(time.RFC3339)).
			SetText(post.PostMatter.Date.Format("2006-01-02"))

		feed.AppendSelection(entry)
	}
	doc.Find(".u-email").
		SetAttr("href", fmt.Sprintf("mailto:%s", config.Email)).
		SetText(config.Email)

	elsewhere := doc.Find(".elsewhere")
	linkRelMe := elsewhere.Find(".u-url[rel=me]").ParentsUntil("ul")
	linkRelMe.Remove()

	for _, link := range config.Menus["me"] {
		el := linkRelMe.Clone()
		el.Find("a").SetAttr("href", link.URL).SetText(link.Name)
		elsewhere.AppendSelection(el)
	}

	return renderHTML(doc), nil
}

func renderRobotsTXT(config config.Config) (io.Reader, error) {
	r, w := io.Pipe()
	tpl, err := template.ParseFiles("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 render404(config config.Config, url string) (io.Reader, error) {
	doc, err := layout("templates/404.html", config, "404 Not Found", url)
	if err != nil {
		return nil, err
	}

	return renderHTML(doc), nil
}

func renderFeed(
	title string,
	config config.Config,
	posts []Post,
	specific string,
) (io.Reader, error) {
	reader, err := loadTemplate("templates/feed.xml")
	if err != nil {
		return nil, err
	}
	defer func() {
		_, err := reader.Seek(0, io.SeekStart)
		if err != nil {
			panic("could not reset reader: " + err.Error())
		}
	}()
	doc, err := xmlquery.Parse(reader)
	if err != nil {
		return nil, errors.Wrap(err, "could not parse XML")
	}
	feed := doc.SelectElement("feed")
	feed.SelectElement("title").FirstChild.Data = title
	feed.SelectElement("link").SetAttr("href", config.BaseURL.String())
	feed.SelectElement("id").FirstChild.Data = atom.MakeTagURI(config, specific)
	datetime, err := posts[0].Date.UTC().MarshalText()
	if err != nil {
		return nil, errors.Wrap(err, "could not convert post date to text")
	}
	feed.SelectElement("updated").FirstChild.Data = string(datetime)
	tpl := feed.SelectElement("entry")
	xmlquery.RemoveFromTree(tpl)

	for _, post := range posts {
		fullURL := config.BaseURL.JoinPath(post.URL).String()
		text, err := xml.MarshalIndent(&atom.FeedEntry{
			Title:   post.Title,
			Link:    atom.MakeLink(fullURL),
			ID:      atom.MakeTagURI(config, post.Basename),
			Updated: post.Date.UTC(),
			Summary: post.Description,
			Author:  config.Title,
			Content: atom.FeedContent{
				Content: post.Content,
				Type:    "html",
			},
		}, "  ", "    ")
		if err != nil {
			return nil, errors.Wrap(err, "could not marshal xml")
		}
		entry, err := xmlquery.ParseWithOptions(
			strings.NewReader(string(text)),
			xmlquery.ParserOptions{
				Decoder: &xmlquery.DecoderOptions{
					Strict:    false,
					AutoClose: xml.HTMLAutoClose,
					Entity:    xml.HTMLEntity,
				},
			},
		)
		if err != nil {
			return nil, errors.Wrap(err, "could not parse XML")
		}
		xmlquery.AddChild(feed, entry.SelectElement("entry"))
	}

	return strings.NewReader(doc.OutputXML(true)), nil
}

func renderFeedStyles() (io.Reader, error) {
	reader, err := loadTemplate("templates/feed-styles.xsl")
	if err != nil {
		return nil, err
	}
	defer func() {
		_, err := reader.Seek(0, io.SeekStart)
		if err != nil {
			panic("could not reset reader: " + err.Error())
		}
	}()
	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",
	}
	doc, err := xmlquery.Parse(reader)
	if err != nil {
		return nil, errors.Wrap(err, "could not parse XML")
	}
	expr, err := xpath.CompileWithNS("//xhtml:style", nsMap)
	if err != nil {
		return nil, errors.Wrap(err, "could not parse XML")
	}
	style := xmlquery.QuerySelector(doc, expr)
	xmlquery.AddChild(style, &xmlquery.Node{
		Type: xmlquery.TextNode,
		Data: css,
	})

	return strings.NewReader(doc.OutputXML(true)), nil
}

func renderHTML(doc *goquery.Document) io.Reader {
	r, w := io.Pipe()

	go func() {
		_, err := w.Write([]byte("<!doctype html>\n"))
		if err != nil {
			log.Error("error writing doctype", "error", err)
			w.CloseWithError(err)
		}
		err = htmlformat.Nodes(w, []*html.Node{doc.Children().Get(0)})
		if err != nil {
			log.Error("error rendering html", "error", err)
			w.CloseWithError(err)

			return
		}
		defer w.Close()
	}()

	return r
}