package osvimport

import (
	"bytes"
	"fmt"
	"go/format"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/garagon/aguara/internal/intel"
)

// RenderConfig controls how RenderGoSource shapes its output.
//
// Both fields are required when calling from production; the
// defaults exist only so tests can rely on a single zero-value call
// when the exact package/variable name is not what they are
// asserting on.
type RenderConfig struct {
	// Package is the Go package the generated file declares
	// (typically "incident").
	Package string
	// VarName is the exported variable the generated snapshot is
	// assigned to (typically "EmbeddedIntelSnapshot").
	VarName string
}

// RenderGoSource returns gofmt'd Go source that declares
// `var <VarName> = intel.Snapshot{...}` populated from snap.
//
// The output begins with a `// Code generated by tools/update-intel;
// DO NOT EDIT.` header so go vet / linters treat the file as
// generated and skip golangci-lint's stylistic checks.
//
// The function is deterministic: identical input produces identical
// output. Records are emitted in the (ecosystem, name, ID) order
// Import already established; we do not re-sort here.
func RenderGoSource(snap intel.Snapshot, cfg RenderConfig) (string, error) {
	if cfg.Package == "" {
		cfg.Package = "incident"
	}
	if cfg.VarName == "" {
		cfg.VarName = "EmbeddedIntelSnapshot"
	}

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "// Code generated by tools/update-intel; DO NOT EDIT.\n")
	fmt.Fprintf(&buf, "// To regenerate, fetch the OSV all.zip for each ecosystem the\n")
	fmt.Fprintf(&buf, "// snapshot's Sources block lists, then re-run with one\n")
	fmt.Fprintf(&buf, "// --from-zip / --ecosystem pair per ecosystem so this header\n")
	fmt.Fprintf(&buf, "// stays in sync with the ecosystems actually written below:\n")
	fmt.Fprintf(&buf, "//\n")
	fmt.Fprintf(&buf, "//   go run ./tools/update-intel \\\n")
	ecosystems := canonicalEcosystemsFromSources(snap.Sources)
	for _, eco := range ecosystems {
		// The OSV bucket URL is case-sensitive
		// (`PyPI/all.zip`, not `pypi/all.zip`), so the
		// --ecosystem flag uses the canonical bucket name and
		// the filename suggestion is lower-cased for
		// filesystem-friendliness. update-intel routes the
		// alias through intel.CanonicaliseEcosystem either
		// way; the canonical form is the unambiguous one.
		fmt.Fprintf(&buf, "//       --from-zip ./osv-%s.zip --ecosystem %s \\\n", strings.ToLower(eco), eco)
	}
	fmt.Fprintf(&buf, "//       --out internal/%s/generated_intel.go\n", cfg.Package)
	fmt.Fprintf(&buf, "\npackage %s\n\n", cfg.Package)
	fmt.Fprintf(&buf, "import (\n")
	fmt.Fprintf(&buf, "\t\"time\"\n\n")
	fmt.Fprintf(&buf, "\t\"github.com/garagon/aguara/internal/intel\"\n")
	fmt.Fprintf(&buf, ")\n\n")
	fmt.Fprintf(&buf, "// %s is the OSV-derived snapshot baked into the binary.\n", cfg.VarName)
	fmt.Fprintf(&buf, "// Combined with KnownCompromisedSnapshot() through the matcher's\n")
	fmt.Fprintf(&buf, "// merge semantics so manual emergency entries keep display priority\n")
	fmt.Fprintf(&buf, "// over OSV refreshes for the same advisory ID.\n")
	fmt.Fprintf(&buf, "var %s = intel.Snapshot{\n", cfg.VarName)
	fmt.Fprintf(&buf, "\tSchemaVersion: %d,\n", snap.SchemaVersion)
	if !snap.GeneratedAt.IsZero() {
		fmt.Fprintf(&buf, "\tGeneratedAt:   %s,\n", goTimeLiteral(snap.GeneratedAt))
	}
	if snap.SHA256 != "" {
		fmt.Fprintf(&buf, "\tSHA256:        %s,\n", strconv.Quote(snap.SHA256))
	}
	if err := writeSources(&buf, snap.Sources); err != nil {
		return "", err
	}
	if err := writeRecords(&buf, snap.Records); err != nil {
		return "", err
	}
	fmt.Fprintf(&buf, "}\n")

	formatted, err := format.Source(buf.Bytes())
	if err != nil {
		// Format error here is a bug in the renderer (mismatched
		// braces, invalid identifier). Surface it with the raw
		// output so the developer can see what we tried to write.
		return "", fmt.Errorf("osvimport: gofmt failed: %w\n--- raw output ---\n%s", err, buf.String())
	}
	return string(formatted), nil
}

// canonicalEcosystemsFromSources recovers the canonical OSV
// bucket names from a snapshot's SourceMeta slice so the
// generated file's regeneration header lists the ecosystems
// actually written below. Sources whose Name does not follow
// the "osv.dev/<lower-bucket>" convention are skipped silently
// (test fixtures occasionally use free-form names); the
// returned list is in SupportedEcosystems registry order so a
// regeneration with the same set produces an identical header.
func canonicalEcosystemsFromSources(sources []intel.SourceMeta) []string {
	const prefix = "osv.dev/"
	present := map[string]bool{}
	for _, src := range sources {
		if !strings.HasPrefix(src.Name, prefix) {
			continue
		}
		canon := intel.CanonicaliseEcosystem(strings.TrimPrefix(src.Name, prefix))
		if canon == "" {
			continue
		}
		present[canon] = true
	}
	out := make([]string, 0, len(present))
	for _, eco := range intel.SupportedEcosystems() {
		if present[eco] {
			out = append(out, eco)
		}
	}
	return out
}

func writeSources(buf *bytes.Buffer, sources []intel.SourceMeta) error {
	if len(sources) == 0 {
		return nil
	}
	fmt.Fprintf(buf, "\tSources: []intel.SourceMeta{\n")
	for _, src := range sources {
		fmt.Fprintf(buf, "\t\t{\n")
		if src.Name != "" {
			fmt.Fprintf(buf, "\t\t\tName: %s,\n", strconv.Quote(src.Name))
		}
		if src.Kind != "" {
			fmt.Fprintf(buf, "\t\t\tKind: intel.%s,\n", sourceKindIdent(src.Kind))
		}
		if src.URL != "" {
			fmt.Fprintf(buf, "\t\t\tURL: %s,\n", strconv.Quote(src.URL))
		}
		if !src.RetrievedAt.IsZero() {
			fmt.Fprintf(buf, "\t\t\tRetrievedAt: %s,\n", goTimeLiteral(src.RetrievedAt))
		}
		if src.License != "" {
			fmt.Fprintf(buf, "\t\t\tLicense: %s,\n", strconv.Quote(src.License))
		}
		fmt.Fprintf(buf, "\t\t},\n")
	}
	fmt.Fprintf(buf, "\t},\n")
	return nil
}

func writeRecords(buf *bytes.Buffer, records []intel.Record) error {
	if len(records) == 0 {
		fmt.Fprintf(buf, "\tRecords: []intel.Record{},\n")
		return nil
	}
	fmt.Fprintf(buf, "\tRecords: []intel.Record{\n")
	for _, rec := range records {
		fmt.Fprintf(buf, "\t\t{\n")
		fmt.Fprintf(buf, "\t\t\tID: %s,\n", strconv.Quote(rec.ID))
		if len(rec.Aliases) > 0 {
			fmt.Fprintf(buf, "\t\t\tAliases: %s,\n", goStringSlice(rec.Aliases))
		}
		fmt.Fprintf(buf, "\t\t\tEcosystem: %s,\n", strconv.Quote(rec.Ecosystem))
		fmt.Fprintf(buf, "\t\t\tName: %s,\n", strconv.Quote(rec.Name))
		if rec.Kind != "" {
			fmt.Fprintf(buf, "\t\t\tKind: intel.%s,\n", recordKindIdent(rec.Kind))
		}
		if rec.Severity != "" {
			fmt.Fprintf(buf, "\t\t\tSeverity: %s,\n", strconv.Quote(rec.Severity))
		}
		if rec.Summary != "" {
			fmt.Fprintf(buf, "\t\t\tSummary: %s,\n", strconv.Quote(rec.Summary))
		}
		if len(rec.Versions) > 0 {
			fmt.Fprintf(buf, "\t\t\tVersions: %s,\n", goStringSlice(rec.Versions))
		}
		if len(rec.References) > 0 {
			fmt.Fprintf(buf, "\t\t\tReferences: %s,\n", goStringSlice(rec.References))
		}
		if rec.Withdrawn {
			fmt.Fprintf(buf, "\t\t\tWithdrawn: true,\n")
		}
		fmt.Fprintf(buf, "\t\t},\n")
	}
	fmt.Fprintf(buf, "\t},\n")
	return nil
}

// goStringSlice renders a []string slice as a Go literal with
// deterministic ordering (the input order is preserved). Used for
// short slices like Versions / Aliases / References where one-line
// output is more readable than multi-line.
func goStringSlice(in []string) string {
	if len(in) == 0 {
		return "nil"
	}
	parts := make([]string, len(in))
	for i, s := range in {
		parts[i] = strconv.Quote(s)
	}
	return "[]string{" + strings.Join(parts, ", ") + "}"
}

// goTimeLiteral renders a time.Time as a constructor call that
// will round-trip exactly. We use time.Date rather than
// time.Unix() so the generated code is readable.
func goTimeLiteral(t time.Time) string {
	t = t.UTC()
	return fmt.Sprintf("time.Date(%d, time.%s, %d, %d, %d, %d, %d, time.UTC)",
		t.Year(), t.Month().String(), t.Day(),
		t.Hour(), t.Minute(), t.Second(), t.Nanosecond())
}

// sourceKindIdent returns the Go identifier for a SourceKind value.
// We hard-code the two known kinds rather than reflect because the
// reverse lookup is a one-liner per known constant and a panic on
// an unknown value is the right behavior -- the generator must not
// silently produce code that references a non-existent identifier.
func sourceKindIdent(k intel.SourceKind) string {
	switch k {
	case intel.SourceManual:
		return "SourceManual"
	case intel.SourceOSV:
		return "SourceOSV"
	default:
		// Defensive: surface unknown kinds at generate time
		// rather than ship invalid Go to the build.
		return "SourceKind(" + strconv.Quote(string(k)) + ")"
	}
}

// recordKindIdent mirrors sourceKindIdent for RecordKind values.
func recordKindIdent(k intel.RecordKind) string {
	switch k {
	case intel.KindMalicious:
		return "KindMalicious"
	case intel.KindCompromised:
		return "KindCompromised"
	default:
		return "RecordKind(" + strconv.Quote(string(k)) + ")"
	}
}

// SortRecords sorts a record slice in place by (ecosystem, name,
// ID). Exposed so test fixtures can produce the same canonical
// order Import emits, keeping renderer expectations stable.
func SortRecords(records []intel.Record) {
	sort.SliceStable(records, func(i, j int) bool {
		a, b := records[i], records[j]
		if a.Ecosystem != b.Ecosystem {
			return a.Ecosystem < b.Ecosystem
		}
		if a.Name != b.Name {
			return a.Name < b.Name
		}
		return a.ID < b.ID
	})
}
