package builder

import (
	"fmt"
	"path"
	"path/filepath"
	"slices"
	"time"

	"go.alanpearce.eu/website/internal/config"
	"go.alanpearce.eu/website/internal/content"
	"go.alanpearce.eu/website/internal/sitemap"
	"go.alanpearce.eu/website/internal/storage/files"
	"go.alanpearce.eu/website/templates"
	"go.alanpearce.eu/x/log"

	mapset "github.com/deckarep/golang-set/v2"
	"gitlab.com/tozd/go/errors"
)

type IOConfig struct {
	Source      string `conf:"default:.,short:s,flag:src"`
	Destination string `conf:"default:public,short:d,flag:dest"`
	Development bool   `conf:"default:false,flag:dev"`
}

type Result struct {
	Hashes []string
}

func joinSourcePath(src string) func(string) string {
	return func(rel string) string {
		return filepath.Join(src, rel)
	}
}

func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, error) {
	storage := files.NewWriter(ioConfig.Destination, log, &files.Options{
		Compress: !ioConfig.Development,
	})

	joinSource := joinSourcePath(ioConfig.Source)

	log.Debug("output", "dir", ioConfig.Destination)
	r := &Result{
		Hashes: make([]string, 0),
	}

	err := storage.CopyRecursive(joinSource("static"))
	if err != nil {
		return nil, errors.WithMessage(err, "could not copy static files")
	}

	log.Debug("reading posts")
	posts, tags, err := content.ReadPosts(&content.Config{
		Root:     joinSource("content"),
		InputDir: "post",
	}, log.Named("content"))
	if err != nil {
		return nil, err
	}

	sitemap := sitemap.New(config)
	lastMod := time.Now()
	if len(posts) > 0 {
		lastMod = posts[0].Date
	}

	for _, post := range posts {
		log.Debug("rendering post", "post", post.Basename)
		sitemap.AddPath(post.URL, post.Date)
		if err := storage.RenderToFile(templates.PostPage(config, post), post.Output); err != nil {
			return nil, err
		}
	}

	log.Debug("rendering tags list")
	if err := storage.RenderToFile(
		templates.TagsPage(config, "tags", mapset.Sorted(tags), "/tags"),
		path.Join("tags", "index.html"),
	); err != nil {
		return nil, err
	}
	sitemap.AddPath("/tags/", lastMod)

	for _, tag := range tags.ToSlice() {
		matchingPosts := []content.Post{}
		for _, post := range posts {
			if slices.Contains(post.Taxonomies.Tags, tag) {
				matchingPosts = append(matchingPosts, post)
			}
		}
		log.Debug("rendering tags page", "tag", tag)
		url := "/tags/" + tag
		if err := storage.RenderToFile(
			templates.TagPage(config, tag, matchingPosts, url),
			path.Join("tags", tag, "index.html"),
		); err != nil {
			return nil, err
		}
		sitemap.AddPath(url, matchingPosts[0].Date)

		log.Debug("rendering tags feed", "tag", tag)
		feed, err := renderFeed(
			fmt.Sprintf("%s - %s", config.Title, tag),
			config,
			matchingPosts,
			tag,
		)
		if err != nil {
			return nil, errors.WithMessage(err, "could not render tag feed page")
		}
		if err := storage.WriterToFile(feed, path.Join("tags", tag, "atom.xml")); err != nil {
			return nil, err
		}
	}

	log.Debug("rendering list page")
	if err := storage.RenderToFile(
		templates.ListPage(config, posts, "/post"),
		path.Join("post", "index.html"),
	); err != nil {
		return nil, err
	}
	sitemap.AddPath("/post/", lastMod)

	log.Debug("rendering feed")
	feed, err := renderFeed(config.Title, config, posts, "feed")
	if err != nil {
		return nil, errors.WithMessage(err, "could not render feed")
	}
	if err := storage.WriterToFile(feed, "atom.xml"); err != nil {
		return nil, err
	}

	log.Debug("rendering feed styles")
	feedStyles, err := renderFeedStyles(ioConfig.Source)
	if err != nil {
		return nil, errors.WithMessage(err, "could not render feed styles")
	}
	if err := storage.OutputToFile(feedStyles, "feed-styles.xsl"); err != nil {
		return nil, err
	}
	_, err = feedStyles.Seek(0, 0)
	if err != nil {
		return nil, err
	}
	h, err := getFeedStylesHash(feedStyles)
	if err != nil {
		return nil, err
	}
	r.Hashes = append(r.Hashes, h)

	log.Debug("rendering homepage")
	_, text, err := content.GetPost(joinSource(filepath.Join("content", "index.md")))
	if err != nil {
		return nil, err
	}
	content, err := content.RenderMarkdown(text)
	if err != nil {
		return nil, err
	}
	if err := storage.RenderToFile(templates.Homepage(config, posts, content), "index.html"); err != nil {
		return nil, err
	}
	// it would be nice to set LastMod here, but using the latest post
	// date would be wrong as the homepage has its own content file
	// without a date, which could be newer
	sitemap.AddPath("/", time.Time{})
	f, err := storage.Open("index.html")
	if err != nil {
		return nil, err
	}
	defer f.Close()
	h, _ = getHTMLStyleHash(f)
	r.Hashes = append(r.Hashes, h)

	log.Debug("rendering sitemap")
	if err := storage.WriterToFile(sitemap, "sitemap.xml"); err != nil {
		return nil, err
	}

	log.Debug("rendering robots.txt")
	rob, err := renderRobotsTXT(ioConfig.Source, config)
	if err != nil {
		return nil, err
	}
	if err := storage.OutputToFile(rob, "robots.txt"); err != nil {
		return nil, err
	}

	return r, nil
}

func BuildSite(ioConfig *IOConfig, cfg *config.Config, log *log.Logger) (*Result, error) {
	if cfg == nil {
		return nil, errors.New("config is nil")
	}
	cfg.InjectLiveReload = ioConfig.Development

	templates.Setup()
	loadCSS(ioConfig.Source)

	return build(ioConfig, cfg, log)
}