package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"go/parser"
	"go/token"
	"io"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/gohugoio/hugo/parser/pageparser"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/codersdk"
)

const (
	examplesDir = "examples"
	examplesSrc = "examples.go"
)

func main() {
	lint := flag.Bool("lint", false, "Lint **all** the examples instead of generating the examples.gen.json file")
	flag.Parse()

	if err := run(*lint); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "error: %+v\n", err)
		os.Exit(1)
	}
}

//nolint:revive // This is a script, not a library.
func run(lint bool) error {
	fset := token.NewFileSet()
	src, err := parser.ParseFile(fset, filepath.Join(examplesDir, examplesSrc), nil, parser.ParseComments)
	if err != nil {
		return err
	}

	projectFS := os.DirFS(".")
	examplesFS := os.DirFS(examplesDir)

	var paths []string
	if lint {
		files, err := fs.ReadDir(examplesFS, "templates")
		if err != nil {
			return err
		}

		for _, f := range files {
			if !f.IsDir() {
				continue
			}
			paths = append(paths, filepath.Join("templates", f.Name()))
		}
	} else {
		for _, comment := range src.Comments {
			for _, line := range comment.List {
				if s, ok := parseEmbedTag(line.Text); ok && !strings.HasSuffix(s, ".json") {
					paths = append(paths, s)
				}
			}
		}
	}

	var examples []codersdk.TemplateExample
	var errs []error
	for _, name := range paths {
		te, err := parseTemplateExample(projectFS, examplesFS, name)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		if te != nil {
			examples = append(examples, *te)
		}
	}

	if len(errs) > 0 {
		return xerrors.Errorf("parse failed: %w", errors.Join(errs...))
	}

	var w io.Writer = os.Stdout
	if lint {
		w = io.Discard
	}

	_, err = fmt.Fprint(w, "// Code generated by examplegen. DO NOT EDIT.\n")
	if err != nil {
		return err
	}

	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")
	return enc.Encode(examples)
}

func parseTemplateExample(projectFS, examplesFS fs.FS, name string) (te *codersdk.TemplateExample, err error) {
	var errs []error
	defer func() {
		if err != nil {
			errs = append([]error{err}, errs...)
		}
		if len(errs) > 0 {
			err = xerrors.Errorf("example %q has errors", name)
			for _, e := range errs {
				err = errors.Join(err, e)
			}
		}
	}()

	dir, err := fs.Stat(examplesFS, name)
	if err != nil {
		return nil, err
	}
	if !dir.IsDir() {
		//nolint:nilnil // This is a script, not a library.
		return nil, nil
	}

	exampleID := dir.Name()
	// Each one of these is a example!
	readme, err := fs.ReadFile(examplesFS, path.Join(name, "README.md"))
	if err != nil {
		return nil, xerrors.New("missing README.md")
	}

	frontMatter, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(readme))
	if err != nil {
		return nil, xerrors.Errorf("parse front matter: %w", err)
	}

	// Make sure validation here is in sync with requirements for
	// coder/registry.
	displayName, err := getString(frontMatter.FrontMatter, "display_name")
	if err != nil {
		errs = append(errs, err)
	}

	description, err := getString(frontMatter.FrontMatter, "description")
	if err != nil {
		errs = append(errs, err)
	}

	_, err = getString(frontMatter.FrontMatter, "maintainer_github")
	if err != nil {
		errs = append(errs, err)
	}

	tags := []string{}
	tagsRaw, exists := frontMatter.FrontMatter["tags"]
	if exists {
		tagsI, valid := tagsRaw.([]interface{})
		if !valid {
			errs = append(errs, xerrors.Errorf("tags isn't a slice: type %T", tagsRaw))
		} else {
			for _, tagI := range tagsI {
				tag, valid := tagI.(string)
				if !valid {
					errs = append(errs, xerrors.Errorf("tag isn't a string: type %T", tagI))
					continue
				}
				tags = append(tags, tag)
			}
		}
	}

	var icon string
	icon, err = getString(frontMatter.FrontMatter, "icon")
	if err != nil {
		errs = append(errs, err)
	} else {
		cleanPath := filepath.Clean(filepath.Join(examplesDir, name, icon))
		_, err := fs.Stat(projectFS, cleanPath)
		if err != nil {
			errs = append(errs, xerrors.Errorf("icon does not exist: %w", err))
		}
		if !strings.HasPrefix(cleanPath, filepath.Join("site", "static")) {
			errs = append(errs, xerrors.Errorf("icon is not in site/static/: %q", icon))
		}
		icon, err = filepath.Rel(filepath.Join("site", "static"), cleanPath)
		if err != nil {
			errs = append(errs, xerrors.Errorf("cannot make icon relative to site/static: %w", err))
		}
	}

	if len(errs) > 0 {
		return nil, xerrors.New("front matter validation failed")
	}

	return &codersdk.TemplateExample{
		ID:          exampleID,
		Name:        displayName,
		Description: description,
		Icon:        "/" + icon, // The FE needs a static path!
		Tags:        tags,
		Markdown:    string(frontMatter.Content),

		// URL is set by examples/examples.go.
	}, nil
}

func getString(m map[string]any, key string) (string, error) {
	v, ok := m[key]
	if !ok {
		return "", xerrors.Errorf("front matter does not contain %q", key)
	}
	vv, ok := v.(string)
	if !ok {
		return "", xerrors.Errorf("%q isn't a string", key)
	}
	return vv, nil
}

func parseEmbedTag(s string) (string, bool) {
	if !strings.HasPrefix(s, "//go:embed") {
		return "", false
	}
	return strings.TrimSpace(strings.TrimPrefix(s, "//go:embed")), true
}
