package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"
)

// ChangelogInfo is what FetchChangelog returns. Body is the markdown
// from the GitHub release page; URL points back to that page so the
// dialog can offer "open in browser".
type ChangelogInfo struct {
	Version string
	Body    string
	URL     string
}

// FetchChangelog hits the GitHub releases API for the given version
// (must already include the leading "v") and returns the release
// notes body. Returns an empty ChangelogInfo on any failure or when
// the body is empty; the caller treats either as "skip silently".
//
// Honours $GITHUB_TOKEN for private-repo access. Times out at 4s so
// startup never blocks on a flaky network.
// semverOnly strips commit hash and date suffixes from version strings
// like "0.1.12 (25b2bd4, 2026-04-25T09:25:45Z)" to get just "0.1.12".
func semverOnly(v string) string {
	if i := strings.IndexByte(v, ' '); i > 0 {
		return v[:i]
	}
	return v
}

func FetchChangelog(ctx context.Context, version string) (ChangelogInfo, error) {
	version = semverOnly(version)
	if version == "" || version == "dev" {
		return ChangelogInfo{}, nil
	}

	// For local dev builds (0.0.0), fetch the latest release instead
	// of a tagged one so developers always see the newest changelog.
	var url string
	if version == "0.0.0" {
		url = "https://api.github.com/repos/patriceckhart/zot/releases/latest"
	} else {
		tag := version
		if !strings.HasPrefix(tag, "v") {
			tag = "v" + tag
		}
		url = fmt.Sprintf("https://api.github.com/repos/patriceckhart/zot/releases/tags/%s", tag)
	}
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return ChangelogInfo{}, err
	}
	req.Header.Set("accept", "application/vnd.github+json")
	req.Header.Set("x-github-api-version", "2022-11-28")
	if tok := os.Getenv("GITHUB_TOKEN"); tok != "" {
		req.Header.Set("authorization", "Bearer "+tok)
	}

	client := &http.Client{Timeout: 4 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return ChangelogInfo{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return ChangelogInfo{}, fmt.Errorf("github api %d", resp.StatusCode)
	}

	var body struct {
		TagName string `json:"tag_name"`
		HTMLURL string `json:"html_url"`
		Body    string `json:"body"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return ChangelogInfo{}, err
	}
	body.Body = strings.TrimSpace(body.Body)
	if body.Body == "" {
		return ChangelogInfo{}, nil
	}
	// Extract only the changelog section and strip markdown headers.
	body.Body = extractChangelog(body.Body)
	if body.Body == "" {
		return ChangelogInfo{}, nil
	}
	return ChangelogInfo{
		Version: strings.TrimPrefix(body.TagName, "v"),
		Body:    body.Body,
		URL:     body.HTMLURL,
	}, nil
}

// extractChangelog pulls the content starting from "## Changelog"
// (or the whole body if no such header exists) and strips markdown
// heading markers (# ## ###) from every line so the TUI renders
// clean text.
func extractChangelog(body string) string {
	lines := strings.Split(body, "\n")

	// Find the "## Changelog" line.
	start := -1
	for i, l := range lines {
		trimmed := strings.TrimSpace(l)
		if strings.EqualFold(trimmed, "## changelog") ||
			strings.EqualFold(trimmed, "## Changelog") {
			start = i + 1
			break
		}
	}
	if start < 0 {
		// No changelog header found; use the whole body.
		start = 0
	}

	// Process remaining lines: strip # markers but mark headings
	// so the renderer can color them.
	var out []string
	for _, l := range lines[start:] {
		trimmed := strings.TrimSpace(l)
		if trimmed == "" {
			out = append(out, "")
			continue
		}
		// Detect markdown headings and strip the # but keep text.
		if strings.HasPrefix(trimmed, "#") {
			heading := strings.TrimLeft(trimmed, "#")
			heading = strings.TrimSpace(heading)
			if heading != "" {
				// Mark headings with a sentinel the dialog can detect.
				out = append(out, "\x00H:"+heading)
			}
			continue
		}
		out = append(out, l)
	}
	return strings.TrimSpace(strings.Join(out, "\n"))
}

// FetchChangelogAsync runs FetchChangelog on a goroutine and delivers
// the result on the returned channel. Channel always closes.
func FetchChangelogAsync(version string) <-chan ChangelogInfo {
	ch := make(chan ChangelogInfo, 1)
	go func() {
		defer close(ch)
		ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
		defer cancel()
		info, _ := FetchChangelog(ctx, version)
		ch <- info
	}()
	return ch
}

// ShouldShowChangelog reports whether the running binary version
// differs from the last version whose changelog the user dismissed.
// Returns false on dev builds (version "" / "dev" / "0.0.0") and on
// the first-ever launch (no LastChangelogShown stored — we don't
// dump release notes at someone who just installed).
func ShouldShowChangelog(currentVersion string, cfg Config) bool {
	currentVersion = semverOnly(currentVersion)
	if currentVersion == "" || currentVersion == "dev" {
		return false
	}
	if cfg.LastChangelogShown == "" {
		return false
	}
	if currentVersion == "0.0.0" {
		return true
	}
	return semverOnly(cfg.LastChangelogShown) != currentVersion
}

// MarkChangelogShown persists the version whose changelog the user
// just dismissed. Idempotent; safe to call when the dialog wasn't
// actually shown (e.g. fetch failed) so we don't keep retrying.
func MarkChangelogShown(version string) error {
	v := semverOnly(version)
	cfg, _ := LoadConfig()
	if semverOnly(cfg.LastChangelogShown) == v {
		return nil
	}
	cfg.LastChangelogShown = v
	return SaveConfig(cfg)
}

// SeedChangelogVersion sets LastChangelogShown if it's currently
// empty. Called once on first-ever launch so future upgrades
// correctly trigger the dialog while THIS launch (which is also
// "first-ever") doesn't.
func SeedChangelogVersion(version string) {
	version = semverOnly(version)
	if version == "" || version == "dev" {
		return
	}
	cfg, err := LoadConfig()
	if err != nil {
		return
	}
	if cfg.LastChangelogShown != "" {
		return
	}
	cfg.LastChangelogShown = version
	_ = SaveConfig(cfg)
}
