package server

import (
	"database/sql"
	"encoding/json"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
)

// trackerDTO is the on-the-wire shape returned by GET /api/trackers and
// GET /api/trackers/:id. It joins the latest ratio_snapshot for at-a-glance
// freshness; full history is at /api/trackers/:id/snapshots.
type trackerDTO struct {
	ID                    string   `json:"id"`
	Name                  string   `json:"name"`
	Type                  string   `json:"type"`
	BaseURL               string   `json:"base_url"`
	Enabled               bool     `json:"enabled"`
	ScrapeIntervalSeconds int      `json:"scrape_interval_seconds"`
	LastSnapshotAt        *int64   `json:"last_snapshot_at,omitempty"`
	LastRealRatio         *float64 `json:"last_real_ratio,omitempty"`
	LastUnsatCount        *int64   `json:"last_unsat_count,omitempty"`
	LastUnsatLimit        *int64   `json:"last_unsat_limit,omitempty"`
}

func (s *Server) handleListTrackers(w http.ResponseWriter, r *http.Request) {
	rows, err := s.sqlite.QueryContext(r.Context(), `
		SELECT t.id, t.name, t.type, t.base_url, t.enabled, t.scrape_interval_seconds,
		       latest.timestamp, latest.real_ratio, latest.unsat_count, latest.unsat_limit
		FROM trackers t
		LEFT JOIN (
			SELECT tracker_id, MAX(timestamp) AS timestamp, real_ratio, unsat_count, unsat_limit
			FROM ratio_snapshots
			WHERE simulation_id IS NULL
			GROUP BY tracker_id
		) latest ON latest.tracker_id = t.id
		ORDER BY t.name
	`)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	out := []trackerDTO{}
	for rows.Next() {
		var (
			d         trackerDTO
			enabled   int
			ts        sql.NullInt64
			ratio     sql.NullFloat64
			unsatN    sql.NullInt64
			unsatL    sql.NullInt64
		)
		if err := rows.Scan(&d.ID, &d.Name, &d.Type, &d.BaseURL, &enabled, &d.ScrapeIntervalSeconds, &ts, &ratio, &unsatN, &unsatL); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		d.Enabled = enabled != 0
		if ts.Valid {
			v := ts.Int64
			d.LastSnapshotAt = &v
		}
		if ratio.Valid {
			v := ratio.Float64
			d.LastRealRatio = &v
		}
		if unsatN.Valid {
			v := unsatN.Int64
			d.LastUnsatCount = &v
		}
		if unsatL.Valid {
			v := unsatL.Int64
			d.LastUnsatLimit = &v
		}
		out = append(out, d)
	}
	writeJSON(w, http.StatusOK, map[string]any{"count": len(out), "trackers": out})
}

func (s *Server) handleGetTracker(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" {
		http.Error(w, "missing id", http.StatusBadRequest)
		return
	}
	row := s.sqlite.QueryRowContext(r.Context(), `
		SELECT t.id, t.name, t.type, t.base_url, t.enabled, t.scrape_interval_seconds,
		       latest.timestamp, latest.real_ratio, latest.unsat_count, latest.unsat_limit
		FROM trackers t
		LEFT JOIN (
			SELECT tracker_id, MAX(timestamp) AS timestamp, real_ratio, unsat_count, unsat_limit
			FROM ratio_snapshots WHERE simulation_id IS NULL GROUP BY tracker_id
		) latest ON latest.tracker_id = t.id
		WHERE t.id = ?
	`, id)
	var (
		d       trackerDTO
		enabled int
		ts      sql.NullInt64
		ratio   sql.NullFloat64
		unsatN  sql.NullInt64
		unsatL  sql.NullInt64
	)
	err := row.Scan(&d.ID, &d.Name, &d.Type, &d.BaseURL, &enabled, &d.ScrapeIntervalSeconds, &ts, &ratio, &unsatN, &unsatL)
	if err == sql.ErrNoRows {
		http.Error(w, "tracker not found", http.StatusNotFound)
		return
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	d.Enabled = enabled != 0
	if ts.Valid {
		v := ts.Int64
		d.LastSnapshotAt = &v
	}
	if ratio.Valid {
		v := ratio.Float64
		d.LastRealRatio = &v
	}
	if unsatN.Valid {
		v := unsatN.Int64
		d.LastUnsatCount = &v
	}
	if unsatL.Valid {
		v := unsatL.Int64
		d.LastUnsatLimit = &v
	}
	writeJSON(w, http.StatusOK, d)
}

type snapshotDTO struct {
	Timestamp                int64    `json:"timestamp"`
	RealUploadedBytes        *int64   `json:"real_uploaded_bytes,omitempty"`
	RealDownloadedBytes      *int64   `json:"real_downloaded_bytes,omitempty"`
	RealRatio                *float64 `json:"real_ratio,omitempty"`
	DisplayedUploadedBytes   *int64   `json:"displayed_uploaded_bytes,omitempty"`
	DisplayedDownloadedBytes *int64   `json:"displayed_downloaded_bytes,omitempty"`
	DisplayedRatio           *float64 `json:"displayed_ratio,omitempty"`
	BonusPoints              *int64   `json:"bonus_points,omitempty"`
	UnsatCount               *int64   `json:"unsat_count,omitempty"`
	UnsatLimit               *int64   `json:"unsat_limit,omitempty"`
	ClassOrRank              string   `json:"class_or_rank,omitempty"`
}

func (s *Server) handleTrackerSnapshots(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" {
		http.Error(w, "missing id", http.StatusBadRequest)
		return
	}
	rangeArg := r.URL.Query().Get("range")
	cutoff := rangeCutoff(rangeArg)

	rows, err := s.sqlite.QueryContext(r.Context(), `
		SELECT timestamp,
		       real_uploaded_bytes, real_downloaded_bytes, real_ratio,
		       displayed_uploaded_bytes, displayed_downloaded_bytes, displayed_ratio,
		       bonus_points, unsat_count, unsat_limit, class_or_rank
		FROM ratio_snapshots
		WHERE tracker_id = ?
		  AND simulation_id IS NULL
		  AND timestamp >= ?
		ORDER BY timestamp ASC
	`, id, cutoff)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	out := []snapshotDTO{}
	for rows.Next() {
		var (
			d                              snapshotDTO
			rUp, rDn, dUp, dDn             sql.NullInt64
			rRatio, dRatio                 sql.NullFloat64
			bonus, unsatN, unsatL          sql.NullInt64
			class                          sql.NullString
		)
		if err := rows.Scan(&d.Timestamp, &rUp, &rDn, &rRatio, &dUp, &dDn, &dRatio, &bonus, &unsatN, &unsatL, &class); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		d.RealUploadedBytes = nullableInt(rUp)
		d.RealDownloadedBytes = nullableInt(rDn)
		d.RealRatio = nullableFloat(rRatio)
		d.DisplayedUploadedBytes = nullableInt(dUp)
		d.DisplayedDownloadedBytes = nullableInt(dDn)
		d.DisplayedRatio = nullableFloat(dRatio)
		d.BonusPoints = nullableInt(bonus)
		d.UnsatCount = nullableInt(unsatN)
		d.UnsatLimit = nullableInt(unsatL)
		if class.Valid {
			d.ClassOrRank = class.String
		}
		out = append(out, d)
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"tracker_id": id,
		"range":      rangeArg,
		"count":      len(out),
		"snapshots":  out,
	})
}

func (s *Server) handleTrackerRefresh(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" {
		http.Error(w, "missing id", http.StatusBadRequest)
		return
	}
	if s.scheduler == nil {
		http.Error(w, "scheduler not running", http.StatusServiceUnavailable)
		return
	}
	if err := s.scheduler.Refresh(id); err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	writeJSON(w, http.StatusAccepted, map[string]any{"queued": id})
}

// rangeCutoff turns "24h"/"7d"/"30d"/"all"/empty into a unix-second cutoff.
func rangeCutoff(s string) int64 {
	now := time.Now()
	switch s {
	case "24h", "":
		return now.Add(-24 * time.Hour).Unix()
	case "7d":
		return now.Add(-7 * 24 * time.Hour).Unix()
	case "30d":
		return now.Add(-30 * 24 * time.Hour).Unix()
	case "all":
		return 0
	default:
		return now.Add(-24 * time.Hour).Unix()
	}
}

func writeJSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(payload)
}

func nullableInt(v sql.NullInt64) *int64 {
	if !v.Valid {
		return nil
	}
	return &v.Int64
}
func nullableFloat(v sql.NullFloat64) *float64 {
	if !v.Valid {
		return nil
	}
	return &v.Float64
}
