package webui

import (
	"database/sql"
	"encoding/csv"
	"io"
	"strconv"
	"time"

	"bee/audit/internal/platform"
	_ "modernc.org/sqlite"
)

const metricsDBPath = "/appdata/bee/metrics.db"

// MetricsDB persists live metric samples to SQLite.
type MetricsDB struct {
	db *sql.DB
}

// openMetricsDB opens (or creates) the metrics database at the given path.
func openMetricsDB(path string) (*MetricsDB, error) {
	db, err := sql.Open("sqlite", path+"?_journal=WAL&_busy_timeout=5000")
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(1)
	if err := initMetricsSchema(db); err != nil {
		_ = db.Close()
		return nil, err
	}
	return &MetricsDB{db: db}, nil
}

func initMetricsSchema(db *sql.DB) error {
	_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS sys_metrics (
  ts           INTEGER NOT NULL,
  cpu_load_pct REAL,
  mem_load_pct REAL,
  power_w      REAL,
  PRIMARY KEY (ts)
);
CREATE TABLE IF NOT EXISTS gpu_metrics (
  ts            INTEGER NOT NULL,
  gpu_index     INTEGER NOT NULL,
  temp_c        REAL,
  usage_pct     REAL,
  mem_usage_pct REAL,
  power_w       REAL,
  PRIMARY KEY (ts, gpu_index)
);
CREATE TABLE IF NOT EXISTS fan_metrics (
  ts   INTEGER NOT NULL,
  name TEXT NOT NULL,
  rpm  REAL,
  PRIMARY KEY (ts, name)
);
CREATE TABLE IF NOT EXISTS temp_metrics (
  ts      INTEGER NOT NULL,
  name    TEXT NOT NULL,
  grp     TEXT NOT NULL,
  celsius REAL,
  PRIMARY KEY (ts, name)
);
`)
	return err
}

// Write inserts one sample into all relevant tables.
func (m *MetricsDB) Write(s platform.LiveMetricSample) error {
	ts := s.Timestamp.Unix()
	tx, err := m.db.Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	_, err = tx.Exec(
		`INSERT OR REPLACE INTO sys_metrics(ts,cpu_load_pct,mem_load_pct,power_w) VALUES(?,?,?,?)`,
		ts, s.CPULoadPct, s.MemLoadPct, s.PowerW,
	)
	if err != nil {
		return err
	}
	for _, g := range s.GPUs {
		_, err = tx.Exec(
			`INSERT OR REPLACE INTO gpu_metrics(ts,gpu_index,temp_c,usage_pct,mem_usage_pct,power_w) VALUES(?,?,?,?,?,?)`,
			ts, g.GPUIndex, g.TempC, g.UsagePct, g.MemUsagePct, g.PowerW,
		)
		if err != nil {
			return err
		}
	}
	for _, f := range s.Fans {
		_, err = tx.Exec(
			`INSERT OR REPLACE INTO fan_metrics(ts,name,rpm) VALUES(?,?,?)`,
			ts, f.Name, f.RPM,
		)
		if err != nil {
			return err
		}
	}
	for _, t := range s.Temps {
		_, err = tx.Exec(
			`INSERT OR REPLACE INTO temp_metrics(ts,name,grp,celsius) VALUES(?,?,?,?)`,
			ts, t.Name, t.Group, t.Celsius,
		)
		if err != nil {
			return err
		}
	}
	return tx.Commit()
}

// LoadRecent returns up to n samples in chronological order (oldest first).
func (m *MetricsDB) LoadRecent(n int) ([]platform.LiveMetricSample, error) {
	return m.loadSamples(`SELECT ts,cpu_load_pct,mem_load_pct,power_w FROM sys_metrics ORDER BY ts DESC LIMIT ?`, n)
}

// LoadAll returns all persisted samples in chronological order (oldest first).
func (m *MetricsDB) LoadAll() ([]platform.LiveMetricSample, error) {
	return m.loadSamples(`SELECT ts,cpu_load_pct,mem_load_pct,power_w FROM sys_metrics ORDER BY ts`, nil)
}

// loadSamples reconstructs LiveMetricSample rows from the normalized tables.
func (m *MetricsDB) loadSamples(query string, args ...any) ([]platform.LiveMetricSample, error) {
	rows, err := m.db.Query(query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	type sysRow struct {
		ts          int64
		cpu, mem, pwr float64
	}
	var sysRows []sysRow
	for rows.Next() {
		var r sysRow
		if err := rows.Scan(&r.ts, &r.cpu, &r.mem, &r.pwr); err != nil {
			continue
		}
		sysRows = append(sysRows, r)
	}
	if len(sysRows) == 0 {
		return nil, nil
	}
	// Reverse to chronological order
	for i, j := 0, len(sysRows)-1; i < j; i, j = i+1, j-1 {
		sysRows[i], sysRows[j] = sysRows[j], sysRows[i]
	}

	// Collect min/max ts for range query
	minTS := sysRows[0].ts
	maxTS := sysRows[len(sysRows)-1].ts

	// Load GPU rows in range
	type gpuKey struct{ ts int64; idx int }
	gpuData := map[gpuKey]platform.GPUMetricRow{}
	gRows, err := m.db.Query(
		`SELECT ts,gpu_index,temp_c,usage_pct,mem_usage_pct,power_w FROM gpu_metrics WHERE ts>=? AND ts<=? ORDER BY ts,gpu_index`,
		minTS, maxTS,
	)
	if err == nil {
		defer gRows.Close()
		for gRows.Next() {
			var ts int64
			var g platform.GPUMetricRow
			if err := gRows.Scan(&ts, &g.GPUIndex, &g.TempC, &g.UsagePct, &g.MemUsagePct, &g.PowerW); err == nil {
				gpuData[gpuKey{ts, g.GPUIndex}] = g
			}
		}
	}

	// Load fan rows in range
	type fanKey struct{ ts int64; name string }
	fanData := map[fanKey]float64{}
	fRows, err := m.db.Query(
		`SELECT ts,name,rpm FROM fan_metrics WHERE ts>=? AND ts<=?`, minTS, maxTS,
	)
	if err == nil {
		defer fRows.Close()
		for fRows.Next() {
			var ts int64
			var name string
			var rpm float64
			if err := fRows.Scan(&ts, &name, &rpm); err == nil {
				fanData[fanKey{ts, name}] = rpm
			}
		}
	}

	// Load temp rows in range
	type tempKey struct{ ts int64; name string }
	tempData := map[tempKey]platform.TempReading{}
	tRows, err := m.db.Query(
		`SELECT ts,name,grp,celsius FROM temp_metrics WHERE ts>=? AND ts<=?`, minTS, maxTS,
	)
	if err == nil {
		defer tRows.Close()
		for tRows.Next() {
			var ts int64
			var t platform.TempReading
			if err := tRows.Scan(&ts, &t.Name, &t.Group, &t.Celsius); err == nil {
				tempData[tempKey{ts, t.Name}] = t
			}
		}
	}

	// Collect unique GPU indices and fan names from loaded data (preserve order)
	seenGPU := map[int]bool{}
	var gpuIndices []int
	for k := range gpuData {
		if !seenGPU[k.idx] {
			seenGPU[k.idx] = true
			gpuIndices = append(gpuIndices, k.idx)
		}
	}
	seenFan := map[string]bool{}
	var fanNames []string
	for k := range fanData {
		if !seenFan[k.name] {
			seenFan[k.name] = true
			fanNames = append(fanNames, k.name)
		}
	}
	seenTemp := map[string]bool{}
	var tempNames []string
	for k := range tempData {
		if !seenTemp[k.name] {
			seenTemp[k.name] = true
			tempNames = append(tempNames, k.name)
		}
	}

	samples := make([]platform.LiveMetricSample, len(sysRows))
	for i, r := range sysRows {
		s := platform.LiveMetricSample{
			Timestamp:  time.Unix(r.ts, 0).UTC(),
			CPULoadPct: r.cpu,
			MemLoadPct: r.mem,
			PowerW:     r.pwr,
		}
		for _, idx := range gpuIndices {
			if g, ok := gpuData[gpuKey{r.ts, idx}]; ok {
				s.GPUs = append(s.GPUs, g)
			}
		}
		for _, name := range fanNames {
			if rpm, ok := fanData[fanKey{r.ts, name}]; ok {
				s.Fans = append(s.Fans, platform.FanReading{Name: name, RPM: rpm})
			}
		}
		for _, name := range tempNames {
			if t, ok := tempData[tempKey{r.ts, name}]; ok {
				s.Temps = append(s.Temps, t)
			}
		}
		samples[i] = s
	}
	return samples, nil
}

// ExportCSV writes all sys+gpu data as CSV to w.
func (m *MetricsDB) ExportCSV(w io.Writer) error {
	rows, err := m.db.Query(`
		SELECT s.ts, s.cpu_load_pct, s.mem_load_pct, s.power_w,
		       g.gpu_index, g.temp_c, g.usage_pct, g.mem_usage_pct, g.power_w
		FROM sys_metrics s
		LEFT JOIN gpu_metrics g ON g.ts = s.ts
		ORDER BY s.ts, g.gpu_index
	`)
	if err != nil {
		return err
	}
	defer rows.Close()

	cw := csv.NewWriter(w)
	_ = cw.Write([]string{"ts", "cpu_load_pct", "mem_load_pct", "sys_power_w", "gpu_index", "gpu_temp_c", "gpu_usage_pct", "gpu_mem_pct", "gpu_power_w"})
	for rows.Next() {
		var ts int64
		var cpu, mem, pwr float64
		var gpuIdx sql.NullInt64
		var gpuTemp, gpuUse, gpuMem, gpuPow sql.NullFloat64
		if err := rows.Scan(&ts, &cpu, &mem, &pwr, &gpuIdx, &gpuTemp, &gpuUse, &gpuMem, &gpuPow); err != nil {
			continue
		}
		row := []string{
			strconv.FormatInt(ts, 10),
			strconv.FormatFloat(cpu, 'f', 2, 64),
			strconv.FormatFloat(mem, 'f', 2, 64),
			strconv.FormatFloat(pwr, 'f', 1, 64),
		}
		if gpuIdx.Valid {
			row = append(row,
				strconv.FormatInt(gpuIdx.Int64, 10),
				strconv.FormatFloat(gpuTemp.Float64, 'f', 1, 64),
				strconv.FormatFloat(gpuUse.Float64, 'f', 1, 64),
				strconv.FormatFloat(gpuMem.Float64, 'f', 1, 64),
				strconv.FormatFloat(gpuPow.Float64, 'f', 1, 64),
			)
		} else {
			row = append(row, "", "", "", "", "")
		}
		_ = cw.Write(row)
	}
	cw.Flush()
	return cw.Error()
}

// Close closes the database.
func (m *MetricsDB) Close() { _ = m.db.Close() }

func nullFloat(v float64) sql.NullFloat64 {
	return sql.NullFloat64{Float64: v, Valid: true}
}
