package server

import (
	"crypto/subtle"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/operator/command-center/internal/notifications"
)

// Disk alert buckets. Crossing UP into a higher bucket fires one push;
// dropping back to ok resets so the next climb alerts again.
const (
	diskWarnPercent = 90
	diskCritPercent = 97
)

type diskReportReq struct {
	UsedBytes  int64  `json:"used_bytes"`
	QuotaBytes int64  `json:"quota_bytes"`
	Source     string `json:"source"`
}

type diskLatestResp struct {
	UsedBytes  int64   `json:"used_bytes"`
	QuotaBytes int64   `json:"quota_bytes"`
	Percent    float64 `json:"percent"`
	ReportedAt int64   `json:"reported_at"`
	Source     string  `json:"source"`
}

// handleDiskReport ingests a usage figure from the Feral cron. It is exempt
// from the WebAuthn session middleware (see internal/auth/middleware.go) and
// authenticates with a bearer token from the CC_DISK_TOKEN env var instead —
// the cron can't do a passkey ceremony. When usage crosses an alert bucket it
// fires a single Web Push.
func (s *Server) handleDiskReport(w http.ResponseWriter, r *http.Request) {
	if s.sqlite == nil {
		http.Error(w, "disk reporting unavailable", http.StatusServiceUnavailable)
		return
	}
	if s.diskReportToken == "" {
		http.Error(w, "disk reporting not configured (no CC_DISK_TOKEN)", http.StatusServiceUnavailable)
		return
	}
	// Constant-time bearer check.
	got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
	if subtle.ConstantTimeCompare([]byte(got), []byte(s.diskReportToken)) != 1 {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req diskReportReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}
	if req.UsedBytes < 0 || req.QuotaBytes <= 0 {
		http.Error(w, "used_bytes and quota_bytes must be positive", http.StatusBadRequest)
		return
	}
	if req.Source == "" {
		req.Source = "unknown"
	}

	now := time.Now().Unix()
	if _, err := s.sqlite.ExecContext(r.Context(),
		`INSERT INTO disk_usage (reported_at, used_bytes, quota_bytes, source) VALUES (?,?,?,?)`,
		now, req.UsedBytes, req.QuotaBytes, req.Source); err != nil {
		s.logger.Error().Err(err).Msg("disk report: insert failed")
		http.Error(w, "store failed", http.StatusInternalServerError)
		return
	}

	pct := float64(req.UsedBytes) / float64(req.QuotaBytes) * 100
	s.maybeAlertDisk(r, pct, req.UsedBytes, req.QuotaBytes)

	writeJSON(w, http.StatusAccepted, map[string]any{
		"ok":      true,
		"percent": pct,
	})
}

func bucketFor(pct float64) int {
	switch {
	case pct >= diskCritPercent:
		return 2
	case pct >= diskWarnPercent:
		return 1
	default:
		return 0
	}
}

// maybeAlertDisk pushes once per upward bucket transition and resets state on
// the way back down. Failures here are logged, never fatal to the report.
func (s *Server) maybeAlertDisk(r *http.Request, pct float64, used, quota int64) {
	ctx := r.Context()
	var lastBucket int
	if err := s.sqlite.QueryRowContext(ctx,
		`SELECT last_bucket FROM disk_alert_state WHERE id = 1`).Scan(&lastBucket); err != nil && err != sql.ErrNoRows {
		s.logger.Error().Err(err).Msg("disk alert: read state failed")
		return
	}
	cur := bucketFor(pct)
	if cur == lastBucket {
		return
	}
	// Persist the new bucket regardless of direction.
	_, _ = s.sqlite.ExecContext(ctx,
		`UPDATE disk_alert_state SET last_bucket = ?, last_alerted_at = ? WHERE id = 1`,
		cur, time.Now().Unix())

	// Only push when climbing into warn/critical.
	if cur <= lastBucket || cur == 0 {
		return
	}
	if s.notifications == nil || s.notifications.Dispatcher == nil {
		return
	}
	usedGB := float64(used) / 1e9
	quotaGB := float64(quota) / 1e9
	sev := notifications.SeverityWarning
	label := "Seedbox disk filling"
	if cur == 2 {
		sev = notifications.SeverityCritical
		label = "Seedbox disk critical"
	}
	s.notifications.Dispatcher.Dispatch(ctx, notifications.Notification{
		Title:    label,
		Body:     fmt.Sprintf("%.0f%% used (%.0f / %.0f GB). Prune seeded-≥72h torrents to reclaim space.", pct, usedGB, quotaGB),
		Severity: sev,
		URL:      "/torrents",
		Data:     map[string]any{"used_bytes": used, "quota_bytes": quota, "percent": pct},
	}, []string{"push"}, nil)
	s.logger.Info().Float64("percent", pct).Int("bucket", cur).Msg("disk alert pushed")
}

// handleDiskLatest returns the most recent usage figure for the dashboard.
func (s *Server) handleDiskLatest(w http.ResponseWriter, r *http.Request) {
	if s.sqlite == nil {
		http.Error(w, "unavailable", http.StatusServiceUnavailable)
		return
	}
	var resp diskLatestResp
	err := s.sqlite.QueryRowContext(r.Context(),
		`SELECT used_bytes, quota_bytes, reported_at, source FROM disk_usage ORDER BY reported_at DESC LIMIT 1`).
		Scan(&resp.UsedBytes, &resp.QuotaBytes, &resp.ReportedAt, &resp.Source)
	if err == sql.ErrNoRows {
		// No report yet — return zeros so the UI can render "awaiting first report".
		writeJSON(w, http.StatusOK, diskLatestResp{})
		return
	}
	if err != nil {
		http.Error(w, "query failed", http.StatusInternalServerError)
		return
	}
	if resp.QuotaBytes > 0 {
		resp.Percent = float64(resp.UsedBytes) / float64(resp.QuotaBytes) * 100
	}
	writeJSON(w, http.StatusOK, resp)
}
