package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"sort"
	"strings"

	"github.com/aymerick/raymond"
	"github.com/mitchellh/mapstructure"
	"github.com/yuin/goldmark"
	highlighting "github.com/yuin/goldmark-highlighting"
	meta "github.com/yuin/goldmark-meta"
	"github.com/yuin/goldmark/extension"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/renderer/html"
)

// Metadata stores the data about a post that needs to be visible
// at the home page.
type Metadata struct {
	Title   string
	Summary string
	Time    int64 // unix timestamp
}

// Post stores the contents of a blog post.
type Post struct {
	Slug     string
	Metadata Metadata
	Contents string
	Image    []byte
}

func newPost(slug string) (*Post, error) {
	data, err := os.ReadFile("posts/" + slug + ".md")
	if err != nil {
		return nil, fmt.Errorf("could not read file: %s", err)
	}

	md := goldmark.New(
		goldmark.WithExtensions(
			extension.Linkify,
			extension.Strikethrough,
			extension.Typographer,
			meta.Meta,
			highlighting.Highlighting,
		),
		goldmark.WithRendererOptions(
			html.WithUnsafe(),
		),
	)
	var converted bytes.Buffer
	ctx := parser.NewContext()
	err = md.Convert(data, &converted, parser.WithContext(ctx))
	if err != nil {
		return nil, fmt.Errorf("could not parse markdown: %s", err)
	}
	mdMap, err := meta.TryGet(ctx)
	if err != nil {
		return nil, fmt.Errorf("could not parse metadata: %s", err)
	}
	var metadata Metadata
	err = mapstructure.Decode(mdMap, &metadata)
	if err != nil {
		return nil, fmt.Errorf("could not destructure metadata: %s", err)
	}

	post := &Post{
		Slug:     slug,
		Metadata: metadata,
		Contents: converted.String(),
	}

	url := blogURL + "/" + slug
	var buf bytes.Buffer
	err = createImage(post.Metadata.Title, post.Metadata.Summary, url, &buf)
	if err != nil {
		return nil, fmt.Errorf("could not create post image: %v", err)
	}
	post.Image, err = io.ReadAll(&buf)
	if err != nil {
		return nil, err
	}

	return post, nil
}

func (p *Post) render(tpl *raymond.Template) (string, error) {
	return tpl.Exec(p)
}

func (p *Post) String() string {
	return p.Slug
}

type postList []*Post

func newPostList() (postList, error) {
	files, err := os.ReadDir("posts/")
	if err != nil {
		return nil, err
	}

	pl := make(postList, 0, len(files))
	for _, f := range files {
		filename := f.Name()

		if strings.HasSuffix(filename, ".md") {
			post, err := newPost(strings.TrimSuffix(filename, ".md"))
			if err != nil {
				return nil, fmt.Errorf("could not render %s: %s", filename, err)
			}
			pl = append(pl, post)
			log.Printf("Loaded post %s", filename)
		}
	}
	sort.Sort(pl)

	return pl, nil
}

func insertOrUpdatePost(pl postList, p *Post) postList {
	for i, post := range pl {
		if post.Slug == p.Slug {
			pl[i] = p
			sort.Sort(pl)
			return pl
		}
	}
	pl = append(pl, p)
	sort.Sort(pl)
	return pl
}

func removePost(pl postList, slug string) postList {
	for i, post := range pl {
		if post.Slug == slug {
			pl = append(pl[:i], pl[i+1:]...)
			break
		}
	}
	fmt.Println(pl)
	return pl
}

// Len implements sort.Interface
func (pl postList) Len() int {
	return len(pl)
}

// Less implements sort.Interface
func (pl postList) Less(i, j int) bool {
	return pl[i].Metadata.Time > pl[j].Metadata.Time
}

// Swap implements sort.Interface
func (pl postList) Swap(i, j int) {
	temp := pl[i]
	pl[i] = pl[j]
	pl[j] = temp
}

func newPostListener(update func(func(postList) postList)) *listener {
	ln := &listener{
		folder: "posts/",
		update: func(file string) error {
			post, err := newPost(strings.TrimSuffix(file, ".md"))
			if err != nil {
				return err
			}
			update(func(oldList postList) postList {
				return insertOrUpdatePost(oldList, post)
			})
			return nil
		},
		clean: func(file string) error {
			update(func(oldList postList) postList {
				return removePost(oldList, strings.TrimSuffix(file, ".md"))
			})
			return nil
		},
	}
	return ln
}