/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
:                                                                               :
:   █▀ █ █▀▀   ·   Blazing-fast pentesting suite                                :
:   ▄█ █ █▀    ·   BSD 3-Clause License                                         :
:                                                                               :
:   (c) 2022-2026 vmfunc, xyzeva,                                               :
:                 lunchcat alumni & contributors                                :
:                                                                               :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

// Package patchnotes shows release notes pulled from the github releases.
package patchnotes

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/charmbracelet/glamour"
)

const releasesAPI = "https://api.github.com/repos/vmfunc/sif/releases"

type release struct {
	TagName string `json:"tag_name"`
	Name    string `json:"name"`
	Body    string `json:"body"`
	URL     string `json:"html_url"`
}

func fetch(ctx context.Context, path string) (*release, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, releasesAPI+path, http.NoBody)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/vnd.github+json")

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("github returned %s", resp.Status)
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
	if err != nil {
		return nil, err
	}

	var r release
	if err := json.Unmarshal(body, &r); err != nil {
		return nil, err
	}
	return &r, nil
}

// render turns a release's markdown body into styled terminal output, falling
// back to the raw body if glamour can't render it.
func render(r *release) string {
	out, err := glamour.Render(r.Body, "dark")
	if err != nil {
		return r.Body
	}
	return fmt.Sprintf("%s\n%s", r.TagName, out)
}

// Print fetches the latest release and writes its notes to stdout. tag may be
// empty for the latest release, or a "vX" tag for a specific one.
func Print(tag string) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	path := "/latest"
	if tag != "" {
		path = "/tags/" + tag
	}

	r, err := fetch(ctx, path)
	if err != nil {
		fmt.Printf("couldn't fetch patch notes: %v\n", err)
		return
	}
	fmt.Print(render(r))
}

// ShowOnce prints the running version's notes the first time that version runs,
// then records it so it isn't shown again. best-effort: dev builds, the
// SIF_NO_PATCHNOTES opt-out, and any network failure stay silent.
func ShowOnce(version string) {
	// only clean release tags (e.g. 2026.6.7) map to a github release; skip dev
	// and pseudo-versions (a commit/dirty build) so we don't make a doomed call.
	if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" {
		return
	}

	path, err := statePath()
	if err != nil || hasSeen(path, version) {
		return
	}
	// record before fetching so a flaky network doesn't nag on every run
	recordSeen(path, version)

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	r, err := fetch(ctx, "/tags/v"+version)
	if err != nil {
		return
	}
	fmt.Printf("\nwhat's new in this release:\n%s", render(r))
}

func statePath() (string, error) {
	dir, err := os.UserConfigDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, "sif", "seen_version"), nil
}

func hasSeen(path, version string) bool {
	data, err := os.ReadFile(path)
	if err != nil {
		return false
	}
	return strings.TrimSpace(string(data)) == version
}

func recordSeen(path, version string) {
	if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
		return
	}
	_ = os.WriteFile(path, []byte(version), 0o600)
}
