package blog

import (
	"bytes"
	"cmp"
	"embed"
	"html/template"
	"io"
	"io/fs"
	"os"
	"slices"
	"strings"
	"time"

	"github.com/adrg/frontmatter"
	"github.com/gorilla/feeds"

	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/pages/markup"
	textension "tangled.org/core/appview/pages/markup/extension"
)

//go:embed posts
var PostsFS embed.FS

type Author struct {
	Name   string `yaml:"name"`
	Email  string `yaml:"email"`
	Handle string `yaml:"handle"`
}

type PostMeta struct {
	Slug     string   `yaml:"slug"`
	Title    string   `yaml:"title"`
	Subtitle string   `yaml:"subtitle"`
	Date     string   `yaml:"date"`
	Authors  []Author `yaml:"authors"`
	Image    string   `yaml:"image"`
	Draft    bool     `yaml:"draft"`
}

type Post struct {
	Meta PostMeta
	Body template.HTML
}

func (p Post) ParsedDate() time.Time {
	t, _ := time.Parse("2006-01-02", p.Meta.Date)
	return t
}

type indexParams struct {
	LoggedInUser any
	Posts        []Post
	Featured     []Post
}

type postParams struct {
	LoggedInUser any
	Post         Post
}

// Posts parses and returns all non-draft posts sorted newest-first.
func Posts(postsDir string) ([]Post, error) {
	return parsePosts(postsDir, false)
}

// AllPosts parses and returns all posts including drafts, sorted newest-first.
func AllPosts(postsDir string) ([]Post, error) {
	return parsePosts(postsDir, true)
}

func parsePosts(postsDir string, includeDrafts bool) ([]Post, error) {
	fsys := os.DirFS(postsDir)

	entries, err := fs.ReadDir(fsys, ".")
	if err != nil {
		return nil, err
	}

	rctx := &markup.RenderContext{
		RendererType: markup.RendererTypeDefault,
	}
	var posts []Post
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
			continue
		}

		data, err := fs.ReadFile(fsys, entry.Name())
		if err != nil {
			return nil, err
		}

		var meta PostMeta
		rest, err := frontmatter.Parse(bytes.NewReader(data), &meta)
		if err != nil {
			return nil, err
		}

		if meta.Draft && !includeDrafts {
			continue
		}

		htmlStr := rctx.RenderMarkdownWith(string(rest), markup.NewMarkdownWith("", textension.Dashes))

		posts = append(posts, Post{
			Meta: meta,
			Body: template.HTML(htmlStr),
		})
	}

	slices.SortFunc(posts, func(a, b Post) int {
		return cmp.Compare(b.Meta.Date, a.Meta.Date)
	})

	return posts, nil
}

func AtomFeed(posts []Post, baseURL string) (string, error) {
	feed := &feeds.Feed{
		Title:   "The Tangled Blog",
		Link:    &feeds.Link{Href: baseURL},
		Author:  &feeds.Author{Name: "Tangled"},
		Created: time.Now(),
	}

	for _, p := range posts {
		postURL := strings.TrimRight(baseURL, "/") + "/" + p.Meta.Slug

		var authorName strings.Builder
		for i, a := range p.Meta.Authors {
			if i > 0 {
				authorName.WriteString(" & ")
			}
			authorName.WriteString(a.Name)
		}

		feed.Items = append(feed.Items, &feeds.Item{
			Title:       p.Meta.Title,
			Link:        &feeds.Link{Href: postURL},
			Description: p.Meta.Subtitle,
			Author:      &feeds.Author{Name: authorName.String()},
			Created:     p.ParsedDate(),
		})
	}

	return feed.ToAtom()
}

// parseLayout builds a template set from the appview's fragments together with
// the blog's own layouts, fragments, and the given page template (relative to
// templatesDir). The result is ready to ExecuteTemplate with "layouts/blogbase".
func parseLayout(p *pages.Pages, templatesDir, page string) (*template.Template, error) {
	fragmentPaths, err := p.FragmentPaths()
	if err != nil {
		return nil, err
	}

	tpl, err := template.New("layouts/blogbase").
		Funcs(p.FuncMap()).
		ParseFS(p.EmbedFS(), fragmentPaths...)
	if err != nil {
		return nil, err
	}

	extraFS := os.DirFS(templatesDir)
	err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() || !strings.HasSuffix(path, ".html") {
			return nil
		}
		// only the requested page, plus shared layouts and fragments
		if path != page && !strings.Contains(path, "fragments/") && !strings.Contains(path, "layouts/") {
			return nil
		}
		data, err := fs.ReadFile(extraFS, path)
		if err != nil {
			return err
		}
		if _, err = tpl.New(path).Parse(string(data)); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	return tpl, nil
}

// RenderIndex renders the blog index page to w.
func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error {
	tpl, err := parseLayout(p, templatesDir, "index.html")
	if err != nil {
		return err
	}
	var featured []Post
	for _, post := range posts {
		if post.Meta.Image != "" {
			featured = append(featured, post)
			if len(featured) == 3 {
				break
			}
		}
	}
	return tpl.ExecuteTemplate(w, "layouts/blogbase", indexParams{Posts: posts, Featured: featured})
}

// RenderPost renders a single blog post page to w.
func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error {
	tpl, err := parseLayout(p, templatesDir, "post.html")
	if err != nil {
		return err
	}
	return tpl.ExecuteTemplate(w, "layouts/blogbase", postParams{Post: post})
}
