// Package runreport records a failed deployment run as a Forgejo issue in the
// x9/xetup-runs tracker, so recurring failures can be triaged and fixed.
//
// It only fires when a run has at least one ERROR step - successful runs make
// no noise. It is best-effort and non-blocking: any failure to post is logged
// to stderr and never blocks completion (the email report is the fallback).
package runreport

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"git.xetup.x9.cz/x9/xetup/internal/buildinfo"
	"git.xetup.x9.cz/x9/xetup/internal/report"
)

const (
	// Forgejo API + web base for the runs tracker.
	issuesAPI   = "https://git.xetup.x9.cz/api/v1/repos/x9/xetup-runs/issues"
	failedLabel = 1 // id of the "failed" label in x9/xetup-runs

	// Cap the inline log so a huge Deploy.log does not bloat the issue; keep
	// the tail (the end is where the failure and its context live).
	maxLogBytes = 60 * 1024
)

// Report creates a Forgejo issue for a failed run and returns the issue's web
// URL (empty string if no issue was created - clean run, no token, or the post
// failed). The returned URL is meant to be linked from the email report.
func Report(results []report.StepResult, logPath string) string {
	// No token -> local/dev build, reporting disabled.
	if buildinfo.RunsToken == "" {
		return ""
	}

	errs := 0
	for _, r := range results {
		if r.Status == "ERROR" {
			errs++
		}
	}
	if errs == 0 {
		return "" // only failures get an issue
	}

	hostname, _ := os.Hostname()
	now := time.Now().Format("2006-01-02 15:04")
	title := fmt.Sprintf("FAILED %s - %s - %s", hostname, buildinfo.Version, now)
	body := buildBody(results, hostname, now, logPath)

	payload, err := json.Marshal(map[string]any{
		"title":  title,
		"body":   body,
		"labels": []int{failedLabel},
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "[WARN] runreport: marshal failed: %v\n", err)
		return ""
	}

	client := &http.Client{Timeout: 15 * time.Second}
	delays := []time.Duration{0, 2 * time.Second, 5 * time.Second}
	for attempt, delay := range delays {
		if delay > 0 {
			time.Sleep(delay)
		}
		url, err := postIssue(client, payload)
		if err != nil {
			fmt.Fprintf(os.Stderr, "[WARN] runreport attempt %d/3 failed: %v\n", attempt+1, err)
			continue
		}
		fmt.Fprintf(os.Stderr, "[OK] runreport: issue created %s\n", url)
		return url
	}
	fmt.Fprintf(os.Stderr, "[ERROR] runreport: all attempts failed; run recorded only in email\n")
	return ""
}

// postIssue sends one create-issue request and returns the new issue's html_url.
func postIssue(client *http.Client, payload []byte) (string, error) {
	req, err := http.NewRequest(http.MethodPost, issuesAPI, bytes.NewReader(payload))
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "token "+buildinfo.RunsToken)
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode != http.StatusCreated {
		return "", fmt.Errorf("unexpected status %s", resp.Status)
	}
	var out struct {
		HTMLURL string `json:"html_url"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return "", err
	}
	return out.HTMLURL, nil
}

// buildBody renders the issue markdown: a summary line, a per-step table, and
// the (tail-capped) Deploy.log in a collapsed block.
func buildBody(results []report.StepResult, hostname, now, logPath string) string {
	var ok, errs, skipped int
	var table bytes.Buffer
	table.WriteString("| Status | Krok | Nazev | Cas |\n|---|---|---|---|\n")
	for _, r := range results {
		switch r.Status {
		case "OK":
			ok++
		case "ERROR":
			errs++
		default:
			skipped++
		}
		elapsed := ""
		if r.Elapsed > 0 {
			elapsed = r.Elapsed.Round(time.Second).String()
		}
		fmt.Fprintf(&table, "| %s | %s | %s | %s |\n", r.Status, r.Num, r.Name, elapsed)
	}

	var b bytes.Buffer
	fmt.Fprintf(&b, "**Host:** %s\n", hostname)
	fmt.Fprintf(&b, "**xetup:** %s\n", buildinfo.Version)
	fmt.Fprintf(&b, "**Cas:** %s\n", now)
	fmt.Fprintf(&b, "**Vysledek:** OK %d / CHYBY %d / PRESKOCENO %d\n\n", ok, errs, skipped)
	b.WriteString(table.String())
	b.WriteString("\n")

	b.WriteString("<details><summary>Deploy.log</summary>\n\n```\n")
	b.WriteString(readLogTail(logPath))
	b.WriteString("\n```\n\n</details>\n")
	return b.String()
}

// readLogTail returns the Deploy.log content, capped to the last maxLogBytes
// with a truncation note. Returns a placeholder if the log cannot be read.
func readLogTail(logPath string) string {
	data, err := os.ReadFile(logPath)
	if err != nil {
		return fmt.Sprintf("(log unavailable: %v)", err)
	}
	if len(data) > maxLogBytes {
		note := fmt.Sprintf("... (truncated to last %d KB) ...\n", maxLogBytes/1024)
		return note + string(data[len(data)-maxLogBytes:])
	}
	return string(data)
}
