package blog

import (
	"bytes"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/yuin/goldmark"
)

type Post struct {
	Slug  string
	Title string
	Date  time.Time
	HTML  string
}

type Store struct {
	posts []Post
	index map[string]int
}

func NewStore() *Store {
	return &Store{index: make(map[string]int)}
}

func (s *Store) Load(dir string) error {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return err
	}

	s.posts = s.posts[:0]
	s.index = make(map[string]int)

	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
			continue
		}

		path := filepath.Join(dir, e.Name())
		raw, err := os.ReadFile(path)
		if err != nil {
			continue
		}

		slug := strings.TrimSuffix(e.Name(), ".md")
		title, body, date := parseFrontMatter(raw, slug, path)

		var buf bytes.Buffer
		if err := goldmark.Convert(body, &buf); err != nil {
			continue
		}

		s.index[slug] = len(s.posts)
		s.posts = append(s.posts, Post{
			Slug:  slug,
			Title: title,
			Date:  date,
			HTML:  buf.String(),
		})
	}

	// sort newest first
	sort.Slice(s.posts, func(i, j int) bool {
		return s.posts[i].Date.After(s.posts[j].Date)
	})
	for i, p := range s.posts {
		s.index[p.Slug] = i
	}

	return nil
}

func (s *Store) All() []Post {
	return s.posts
}

func (s *Store) Get(slug string) (Post, bool) {
	i, ok := s.index[slug]
	if !ok {
		return Post{}, false
	}
	return s.posts[i], true
}

func (s *Store) Count() int {
	return len(s.posts)
}

var h1Re = regexp.MustCompile(`(?m)^#\s+(.+)$`)

// Date formats we recognize on the line after the title
var dateFormats = []string{
	"2006-01-02",
	"January 2, 2006",
	"Jan 2, 2006",
	"02/01/2006",
}

// parseFrontMatter extracts the title from the first `# heading`,
// strips that line from the body, and tries to parse a date from
// the line immediately after. Falls back to file mod time.
func parseFrontMatter(raw []byte, slug, path string) (title string, body []byte, date time.Time) {
	body = raw

	// Extract title
	m := h1Re.FindSubmatch(raw)
	if len(m) >= 2 {
		title = strings.TrimSpace(string(m[1]))
	} else {
		title = slug
	}

	// Strip the first `# title` line from body so it's not rendered twice
	if loc := h1Re.FindIndex(raw); loc != nil {
		// Find end of that line
		lineEnd := loc[1]
		// advance past newline
		for lineEnd < len(raw) && (raw[lineEnd] == '\n' || raw[lineEnd] == '\r') {
			lineEnd++
		}
		body = raw[lineEnd:]
	}

	// Try to parse date from first remaining line
	date = fileModTime(path)
	rest := strings.TrimSpace(string(body))
	if rest != "" {
		firstLine := rest
		if idx := strings.IndexByte(rest, '\n'); idx >= 0 {
			firstLine = strings.TrimSpace(rest[:idx])
		}
		for _, fmt := range dateFormats {
			if t, err := time.Parse(fmt, firstLine); err == nil {
				date = t
				// strip the date line from body too
				if idx := strings.IndexByte(string(body), '\n'); idx >= 0 {
					body = body[idx+1:]
				} else {
					body = nil
				}
				break
			}
		}
	}

	return title, body, date
}

func fileModTime(path string) time.Time {
	info, err := os.Stat(path)
	if err != nil {
		return time.Now()
	}
	return info.ModTime()
}
