package cms

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

func FindRoot(start string) (string, error) {
	abs, err := filepath.Abs(start)
	if err != nil {
		return "", err
	}
	info, err := os.Stat(abs)
	if err != nil {
		return "", err
	}
	if !info.IsDir() {
		abs = filepath.Dir(abs)
	}

	for {
		if _, err := os.Stat(filepath.Join(abs, "capsule.toml")); err == nil {
			return abs, nil
		}
		parent := filepath.Dir(abs)
		if parent == abs {
			return "", errors.New("not inside a gemcms capsule; run gemcms create site first")
		}
		abs = parent
	}
}

func CreateSite(parent string, opts SiteOptions) (string, error) {
	name := slugify(opts.Name)
	if name == "" || name == "untitled" {
		return "", errors.New("site name is required")
	}
	root, err := filepath.Abs(filepath.Join(parent, name))
	if err != nil {
		return "", err
	}
	if _, err := os.Stat(root); err == nil {
		return "", fmt.Errorf("%s already exists", root)
	}

	title := opts.Title
	if title == "" {
		title = titleize(name)
	}
	language := opts.Language
	if language == "" {
		language = "en"
	}
	cfg := Config{
		Title:    title,
		Host:     opts.Host,
		Author:   opts.Author,
		Language: language,
		Theme:    "default",
	}

	dirs := []string{
		"content/pages",
		"content/articles",
		"assets",
		"site/menus",
		"site/sections",
		"site/widgets",
		"public",
	}
	for _, dir := range dirs {
		if err := os.MkdirAll(filepath.Join(root, dir), 0o755); err != nil {
			return "", err
		}
	}

	if err := writeNewFile(filepath.Join(root, "capsule.toml"), []byte(renderConfig(cfg))); err != nil {
		return "", err
	}
	if _, err := CreateMenu(root, "top"); err != nil {
		return "", err
	}
	if err := AddMenuItem(root, MenuItemOptions{Menu: "top", Kind: "home"}); err != nil {
		return "", err
	}
	if _, err := CreateWidget(root, WidgetOptions{Name: "recent-articles", Type: "recent", Section: "articles", Limit: 5}); err != nil {
		return "", err
	}

	index := Document{
		Type:  "page",
		Slug:  "index",
		Title: title,
		Body: strings.TrimSpace(`# `+title+`

{{ menu top }}

## Latest articles

{{ widget recent-articles }}
`) + "\n",
	}
	if err := writeDocument(filepath.Join(root, "content/pages/index.gmi"), index); err != nil {
		return "", err
	}

	return root, nil
}

func CreatePage(root string, opts PageOptions) (Document, error) {
	slug := slugify(opts.Slug)
	if slug == "" || slug == "untitled" {
		return Document{}, errors.New("page slug is required")
	}
	title := opts.Title
	if title == "" {
		title = titleize(slug)
	}
	doc := Document{
		Type:  "page",
		Slug:  slug,
		Title: title,
		Body: strings.TrimSpace(`{{ menu top }}

# `+title+`

Write this page.
`) + "\n",
	}
	path := filepath.Join(root, "content/pages", slug+".gmi")
	if err := writeDocument(path, doc); err != nil {
		return Document{}, err
	}
	doc.SourcePath = path
	if opts.Menu != "" {
		if err := AddMenuItem(root, MenuItemOptions{Menu: opts.Menu, Kind: "page", Target: slug, Label: title}); err != nil {
			return Document{}, err
		}
	}
	return doc, nil
}

func CreateArticle(root string, opts ArticleOptions) (Document, error) {
	title := strings.TrimSpace(opts.Title)
	if title == "" {
		return Document{}, errors.New("article title is required")
	}
	section := slugify(opts.Section)
	if section == "" || section == "untitled" {
		section = "articles"
	}
	slug := slugify(title)
	date := time.Now().Format("2006-01-02")
	doc := Document{
		Type:    "article",
		Slug:    slug,
		Title:   title,
		Date:    date,
		Section: section,
		Tags:    opts.Tags,
		Draft:   opts.Draft,
		Body: strings.TrimSpace(`{{ menu top }}

# `+title+`

Write this article.
`) + "\n",
	}

	path := filepath.Join(root, "content/articles", date+"-"+slug+".gmi")
	path = nextAvailablePath(path)
	if err := writeDocument(path, doc); err != nil {
		return Document{}, err
	}
	doc.SourcePath = path
	if opts.Menu != "" && !doc.Draft {
		if err := AddMenuItem(root, MenuItemOptions{Menu: opts.Menu, Kind: "custom", Label: title, URL: docURL(doc)}); err != nil {
			return Document{}, err
		}
	}
	return doc, nil
}

func AddFile(root string, opts AddOptions) (Document, error) {
	sourcePath, err := expandPath(opts.Path)
	if err != nil {
		return Document{}, err
	}
	data, err := os.ReadFile(sourcePath)
	if err != nil {
		return Document{}, err
	}

	meta, body, hasMeta := splitFrontmatter(string(data))
	doc := Document{
		Type: opts.Type,
		Body: strings.TrimLeft(body, "\n"),
	}
	if hasMeta {
		table, err := parseMetadata(meta)
		if err != nil {
			return Document{}, fmt.Errorf("%s: parse frontmatter: %w", sourcePath, err)
		}
		if doc.Type == "" {
			doc.Type = table.values["type"]
		}
		doc.Title = table.values["title"]
		doc.Slug = table.values["slug"]
		doc.Date = table.values["date"]
		doc.Section = table.values["section"]
		doc.Tags = table.lists["tags"]
		doc.Draft = table.values["draft"] == "true"
	}

	ext := strings.ToLower(filepath.Ext(sourcePath))
	switch ext {
	case ".md", ".markdown":
		doc.Body = markdownToGemtext(doc.Body)
	case ".gmi", ".gemini":
	default:
		return Document{}, fmt.Errorf("unsupported input extension %q; use .md or .gmi", ext)
	}

	if opts.Type != "" {
		doc.Type = opts.Type
	}
	if doc.Type == "" {
		doc.Type = "article"
	}
	if doc.Type != "article" && doc.Type != "page" {
		return Document{}, fmt.Errorf("unsupported content type %q", doc.Type)
	}
	if opts.Title != "" {
		doc.Title = opts.Title
	}
	if doc.Title == "" {
		doc.Title = inferTitle(doc.Body, inferSlug(sourcePath))
	}
	if opts.Slug != "" {
		doc.Slug = slugify(opts.Slug)
	}
	if doc.Slug == "" {
		doc.Slug = slugify(doc.Title)
	}
	if len(opts.Tags) > 0 {
		doc.Tags = opts.Tags
	}
	if opts.Draft {
		doc.Draft = true
	}

	var dest string
	if doc.Type == "page" {
		dest = filepath.Join(root, "content/pages", doc.Slug+".gmi")
		dest = nextAvailablePath(dest)
	} else {
		if opts.Section != "" {
			doc.Section = slugify(opts.Section)
		}
		if doc.Section == "" || doc.Section == "untitled" {
			doc.Section = "articles"
		}
		if doc.Date == "" {
			doc.Date = time.Now().Format("2006-01-02")
		}
		dest = filepath.Join(root, "content/articles", doc.Date+"-"+doc.Slug+".gmi")
		dest = nextAvailablePath(dest)
	}
	if err := writeDocument(dest, doc); err != nil {
		return Document{}, err
	}
	doc.SourcePath = dest
	if opts.Menu != "" && !doc.Draft {
		if err := AddMenuItem(root, MenuItemOptions{Menu: opts.Menu, Kind: "custom", Label: doc.Title, URL: docURL(doc)}); err != nil {
			return Document{}, err
		}
	}
	return doc, nil
}

func CreateSection(root string, opts SectionOptions) (string, error) {
	name := slugify(opts.Name)
	if name == "" || name == "untitled" {
		return "", errors.New("section name is required")
	}
	title := opts.Title
	if title == "" {
		title = titleize(name)
	}
	path := filepath.Join(root, "site/sections", name+".toml")
	data := "name = " + formatString(name) + "\n" +
		"title = " + formatString(title) + "\n"
	return path, writeNewFile(path, []byte(data))
}

func LoadProject(root string) (Project, error) {
	cfg, err := LoadConfig(root)
	if err != nil {
		return Project{}, err
	}
	docs, err := LoadDocuments(root)
	if err != nil {
		return Project{}, err
	}
	menus, err := LoadMenus(root)
	if err != nil {
		return Project{}, err
	}
	sections, err := LoadSections(root)
	if err != nil {
		return Project{}, err
	}
	widgets, err := LoadWidgets(root)
	if err != nil {
		return Project{}, err
	}
	return Project{
		Root:     root,
		Config:   cfg,
		Docs:     docs,
		Menus:    menus,
		Sections: sections,
		Widgets:  widgets,
	}, nil
}

func List(root, kind string) ([]string, error) {
	project, err := LoadProject(root)
	if err != nil {
		return nil, err
	}
	var items []string
	switch kind {
	case "pages":
		for _, doc := range project.Docs {
			if doc.Type == "page" {
				items = append(items, doc.Slug+"\t"+doc.Title)
			}
		}
	case "articles":
		for _, doc := range project.Docs {
			if doc.Type == "article" {
				items = append(items, doc.Date+"\t"+doc.Section+"\t"+doc.Slug+"\t"+doc.Title)
			}
		}
	case "menus":
		for name, menu := range project.Menus {
			items = append(items, fmt.Sprintf("%s\t%d items", name, len(menu.Items)))
		}
	case "widgets":
		for name, widget := range project.Widgets {
			items = append(items, fmt.Sprintf("%s\t%s", name, widget.Type))
		}
	default:
		return nil, fmt.Errorf("unknown list target %q", kind)
	}
	sort.Strings(items)
	return items, nil
}

func writeNewFile(path string, data []byte) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	if _, err := os.Stat(path); err == nil {
		return fmt.Errorf("%s already exists", path)
	}
	return os.WriteFile(path, data, 0o644)
}

func writeFile(path string, data []byte) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

func nextAvailablePath(path string) string {
	if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
		return path
	}
	ext := filepath.Ext(path)
	base := strings.TrimSuffix(path, ext)
	for i := 2; ; i++ {
		candidate := fmt.Sprintf("%s-%d%s", base, i, ext)
		if _, err := os.Stat(candidate); errors.Is(err, fs.ErrNotExist) {
			return candidate
		}
	}
}

func expandPath(pathName string) (string, error) {
	if strings.HasPrefix(pathName, "~/") {
		home, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		pathName = filepath.Join(home, strings.TrimPrefix(pathName, "~/"))
	}
	return filepath.Abs(pathName)
}
