package cms

import (
	"fmt"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
)

func Build(root string) (BuildReport, error) {
	project, err := LoadProject(root)
	if err != nil {
		return BuildReport{}, err
	}
	outDir := filepath.Join(root, "public")
	if err := os.RemoveAll(outDir); err != nil {
		return BuildReport{}, err
	}
	if err := os.MkdirAll(outDir, 0o755); err != nil {
		return BuildReport{}, err
	}

	report := BuildReport{OutputDir: outDir}
	if err := copyAssets(filepath.Join(root, "assets"), outDir, &report); err != nil {
		return BuildReport{}, err
	}

	hasIndex := false
	for _, doc := range project.Docs {
		if doc.Draft {
			continue
		}
		if doc.Type == "page" && doc.Slug == "index" {
			hasIndex = true
		}
		rel := outputRel(doc)
		rendered := renderDocument(project, doc)
		if err := writeFile(filepath.Join(outDir, filepath.FromSlash(rel)), []byte(rendered)); err != nil {
			return BuildReport{}, err
		}
		report.FilesWritten++
	}
	if !hasIndex {
		rendered := renderGeneratedIndex(project)
		if err := writeFile(filepath.Join(outDir, "index.gmi"), []byte(rendered)); err != nil {
			return BuildReport{}, err
		}
		report.FilesWritten++
	}
	for section, docs := range articlesBySection(project.Docs) {
		rendered := renderSectionIndex(project, section, docs)
		if err := writeFile(filepath.Join(outDir, section, "index.gmi"), []byte(rendered)); err != nil {
			return BuildReport{}, err
		}
		report.FilesWritten++
	}

	return report, nil
}

func renderDocument(project Project, doc Document) string {
	body := expandBlocks(project, doc.Body)
	return strings.TrimRight(body, "\n") + "\n"
}

func renderGeneratedIndex(project Project) string {
	var b strings.Builder
	b.WriteString("# " + project.Config.Title + "\n\n")
	b.WriteString(renderMenu(project.Menus["top"]))
	b.WriteString("\n## Latest articles\n\n")
	b.WriteString(renderWidget(project, Widget{Name: "recent-articles", Type: "recent", Section: "articles", Limit: 5}))
	return strings.TrimRight(b.String(), "\n") + "\n"
}

func renderSectionIndex(project Project, section string, docs []Document) string {
	title := titleize(section)
	if cfg, ok := project.Sections[section]; ok {
		title = cfg.Title
	}
	var b strings.Builder
	b.WriteString("# " + title + "\n\n")
	if menu, ok := project.Menus["top"]; ok {
		b.WriteString(renderMenu(menu))
		b.WriteString("\n")
	}
	for _, doc := range docs {
		b.WriteString("=> " + docURL(doc) + " " + doc.Date + " " + doc.Title + "\n")
	}
	return strings.TrimRight(b.String(), "\n") + "\n"
}

func expandBlocks(project Project, body string) string {
	var out []string
	for _, line := range strings.Split(body, "\n") {
		name, arg, ok := parseBlock(line)
		if !ok {
			out = append(out, line)
			continue
		}
		switch name {
		case "menu":
			out = append(out, strings.TrimRight(renderMenu(project.Menus[arg]), "\n"))
		case "widget":
			widget, ok := project.Widgets[arg]
			if !ok {
				out = append(out, "Missing widget: "+arg)
				continue
			}
			out = append(out, strings.TrimRight(renderWidget(project, widget), "\n"))
		default:
			out = append(out, line)
		}
	}
	return strings.Join(out, "\n")
}

func parseBlock(line string) (string, string, bool) {
	line = strings.TrimSpace(line)
	if !strings.HasPrefix(line, "{{") || !strings.HasSuffix(line, "}}") {
		return "", "", false
	}
	line = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "{{"), "}}"))
	parts := strings.Fields(line)
	if len(parts) != 2 {
		return "", "", false
	}
	return parts[0], parts[1], true
}

func renderMenu(menu Menu) string {
	if len(menu.Items) == 0 {
		return ""
	}
	var b strings.Builder
	for _, item := range menu.Items {
		b.WriteString("=> " + item.URL + " " + item.Label + "\n")
	}
	return b.String()
}

func renderWidget(project Project, widget Widget) string {
	switch widget.Type {
	case "recent":
		return renderRecentWidget(project, widget)
	default:
		return "Unsupported widget: " + widget.Type + "\n"
	}
}

func renderRecentWidget(project Project, widget Widget) string {
	var docs []Document
	for _, doc := range project.Docs {
		if doc.Type == "article" && !doc.Draft && doc.Section == widget.Section {
			docs = append(docs, doc)
		}
	}
	sortArticles(docs)
	if widget.Limit > 0 && len(docs) > widget.Limit {
		docs = docs[:widget.Limit]
	}
	if len(docs) == 0 {
		return "No articles yet.\n"
	}
	var b strings.Builder
	for _, doc := range docs {
		b.WriteString("=> " + docURL(doc) + " " + doc.Date + " " + doc.Title + "\n")
	}
	return b.String()
}

func articlesBySection(docs []Document) map[string][]Document {
	sections := map[string][]Document{}
	for _, doc := range docs {
		if doc.Type != "article" || doc.Draft {
			continue
		}
		sections[doc.Section] = append(sections[doc.Section], doc)
	}
	for section := range sections {
		sortArticles(sections[section])
	}
	return sections
}

func sortArticles(docs []Document) {
	sort.Slice(docs, func(i, j int) bool {
		if docs[i].Date != docs[j].Date {
			return docs[i].Date > docs[j].Date
		}
		return docs[i].Slug < docs[j].Slug
	})
}

func outputRel(doc Document) string {
	if doc.Type == "page" {
		if doc.Slug == "index" || doc.Slug == "" {
			return "index.gmi"
		}
		return doc.Slug + ".gmi"
	}
	section := doc.Section
	if section == "" {
		section = "articles"
	}
	return path.Join(section, doc.Slug+".gmi")
}

func docURL(doc Document) string {
	rel := outputRel(doc)
	if rel == "index.gmi" {
		return "/"
	}
	return "/" + rel
}

func copyAssets(src, outDir string, report *BuildReport) error {
	if _, err := os.Stat(src); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	return filepath.WalkDir(src, func(pathName string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			return nil
		}
		rel, err := filepath.Rel(src, pathName)
		if err != nil {
			return err
		}
		dest := filepath.Join(outDir, filepath.FromSlash(filepath.ToSlash(rel)))
		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
			return err
		}
		if err := copyFile(pathName, dest); err != nil {
			return err
		}
		report.FilesWritten++
		return nil
	})
}

func copyFile(src, dest string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer out.Close()

	if _, err := io.Copy(out, in); err != nil {
		return err
	}
	if err := out.Close(); err != nil {
		return err
	}
	return nil
}

func debugProject(project Project) string {
	return fmt.Sprintf("%d docs, %d menus, %d widgets", len(project.Docs), len(project.Menus), len(project.Widgets))
}
