// Package generator downloads an updated version of the PSL list and compiles it into go code.
//
// It is meant to be used by maintainers in conjunction with the go generate tool
// to update the list.
package generator

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"go/format"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"text/template"
	"time"

	"github.com/weppos/publicsuffix-go/publicsuffix"
)

const (
	list = `// This file is automatically generated
// Run "go run cmd/gen/gen.go" to update the list.

package publicsuffix

const ListVersion = "PSL version {{.VersionSHA}} ({{.VersionDate}})"

func DefaultRules() [{{len .Rules}}]Rule {
	return r
}

var r = [{{len .Rules}}]Rule{
	{{range $r := .Rules}} \
	{ {{$r.Type}}, "{{$r.Value}}", {{$r.Length}}, {{$r.Private}} },
	{{end}}
}

func init() {
	for i := range r {
		DefaultList.AddRule(&r[i])
	}
}

`
)

var listTmpl = template.Must(template.New("list").Parse(cont(list)))

// https://github.com/golang/go/issues/9969
// Requires go1.6
func cont(s string) string {
	return strings.Replace(s, "\\\n", "", -1)
}

type headInfo struct {
	SHA      string
	Datetime time.Time
}

type githubNodes struct {
	SHA    string       `json:"sha"`
	Commit githubCommit `json:"commit"`
}

type githubCommit struct {
	Commiter githubCommitter `json:"committer"`
}

type githubCommitter struct {
	Date time.Time `json:"date"`
}

func extractHeadInfo(ctx context.Context) (*headInfo, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/publicsuffix/list/commits", nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequestWithContext: %w", err)
	}
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("X-Github-Api-Version", "2022-11-28")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http.DefaultClient.Do: %w", err)
	}
	defer resp.Body.Close()

	respString, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("io.ReadAll: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d %s", resp.StatusCode, respString)
	}

	var respBody []githubNodes
	err = json.Unmarshal(respString, &respBody)
	if err != nil {
		return nil, fmt.Errorf("json.Unmarshal %s: %w", respBody, err)
	}

	if len(respBody) == 0 {
		return nil, fmt.Errorf("no nodes found")
	}

	return &headInfo{
		SHA:      respBody[0].SHA,
		Datetime: respBody[0].Commit.Commiter.Date,
	}, nil
}

// Generator represents a generator.
type Generator struct {
	Verbose bool
}

// NewGenerator creates a Generator with default settings.
func NewGenerator() *Generator {
	g := &Generator{
		Verbose: false,
	}
	return g
}

// Write ...
func (g *Generator) Write(ctx context.Context, filename string) error {
	content, err := g.generate(ctx)
	if err != nil {
		return err
	}

	g.log("Writing %v...\n", filename)
	return os.WriteFile(filename, content, 0o644)
}

// Print ...
func (g *Generator) Print(ctx context.Context) error {
	content, err := g.generate(ctx)
	if err != nil {
		return err
	}

	_, err = os.Stdout.Write(content)
	return err
}

// Generate downloads an updated version of the PSL list and compiles it into go code.
func (g *Generator) generate(ctx context.Context) ([]byte, error) {
	g.log("Fetching PSL version...\n")
	headInfo, err := extractHeadInfo(ctx)
	if err != nil {
		return nil, err
	}

	g.log("Downloading PSL %s...\n", headInfo.SHA[:6])
	reqURL := fmt.Sprintf("https://raw.githubusercontent.com/publicsuffix/list/%s/public_suffix_list.dat", headInfo.SHA)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		return nil, err
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	list := publicsuffix.NewList()
	rules, err := list.Load(resp.Body, nil)
	if err != nil {
		return nil, err
	}

	data := struct {
		VersionSHA  string
		VersionDate string
		Rules       []publicsuffix.Rule
	}{
		headInfo.SHA[:6],
		headInfo.Datetime.Format(time.ANSIC),
		rules,
	}

	g.log("Parsing PSL...\n")
	buf := new(bytes.Buffer)
	err = listTmpl.Execute(buf, &data)
	if err != nil {
		return nil, err
	}

	return format.Source(buf.Bytes())
}

func (g *Generator) log(format string, v ...interface{}) {
	if !g.Verbose {
		return
	}

	log.Printf(format, v...)
}
