package report

import (
	"log"
	"strings"
	"sync"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
)

// On-demand log tail (pull-based, same ACK-flag pattern as escrow/config-refresh):
// the hub stores a pending request per (customer, app); the report ACK advertises the
// requested app names in log_tail_requests; the NEXT report carries the collected tails
// in log_tails. The hub clears the pending request when a tail arrives (consume-once) —
// the controller listens to no one, it only pushes on its own cycle.

const (
	logTailLines        = 200
	logTailMaxLineChars = 400
	logTailMaxBytes     = 64 * 1024 // per app; head-truncated (newest lines kept)
)

// LogTail is one app's on-demand ordered log tail riding the report.
type LogTail struct {
	App         string    `json:"app"`
	CollectedAt time.Time `json:"collected_at"`
	Lines       []string  `json:"lines"`
}

var (
	pendingTailsMu sync.Mutex
	pendingTails   []string
)

// SetPendingLogTails records the hub-requested apps from the latest ACK. The hub is
// the source of truth: an ACK without requests clears any stale local pending set.
func SetPendingLogTails(apps []string) {
	pendingTailsMu.Lock()
	defer pendingTailsMu.Unlock()
	pendingTails = append([]string(nil), apps...)
}

// drainPendingLogTails takes and CLEARS the pending set (consume-once controller-side).
// If the subsequent push fails, the hub's request is still pending and the next ACK
// re-arms it — fail-safe retry, no duplicate shipping.
func drainPendingLogTails() []string {
	pendingTailsMu.Lock()
	defer pendingTailsMu.Unlock()
	apps := pendingTails
	pendingTails = nil
	return apps
}

// buildLogTailsSection collects the requested tails via fetch (seam: stacks.GetLogs or
// metrics.FetchContainerLogTail). Lines stay ordered as emitted; each line is truncated
// to 400 chars and redacted; per-app total is capped at 64KB keeping the NEWEST lines.
// A failed fetch is skipped (logged) — the hub request stays pending and retries next cycle.
func buildLogTailsSection(apps []string, fetch func(app string, lines int) (string, error), logger *log.Logger) []LogTail {
	var tails []LogTail
	for _, app := range apps {
		raw, err := fetch(app, logTailLines)
		if err != nil {
			if logger != nil {
				logger.Printf("[WARN] [report] log-tail fetch for %s failed: %v", app, err)
			}
			continue
		}
		lines := capTailLines(strings.Split(raw, "\n"))
		tails = append(tails, LogTail{App: app, CollectedAt: time.Now().UTC(), Lines: lines})
		if logger != nil {
			logger.Printf("[INFO] [report] log-tail collected for %s: %d lines", app, len(lines))
		}
	}
	return tails
}

// capTailLines truncates each line to logTailMaxLineChars, redacts it, and enforces the
// per-app byte budget by dropping from the HEAD (oldest) so the newest lines survive.
func capTailLines(lines []string) []string {
	// Drop a trailing empty split artifact.
	for len(lines) > 0 && lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	}
	out := make([]string, 0, len(lines))
	for _, l := range lines {
		l = metrics.RedactLine(truncateTailLine(l, logTailMaxLineChars))
		out = append(out, l)
	}
	// Head-truncate to the byte budget: walk from the end accumulating.
	total := 0
	start := len(out)
	for i := len(out) - 1; i >= 0; i-- {
		total += len(out[i]) + 1 // +1 for the newline it represents
		if total > logTailMaxBytes {
			break
		}
		start = i
	}
	return out[start:]
}

func truncateTailLine(s string, max int) string {
	runes := []rune(s)
	if len(runes) <= max {
		return s
	}
	return string(runes[:max]) + "…"
}
