package main

import (
	"bytes"
	"flag"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"log"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/russross/blackfriday/v2"
)

var (
	contentDir  = flag.String("content-dir", "content", "path to the content directory")
	staticDir   = flag.String("static-dir", "static", "path to the static files")
	templateDir = flag.String("template-dir", "templates", "path to the template directory")
	outputDir   = flag.String("output-dir", "output", "path to output all files from the render process")
	listen      = flag.String("listen", "", "When provided with a listen port, start serving the content")
)

type (
	Metadata struct {
		URLPath  string
		FilePath string
		Template string
		Title    string
		Date     time.Time
		Author   string
		Draft    bool
	}
)

func main() {
	flag.Parse()
	var err error

	tmplDirFS := os.DirFS(*templateDir)
	templates := template.New("")
	templates = templates.Funcs(template.FuncMap(
		map[string]interface{}{
			"formatTime": func(t time.Time) string {
				return t.Format("2006-01-02")
			},
		},
	))
	templates, err = templates.ParseFS(tmplDirFS, "*")
	if err != nil {
		log.Fatalf("could not parse template files: %s", err)
	}

	content := []Metadata{}
	if err := filepath.Walk(*contentDir, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		m, err := MetadataFromFile(*contentDir, path)
		if err != nil {
			return fmt.Errorf("could not parse metadata from '%s': %w", path, err)
		}
		content = append(content, m)
		return nil
	}); err != nil {
		log.Fatalf("could not read content: %s", err)
	}

	if *outputDir != "" {
		if err := os.MkdirAll(*outputDir, 0755); err != nil && !os.IsExist(err) {
			log.Fatalf("could not create directory '%s': %s", *outputDir, err)
		}

		dirsCreated := map[string]bool{}
		for _, m := range content {
			filePath := path.Join(*outputDir, m.URLPath)
			dir := path.Dir(filePath)
			_, found := dirsCreated[dir]
			if !found {
				if err := os.MkdirAll(dir, 0755); err != nil {
					log.Fatalf("could not create directory '%s': %s", dir, err)
				}
				dirsCreated[dir] = true
			}

			f, err := os.Create(filePath)
			if err != nil {
				log.Fatalf("could not open file '%s': %s", filePath, err)
			}
			defer f.Close()
			if err := m.Render(f, templates); err != nil {
				log.Printf("could not render '%s' into file '%s': %s", m.FilePath, filePath, err)
			}

			f.Close()
		}

		staticFiles, err := filepath.Glob(*staticDir + "/**")
		if err != nil {
			log.Fatalf("could not get all static files from '%s': %s", *staticDir, err)
		}
		buf := []byte{}
		for _, fileName := range staticFiles {
			filePath := path.Join(*outputDir, fileName)
			dir := path.Dir(filePath)
			isDir := false
			if info, err := os.Stat(fileName); err != nil {
				log.Fatalf("could not stat file '%s': %s", fileName, err)
			} else {
				isDir = info.IsDir()
				if isDir {
					dir = filePath
				}
			}
			if _, found := dirsCreated[dir]; !found {
				if err := os.MkdirAll(dir, 0755); err != nil {
					log.Fatalf("could not create directory '%s': %s", dir, err)
				}
			}
			if isDir {
				continue
			}
			buf, err = os.ReadFile(fileName)
			if err != nil {
				log.Fatalf("could not read static file '%s': %s", fileName, err)
			}
			if err := os.WriteFile(path.Join(*outputDir, fileName), buf, 0755); err != nil {
				log.Fatalf("could not write static file '%s': %s", fileName, err)
			}
		}
	}

	if *listen != "" {
		http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(*staticDir))))
		for _, metadata := range content {
			func(m Metadata) {
				http.HandleFunc(m.URLPath, func(w http.ResponseWriter, r *http.Request) {
					log.Printf("%s -> %s", r.URL, m.URLPath)
					w.Header()["Content-Type"] = []string{"text/html"}
					if err := m.Render(w, templates); err != nil {
						log.Printf("could not render '%s': %s", m.FilePath, err)
					}
				})
			}(metadata)
		}
		log.Fatalf("stopped listening: %s", http.ListenAndServe(*listen, nil))
	}
}

var (
	metadataStart = []byte("+++\n")
	metadataEnd   = []byte("\n+++\n")

	headerTitle    = "title"
	headerDate     = "date"
	headerAuthor   = "author"
	headerURLPath  = "url"
	headerDraft    = "draft"
	headerTemplate = "template"
)

// ContentFromFile reads the header of the file to create the metadata.
//
// basePath is stripped from the path when generating the default URL path.
func MetadataFromFile(basePath string, path string) (Metadata, error) {
	urlPath := path[len(basePath):]
	if strings.HasSuffix(urlPath, ".md") {
		urlPath = strings.TrimSuffix(urlPath, ".md") + ".html"
	}
	m := Metadata{
		FilePath: path,
		URLPath:  urlPath,
		Template: "content.html",
	}
	raw, err := os.ReadFile(m.FilePath)
	if err != nil {
		return m, err
	}
	if !bytes.HasPrefix(raw, metadataStart) {
		return m, fmt.Errorf("missing metadata header, must start with +++")
	}
	last := bytes.Index(raw, metadataEnd)
	if last == -1 {
		return m, fmt.Errorf("missing metadata header, must end with +++ on a single line")
	}
	rawHeader := raw[len(metadataStart):last]
	lineNum := 0
	for _, headerLine := range bytes.Split(rawHeader, []byte("\n")) {
		if len(headerLine) == 0 {
			continue
		}
		line := bytes.SplitN(headerLine, []byte("="), 2)
		if len(line) != 2 {
			return m, fmt.Errorf("line %d: format must be 'key = value'", lineNum)
		}
		key := string(bytes.Trim(line[0], " "))
		val := string(bytes.Trim(line[1], ` "'`))
		switch string(key) {
		case headerTitle:
			m.Title = val
		case headerAuthor:
			m.Author = val
		case headerDraft:
			if headerDraft == "true" {
				m.Draft = true
			}
		case headerTemplate:
			m.Template = val
		case headerDate:
			m.Date, err = time.Parse(time.RFC3339, val)
			if err != nil {
				log.Printf("line %d: date must match RFC3339 format", lineNum)
			}
		case headerURLPath:
			m.URLPath = val
		default:
			log.Printf("line %d: unknown header %s found in %s", lineNum, key, path)
		}
		lineNum += 1
	}

	return m, nil
}

func (m Metadata) Content() template.HTML {
	result := ""
	raw, err := os.ReadFile(m.FilePath)
	if err != nil {
		log.Printf("error reading file: %w", err)
		return template.HTML("")
	}

	end := bytes.Index(raw, metadataEnd)
	if end == -1 {
		log.Printf("could not find metadata end")
		return template.HTML("")
	}

	result = string(blackfriday.Run(raw[end+len(metadataEnd):]))
	return template.HTML(result)
}

func (m Metadata) Render(w io.Writer, tmpl *template.Template) error {
	if err := tmpl.ExecuteTemplate(w, m.Template, m); err != nil {
		return fmt.Errorf("could not render content path '%s': %w", m.FilePath, err)
	}
	return nil
}
