package webui

import (
	"encoding/json"
	"fmt"
	"html"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"bee/audit/internal/platform"
)

var taskReportMetricsDBPath = metricsDBPath

type taskReport struct {
	ID          string            `json:"id"`
	Name        string            `json:"name"`
	Target      string            `json:"target"`
	Status      string            `json:"status"`
	CreatedAt   time.Time         `json:"created_at"`
	StartedAt   *time.Time        `json:"started_at,omitempty"`
	DoneAt      *time.Time        `json:"done_at,omitempty"`
	DurationSec int               `json:"duration_sec,omitempty"`
	Error       string            `json:"error,omitempty"`
	LogFile     string            `json:"log_file,omitempty"`
	Charts      []taskReportChart `json:"charts,omitempty"`
	GeneratedAt time.Time         `json:"generated_at"`
}

type taskReportChart struct {
	Title string `json:"title"`
	File  string `json:"file"`
}

type taskChartSpec struct {
	Path string
	File string
}

var taskDashboardChartSpecs = []taskChartSpec{
	{Path: "server-load", File: "server-load.svg"},
	{Path: "server-temp-cpu", File: "server-temp-cpu.svg"},
	{Path: "server-temp-ambient", File: "server-temp-ambient.svg"},
	{Path: "server-power", File: "server-power.svg"},
	{Path: "server-fans", File: "server-fans.svg"},
	{Path: "gpu-all-load", File: "gpu-all-load.svg"},
	{Path: "gpu-all-memload", File: "gpu-all-memload.svg"},
	{Path: "gpu-all-clock", File: "gpu-all-clock.svg"},
	{Path: "gpu-all-power", File: "gpu-all-power.svg"},
	{Path: "gpu-all-temp", File: "gpu-all-temp.svg"},
}

func taskChartSpecsForSamples(samples []platform.LiveMetricSample) []taskChartSpec {
	specs := make([]taskChartSpec, 0, len(taskDashboardChartSpecs)+len(taskGPUIndices(samples)))
	specs = append(specs, taskDashboardChartSpecs...)
	for _, idx := range taskGPUIndices(samples) {
		specs = append(specs, taskChartSpec{
			Path: fmt.Sprintf("gpu/%d-overview", idx),
			File: fmt.Sprintf("gpu-%d-overview.svg", idx),
		})
	}
	return specs
}

func writeTaskReportArtifacts(t *Task) error {
	if t == nil {
		return nil
	}
	ensureTaskReportPaths(t)
	if strings.TrimSpace(t.ArtifactsDir) == "" {
		return nil
	}
	if err := os.MkdirAll(t.ArtifactsDir, 0755); err != nil {
		return err
	}

	start, end := taskTimeWindow(t)
	samples, _ := loadTaskMetricSamples(start, end)
	charts, inlineCharts := writeTaskCharts(t.ArtifactsDir, start, end, samples)

	logText := ""
	if data, err := os.ReadFile(t.LogPath); err == nil {
		logText = string(data)
	}

	report := taskReport{
		ID:          t.ID,
		Name:        t.Name,
		Target:      t.Target,
		Status:      t.Status,
		CreatedAt:   t.CreatedAt,
		StartedAt:   t.StartedAt,
		DoneAt:      t.DoneAt,
		DurationSec: taskElapsedSec(t, reportDoneTime(t)),
		Error:       t.ErrMsg,
		LogFile:     filepath.Base(t.LogPath),
		Charts:      charts,
		GeneratedAt: time.Now().UTC(),
	}
	if err := writeJSONFile(t.ReportJSONPath, report); err != nil {
		return err
	}
	return os.WriteFile(t.ReportHTMLPath, []byte(renderTaskReportFragment(report, inlineCharts, logText)), 0644)
}

func reportDoneTime(t *Task) time.Time {
	if t != nil && t.DoneAt != nil && !t.DoneAt.IsZero() {
		return *t.DoneAt
	}
	return time.Now()
}

func taskTimeWindow(t *Task) (time.Time, time.Time) {
	if t == nil {
		now := time.Now().UTC()
		return now, now
	}
	start := t.CreatedAt.UTC()
	if t.StartedAt != nil && !t.StartedAt.IsZero() {
		start = t.StartedAt.UTC()
	}
	end := time.Now().UTC()
	if t.DoneAt != nil && !t.DoneAt.IsZero() {
		end = t.DoneAt.UTC()
	}
	if end.Before(start) {
		end = start
	}
	return start, end
}

func loadTaskMetricSamples(start, end time.Time) ([]platform.LiveMetricSample, error) {
	db, err := openMetricsDB(taskReportMetricsDBPath)
	if err != nil {
		return nil, err
	}
	defer db.Close()
	return db.LoadBetween(start, end)
}

func writeTaskCharts(dir string, start, end time.Time, samples []platform.LiveMetricSample) ([]taskReportChart, map[string]string) {
	if len(samples) == 0 {
		return nil, nil
	}
	timeline := []chartTimelineSegment{{Start: start, End: end, Active: true}}
	var charts []taskReportChart
	inline := make(map[string]string)
	for _, spec := range taskChartSpecsForSamples(samples) {
		title, svg, ok := renderTaskChartSVG(spec.Path, samples, timeline)
		if !ok || len(svg) == 0 {
			continue
		}
		path := filepath.Join(dir, spec.File)
		if err := os.WriteFile(path, svg, 0644); err != nil {
			continue
		}
		charts = append(charts, taskReportChart{Title: title, File: spec.File})
		inline[spec.File] = string(svg)
	}
	return charts, inline
}

func renderTaskChartSVG(path string, samples []platform.LiveMetricSample, timeline []chartTimelineSegment) (string, []byte, bool) {
	if idx, sub, ok := parseGPUChartPath(path); ok && sub == "overview" {
		buf, hasData, err := renderGPUOverviewChartSVG(idx, samples, timeline)
		if err != nil || !hasData {
			return "", nil, false
		}
		return gpuDisplayLabel(idx) + " Overview", buf, true
	}
	datasets, names, labels, title, yMin, yMax, ok := chartDataFromSamples(path, samples)
	if !ok {
		return "", nil, false
	}
	buf, err := renderMetricChartSVG(
		title,
		labels,
		sampleTimes(samples),
		datasets,
		names,
		yMin,
		yMax,
		chartCanvasHeightForPath(path, len(names)),
		timeline,
	)
	if err != nil {
		return "", nil, false
	}
	return title, buf, true
}

func taskGPUIndices(samples []platform.LiveMetricSample) []int {
	seen := map[int]bool{}
	var out []int
	for _, s := range samples {
		for _, g := range s.GPUs {
			if seen[g.GPUIndex] {
				continue
			}
			seen[g.GPUIndex] = true
			out = append(out, g.GPUIndex)
		}
	}
	sort.Ints(out)
	return out
}

func writeJSONFile(path string, v any) error {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}

func renderTaskReportFragment(report taskReport, charts map[string]string, logText string) string {
	var b strings.Builder
	b.WriteString(`<div class="card"><div class="card-head">Task Report</div><div class="card-body">`)
	b.WriteString(`<div class="grid2">`)
	b.WriteString(`<div><div style="font-size:12px;color:var(--muted);margin-bottom:6px">Task</div><div style="font-size:16px;font-weight:700">` + html.EscapeString(report.Name) + `</div>`)
	b.WriteString(`<div style="font-size:13px;color:var(--muted)">` + html.EscapeString(report.Target) + `</div></div>`)
	b.WriteString(`<div><div style="font-size:12px;color:var(--muted);margin-bottom:6px">Status</div><div>` + renderTaskStatusBadge(report.Status) + `</div>`)
	if strings.TrimSpace(report.Error) != "" {
		b.WriteString(`<div style="margin-top:8px;font-size:13px;color:var(--crit-fg)">` + html.EscapeString(report.Error) + `</div>`)
	}
	b.WriteString(`</div></div>`)
	b.WriteString(`<div style="margin-top:14px;font-size:13px;color:var(--muted)">`)
	b.WriteString(`Started: ` + formatTaskTime(report.StartedAt, report.CreatedAt) + ` | Finished: ` + formatTaskTime(report.DoneAt, time.Time{}) + ` | Duration: ` + formatTaskDuration(report.DurationSec))
	b.WriteString(`</div></div></div>`)
	if benchmarkCard := renderTaskBenchmarkResultsCard(report.Target, logText); benchmarkCard != "" {
		b.WriteString(benchmarkCard)
	}
	if powerCard := renderTaskPowerResultsCard(report.Target, logText); powerCard != "" {
		b.WriteString(powerCard)
	}

	if len(report.Charts) > 0 {
		for _, chart := range report.Charts {
			b.WriteString(`<div class="card"><div class="card-head">` + html.EscapeString(chart.Title) + `</div><div class="card-body" style="padding:12px">`)
			b.WriteString(charts[chart.File])
			b.WriteString(`</div></div>`)
		}
	} else {
		b.WriteString(`<div class="alert alert-info">No metric samples were captured during this task window.</div>`)
	}

	b.WriteString(`<div class="card"><div class="card-head">Logs</div><div class="card-body">`)
	b.WriteString(`<div class="terminal" style="max-height:none;white-space:pre-wrap">` + html.EscapeString(strings.TrimSpace(logText)) + `</div>`)
	b.WriteString(`</div></div>`)
	return b.String()
}

func renderTaskBenchmarkResultsCard(target, logText string) string {
	switch strings.TrimSpace(target) {
	case "nvidia-bench-perf":
	default:
		return ""
	}
	resultPath := taskBenchmarkResultPath(logText)
	if strings.TrimSpace(resultPath) == "" {
		return ""
	}
	columns, runs := loadBenchmarkHistoryFromPaths([]string{resultPath})
	if len(runs) == 0 {
		return ""
	}
	return renderBenchmarkResultsCardFromRuns(
		"Perf Results",
		"Composite score for this benchmark task.",
		"No benchmark results were saved for this task.",
		columns,
		runs,
	)
}

func renderTaskPowerResultsCard(target, logText string) string {
	if strings.TrimSpace(target) != "nvidia-bench-power" {
		return ""
	}
	resultPath := taskBenchmarkResultPath(logText)
	if strings.TrimSpace(resultPath) == "" {
		return ""
	}
	raw, err := os.ReadFile(resultPath)
	if err != nil {
		return ""
	}
	var result platform.NvidiaPowerBenchResult
	if err := json.Unmarshal(raw, &result); err != nil {
		return ""
	}
	var b strings.Builder
	b.WriteString(`<div class="card"><div class="card-head">Power Results</div><div class="card-body">`)
	if len(result.RecommendedSlotOrder) > 0 {
		b.WriteString(`<p style="margin-bottom:10px"><strong>Recommended slot order:</strong> ` + html.EscapeString(joinTaskIndices(result.RecommendedSlotOrder)) + `</p>`)
	}
	b.WriteString(`<table><tr><th>GPU</th><th>Status</th><th>Max Power</th><th>Applied Limit</th></tr>`)
	for _, gpu := range result.GPUs {
		fmt.Fprintf(&b, `<tr><td>GPU %d</td><td>%s</td><td>%.0f W</td><td>%.0f W</td></tr>`,
			gpu.Index, html.EscapeString(gpu.Status), gpu.MaxObservedPowerW, gpu.AppliedPowerLimitW)
	}
	b.WriteString(`</table></div></div>`)
	return b.String()
}

func taskBenchmarkResultPath(logText string) string {
	archivePath := taskArchivePathFromLog(logText)
	if archivePath == "" {
		return ""
	}
	runDir := strings.TrimSuffix(archivePath, ".tar.gz")
	return filepath.Join(runDir, "result.json")
}

func taskArchivePathFromLog(logText string) string {
	lines := strings.Split(logText, "\n")
	for i := len(lines) - 1; i >= 0; i-- {
		line := strings.TrimSpace(lines[i])
		if line == "" || !strings.HasPrefix(line, "Archive:") {
			continue
		}
		path := strings.TrimSpace(strings.TrimPrefix(line, "Archive:"))
		if strings.HasPrefix(path, "Archive written to ") {
			path = strings.TrimSpace(strings.TrimPrefix(path, "Archive written to "))
		}
		if strings.HasSuffix(path, ".tar.gz") {
			return path
		}
	}
	return ""
}

func renderTaskStatusBadge(status string) string {
	className := map[string]string{
		TaskRunning:   "badge-ok",
		TaskPending:   "badge-unknown",
		TaskDone:      "badge-ok",
		TaskFailed:    "badge-err",
		TaskCancelled: "badge-unknown",
	}[status]
	if className == "" {
		className = "badge-unknown"
	}
	label := strings.TrimSpace(status)
	if label == "" {
		label = "unknown"
	}
	return `<span class="badge ` + className + `">` + html.EscapeString(label) + `</span>`
}

func formatTaskTime(ts *time.Time, fallback time.Time) string {
	if ts != nil && !ts.IsZero() {
		return ts.Local().Format("2006-01-02 15:04:05")
	}
	if !fallback.IsZero() {
		return fallback.Local().Format("2006-01-02 15:04:05")
	}
	return "n/a"
}

func formatTaskDuration(sec int) string {
	if sec <= 0 {
		return "n/a"
	}
	if sec < 60 {
		return fmt.Sprintf("%ds", sec)
	}
	if sec < 3600 {
		return fmt.Sprintf("%dm %02ds", sec/60, sec%60)
	}
	return fmt.Sprintf("%dh %02dm %02ds", sec/3600, (sec%3600)/60, sec%60)
}
