package admin

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
)

type Exporter struct {
	postsDir string
}

type postFrontmatter struct {
	ID          string   `yaml:"id"`
	Slug        string   `yaml:"slug"`
	Title       string   `yaml:"title"`
	Summary     string   `yaml:"summary"`
	Status      string   `yaml:"status"`
	Tags        []string `yaml:"tags"`
	Cover       string   `yaml:"cover"`
	Version     int      `yaml:"version"`
	SlugSource  string   `yaml:"slug_source"`
	SlugLocked  bool     `yaml:"slug_locked"`
	PublishedAt string   `yaml:"published_at"`
	CreatedAt   string   `yaml:"created_at"`
	UpdatedAt   string   `yaml:"updated_at"`
}

func NewExporter(postsDir string) *Exporter {
	return &Exporter{postsDir: postsDir}
}

func (e *Exporter) ExportPublishedPosts(ctx context.Context, posts []Post) error {
	if err := os.MkdirAll(e.postsDir, 0o755); err != nil {
		return fmt.Errorf("create posts dir: %w", err)
	}

	publishedFiles := make(map[string]bool, len(posts))
	for _, post := range posts {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		publishedFiles[post.Slug+".md"] = true
		if err := e.writePost(post); err != nil {
			return err
		}
	}
	if err := e.removeStalePosts(publishedFiles); err != nil {
		return err
	}
	return nil
}

func (e *Exporter) removeStalePosts(publishedFiles map[string]bool) error {
	entries, err := os.ReadDir(e.postsDir)
	if err != nil {
		return fmt.Errorf("read posts dir: %w", err)
	}

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
			continue
		}
		if publishedFiles[entry.Name()] {
			continue
		}
		if err := os.Remove(filepath.Join(e.postsDir, entry.Name())); err != nil {
			return fmt.Errorf("remove stale post %s: %w", entry.Name(), err)
		}
	}
	return nil
}

func (e *Exporter) writePost(post Post) error {
	path := filepath.Join(e.postsDir, post.Slug+".md")
	frontmatter := postFrontmatter{
		ID:          post.ID,
		Slug:        post.Slug,
		Title:       post.Title,
		Summary:     post.Summary,
		Status:      string(post.Status),
		Tags:        post.Tags,
		Cover:       post.Cover,
		Version:     post.Version,
		SlugSource:  post.SlugSource,
		SlugLocked:  post.SlugLocked,
		PublishedAt: formatFrontmatterTime(post.PublishedAt),
		CreatedAt:   formatFrontmatterTime(&post.CreatedAt),
		UpdatedAt:   formatFrontmatterTime(&post.UpdatedAt),
	}

	var meta bytes.Buffer
	encoder := yaml.NewEncoder(&meta)
	encoder.SetIndent(2)
	if err := encoder.Encode(frontmatter); err != nil {
		return fmt.Errorf("encode frontmatter: %w", err)
	}
	if err := encoder.Close(); err != nil {
		return fmt.Errorf("close frontmatter encoder: %w", err)
	}

	var output bytes.Buffer
	output.WriteString("---\n")
	output.Write(meta.Bytes())
	output.WriteString("---\n\n")
	output.WriteString(strings.TrimLeft(post.BodyMarkdown, "\n"))
	if !strings.HasSuffix(output.String(), "\n") {
		output.WriteByte('\n')
	}

	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, output.Bytes(), 0o644); err != nil {
		return fmt.Errorf("write post %s: %w", post.Slug, err)
	}
	if err := os.Rename(tmp, path); err != nil {
		return fmt.Errorf("replace post %s: %w", post.Slug, err)
	}
	return nil
}

func formatFrontmatterTime(value *time.Time) string {
	if value == nil {
		return ""
	}
	return value.Format(time.RFC3339)
}
